UNPKG

@wordpress/components

Version:
565 lines (555 loc) 20.7 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _reactNative = require("react-native"); var _reactNativeModal = _interopRequireDefault(require("react-native-modal")); var _reactNativeSafeArea = _interopRequireDefault(require("react-native-safe-area")); var _reactNativeBridge = require("@wordpress/react-native-bridge"); var _element = require("@wordpress/element"); var _compose = require("@wordpress/compose"); var _styles = _interopRequireDefault(require("./styles.scss")); var _button = _interopRequireDefault(require("./button")); var _cell = _interopRequireDefault(require("./cell")); var _cyclePickerCell = _interopRequireDefault(require("./cycle-picker-cell")); var _pickerCell = _interopRequireDefault(require("./picker-cell")); var _switchCell = _interopRequireDefault(require("./switch-cell")); var _rangeCell = _interopRequireDefault(require("./range-cell")); var _colorCell = _interopRequireDefault(require("./color-cell")); var _linkCell = _interopRequireDefault(require("./link-cell")); var _linkSuggestionItemCell = _interopRequireDefault(require("./link-suggestion-item-cell")); var _radioCell = _interopRequireDefault(require("./radio-cell")); var _navigationScreen = _interopRequireDefault(require("./bottom-sheet-navigation/navigation-screen")); var _navigationContainer = _interopRequireDefault(require("./bottom-sheet-navigation/navigation-container")); var _keyboardAvoidingView = _interopRequireDefault(require("./keyboard-avoiding-view")); var _subSheet = _interopRequireDefault(require("./sub-sheet")); var _navBar = _interopRequireDefault(require("./nav-bar")); var _bottomSheetContext = require("./bottom-sheet-context"); var _jsxRuntime = require("react/jsx-runtime"); /** * External dependencies */ /** * WordPress dependencies */ /** * Internal dependencies */ const DEFAULT_LAYOUT_ANIMATION = _reactNative.LayoutAnimation.Presets.easeInEaseOut; class BottomSheet extends _element.Component { constructor() { super(...arguments); this.onSafeAreaInsetsUpdate = this.onSafeAreaInsetsUpdate.bind(this); this.onScroll = this.onScroll.bind(this); this.isScrolling = this.isScrolling.bind(this); this.onShouldEnableScroll = this.onShouldEnableScroll.bind(this); this.onDismiss = this.onDismiss.bind(this); this.onShouldSetBottomSheetMaxHeight = this.onShouldSetBottomSheetMaxHeight.bind(this); this.setIsFullScreen = this.setIsFullScreen.bind(this); this.onDimensionsChange = this.onDimensionsChange.bind(this); this.onHeaderLayout = this.onHeaderLayout.bind(this); this.onCloseBottomSheet = this.onCloseBottomSheet.bind(this); this.onHandleClosingBottomSheet = this.onHandleClosingBottomSheet.bind(this); this.onHardwareButtonPress = this.onHardwareButtonPress.bind(this); this.onHandleHardwareButtonPress = this.onHandleHardwareButtonPress.bind(this); this.keyboardShow = this.keyboardShow.bind(this); this.keyboardHide = this.keyboardHide.bind(this); this.headerHeight = 0; this.keyboardHeight = 0; this.lastLayoutAnimation = null; this.lastLayoutAnimationFinished = false; this.state = { safeAreaBottomInset: 0, safeAreaTopInset: 0, bounces: false, maxHeight: 0, scrollEnabled: true, isScrolling: false, handleClosingBottomSheet: null, handleHardwareButtonPress: null, isMaxHeightSet: true, isFullScreen: this.props.isFullScreen || false }; } keyboardShow(e) { if (!this.props.isVisible) { return; } const { height } = e.endCoordinates; this.keyboardHeight = height; this.performKeyboardLayoutAnimation(e); this.onSetMaxHeight(); this.props.onKeyboardShow?.(); } keyboardHide(e) { if (!this.props.isVisible) { return; } this.keyboardHeight = 0; this.performKeyboardLayoutAnimation(e); this.onSetMaxHeight(); this.props.onKeyboardHide?.(); } performKeyboardLayoutAnimation(event) { const { duration, easing } = event; if (duration && easing) { // This layout animation is the same as the React Native's KeyboardAvoidingView component. // Reference: https://github.com/facebook/react-native/blob/266b21baf35e052ff28120f79c06c4f6dddc51a9/Libraries/Components/Keyboard/KeyboardAvoidingView.js#L119-L128. const animationConfig = { // We have to pass the duration equal to minimal accepted duration defined here: RCTLayoutAnimation.m. duration: duration > 10 ? duration : 10, type: _reactNative.LayoutAnimation.Types[easing] || 'keyboard' }; const layoutAnimation = { duration: animationConfig.duration, update: animationConfig, create: { ...animationConfig, property: _reactNative.LayoutAnimation.Properties.opacity }, delete: { ...animationConfig, property: _reactNative.LayoutAnimation.Properties.opacity } }; this.lastLayoutAnimationFinished = false; _reactNative.LayoutAnimation.configureNext(layoutAnimation, () => { this.lastLayoutAnimationFinished = true; }); this.lastLayoutAnimation = layoutAnimation; } else { // TODO: Reinstate animations, possibly replacing `LayoutAnimation` with // more nuanced `Animated` usage or replacing our custom `BottomSheet` // with `@gorhom/bottom-sheet`. This animation was disabled to avoid a // preexisting bug: https://github.com/WordPress/gutenberg/issues/30562 // this.performRegularLayoutAnimation( { // useLastLayoutAnimation: false, // } );. } } performRegularLayoutAnimation({ useLastLayoutAnimation }) { // On Android, we should prevent triggering multiple layout animations at the same time because it can produce visual glitches. if (_reactNative.Platform.OS === 'android' && this.lastLayoutAnimation && !this.lastLayoutAnimationFinished) { return; } const layoutAnimation = useLastLayoutAnimation ? this.lastLayoutAnimation || DEFAULT_LAYOUT_ANIMATION : DEFAULT_LAYOUT_ANIMATION; this.lastLayoutAnimationFinished = false; _reactNative.LayoutAnimation.configureNext(layoutAnimation, () => { this.lastLayoutAnimationFinished = true; }); this.lastLayoutAnimation = layoutAnimation; } componentDidMount() { _reactNativeSafeArea.default.getSafeAreaInsetsForRootView().then(this.onSafeAreaInsetsUpdate); if (_reactNative.Platform.OS === 'android') { this.androidModalClosedSubscription = (0, _reactNativeBridge.subscribeAndroidModalClosed)(() => { this.props.onClose(); }); } this.dimensionsChangeSubscription = _reactNative.Dimensions.addEventListener('change', this.onDimensionsChange); // 'Will' keyboard events are not available on Android. // Reference: https://reactnative.dev/docs/0.61/keyboard#addlistener. this.keyboardShowListener = _reactNative.Keyboard.addListener(_reactNative.Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow', this.keyboardShow); this.keyboardHideListener = _reactNative.Keyboard.addListener(_reactNative.Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide', this.keyboardHide); this.safeAreaEventSubscription = _reactNativeSafeArea.default.addEventListener('safeAreaInsetsForRootViewDidChange', this.onSafeAreaInsetsUpdate); this.onSetMaxHeight(); } componentWillUnmount() { this.dimensionsChangeSubscription.remove(); this.keyboardShowListener.remove(); this.keyboardHideListener.remove(); if (this.androidModalClosedSubscription) { this.androidModalClosedSubscription.remove(); } if (this.props.isVisible) { (0, _reactNativeBridge.showAndroidSoftKeyboard)(); } if (this.safeAreaEventSubscription === null) { return; } this.safeAreaEventSubscription.remove(); this.safeAreaEventSubscription = null; } onSafeAreaInsetsUpdate(result) { const { safeAreaBottomInset, safeAreaTopInset } = this.state; if (this.safeAreaEventSubscription === null) { return; } const { safeAreaInsets } = result; if (safeAreaBottomInset !== safeAreaInsets.bottom || safeAreaTopInset !== safeAreaInsets.top) { this.setState({ safeAreaBottomInset: safeAreaInsets.bottom, safeAreaTopInset: safeAreaInsets.top }); } } onSetMaxHeight() { const { height, width } = _reactNative.Dimensions.get('window'); const { safeAreaBottomInset } = this.state; const statusBarHeight = _reactNative.Platform.OS === 'android' ? _reactNative.StatusBar.currentHeight : 0; // `maxHeight` when modal is opened along with a keyboard. const maxHeightWithOpenKeyboard = 0.95 * (_reactNative.Dimensions.get('window').height - this.keyboardHeight - statusBarHeight - this.headerHeight); // In landscape orientation, set `maxHeight` to ~96% of the height. if (width > height) { this.setState({ maxHeight: Math.min(0.96 * height - this.headerHeight, maxHeightWithOpenKeyboard) }); // In portrait orientation, set `maxHeight` to ~59% of the height. } else { this.setState({ maxHeight: Math.min(height * 0.59 - safeAreaBottomInset - this.headerHeight, maxHeightWithOpenKeyboard) }); } } onDimensionsChange() { this.onSetMaxHeight(); this.setState({ bounces: false }); } onHeaderLayout({ nativeEvent }) { const { height } = nativeEvent.layout; // The layout animation should only be triggered if the header // height has changed after being mounted. if (this.headerHeight !== 0 && Math.round(height) !== Math.round(this.headerHeight)) { this.performRegularLayoutAnimation({ useLastLayoutAnimation: true }); } this.headerHeight = height; this.onSetMaxHeight(); } isCloseToBottom({ layoutMeasurement, contentOffset, contentSize }) { return layoutMeasurement.height + contentOffset.y >= contentSize.height - contentOffset.y; } isCloseToTop({ contentOffset }) { return contentOffset.y < 10; } onScroll({ nativeEvent }) { if (this.isCloseToTop(nativeEvent)) { this.setState({ bounces: false }); } else { this.setState({ bounces: true }); } } onDismiss() { const { onDismiss } = this.props; // Restore Keyboard Visibility (0, _reactNativeBridge.showAndroidSoftKeyboard)(); if (onDismiss) { onDismiss(); } this.onCloseBottomSheet(); } onShouldEnableScroll(value) { this.setState({ scrollEnabled: value }); } onShouldSetBottomSheetMaxHeight(value) { this.setState({ isMaxHeightSet: value }); } isScrolling(value) { this.setState({ isScrolling: value }); } onHandleClosingBottomSheet(action) { this.setState({ handleClosingBottomSheet: action }); } onHandleHardwareButtonPress(action) { this.setState({ handleHardwareButtonPress: action }); } onCloseBottomSheet() { const { onClose } = this.props; const { handleClosingBottomSheet } = this.state; if (handleClosingBottomSheet) { handleClosingBottomSheet(); this.onHandleClosingBottomSheet(null); } if (onClose) { onClose(); } this.onShouldSetBottomSheetMaxHeight(true); } setIsFullScreen(isFullScreen) { if (isFullScreen !== this.state.isFullScreen) { if (isFullScreen) { this.setState({ isFullScreen, isMaxHeightSet: false }); } else { this.setState({ isFullScreen, isMaxHeightSet: true }); } } } onHardwareButtonPress() { const { onClose } = this.props; const { handleHardwareButtonPress } = this.state; if (handleHardwareButtonPress && handleHardwareButtonPress()) { return; } if (onClose) { return onClose(); } } getContentStyle() { const { safeAreaBottomInset } = this.state; return { paddingBottom: (safeAreaBottomInset || 0) + _styles.default.scrollableContent.paddingBottom }; } render() { const { title = '', isVisible, leftButton, rightButton, header, hideHeader, style = {}, contentStyle = {}, getStylesFromColorScheme, children, withHeaderSeparator = false, hasNavigation, onDismiss, ...rest } = this.props; const { maxHeight, bounces, safeAreaBottomInset, safeAreaTopInset, isScrolling, scrollEnabled, isMaxHeightSet, isFullScreen } = this.state; const panResponder = _reactNative.PanResponder.create({ onMoveShouldSetPanResponder: (evt, gestureState) => { // 'swiping-to-close' option is temporarily and partially disabled // on Android ( swipe / drag is still available in the top most area - near drag indicator). if (_reactNative.Platform.OS === 'ios') { // Activates swipe down over child Touchables if the swipe is long enough. // With this we can adjust sensibility on the swipe vs tap gestures. if (gestureState.dy > 3 && !bounces) { gestureState.dy = 0; return true; } } return false; } }); const backgroundStyle = getStylesFromColorScheme(_styles.default.background, _styles.default.backgroundDark); const bottomSheetHeaderTitleStyle = getStylesFromColorScheme(_styles.default.bottomSheetHeaderTitle, _styles.default.bottomSheetHeaderTitleDark); let listStyle = {}; if (isFullScreen) { listStyle = { flexGrow: 1, flexShrink: 1 }; } else if (isMaxHeightSet) { listStyle = { maxHeight }; // Allow setting a "static" height of the bottom sheet // by setting the min height to the max height. if (this.props.setMinHeightToMaxHeight) { listStyle.minHeight = maxHeight; } } const listProps = { disableScrollViewPanResponder: true, bounces, onScroll: this.onScroll, onScrollBeginDrag: this.onScrollBeginDrag, onScrollEndDrag: this.onScrollEndDrag, scrollEventThrottle: 16, contentContainerStyle: [_styles.default.content, hideHeader && _styles.default.emptyHeader, contentStyle, isFullScreen && { flexGrow: 1 }], style: listStyle, safeAreaBottomInset, scrollEnabled, automaticallyAdjustContentInsets: false }; const WrapperView = hasNavigation ? _reactNative.View : _reactNative.ScrollView; const getHeader = () => /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [header || /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: _styles.default.bottomSheetHeader, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: _styles.default.flex, children: leftButton }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: bottomSheetHeaderTitleStyle, maxFontSizeMultiplier: 3, children: title }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: _styles.default.flex, children: rightButton })] }), withHeaderSeparator && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: _styles.default.separator })] }); const showDragIndicator = () => { // If iOS or not fullscreen show the drag indicator. if (_reactNative.Platform.OS === 'ios' || !this.state.isFullScreen) { return true; } // Otherwise check the allowDragIndicator. return this.props.allowDragIndicator; }; return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeModal.default, { isVisible: isVisible, style: _styles.default.bottomModal, animationInTiming: 400, animationOutTiming: 300, backdropTransitionInTiming: 50, backdropTransitionOutTiming: 50, backdropOpacity: 0.2, onBackdropPress: this.onCloseBottomSheet, onBackButtonPress: this.onHardwareButtonPress, onSwipeComplete: this.onCloseBottomSheet, onDismiss: _reactNative.Platform.OS === 'ios' ? this.onDismiss : undefined, onModalHide: _reactNative.Platform.OS === 'android' ? this.onDismiss : undefined, swipeDirection: "down", onMoveShouldSetResponder: scrollEnabled && panResponder.panHandlers.onMoveShouldSetResponder, onMoveShouldSetResponderCapture: scrollEnabled && panResponder.panHandlers.onMoveShouldSetResponderCapture, onAccessibilityEscape: this.onCloseBottomSheet, testID: "bottom-sheet", hardwareAccelerated: true, useNativeDriverForBackdrop: true, ...rest, children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_keyboardAvoidingView.default, { behavior: _reactNative.Platform.OS === 'ios' && 'padding', style: { ...backgroundStyle, borderColor: 'rgba(0, 0, 0, 0.1)', marginTop: _reactNative.Platform.OS === 'ios' && isFullScreen ? safeAreaTopInset : 0, flex: isFullScreen ? 1 : undefined, ...(_reactNative.Platform.OS === 'android' && isFullScreen ? _styles.default.backgroundFullScreen : {}), ...style }, keyboardVerticalOffset: -safeAreaBottomInset, children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: _styles.default.header, onLayout: this.onHeaderLayout, testID: `${rest.testID || 'bottom-sheet'}-header`, children: [showDragIndicator() && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: _styles.default.dragIndicator }), !hideHeader && getHeader()] }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(WrapperView, { ...(hasNavigation ? { style: listProps.style } : listProps), children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_bottomSheetContext.BottomSheetProvider, { value: { shouldEnableBottomSheetScroll: this.onShouldEnableScroll, shouldEnableBottomSheetMaxHeight: this.onShouldSetBottomSheetMaxHeight, isBottomSheetContentScrolling: isScrolling, onHandleClosingBottomSheet: this.onHandleClosingBottomSheet, onHandleHardwareButtonPress: this.onHandleHardwareButtonPress, listProps, setIsFullScreen: this.setIsFullScreen, safeAreaBottomInset, maxHeight, isMaxHeightSet }, children: hasNavigation ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_jsxRuntime.Fragment, { children: children }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableHighlight, { accessible: false, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_jsxRuntime.Fragment, { children: children }) }) }), !hasNavigation && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: { height: safeAreaBottomInset || _styles.default.scrollableContent.paddingBottom } })] })] }) }); } } function getWidth() { return Math.min(_reactNative.Dimensions.get('window').width, _styles.default.background.maxWidth); } const ThemedBottomSheet = (0, _compose.withPreferredColorScheme)(BottomSheet); ThemedBottomSheet.getWidth = getWidth; ThemedBottomSheet.Button = _button.default; ThemedBottomSheet.Cell = _cell.default; ThemedBottomSheet.SubSheet = _subSheet.default; ThemedBottomSheet.NavBar = _navBar.default; ThemedBottomSheet.CyclePickerCell = _cyclePickerCell.default; ThemedBottomSheet.PickerCell = _pickerCell.default; ThemedBottomSheet.SwitchCell = _switchCell.default; ThemedBottomSheet.RangeCell = _rangeCell.default; ThemedBottomSheet.ColorCell = _colorCell.default; ThemedBottomSheet.LinkCell = _linkCell.default; ThemedBottomSheet.LinkSuggestionItemCell = _linkSuggestionItemCell.default; ThemedBottomSheet.RadioCell = _radioCell.default; ThemedBottomSheet.NavigationScreen = _navigationScreen.default; ThemedBottomSheet.NavigationContainer = _navigationContainer.default; var _default = exports.default = ThemedBottomSheet; //# sourceMappingURL=index.native.js.map