@wordpress/components
Version:
UI components for WordPress.
565 lines (555 loc) • 20.7 kB
JavaScript
"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