diff --git a/packages/block-editor/src/components/block-settings/container.native.js b/packages/block-editor/src/components/block-settings/container.native.js
index 4ceb4fab58778c..de2cc7f4edbac6 100644
--- a/packages/block-editor/src/components/block-settings/container.native.js
+++ b/packages/block-editor/src/components/block-settings/container.native.js
@@ -44,6 +44,12 @@ function BottomSheetSettings( {
>
+
+
+
+
diff --git a/packages/block-library/src/button/edit.native.js b/packages/block-library/src/button/edit.native.js
index 877809af599efd..2a11a1948f9973 100644
--- a/packages/block-library/src/button/edit.native.js
+++ b/packages/block-library/src/button/edit.native.js
@@ -21,6 +21,7 @@ import {
ToolbarGroup,
ToolbarButton,
LinkSettingsNavigation,
+ BottomSheetSelectControl,
} from '@wordpress/components';
import { Component } from '@wordpress/element';
import { withSelect, withDispatch } from '@wordpress/data';
@@ -39,6 +40,48 @@ const MIN_BORDER_RADIUS_VALUE = 0;
const MAX_BORDER_RADIUS_VALUE = 50;
const INITIAL_MAX_WIDTH = 108;
const MIN_WIDTH = 40;
+// Map of the percentage width to pixel subtraction that make the buttons fit nicely into columns.
+const MIN_WIDTH_MARGINS = {
+ 100: 0,
+ 75: styles.button75?.marginLeft,
+ 50: styles.button50?.marginLeft,
+ 25: styles.button25?.marginLeft,
+};
+
+function WidthPanel( { selectedWidth, setAttributes } ) {
+ function handleChange( newWidth ) {
+ // Check if we are toggling the width off
+ let width = selectedWidth === newWidth ? undefined : newWidth;
+ if ( newWidth === 'auto' ) {
+ width = undefined;
+ }
+ // Update attributes
+ setAttributes( { width } );
+ }
+
+ const options = [
+ { value: 'auto', label: __( 'Auto' ) },
+ { value: 25, label: '25%' },
+ { value: 50, label: '50%' },
+ { value: 75, label: '75%' },
+ { value: 100, label: '100%' },
+ ];
+
+ if ( ! selectedWidth ) {
+ selectedWidth = 'auto';
+ }
+
+ return (
+
+
+
+ );
+}
class ButtonEdit extends Component {
constructor( props ) {
@@ -52,6 +95,7 @@ class ButtonEdit extends Component {
this.onShowLinkSettings = this.onShowLinkSettings.bind( this );
this.onHideLinkSettings = this.onHideLinkSettings.bind( this );
this.onToggleButtonFocus = this.onToggleButtonFocus.bind( this );
+ this.onPlaceholderTextWidth = this.onPlaceholderTextWidth.bind( this );
this.setRef = this.setRef.bind( this );
this.onRemove = this.onRemove.bind( this );
this.getPlaceholderWidth = this.getPlaceholderWidth.bind( this );
@@ -108,7 +152,7 @@ class ButtonEdit extends Component {
}
if ( prevProps.parentWidth !== parentWidth ) {
- this.onSetMaxWidth();
+ this.onSetMaxWidth( null, true );
}
// Blur `RichText` on Android when link settings sheet or button settings sheet is opened,
@@ -211,20 +255,19 @@ class ButtonEdit extends Component {
this.onSetMaxWidth( width );
}
- onSetMaxWidth( width ) {
+ onSetMaxWidth( width, isParentWidthDidChange = false ) {
const { maxWidth } = this.state;
const { parentWidth } = this.props;
const { marginRight: spacing } = styles.defaultButton;
- const isParentWidthChanged = maxWidth !== parentWidth;
+ const isParentWidthChanged = isParentWidthDidChange
+ ? isParentWidthDidChange
+ : maxWidth !== parentWidth;
const isWidthChanged = maxWidth !== width;
if ( parentWidth && ! width && isParentWidthChanged ) {
this.setState( {
- maxWidth: Math.min(
- parentWidth,
- this.props.maxWidth - 2 * spacing
- ),
+ maxWidth: parentWidth - spacing,
} );
} else if ( ! parentWidth && width && isWidthChanged ) {
this.setState( { maxWidth: width - spacing } );
@@ -277,28 +320,28 @@ class ButtonEdit extends Component {
// Render `Text` with `placeholderText` styled as a placeholder
// to calculate its width which then is set as a `minWidth`
getPlaceholderWidth( placeholderText ) {
- const { maxWidth, placeholderTextWidth } = this.state;
return (
{
- const textWidth =
- nativeEvent.lines[ 0 ] && nativeEvent.lines[ 0 ].width;
- if ( textWidth && textWidth !== placeholderTextWidth ) {
- this.setState( {
- placeholderTextWidth: Math.min(
- textWidth,
- maxWidth
- ),
- } );
- }
- } }
+ onTextLayout={ this.onPlaceholderTextWidth }
>
{ placeholderText }
);
}
+ onPlaceholderTextWidth( { nativeEvent } ) {
+ const { maxWidth, placeholderTextWidth } = this.state;
+ const textWidth =
+ nativeEvent.lines[ 0 ] && nativeEvent.lines[ 0 ].width;
+
+ if ( textWidth && textWidth !== placeholderTextWidth ) {
+ this.setState( {
+ placeholderTextWidth: Math.min( textWidth, maxWidth ),
+ } );
+ }
+ }
+
render() {
const {
attributes,
@@ -307,6 +350,7 @@ class ButtonEdit extends Component {
onReplace,
mergeBlocks,
parentWidth,
+ setAttributes,
} = this.props;
const {
placeholder,
@@ -314,6 +358,7 @@ class ButtonEdit extends Component {
borderRadius,
url,
align = 'center',
+ width,
} = attributes;
const { maxWidth, isButtonFocused, placeholderTextWidth } = this.state;
const { paddingTop: spacing, borderWidth } = styles.defaultButton;
@@ -333,10 +378,16 @@ class ButtonEdit extends Component {
// To achieve proper expanding and shrinking `RichText` on iOS, there is a need to set a `minWidth`
// value at least on 1 when `RichText` is focused or when is not focused, but `RichText` value is
// different than empty string.
- const minWidth =
+ let minWidth =
isButtonFocused || ( ! isButtonFocused && text && text !== '' )
? MIN_WIDTH
: placeholderTextWidth;
+ if ( width ) {
+ // Set the width of the button.
+ minWidth = Math.floor(
+ maxWidth * ( width / 100 ) - MIN_WIDTH_MARGINS[ width ]
+ );
+ }
// To achieve proper expanding and shrinking `RichText` on Android, there is a need to set
// a `placeholder` as an empty string when `RichText` is focused,
// because `AztecView` is calculating a `minWidth` based on placeholder text.
@@ -383,8 +434,8 @@ class ButtonEdit extends Component {
}
identifier="text"
tagName="p"
- minWidth={ minWidth }
- maxWidth={ maxWidth }
+ minWidth={ minWidth } // The minimum Button size.
+ maxWidth={ maxWidth } // The width of the screen.
id={ clientId }
isSelected={ isButtonFocused }
withoutInteractiveFormatting
@@ -426,6 +477,10 @@ class ButtonEdit extends Component {
onChange={ this.onChangeBorderRadius }
/>
+
{ this.getLinkSettings( true ) }
@@ -443,18 +498,15 @@ export default compose( [
withColors( 'backgroundColor', { textColor: 'color' } ),
withSelect( ( select, { clientId, isSelected } ) => {
const { isEditorSidebarOpened } = select( 'core/edit-post' );
- const { getBlockCount, getBlockRootClientId, getSettings } = select(
+ const { getBlockCount, getBlockRootClientId } = select(
blockEditorStore
);
- const { maxWidth } = getSettings();
-
const parentId = getBlockRootClientId( clientId );
const numOfButtons = getBlockCount( parentId );
return {
editorSidebarOpened: isSelected && isEditorSidebarOpened(),
numOfButtons,
- maxWidth,
};
} ),
withDispatch( ( dispatch ) => {
diff --git a/packages/block-library/src/button/editor.native.scss b/packages/block-library/src/button/editor.native.scss
index 31ce81004e09aa..64d3a129e194fe 100644
--- a/packages/block-library/src/button/editor.native.scss
+++ b/packages/block-library/src/button/editor.native.scss
@@ -56,3 +56,15 @@ $block-spacing: 4px;
padding-left: 0;
padding-right: 0;
}
+
+.button75 {
+ margin: $solid-border-space - $block-selected-border-width;
+}
+
+.button50 {
+ margin: $solid-border-space * 2 - $block-selected-border-width * 2;
+}
+
+.button25 {
+ margin: $block-edge-to-content * 2 + $block-selected-border-width;
+}
diff --git a/packages/block-library/src/buttons/edit.native.js b/packages/block-library/src/buttons/edit.native.js
index 7589e3b6627c5d..484ea35ae150da 100644
--- a/packages/block-library/src/buttons/edit.native.js
+++ b/packages/block-library/src/buttons/edit.native.js
@@ -73,15 +73,12 @@ export default function ButtonsEdit( {
);
useEffect( () => {
- const margins = 2 * styles.parent.marginRight;
const { width } = sizes || {};
const { isFullWidth } = alignmentHelpers;
if ( width ) {
- const base = width - margins;
const isFullWidthBlock = isFullWidth( align );
-
- setMaxWidth( isFullWidthBlock ? base - 2 * spacing : base );
+ setMaxWidth( isFullWidthBlock ? blockWidth : width );
}
}, [ sizes, align ] );
@@ -149,7 +146,7 @@ export default function ButtonsEdit( {
horizontalAlignment={ contentJustification }
onDeleteBlock={ shouldDelete ? remove : undefined }
onAddBlock={ onAddNextButton }
- parentWidth={ maxWidth }
+ parentWidth={ maxWidth } // This value controls the width of that the buttons are able to expand to.
marginHorizontal={ spacing }
marginVertical={ spacing }
__experimentalLayout={ layoutProp }
diff --git a/packages/components/src/index.native.js b/packages/components/src/index.native.js
index 45c47fbb6764c5..6f9b4c2915e8f1 100644
--- a/packages/components/src/index.native.js
+++ b/packages/components/src/index.native.js
@@ -66,6 +66,7 @@ export {
BottomSheetProvider,
BottomSheetContext,
} from './mobile/bottom-sheet/bottom-sheet-context';
+export { default as BottomSheetSelectControl } from './mobile/bottom-sheet-select-control';
export { default as HTMLTextInput } from './mobile/html-text-input';
export { default as KeyboardAvoidingView } from './mobile/keyboard-avoiding-view';
export { default as KeyboardAwareFlatList } from './mobile/keyboard-aware-flat-list';
diff --git a/packages/components/src/mobile/bottom-sheet-select-control/README.md b/packages/components/src/mobile/bottom-sheet-select-control/README.md
new file mode 100644
index 00000000000000..4fc8a0be79b097
--- /dev/null
+++ b/packages/components/src/mobile/bottom-sheet-select-control/README.md
@@ -0,0 +1,90 @@
+# BottomSheetSelectControl
+
+`BottomSheetSelectControl` allows users to select an item from a single-option menu just like [`SelectControl`](/packages/components/src/select-control/readme.md),
+However, instead of opening up the selection in a modal, the selection opens up in a BottomSheet.
+
+### Usage
+
+```jsx
+/**
+ * WordPress dependencies
+ */
+import { BottomSheetSelectControl } from '@wordpress/components';
+import { useState } from '@wordpress/compose';
+
+const options = [
+ {
+ key: 'small',
+ name: 'Small',
+ style: { fontSize: '50%' },
+ },
+ {
+ key: 'normal',
+ name: 'Normal',
+ style: { fontSize: '100%' },
+ },
+ {
+ key: 'large',
+ name: 'Large',
+ style: { fontSize: '200%' },
+ },
+ {
+ key: 'huge',
+ name: 'Huge',
+ style: { fontSize: '300%' },
+ },
+];
+
+function MyCustomSelectControl() {
+ const [ fontSize, setFontSize ] = useState();
+ return (
+ setFontSize( selectedItem ) }
+ />
+ );
+}
+
+function MyControlledCustomSelectControl() {
+ const [ fontSize, setFontSize ] = useState( options[ 0 ] );
+ return (
+ setFontSize( selectedItem ) }
+ value={ options.find( ( option ) => option.key === fontSize.key ) }
+ />
+ );
+}
+```
+
+### Props
+
+#### label
+
+The label for the control.
+
+- Type: `String`
+- Required: Yes
+
+#### options
+
+The options that can be chosen from.
+
+- Type: `Array<{ key: String, name: String, ...rest }>`
+- Required: Yes
+
+#### onChange
+
+Function called with the control's internal state changes. The `selectedItem` property contains the next selected item.
+
+- Type: `Function`
+- Required: No
+
+#### value
+
+Can be used to externally control the value of the control, like in the `MyControlledCustomSelectControl` example above.
+
+- Type: `Object`
+- Required: No
diff --git a/packages/components/src/mobile/bottom-sheet-select-control/index.native.js b/packages/components/src/mobile/bottom-sheet-select-control/index.native.js
new file mode 100644
index 00000000000000..3df7f185c77103
--- /dev/null
+++ b/packages/components/src/mobile/bottom-sheet-select-control/index.native.js
@@ -0,0 +1,107 @@
+/**
+ * External dependencies
+ */
+import { View } from 'react-native';
+
+/**
+ * WordPress dependencies
+ */
+import { useNavigation } from '@react-navigation/native';
+import { useState } from '@wordpress/element';
+import { Icon, chevronRight, check } from '@wordpress/icons';
+import { __, sprintf } from '@wordpress/i18n';
+import { BottomSheet } from '@wordpress/components';
+/**
+ * Internal dependencies
+ */
+import styles from './style.scss';
+
+const BottomSheetSelectControl = ( {
+ label,
+ options: items,
+ onChange,
+ value: selectedValue,
+} ) => {
+ const [ showSubSheet, setShowSubSheet ] = useState( false );
+ const navigation = useNavigation();
+
+ const onChangeValue = ( value ) => {
+ return () => {
+ goBack();
+ onChange( value );
+ };
+ };
+
+ const selectedOption = items.find(
+ ( option ) => option.value === selectedValue
+ );
+
+ const goBack = () => {
+ setShowSubSheet( false );
+ navigation.goBack();
+ };
+
+ const openSubSheet = () => {
+ navigation.navigate( BottomSheet.SubSheet.screenName );
+ setShowSubSheet( true );
+ };
+
+ return (
+
+
+
+ }
+ showSheet={ showSubSheet }
+ >
+ <>
+
+
+ { items.map( ( item, index ) => (
+
+ { item.value === selectedValue && (
+
+ ) }
+
+ ) ) }
+
+ >
+
+ );
+};
+
+export default BottomSheetSelectControl;
diff --git a/packages/components/src/mobile/bottom-sheet-select-control/style.native.scss b/packages/components/src/mobile/bottom-sheet-select-control/style.native.scss
new file mode 100644
index 00000000000000..c6f9b0d71e16f2
--- /dev/null
+++ b/packages/components/src/mobile/bottom-sheet-select-control/style.native.scss
@@ -0,0 +1,3 @@
+.selectControl {
+ padding: 0 $block-edge-to-content;
+}
diff --git a/packages/components/src/mobile/bottom-sheet/index.native.js b/packages/components/src/mobile/bottom-sheet/index.native.js
index 63b7ea41f1436a..aa58172dfa84d7 100644
--- a/packages/components/src/mobile/bottom-sheet/index.native.js
+++ b/packages/components/src/mobile/bottom-sheet/index.native.js
@@ -39,6 +39,8 @@ import RadioCell from './radio-cell';
import NavigationScreen from './bottom-sheet-navigation/navigation-screen';
import NavigationContainer from './bottom-sheet-navigation/navigation-container';
import KeyboardAvoidingView from './keyboard-avoiding-view';
+import BottomSheetSubSheet from './sub-sheet';
+import NavigationHeader from './navigation-header';
import { BottomSheetProvider } from './bottom-sheet-context';
class BottomSheet extends Component {
@@ -486,6 +488,8 @@ const ThemedBottomSheet = withPreferredColorScheme( BottomSheet );
ThemedBottomSheet.getWidth = getWidth;
ThemedBottomSheet.Button = Button;
ThemedBottomSheet.Cell = Cell;
+ThemedBottomSheet.SubSheet = BottomSheetSubSheet;
+ThemedBottomSheet.NavigationHeader = NavigationHeader;
ThemedBottomSheet.CyclePickerCell = CyclePickerCell;
ThemedBottomSheet.PickerCell = PickerCell;
ThemedBottomSheet.SwitchCell = SwitchCell;
diff --git a/packages/components/src/mobile/bottom-sheet/sub-sheet/README.md b/packages/components/src/mobile/bottom-sheet/sub-sheet/README.md
new file mode 100644
index 00000000000000..15403280aca93d
--- /dev/null
+++ b/packages/components/src/mobile/bottom-sheet/sub-sheet/README.md
@@ -0,0 +1,91 @@
+# BottomSheetSubSheet
+
+BottomSheetSubSheet allows for adding controls inside the React Native bottom sheet settings.
+
+### Usage
+
+```jsx
+/**
+ * External dependencies
+ */
+import { SafeAreaView, View } from 'react-native';
+import { useNavigation } from '@react-navigation/native';
+
+/**
+ * WordPress dependencies
+ */
+import { useState } from '@wordpress/element';
+import { Icon, chevronRight } from '@wordpress/icons';
+import { BottomSheet } from '@wordpress/components';
+
+const ExampleControl = () => {
+ const [ showSubSheet, setShowSubSheet ] = useState( false );
+ const navigation = useNavigation();
+
+ const goBack = () => {
+ setShowSubSheet( false );
+ navigation.goBack();
+ };
+
+ const openSubSheet = () => {
+ navigation.navigate( BottomSheet.SubSheet.screenName );
+ setShowSubSheet( true );
+ };
+
+ return (
+
+
+
+ }
+ showSheet={ showSubSheet }
+ >
+ <>
+
+
+ { 'World' }
+
+ >
+
+ );
+};
+
+export default ExampleControl;
+```
+
+### Props
+
+#### showSheet
+
+Controls the Sub Sheet content visibility.
+
+- Type: `Boolean`
+- Required: Yes
+
+#### navigationButton
+
+UI rendered to allow navigating to the Sub Sheet when tapped.
+
+- Type: `ReactComponent`
+- Required: Yes
+
+
+#### isFullScreen
+
+Toggles the Sub Sheet height filling the entire device height.
+
+- Type: `Boolean`
+- Required: Yes
+
+See `BottomSheetSelectControl` as an example.
diff --git a/packages/components/src/mobile/bottom-sheet/sub-sheet/index.native.js b/packages/components/src/mobile/bottom-sheet/sub-sheet/index.native.js
new file mode 100644
index 00000000000000..f1e7cdfd1a1fc9
--- /dev/null
+++ b/packages/components/src/mobile/bottom-sheet/sub-sheet/index.native.js
@@ -0,0 +1,42 @@
+/**
+ * External dependencies
+ */
+import { SafeAreaView } from 'react-native';
+
+/**
+ * WordPress dependencies
+ */
+import { Children } from '@wordpress/element';
+import { createSlotFill, BottomSheetConsumer } from '@wordpress/components';
+
+const { Fill, Slot } = createSlotFill( 'BottomSheetSubSheet' );
+
+const BottomSheetSubSheet = ( {
+ children,
+ navigationButton,
+ showSheet,
+ isFullScreen,
+} ) => {
+ return (
+ <>
+ { showSheet && (
+
+
+
+ { ( { setIsFullScreen } ) => {
+ setIsFullScreen( isFullScreen );
+ return children;
+ } }
+
+
+
+ ) }
+ { Children.count( children ) > 0 && navigationButton }
+ >
+ );
+};
+
+BottomSheetSubSheet.Slot = Slot;
+BottomSheetSubSheet.screenName = 'BottomSheetSubSheet';
+
+export default BottomSheetSubSheet;
diff --git a/packages/components/src/mobile/utils/alignments.native.js b/packages/components/src/mobile/utils/alignments.native.js
index 219bdefb8a721c..52d3ec9b43aa93 100644
--- a/packages/components/src/mobile/utils/alignments.native.js
+++ b/packages/components/src/mobile/utils/alignments.native.js
@@ -5,7 +5,13 @@ export const WIDE_ALIGNMENTS = {
},
// `innerContainers`: Group of blocks based on `InnerBlocks` component,
// used to nest other blocks inside
- innerContainers: [ 'core/group', 'core/columns', 'core/column' ],
+ innerContainers: [
+ 'core/group',
+ 'core/columns',
+ 'core/column',
+ 'core/buttons',
+ 'core/button',
+ ],
excludeBlocks: [ 'core/heading' ],
};