react-native-macos/Libraries/Text/Text.js

297 строки
8.9 KiB
JavaScript
Исходник Обычный вид История

/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
const DeprecatedTextPropTypes = require('../DeprecatedPropTypes/DeprecatedTextPropTypes');
const React = require('react');
const ReactNativeViewAttributes = require('../Components/View/ReactNativeViewAttributes');
const TextAncestor = require('./TextAncestor');
const Touchable = require('../Components/Touchable/Touchable');
const UIManager = require('../ReactNative/UIManager');
const createReactNativeComponentClass = require('../Renderer/shims/createReactNativeComponentClass');
const nullthrows = require('nullthrows');
const processColor = require('../StyleSheet/processColor');
import type {PressEvent} from '../Types/CoreEventTypes';
import type {NativeComponent} from '../Renderer/shims/ReactNative';
import type {PressRetentionOffset, TextProps} from './TextProps';
type ResponseHandlers = $ReadOnly<{|
onStartShouldSetResponder: () => boolean,
onResponderGrant: (event: PressEvent, dispatchID: string) => void,
onResponderMove: (event: PressEvent) => void,
onResponderRelease: (event: PressEvent) => void,
onResponderTerminate: (event: PressEvent) => void,
onResponderTerminationRequest: () => boolean,
|}>;
type Props = $ReadOnly<{|
...TextProps,
forwardedRef: ?React.Ref<'RCTText' | 'RCTVirtualText'>,
|}>;
type State = {|
touchable: {|
touchState: ?string,
responderID: ?number,
|},
isHighlighted: boolean,
createResponderHandlers: () => ResponseHandlers,
responseHandlers: ?ResponseHandlers,
|};
const PRESS_RECT_OFFSET = {top: 20, left: 20, right: 20, bottom: 30};
const viewConfig = {
validAttributes: {
...ReactNativeViewAttributes.UIView,
isHighlighted: true,
numberOfLines: true,
ellipsizeMode: true,
allowFontScaling: true,
iOS: Add a maxFontSizeMultiplier prop to <Text> and <TextInput> (#20915) Summary: **Motivation** Whenever a user changes the system font size to its maximum allowable setting, React Native apps that allow font scaling can become unusable because the text gets too big. Experimenting with a native app like iMessage on iOS, the font size used for non-body text (e.g. header, navigational elements) is capped while the body text (e.g. text in the message bubbles) is allowed to grow. This PR introduces a new prop on `<Text>` and `<TextInput>` called `maxFontSizeMultiplier`. This enables devs to set the maximum allowed text scale factor on a Text/TextInput. The default is 0 which means no limit. Another PR will add this feature to Android. **Test Plan** I created a test app which utilizes all categories of values of `maxFontSizeMultiplier`: - `undefined`: inherit from parent - `0`: no limit - `1`, `1.2`: fixed limits I tried this with `Text`, `TextInput` with `value`, and `TextInput` with children. For `Text`, I also verified that nesting works properly (if a child `Text` doesn't specify `maxFontSizeMultiplier`, it inherits it from its parent). Lastly, we've been using a version of this in Skype for several months. **Release Notes** [GENERAL] [ENHANCEMENT] [Text/TextInput] - Added maxFontSizeMultiplier prop to prevent some text from getting unusably large as user increases OS's font scale setting (iOS) Adam Comella Microsoft Corp. Pull Request resolved: https://github.com/facebook/react-native/pull/20915 Differential Revision: D9646739 Pulled By: shergin fbshipit-source-id: c823f59c1e342c22d6297b88b2cb11c5a1f10310
2018-09-05 03:33:06 +03:00
maxFontSizeMultiplier: true,
disabled: true,
selectable: true,
selectionColor: true,
adjustsFontSizeToFit: true,
minimumFontScale: true,
textBreakStrategy: true,
onTextLayout: true,
Android: Enable views to be nested within <Text> (#23195) Summary: Potential breaking change: The signature of ReactShadowNode's onBeforeLayout method was changed - Before: public void onBeforeLayout() - After: public void onBeforeLayout(NativeViewHierarchyOptimizer nativeViewHierarchyOptimizer) Implements same feature as this iOS PR: https://github.com/facebook/react-native/pull/7304 Previously, only Text and Image could be nested within Text. Now, any view can be nested within Text. One restriction of this feature is that developers must give inline views a width and a height via the style prop. Previously, inline Images were supported via FrescoBasedReactTextInlineImageSpan. To get support for nesting views within Text, we create one special kind of span per inline view. This span is called TextInlineViewPlaceholderSpan. It is the same size as the inline view. Its job is just to occupy space -- it doesn't render any visual. After the text is rendered, we query the Android Layout object associated with the TextView to find out where it has positioned each TextInlineViewPlaceholderSpan. We then position the views to be at those locations. One tricky aspect of the implementation is that the Text component needs to be able to render native children (the inline views) but the Android TextView cannot have children. This is solved by having the native parent of the ReactTextView also host the inline views. Implementation-wise, this was accomplished by extending the NativeViewHierarchyOptimizer to handle this case. The optimizer now handles these cases: - Node is not in the native tree. An ancestor must host its children. - Node is in the native tree and it can host its own children. - (new) Node is in the native tree but it cannot host its own children. An ancestor must host both this node and its children. I added the `onInlineViewLayout` event which is useful for writing tests for verifying that the inline views are positioned properly. Limitation: Clipping ---------- If Text's height/width is small such that an inline view doesn't completely fit, the inline view may still be fully visible due to hoisting (the inline view isn't actually parented to the Text which has the limited size. It is parented to an ancestor which may have a different clipping rectangle.). Prior to this change, layout-only views had a similar limitation. Pull Request resolved: https://github.com/facebook/react-native/pull/23195 Differential Revision: D14014668 Pulled By: shergin fbshipit-source-id: d46130f3d19cc83ac7ddf423adcc9e23988245d3
2019-04-02 05:52:38 +03:00
onInlineViewLayout: true,
dataDetectorType: true,
},
directEventTypes: {
topTextLayout: {
registrationName: 'onTextLayout',
},
Android: Enable views to be nested within <Text> (#23195) Summary: Potential breaking change: The signature of ReactShadowNode's onBeforeLayout method was changed - Before: public void onBeforeLayout() - After: public void onBeforeLayout(NativeViewHierarchyOptimizer nativeViewHierarchyOptimizer) Implements same feature as this iOS PR: https://github.com/facebook/react-native/pull/7304 Previously, only Text and Image could be nested within Text. Now, any view can be nested within Text. One restriction of this feature is that developers must give inline views a width and a height via the style prop. Previously, inline Images were supported via FrescoBasedReactTextInlineImageSpan. To get support for nesting views within Text, we create one special kind of span per inline view. This span is called TextInlineViewPlaceholderSpan. It is the same size as the inline view. Its job is just to occupy space -- it doesn't render any visual. After the text is rendered, we query the Android Layout object associated with the TextView to find out where it has positioned each TextInlineViewPlaceholderSpan. We then position the views to be at those locations. One tricky aspect of the implementation is that the Text component needs to be able to render native children (the inline views) but the Android TextView cannot have children. This is solved by having the native parent of the ReactTextView also host the inline views. Implementation-wise, this was accomplished by extending the NativeViewHierarchyOptimizer to handle this case. The optimizer now handles these cases: - Node is not in the native tree. An ancestor must host its children. - Node is in the native tree and it can host its own children. - (new) Node is in the native tree but it cannot host its own children. An ancestor must host both this node and its children. I added the `onInlineViewLayout` event which is useful for writing tests for verifying that the inline views are positioned properly. Limitation: Clipping ---------- If Text's height/width is small such that an inline view doesn't completely fit, the inline view may still be fully visible due to hoisting (the inline view isn't actually parented to the Text which has the limited size. It is parented to an ancestor which may have a different clipping rectangle.). Prior to this change, layout-only views had a similar limitation. Pull Request resolved: https://github.com/facebook/react-native/pull/23195 Differential Revision: D14014668 Pulled By: shergin fbshipit-source-id: d46130f3d19cc83ac7ddf423adcc9e23988245d3
2019-04-02 05:52:38 +03:00
topInlineViewLayout: {
registrationName: 'onInlineViewLayout',
},
},
uiViewClassName: 'RCTText',
};
/**
* A React component for displaying text.
*
* See https://facebook.github.io/react-native/docs/text.html
*/
class TouchableText extends React.Component<Props, State> {
static defaultProps = {
accessible: true,
allowFontScaling: true,
ellipsizeMode: 'tail',
};
touchableGetPressRectOffset: ?() => PressRetentionOffset;
touchableHandleActivePressIn: ?() => void;
touchableHandleActivePressOut: ?() => void;
touchableHandleLongPress: ?(event: PressEvent) => void;
touchableHandlePress: ?(event: PressEvent) => void;
touchableHandleResponderGrant: ?(
event: PressEvent,
dispatchID: string,
) => void;
touchableHandleResponderMove: ?(event: PressEvent) => void;
touchableHandleResponderRelease: ?(event: PressEvent) => void;
touchableHandleResponderTerminate: ?(event: PressEvent) => void;
touchableHandleResponderTerminationRequest: ?() => boolean;
state = {
...Touchable.Mixin.touchableGetInitialState(),
isHighlighted: false,
createResponderHandlers: this._createResponseHandlers.bind(this),
responseHandlers: null,
};
static getDerivedStateFromProps(
nextProps: Props,
prevState: State,
): $Shape<State> | null {
return prevState.responseHandlers == null && isTouchable(nextProps)
? {
responseHandlers: prevState.createResponderHandlers(),
}
: null;
}
static viewConfig = viewConfig;
render(): React.Node {
let props = this.props;
if (isTouchable(props)) {
props = {
...props,
...this.state.responseHandlers,
isHighlighted: this.state.isHighlighted,
};
}
if (props.selectionColor != null) {
props = {
...props,
selectionColor: processColor(props.selectionColor),
};
}
if (__DEV__) {
if (Touchable.TOUCH_TARGET_DEBUG && props.onPress != null) {
props = {
...props,
style: [props.style, {color: 'magenta'}],
};
}
}
return (
<TextAncestor.Consumer>
{hasTextAncestor =>
hasTextAncestor ? (
<RCTVirtualText {...props} ref={props.forwardedRef} />
) : (
<TextAncestor.Provider value={true}>
<RCTText {...props} ref={props.forwardedRef} />
</TextAncestor.Provider>
)
}
</TextAncestor.Consumer>
);
}
_createResponseHandlers(): ResponseHandlers {
return {
onStartShouldSetResponder: (): boolean => {
const {onStartShouldSetResponder} = this.props;
const shouldSetResponder =
(onStartShouldSetResponder == null
? false
: onStartShouldSetResponder()) || isTouchable(this.props);
if (shouldSetResponder) {
this._attachTouchHandlers();
}
return shouldSetResponder;
},
onResponderGrant: (event: PressEvent, dispatchID: string): void => {
nullthrows(this.touchableHandleResponderGrant)(event, dispatchID);
if (this.props.onResponderGrant != null) {
this.props.onResponderGrant.call(this, event, dispatchID);
}
},
onResponderMove: (event: PressEvent): void => {
nullthrows(this.touchableHandleResponderMove)(event);
if (this.props.onResponderMove != null) {
this.props.onResponderMove.call(this, event);
}
},
onResponderRelease: (event: PressEvent): void => {
nullthrows(this.touchableHandleResponderRelease)(event);
if (this.props.onResponderRelease != null) {
this.props.onResponderRelease.call(this, event);
}
},
onResponderTerminate: (event: PressEvent): void => {
nullthrows(this.touchableHandleResponderTerminate)(event);
if (this.props.onResponderTerminate != null) {
this.props.onResponderTerminate.call(this, event);
}
},
onResponderTerminationRequest: (): boolean => {
const {onResponderTerminationRequest} = this.props;
if (!nullthrows(this.touchableHandleResponderTerminationRequest)()) {
return false;
}
if (onResponderTerminationRequest == null) {
return true;
}
return onResponderTerminationRequest();
},
};
}
/**
* Lazily attaches Touchable.Mixin handlers.
*/
_attachTouchHandlers(): void {
if (this.touchableGetPressRectOffset != null) {
return;
}
for (const key in Touchable.Mixin) {
if (typeof Touchable.Mixin[key] === 'function') {
(this: any)[key] = Touchable.Mixin[key].bind(this);
}
}
this.touchableHandleActivePressIn = (): void => {
if (!this.props.suppressHighlighting && isTouchable(this.props)) {
this.setState({isHighlighted: true});
}
};
this.touchableHandleActivePressOut = (): void => {
if (!this.props.suppressHighlighting && isTouchable(this.props)) {
this.setState({isHighlighted: false});
}
};
this.touchableHandlePress = (event: PressEvent): void => {
if (this.props.onPress != null) {
this.props.onPress(event);
}
};
this.touchableHandleLongPress = (event: PressEvent): void => {
if (this.props.onLongPress != null) {
this.props.onLongPress(event);
}
};
this.touchableGetPressRectOffset = (): PressRetentionOffset =>
this.props.pressRetentionOffset == null
? PRESS_RECT_OFFSET
: this.props.pressRetentionOffset;
}
}
const isTouchable = (props: Props): boolean =>
props.onPress != null ||
props.onLongPress != null ||
props.onStartShouldSetResponder != null;
const RCTText = createReactNativeComponentClass(
viewConfig.uiViewClassName,
() => viewConfig,
);
const RCTVirtualText =
UIManager.getViewManagerConfig('RCTVirtualText') == null
? RCTText
: createReactNativeComponentClass('RCTVirtualText', () => ({
validAttributes: {
...ReactNativeViewAttributes.UIView,
isHighlighted: true,
iOS: Add a maxFontSizeMultiplier prop to <Text> and <TextInput> (#20915) Summary: **Motivation** Whenever a user changes the system font size to its maximum allowable setting, React Native apps that allow font scaling can become unusable because the text gets too big. Experimenting with a native app like iMessage on iOS, the font size used for non-body text (e.g. header, navigational elements) is capped while the body text (e.g. text in the message bubbles) is allowed to grow. This PR introduces a new prop on `<Text>` and `<TextInput>` called `maxFontSizeMultiplier`. This enables devs to set the maximum allowed text scale factor on a Text/TextInput. The default is 0 which means no limit. Another PR will add this feature to Android. **Test Plan** I created a test app which utilizes all categories of values of `maxFontSizeMultiplier`: - `undefined`: inherit from parent - `0`: no limit - `1`, `1.2`: fixed limits I tried this with `Text`, `TextInput` with `value`, and `TextInput` with children. For `Text`, I also verified that nesting works properly (if a child `Text` doesn't specify `maxFontSizeMultiplier`, it inherits it from its parent). Lastly, we've been using a version of this in Skype for several months. **Release Notes** [GENERAL] [ENHANCEMENT] [Text/TextInput] - Added maxFontSizeMultiplier prop to prevent some text from getting unusably large as user increases OS's font scale setting (iOS) Adam Comella Microsoft Corp. Pull Request resolved: https://github.com/facebook/react-native/pull/20915 Differential Revision: D9646739 Pulled By: shergin fbshipit-source-id: c823f59c1e342c22d6297b88b2cb11c5a1f10310
2018-09-05 03:33:06 +03:00
maxFontSizeMultiplier: true,
},
uiViewClassName: 'RCTVirtualText',
}));
const Text = (
props: TextProps,
forwardedRef: ?React.Ref<'RCTText' | 'RCTVirtualText'>,
) => {
return <TouchableText {...props} forwardedRef={forwardedRef} />;
};
const TextToExport = React.forwardRef(Text);
TextToExport.displayName = 'Text';
// TODO: Deprecate this.
/* $FlowFixMe(>=0.89.0 site=react_native_fb) This comment suppresses an error
* found when Flow v0.89 was deployed. To see the error, delete this comment
* and run Flow. */
TextToExport.propTypes = DeprecatedTextPropTypes;
module.exports = ((TextToExport: $FlowFixMe): Class<
NativeComponent<TextProps>,
>);