mirror of
https://github.com/status-im/react-native.git
synced 2025-01-17 21:11:45 +00:00
c0ea23cfb0
Summary: Android has a text API called breakStrategy for controlling how paragraphs are broken up into lines. For example, some modes support automatically hyphenating words so a word can be split across lines while others do not. One source of complexity is that Android provides different defaults for `breakStrategy` for `TextView` vs `EditText`. `TextView`'s default is `BREAK_STRATEGY_HIGH_QUALITY` while `EditText`'s default is `BREAK_STRATEGY_SIMPLE`. In addition to exposing `textBreakStrategy`, this change also fixes a couple of rendering glitches with `Text` and `TextInput`. `TextView` and `EditText` have different default values for `breakStrategy` and `hyphenationFrequency` than `StaticLayout`. Consequently, we were using different parameters for measuring and rendering. Whenever measuring and rendering parameters are inconsistent, it can result in visual glitches such as the text taking up too much space or being clipped. This change fixes these inconsistencies by setting `breakStrategy` and `hyphenat Closes https://github.com/facebook/react-native/pull/11007 Differential Revision: D4227495 Pulled By: lacker fbshipit-source-id: c2d96bd0ddc7bd315fda016fb4f1b5108a2e35cf
347 lines
11 KiB
JavaScript
347 lines
11 KiB
JavaScript
/**
|
|
* Copyright (c) 2015-present, Facebook, Inc.
|
|
* All rights reserved.
|
|
*
|
|
* This source code is licensed under the BSD-style license found in the
|
|
* LICENSE file in the root directory of this source tree. An additional grant
|
|
* of patent rights can be found in the PATENTS file in the same directory.
|
|
*
|
|
* @providesModule Text
|
|
* @flow
|
|
*/
|
|
'use strict';
|
|
|
|
const NativeMethodsMixin = require('NativeMethodsMixin');
|
|
const Platform = require('Platform');
|
|
const React = require('React');
|
|
const ReactNativeViewAttributes = require('ReactNativeViewAttributes');
|
|
const StyleSheetPropType = require('StyleSheetPropType');
|
|
const TextStylePropTypes = require('TextStylePropTypes');
|
|
const Touchable = require('Touchable');
|
|
|
|
const createReactNativeComponentClass = require('createReactNativeComponentClass');
|
|
const mergeFast = require('mergeFast');
|
|
|
|
const stylePropType = StyleSheetPropType(TextStylePropTypes);
|
|
|
|
const viewConfig = {
|
|
validAttributes: mergeFast(ReactNativeViewAttributes.UIView, {
|
|
isHighlighted: true,
|
|
numberOfLines: true,
|
|
ellipsizeMode: true,
|
|
allowFontScaling: true,
|
|
selectable: true,
|
|
adjustsFontSizeToFit: true,
|
|
minimumFontScale: true,
|
|
textBreakStrategy: true,
|
|
}),
|
|
uiViewClassName: 'RCTText',
|
|
};
|
|
|
|
/**
|
|
* A React component for displaying text.
|
|
*
|
|
* `Text` supports nesting, styling, and touch handling.
|
|
*
|
|
* In the following example, the nested title and body text will inherit the `fontFamily` from
|
|
*`styles.baseText`, but the title provides its own additional styles. The title and body will
|
|
* stack on top of each other on account of the literal newlines:
|
|
*
|
|
* ```ReactNativeWebPlayer
|
|
* import React, { Component } from 'react';
|
|
* import { AppRegistry, Text, StyleSheet } from 'react-native';
|
|
*
|
|
* class TextInANest extends Component {
|
|
* constructor(props) {
|
|
* super(props);
|
|
* this.state = {
|
|
* titleText: "Bird's Nest",
|
|
* bodyText: 'This is not really a bird nest.'
|
|
* };
|
|
* }
|
|
*
|
|
* render() {
|
|
* return (
|
|
* <Text style={styles.baseText}>
|
|
* <Text style={styles.titleText} onPress={this.onPressTitle}>
|
|
* {this.state.titleText}{'\n'}{'\n'}
|
|
* </Text>
|
|
* <Text numberOfLines={5}>
|
|
* {this.state.bodyText}
|
|
* </Text>
|
|
* </Text>
|
|
* );
|
|
* }
|
|
* }
|
|
*
|
|
* const styles = StyleSheet.create({
|
|
* baseText: {
|
|
* fontFamily: 'Cochin',
|
|
* },
|
|
* titleText: {
|
|
* fontSize: 20,
|
|
* fontWeight: 'bold',
|
|
* },
|
|
* });
|
|
*
|
|
* // App registration and rendering
|
|
* AppRegistry.registerComponent('TextInANest', () => TextInANest);
|
|
* ```
|
|
*/
|
|
|
|
const Text = React.createClass({
|
|
propTypes: {
|
|
/**
|
|
* This can be one of the following values:
|
|
*
|
|
* - `head` - The line is displayed so that the end fits in the container and the missing text
|
|
* at the beginning of the line is indicated by an ellipsis glyph. e.g., "...wxyz"
|
|
* - `middle` - The line is displayed so that the beginning and end fit in the container and the
|
|
* missing text in the middle is indicated by an ellipsis glyph. "ab...yz"
|
|
* - `tail` - The line is displayed so that the beginning fits in the container and the
|
|
* missing text at the end of the line is indicated by an ellipsis glyph. e.g., "abcd..."
|
|
* - `clip` - Lines are not drawn past the edge of the text container.
|
|
*
|
|
* The default is `tail`.
|
|
*
|
|
* `numberOfLines` must be set in conjunction with this prop.
|
|
*
|
|
* > `clip` is working only for iOS
|
|
*/
|
|
ellipsizeMode: React.PropTypes.oneOf(['head', 'middle', 'tail', 'clip']),
|
|
/**
|
|
* Used to truncate the text with an ellipsis after computing the text
|
|
* layout, including line wrapping, such that the total number of lines
|
|
* does not exceed this number.
|
|
*
|
|
* This prop is commonly used with `ellipsizeMode`.
|
|
*/
|
|
numberOfLines: React.PropTypes.number,
|
|
/**
|
|
* Set text break strategy on Android API Level 23+, possible values are `simple`, `highQuality`, `balanced`
|
|
* The default value is `highQuality`.
|
|
* @platform android
|
|
*/
|
|
textBreakStrategy: React.PropTypes.oneOf(['simple', 'highQuality', 'balanced']),
|
|
/**
|
|
* Invoked on mount and layout changes with
|
|
*
|
|
* `{nativeEvent: {layout: {x, y, width, height}}}`
|
|
*/
|
|
onLayout: React.PropTypes.func,
|
|
/**
|
|
* This function is called on press.
|
|
*
|
|
* e.g., `onPress={() => console.log('1st')}``
|
|
*/
|
|
onPress: React.PropTypes.func,
|
|
/**
|
|
* This function is called on long press.
|
|
*
|
|
* e.g., `onLongPress={this.increaseSize}>``
|
|
*/
|
|
onLongPress: React.PropTypes.func,
|
|
/**
|
|
* Lets the user select text, to use the native copy and paste functionality.
|
|
*/
|
|
selectable: React.PropTypes.bool,
|
|
/**
|
|
* When `true`, no visual change is made when text is pressed down. By
|
|
* default, a gray oval highlights the text on press down.
|
|
*
|
|
* @platform ios
|
|
*/
|
|
suppressHighlighting: React.PropTypes.bool,
|
|
style: stylePropType,
|
|
/**
|
|
* Used to locate this view in end-to-end tests.
|
|
*/
|
|
testID: React.PropTypes.string,
|
|
/**
|
|
* Specifies whether fonts should scale to respect Text Size accessibility setting on iOS. The
|
|
* default is `true`.
|
|
*/
|
|
allowFontScaling: React.PropTypes.bool,
|
|
/**
|
|
* When set to `true`, indicates that the view is an accessibility element. The default value
|
|
* for a `Text` element is `true`.
|
|
*
|
|
* See the
|
|
* [Accessibility guide](/react-native/docs/accessibility.html#accessible-ios-android)
|
|
* for more information.
|
|
*/
|
|
accessible: React.PropTypes.bool,
|
|
/**
|
|
* Specifies whether font should be scaled down automatically to fit given style constraints.
|
|
* @platform ios
|
|
*/
|
|
adjustsFontSizeToFit: React.PropTypes.bool,
|
|
|
|
/**
|
|
* Specifies smallest possible scale a font can reach when adjustsFontSizeToFit is enabled. (values 0.01-1.0).
|
|
* @platform ios
|
|
*/
|
|
minimumFontScale: React.PropTypes.number,
|
|
},
|
|
getDefaultProps(): Object {
|
|
return {
|
|
accessible: true,
|
|
allowFontScaling: true,
|
|
ellipsizeMode: 'tail',
|
|
};
|
|
},
|
|
getInitialState: function(): Object {
|
|
return mergeFast(Touchable.Mixin.touchableGetInitialState(), {
|
|
isHighlighted: false,
|
|
});
|
|
},
|
|
mixins: [NativeMethodsMixin],
|
|
viewConfig: viewConfig,
|
|
getChildContext(): Object {
|
|
return {isInAParentText: true};
|
|
},
|
|
childContextTypes: {
|
|
isInAParentText: React.PropTypes.bool
|
|
},
|
|
contextTypes: {
|
|
isInAParentText: React.PropTypes.bool
|
|
},
|
|
/**
|
|
* Only assigned if touch is needed.
|
|
*/
|
|
_handlers: (null: ?Object),
|
|
_hasPressHandler(): boolean {
|
|
return !!this.props.onPress || !!this.props.onLongPress;
|
|
},
|
|
/**
|
|
* These are assigned lazily the first time the responder is set to make plain
|
|
* text nodes as cheap as possible.
|
|
*/
|
|
touchableHandleActivePressIn: (null: ?Function),
|
|
touchableHandleActivePressOut: (null: ?Function),
|
|
touchableHandlePress: (null: ?Function),
|
|
touchableHandleLongPress: (null: ?Function),
|
|
touchableGetPressRectOffset: (null: ?Function),
|
|
render(): React.Element<any> {
|
|
let newProps = this.props;
|
|
if (this.props.onStartShouldSetResponder || this._hasPressHandler()) {
|
|
if (!this._handlers) {
|
|
this._handlers = {
|
|
onStartShouldSetResponder: (): bool => {
|
|
const shouldSetFromProps = this.props.onStartShouldSetResponder &&
|
|
this.props.onStartShouldSetResponder();
|
|
const setResponder = shouldSetFromProps || this._hasPressHandler();
|
|
if (setResponder && !this.touchableHandleActivePressIn) {
|
|
// Attach and bind all the other handlers only the first time a touch
|
|
// actually happens.
|
|
for (const key in Touchable.Mixin) {
|
|
if (typeof Touchable.Mixin[key] === 'function') {
|
|
(this: any)[key] = Touchable.Mixin[key].bind(this);
|
|
}
|
|
}
|
|
this.touchableHandleActivePressIn = () => {
|
|
if (this.props.suppressHighlighting || !this._hasPressHandler()) {
|
|
return;
|
|
}
|
|
this.setState({
|
|
isHighlighted: true,
|
|
});
|
|
};
|
|
|
|
this.touchableHandleActivePressOut = () => {
|
|
if (this.props.suppressHighlighting || !this._hasPressHandler()) {
|
|
return;
|
|
}
|
|
this.setState({
|
|
isHighlighted: false,
|
|
});
|
|
};
|
|
|
|
this.touchableHandlePress = (e: SyntheticEvent) => {
|
|
this.props.onPress && this.props.onPress(e);
|
|
};
|
|
|
|
this.touchableHandleLongPress = (e: SyntheticEvent) => {
|
|
this.props.onLongPress && this.props.onLongPress(e);
|
|
};
|
|
|
|
this.touchableGetPressRectOffset = function(): RectOffset {
|
|
return PRESS_RECT_OFFSET;
|
|
};
|
|
}
|
|
return setResponder;
|
|
},
|
|
onResponderGrant: function(e: SyntheticEvent, dispatchID: string) {
|
|
this.touchableHandleResponderGrant(e, dispatchID);
|
|
this.props.onResponderGrant &&
|
|
this.props.onResponderGrant.apply(this, arguments);
|
|
}.bind(this),
|
|
onResponderMove: function(e: SyntheticEvent) {
|
|
this.touchableHandleResponderMove(e);
|
|
this.props.onResponderMove &&
|
|
this.props.onResponderMove.apply(this, arguments);
|
|
}.bind(this),
|
|
onResponderRelease: function(e: SyntheticEvent) {
|
|
this.touchableHandleResponderRelease(e);
|
|
this.props.onResponderRelease &&
|
|
this.props.onResponderRelease.apply(this, arguments);
|
|
}.bind(this),
|
|
onResponderTerminate: function(e: SyntheticEvent) {
|
|
this.touchableHandleResponderTerminate(e);
|
|
this.props.onResponderTerminate &&
|
|
this.props.onResponderTerminate.apply(this, arguments);
|
|
}.bind(this),
|
|
onResponderTerminationRequest: function(): bool {
|
|
// Allow touchable or props.onResponderTerminationRequest to deny
|
|
// the request
|
|
var allowTermination = this.touchableHandleResponderTerminationRequest();
|
|
if (allowTermination && this.props.onResponderTerminationRequest) {
|
|
allowTermination = this.props.onResponderTerminationRequest.apply(this, arguments);
|
|
}
|
|
return allowTermination;
|
|
}.bind(this),
|
|
};
|
|
}
|
|
newProps = {
|
|
...this.props,
|
|
...this._handlers,
|
|
isHighlighted: this.state.isHighlighted,
|
|
};
|
|
}
|
|
if (Touchable.TOUCH_TARGET_DEBUG && newProps.onPress) {
|
|
newProps = {
|
|
...newProps,
|
|
style: [this.props.style, {color: 'magenta'}],
|
|
};
|
|
}
|
|
if (this.context.isInAParentText) {
|
|
return <RCTVirtualText {...newProps} />;
|
|
} else {
|
|
return <RCTText {...newProps} />;
|
|
}
|
|
},
|
|
});
|
|
|
|
type RectOffset = {
|
|
top: number,
|
|
left: number,
|
|
right: number,
|
|
bottom: number,
|
|
}
|
|
|
|
var PRESS_RECT_OFFSET = {top: 20, left: 20, right: 20, bottom: 30};
|
|
|
|
var RCTText = createReactNativeComponentClass(viewConfig);
|
|
var RCTVirtualText = RCTText;
|
|
|
|
if (Platform.OS === 'android') {
|
|
RCTVirtualText = createReactNativeComponentClass({
|
|
validAttributes: mergeFast(ReactNativeViewAttributes.UIView, {
|
|
isHighlighted: true,
|
|
}),
|
|
uiViewClassName: 'RCTVirtualText',
|
|
});
|
|
}
|
|
|
|
module.exports = Text;
|