diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index e8beb4717d1e0..98d725769c4eb 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -35,6 +35,7 @@ - `Tabs`: do not render hidden content ([#57046](https://github.com/WordPress/gutenberg/pull/57046)). - `Tabs`: improve hover and text alignment styles ([#57275](https://github.com/WordPress/gutenberg/pull/57275)). - `Tabs`: make sure `Tab`s are associated to the right `TabPanel`s, regardless of the order they're rendered in ([#57033](https://github.com/WordPress/gutenberg/pull/57033)). +- `BoxControl`: Update design ([#56665](https://github.com/WordPress/gutenberg/pull/56665)). ## 25.14.0 (2023-12-13) diff --git a/packages/components/src/box-control/all-input-control.tsx b/packages/components/src/box-control/all-input-control.tsx index b66e10fdb4ce3..9c18694bbd0b6 100644 --- a/packages/components/src/box-control/all-input-control.tsx +++ b/packages/components/src/box-control/all-input-control.tsx @@ -1,15 +1,25 @@ +/** + * WordPress dependencies + */ +import { useInstanceId } from '@wordpress/compose'; /** * Internal dependencies */ import type { UnitControlProps } from '../unit-control/types'; +import { + FlexedRangeControl, + StyledUnitControl, +} from './styles/box-control-styles'; +import { HStack } from '../h-stack'; import type { BoxControlInputControlProps } from './types'; -import UnitControl from './unit-control'; +import { parseQuantityAndUnitFromRawValue } from '../unit-control'; import { LABELS, applyValueToSides, getAllValue, isValuesMixed, isValuesDefined, + CUSTOM_VALUE_SETTINGS, } from './utils'; const noop = () => {}; @@ -17,26 +27,29 @@ const noop = () => {}; export default function AllInputControl( { onChange = noop, onFocus = noop, - onHoverOn = noop, - onHoverOff = noop, values, sides, selectedUnits, setSelectedUnits, ...props }: BoxControlInputControlProps ) { + const inputId = useInstanceId( AllInputControl, 'box-control-input-all' ); + const allValue = getAllValue( values, selectedUnits, sides ); const hasValues = isValuesDefined( values ); const isMixed = hasValues && isValuesMixed( values, selectedUnits, sides ); const allPlaceholder = isMixed ? LABELS.mixed : undefined; + const [ parsedQuantity, parsedUnit ] = + parseQuantityAndUnitFromRawValue( allValue ); + const handleOnFocus: React.FocusEventHandler< HTMLInputElement > = ( event ) => { onFocus( event, { side: 'all' } ); }; - const handleOnChange: UnitControlProps[ 'onChange' ] = ( next ) => { + const onValueChange = ( next?: string ) => { const isNumeric = next !== undefined && ! isNaN( parseFloat( next ) ); const nextValue = isNumeric ? next : undefined; const nextValues = applyValueToSides( values, nextValue, sides ); @@ -44,6 +57,12 @@ export default function AllInputControl( { onChange( nextValues ); }; + const sliderOnChange = ( next?: number ) => { + onValueChange( + next !== undefined ? [ next, parsedUnit ].join( '' ) : undefined + ); + }; + // Set selected unit so it can be used as fallback by unlinked controls // when individual sides do not have a value containing a unit. const handleOnUnitChange: UnitControlProps[ 'onUnitChange' ] = ( unit ) => { @@ -51,36 +70,37 @@ export default function AllInputControl( { setSelectedUnits( newUnits ); }; - const handleOnHoverOn = () => { - onHoverOn( { - top: true, - bottom: true, - left: true, - right: true, - } ); - }; - - const handleOnHoverOff = () => { - onHoverOff( { - top: false, - bottom: false, - left: false, - right: false, - } ); - }; - return ( - + + + + + ); } diff --git a/packages/components/src/box-control/axial-input-controls.tsx b/packages/components/src/box-control/axial-input-controls.tsx index bc8a4bd420bbd..173605f68a872 100644 --- a/packages/components/src/box-control/axial-input-controls.tsx +++ b/packages/components/src/box-control/axial-input-controls.tsx @@ -1,10 +1,19 @@ +/** + * WordPress dependencies + */ +import { useInstanceId } from '@wordpress/compose'; /** * Internal dependencies */ import { parseQuantityAndUnitFromRawValue } from '../unit-control/utils'; -import UnitControl from './unit-control'; -import { LABELS } from './utils'; -import { Layout } from './styles/box-control-styles'; +import Tooltip from '../tooltip'; +import { CUSTOM_VALUE_SETTINGS, LABELS } from './utils'; +import { + FlexedBoxControlIcon, + FlexedRangeControl, + InputWrapper, + StyledUnitControl, +} from './styles/box-control-styles'; import type { BoxControlInputControlProps } from './types'; const groupedSides = [ 'vertical', 'horizontal' ] as const; @@ -13,14 +22,17 @@ type GroupedSide = ( typeof groupedSides )[ number ]; export default function AxialInputControls( { onChange, onFocus, - onHoverOn, - onHoverOff, values, selectedUnits, setSelectedUnits, sides, ...props }: BoxControlInputControlProps ) { + const generatedId = useInstanceId( + AxialInputControls, + `box-control-input` + ); + const createHandleOnFocus = ( side: GroupedSide ) => ( event: React.FocusEvent< HTMLInputElement > ) => { @@ -30,43 +42,7 @@ export default function AxialInputControls( { onFocus( event, { side } ); }; - const createHandleOnHoverOn = ( side: GroupedSide ) => () => { - if ( ! onHoverOn ) { - return; - } - if ( side === 'vertical' ) { - onHoverOn( { - top: true, - bottom: true, - } ); - } - if ( side === 'horizontal' ) { - onHoverOn( { - left: true, - right: true, - } ); - } - }; - - const createHandleOnHoverOff = ( side: GroupedSide ) => () => { - if ( ! onHoverOff ) { - return; - } - if ( side === 'vertical' ) { - onHoverOff( { - top: false, - bottom: false, - } ); - } - if ( side === 'horizontal' ) { - onHoverOff( { - left: false, - right: false, - } ); - } - }; - - const createHandleOnChange = ( side: GroupedSide ) => ( next?: string ) => { + const handleOnValueChange = ( side: GroupedSide, next?: string ) => { if ( ! onChange ) { return; } @@ -109,16 +85,8 @@ export default function AxialInputControls( { ? groupedSides.filter( ( side ) => sides.includes( side ) ) : groupedSides; - const first = filteredSides[ 0 ]; - const last = filteredSides[ filteredSides.length - 1 ]; - const only = first === last && first; - return ( - + <> { filteredSides.map( ( side ) => { const [ parsedQuantity, parsedUnit ] = parseQuantityAndUnitFromRawValue( @@ -128,26 +96,65 @@ export default function AxialInputControls( { side === 'vertical' ? selectedUnits.top : selectedUnits.left; + + const inputId = [ generatedId, side ].join( '-' ); + return ( - + + + + + handleOnValueChange( side, newValue ) + } + onUnitChange={ createHandleOnUnitChange( + side + ) } + onFocus={ createHandleOnFocus( side ) } + label={ LABELS[ side ] } + hideLabelFromVision + key={ side } + /> + + + handleOnValueChange( + side, + newValue !== undefined + ? [ + newValue, + selectedUnit ?? parsedUnit, + ].join( '' ) + : undefined + ) + } + min={ 0 } + max={ + CUSTOM_VALUE_SETTINGS[ selectedUnit ?? 'px' ] + ?.max ?? 10 + } + step={ + CUSTOM_VALUE_SETTINGS[ selectedUnit ?? 'px' ] + ?.step ?? 0.1 + } + value={ parsedQuantity ?? 0 } + withInputField={ false } + /> + ); } ) } - + ); } diff --git a/packages/components/src/box-control/index.tsx b/packages/components/src/box-control/index.tsx index c7fcf066c545c..dcc890e8e3c51 100644 --- a/packages/components/src/box-control/index.tsx +++ b/packages/components/src/box-control/index.tsx @@ -9,17 +9,16 @@ import { __ } from '@wordpress/i18n'; * Internal dependencies */ import { BaseControl } from '../base-control'; -import Button from '../button'; -import { FlexItem, FlexBlock } from '../flex'; import AllInputControl from './all-input-control'; import InputControls from './input-controls'; import AxialInputControls from './axial-input-controls'; -import BoxControlIcon from './icon'; import LinkedButton from './linked-button'; +import { Grid } from '../grid'; import { - Root, - Header, - HeaderControlWrapper, + FlexedBoxControlIcon, + InputWrapper, + ResetButton, + LinkedButtonWrapper, } from './styles/box-control-styles'; import { parseQuantityAndUnitFromRawValue } from '../unit-control/utils'; import { @@ -155,57 +154,49 @@ function BoxControl( { }; return ( - -
- - - { label } - - - { allowReset && ( - - - - ) } -
- - - - - { isLinked && ( - - - - ) } - { ! isLinked && splitOnAxis && ( - - - - ) } - { ! hasOneSide && ( - - - - ) } - + + + { label } + + { isLinked && ( + + + + + ) } + { ! hasOneSide && ( + + + + ) } + + { ! isLinked && splitOnAxis && ( + + ) } { ! isLinked && ! splitOnAxis && ( ) } -
+ { allowReset && ( + + { __( 'Reset' ) } + + ) } + ); } diff --git a/packages/components/src/box-control/input-controls.tsx b/packages/components/src/box-control/input-controls.tsx index f72179f0d18c1..c8aaeae222749 100644 --- a/packages/components/src/box-control/input-controls.tsx +++ b/packages/components/src/box-control/input-controls.tsx @@ -1,82 +1,81 @@ +/** + * WordPress dependencies + */ +import { useInstanceId } from '@wordpress/compose'; /** * Internal dependencies */ -import UnitControl from './unit-control'; +import Tooltip from '../tooltip'; import { parseQuantityAndUnitFromRawValue } from '../unit-control/utils'; -import { ALL_SIDES, LABELS } from './utils'; -import { LayoutContainer, Layout } from './styles/box-control-styles'; +import { ALL_SIDES, CUSTOM_VALUE_SETTINGS, LABELS } from './utils'; +import { + FlexedBoxControlIcon, + FlexedRangeControl, + InputWrapper, + StyledUnitControl, +} from './styles/box-control-styles'; import type { BoxControlInputControlProps, BoxControlValue } from './types'; -import type { UnitControlProps } from '../unit-control/types'; const noop = () => {}; export default function BoxInputControls( { onChange = noop, onFocus = noop, - onHoverOn = noop, - onHoverOff = noop, values, selectedUnits, setSelectedUnits, sides, ...props }: BoxControlInputControlProps ) { + const generatedId = useInstanceId( BoxInputControls, 'box-control-input' ); + const createHandleOnFocus = ( side: keyof BoxControlValue ) => ( event: React.FocusEvent< HTMLInputElement > ) => { onFocus( event, { side } ); }; - const createHandleOnHoverOn = ( side: keyof BoxControlValue ) => () => { - onHoverOn( { [ side ]: true } ); - }; - - const createHandleOnHoverOff = ( side: keyof BoxControlValue ) => () => { - onHoverOff( { [ side ]: false } ); - }; - const handleOnChange = ( nextValues: BoxControlValue ) => { onChange( nextValues ); }; - const createHandleOnChange: ( - side: keyof BoxControlValue - ) => UnitControlProps[ 'onChange' ] = - ( side ) => - ( next, { event } ) => { - const nextValues = { ...values }; - const isNumeric = - next !== undefined && ! isNaN( parseFloat( next ) ); - const nextValue = isNumeric ? next : undefined; + const handleOnValueChange = ( + side: keyof BoxControlValue, + next?: string, + extra?: { event: React.SyntheticEvent< Element, Event > } + ) => { + const nextValues = { ...values }; + const isNumeric = next !== undefined && ! isNaN( parseFloat( next ) ); + const nextValue = isNumeric ? next : undefined; - nextValues[ side ] = nextValue; + nextValues[ side ] = nextValue; - /** - * Supports changing pair sides. For example, holding the ALT key - * when changing the TOP will also update BOTTOM. - */ - // @ts-expect-error - TODO: event.altKey is only present when the change event was - // triggered by a keyboard event. Should this feature be implemented differently so - // it also works with drag events? - if ( event.altKey ) { - switch ( side ) { - case 'top': - nextValues.bottom = nextValue; - break; - case 'bottom': - nextValues.top = nextValue; - break; - case 'left': - nextValues.right = nextValue; - break; - case 'right': - nextValues.left = nextValue; - break; - } + /** + * Supports changing pair sides. For example, holding the ALT key + * when changing the TOP will also update BOTTOM. + */ + // @ts-expect-error - TODO: event.altKey is only present when the change event was + // triggered by a keyboard event. Should this feature be implemented differently so + // it also works with drag events? + if ( extra?.event.altKey ) { + switch ( side ) { + case 'top': + nextValues.bottom = nextValue; + break; + case 'bottom': + nextValues.top = nextValue; + break; + case 'left': + nextValues.right = nextValue; + break; + case 'right': + nextValues.left = nextValue; + break; } + } - handleOnChange( nextValues ); - }; + handleOnChange( nextValues ); + }; const createHandleOnUnitChange = ( side: keyof BoxControlValue ) => ( next?: string ) => { @@ -90,45 +89,74 @@ export default function BoxInputControls( { ? ALL_SIDES.filter( ( side ) => sides.includes( side ) ) : ALL_SIDES; - const first = filteredSides[ 0 ]; - const last = filteredSides[ filteredSides.length - 1 ]; - const only = first === last && first; - return ( - - - { filteredSides.map( ( side ) => { - const [ parsedQuantity, parsedUnit ] = - parseQuantityAndUnitFromRawValue( values[ side ] ); + <> + { filteredSides.map( ( side ) => { + const [ parsedQuantity, parsedUnit ] = + parseQuantityAndUnitFromRawValue( values[ side ] ); + + const computedUnit = values[ side ] + ? parsedUnit + : selectedUnits[ side ]; + + const inputId = [ generatedId, side ].join( '-' ); - const computedUnit = values[ side ] - ? parsedUnit - : selectedUnits[ side ]; + return ( + + + + + handleOnValueChange( + side, + nextValue, + extra + ) + } + onUnitChange={ createHandleOnUnitChange( + side + ) } + onFocus={ createHandleOnFocus( side ) } + label={ LABELS[ side ] } + hideLabelFromVision + /> + - return ( - { + handleOnValueChange( + side, + newValue !== undefined + ? [ newValue, computedUnit ].join( '' ) + : undefined + ); + } } + min={ 0 } + max={ + CUSTOM_VALUE_SETTINGS[ computedUnit ?? 'px' ] + ?.max ?? 10 + } + step={ + CUSTOM_VALUE_SETTINGS[ computedUnit ?? 'px' ] + ?.step ?? 0.1 + } + value={ parsedQuantity ?? 0 } + withInputField={ false } /> - ); - } ) } - - + + ); + } ) } + ); } diff --git a/packages/components/src/box-control/styles/box-control-styles.ts b/packages/components/src/box-control/styles/box-control-styles.ts index d961d4322ba5a..ce2c8aa227e58 100644 --- a/packages/components/src/box-control/styles/box-control-styles.ts +++ b/packages/components/src/box-control/styles/box-control-styles.ts @@ -1,80 +1,40 @@ /** * External dependencies */ -import { css } from '@emotion/react'; import styled from '@emotion/styled'; /** * Internal dependencies */ -import { Flex } from '../../flex'; -import BaseUnitControl from '../../unit-control'; -import { rtl } from '../../utils'; -import type { BoxUnitControlProps } from '../types'; - -export const Root = styled.div` - box-sizing: border-box; - max-width: 235px; - padding-bottom: 12px; - width: 100%; +import BoxControlIcon from '../icon'; +import Button from '../../button'; +import { HStack } from '../../h-stack'; +import RangeControl from '../../range-control'; +import UnitControl from '../../unit-control'; +import { space } from '../../utils/space'; + +export const StyledUnitControl = styled( UnitControl )` + max-width: 90px; `; -export const Header = styled( Flex )` - margin-bottom: 8px; +export const InputWrapper = styled( HStack )` + grid-column: 1 / span 3; `; -export const HeaderControlWrapper = styled( Flex )` - min-height: 30px; - gap: 0; +export const ResetButton = styled( Button )` + grid-area: 1 / 2; + justify-self: end; `; -export const UnitControlWrapper = styled.div` - box-sizing: border-box; - max-width: 80px; +export const LinkedButtonWrapper = styled.div` + grid-area: 1 / 3; + justify-self: end; `; -export const LayoutContainer = styled( Flex )` - justify-content: center; - padding-top: 8px; +export const FlexedBoxControlIcon = styled( BoxControlIcon )` + flex: 0 0 auto; `; -export const Layout = styled( Flex )` - position: relative; - height: 100%; +export const FlexedRangeControl = styled( RangeControl )` width: 100%; - justify-content: flex-start; -`; - -const unitControlBorderRadiusStyles = ( { - isFirst, - isLast, - isOnly, -}: Pick< BoxUnitControlProps, 'isFirst' | 'isLast' | 'isOnly' > ) => { - if ( isFirst ) { - return rtl( { borderTopRightRadius: 0, borderBottomRightRadius: 0 } )(); - } - if ( isLast ) { - return rtl( { borderTopLeftRadius: 0, borderBottomLeftRadius: 0 } )(); - } - if ( isOnly ) { - return css( { borderRadius: 2 } ); - } - - return css( { - borderRadius: 0, - } ); -}; - -const unitControlMarginStyles = ( { - isFirst, - isOnly, -}: Pick< BoxUnitControlProps, 'isFirst' | 'isOnly' > ) => { - const marginLeft = isFirst || isOnly ? 0 : -1; - - return rtl( { marginLeft } )(); -}; - -export const UnitControl = styled( BaseUnitControl )` - max-width: 60px; - ${ unitControlBorderRadiusStyles }; - ${ unitControlMarginStyles }; + margin-inline-end: ${ space( 2 ) }; `; diff --git a/packages/components/src/box-control/styles/box-control-visualizer-styles.ts b/packages/components/src/box-control/styles/box-control-visualizer-styles.ts deleted file mode 100644 index bbfe66c9a71e9..0000000000000 --- a/packages/components/src/box-control/styles/box-control-visualizer-styles.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * External dependencies - */ -import { css } from '@emotion/react'; -import styled from '@emotion/styled'; - -/** - * Internal dependencies - */ -import { COLORS, rtl } from '../../utils'; - -const containerPositionStyles = ( { - isPositionAbsolute, -}: { - isPositionAbsolute: boolean; -} ) => { - if ( ! isPositionAbsolute ) return ''; - - return css` - bottom: 0; - left: 0; - pointer-events: none; - position: absolute; - right: 0; - top: 0; - z-index: 1; - `; -}; - -export const Container = styled.div` - box-sizing: border-box; - position: relative; - ${ containerPositionStyles }; -`; - -export const Side = styled.div` - box-sizing: border-box; - background: ${ COLORS.theme.accent }; - filter: brightness( 1 ); - opacity: 0; - position: absolute; - pointer-events: none; - transition: opacity 120ms linear; - z-index: 1; - - ${ ( { isActive }: { isActive: boolean } ) => - isActive && - ` - opacity: 0.3; - ` } -`; - -export const TopView = styled( Side )` - top: 0; - left: 0; - right: 0; -`; - -export const RightView = styled( Side )` - top: 0; - bottom: 0; - ${ rtl( { right: 0 } ) }; -`; - -export const BottomView = styled( Side )` - bottom: 0; - left: 0; - right: 0; -`; - -export const LeftView = styled( Side )` - top: 0; - bottom: 0; - ${ rtl( { left: 0 } ) }; -`; diff --git a/packages/components/src/box-control/test/index.tsx b/packages/components/src/box-control/test/index.tsx index 8a861eff37e1b..1ea3c84aae922 100644 --- a/packages/components/src/box-control/test/index.tsx +++ b/packages/components/src/box-control/test/index.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import { render, screen } from '@testing-library/react'; +import { fireEvent, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; /** @@ -33,7 +33,10 @@ describe( 'BoxControl', () => { render( {} } /> ); expect( - screen.getByRole( 'textbox', { name: 'Box Control' } ) + screen.getByRole( 'group', { name: 'Box Control' } ) + ).toBeVisible(); + expect( + screen.getByRole( 'textbox', { name: 'All sides' } ) ).toBeVisible(); } ); @@ -42,15 +45,41 @@ describe( 'BoxControl', () => { render( {} } /> ); - const input = screen.getByRole( 'textbox', { - name: 'Box Control', - } ); + const input = screen.getByRole( 'textbox', { name: 'All sides' } ); await user.type( input, '100' ); await user.keyboard( '{Enter}' ); expect( input ).toHaveValue( '100' ); } ); + + it( 'should update input values when interacting with slider', () => { + render( {} } /> ); + + const slider = screen.getByRole( 'slider' ); + + fireEvent.change( slider, { target: { value: 50 } } ); + + expect( slider ).toHaveValue( '50' ); + expect( + screen.getByRole( 'textbox', { name: 'All sides' } ) + ).toHaveValue( '50' ); + } ); + + it( 'should update slider values when interacting with input', async () => { + const user = userEvent.setup(); + render( {} } /> ); + + const input = screen.getByRole( 'textbox', { + name: 'All sides', + } ); + + await user.type( input, '50' ); + await user.keyboard( '{Enter}' ); + + expect( input ).toHaveValue( '50' ); + expect( screen.getByRole( 'slider' ) ).toHaveValue( '50' ); + } ); } ); describe( 'Reset', () => { @@ -60,7 +89,7 @@ describe( 'BoxControl', () => { render( {} } /> ); const input = screen.getByRole( 'textbox', { - name: 'Box Control', + name: 'All sides', } ); await user.type( input, '100' ); @@ -79,7 +108,7 @@ describe( 'BoxControl', () => { render( ); const input = screen.getByRole( 'textbox', { - name: 'Box Control', + name: 'All sides', } ); await user.type( input, '100' ); @@ -98,7 +127,7 @@ describe( 'BoxControl', () => { render( ); const input = screen.getByRole( 'textbox', { - name: 'Box Control', + name: 'All sides', } ); await user.type( input, '100' ); @@ -118,7 +147,7 @@ describe( 'BoxControl', () => { render( spyChange( v ) } /> ); const input = screen.getByRole( 'textbox', { - name: 'Box Control', + name: 'All sides', } ); await user.type( input, '100' ); @@ -152,21 +181,49 @@ describe( 'BoxControl', () => { ); await user.type( - screen.getByRole( 'textbox', { name: 'Top' } ), + screen.getByRole( 'textbox', { name: 'Top side' } ), '100' ); expect( - screen.getByRole( 'textbox', { name: 'Top' } ) + screen.getByRole( 'textbox', { name: 'Top side' } ) ).toHaveValue( '100' ); expect( - screen.getByRole( 'textbox', { name: 'Right' } ) + screen.getByRole( 'textbox', { name: 'Right side' } ) ).not.toHaveValue(); expect( - screen.getByRole( 'textbox', { name: 'Bottom' } ) + screen.getByRole( 'textbox', { name: 'Bottom side' } ) ).not.toHaveValue(); expect( - screen.getByRole( 'textbox', { name: 'Left' } ) + screen.getByRole( 'textbox', { name: 'Left side' } ) + ).not.toHaveValue(); + } ); + + it( 'should update a single side value when using slider unlinked', async () => { + const user = userEvent.setup(); + + render( ); + + await user.click( + screen.getByRole( 'button', { name: 'Unlink sides' } ) + ); + + const slider = screen.getByRole( 'slider', { name: 'Right side' } ); + + fireEvent.change( slider, { target: { value: 50 } } ); + + expect( slider ).toHaveValue( '50' ); + expect( + screen.getByRole( 'textbox', { name: 'Top side' } ) + ).not.toHaveValue(); + expect( + screen.getByRole( 'textbox', { name: 'Right side' } ) + ).toHaveValue( '50' ); + expect( + screen.getByRole( 'textbox', { name: 'Bottom side' } ) + ).not.toHaveValue(); + expect( + screen.getByRole( 'textbox', { name: 'Left side' } ) ).not.toHaveValue(); } ); @@ -181,17 +238,68 @@ describe( 'BoxControl', () => { await user.type( screen.getByRole( 'textbox', { - name: 'Vertical', + name: 'Top and bottom sides', } ), '100' ); expect( - screen.getByRole( 'textbox', { name: 'Vertical' } ) + screen.getByRole( 'textbox', { name: 'Top and bottom sides' } ) ).toHaveValue( '100' ); expect( - screen.getByRole( 'textbox', { name: 'Horizontal' } ) + screen.getByRole( 'textbox', { name: 'Left and right sides' } ) + ).not.toHaveValue(); + } ); + + it( 'should update a whole axis using a slider when value is changed when unlinked', async () => { + const user = userEvent.setup(); + + render( ); + + await user.click( + screen.getByRole( 'button', { name: 'Unlink sides' } ) + ); + + const slider = screen.getByRole( 'slider', { + name: 'Left and right sides', + } ); + + fireEvent.change( slider, { target: { value: 50 } } ); + + expect( slider ).toHaveValue( '50' ); + expect( + screen.getByRole( 'textbox', { name: 'Top and bottom sides' } ) ).not.toHaveValue(); + expect( + screen.getByRole( 'textbox', { name: 'Left and right sides' } ) + ).toHaveValue( '50' ); + } ); + + it( 'should show "Mixed" label when sides have different values but are linked', async () => { + const user = userEvent.setup(); + + render( ); + + const unlinkButton = screen.getByRole( 'button', { + name: 'Unlink sides', + } ); + + await user.click( unlinkButton ); + + await user.type( + screen.getByRole( 'textbox', { + name: 'Right side', + } ), + '13' + ); + + expect( + screen.getByRole( 'textbox', { name: 'Right side' } ) + ).toHaveValue( '13' ); + + await user.click( unlinkButton ); + + expect( screen.getByPlaceholderText( 'Mixed' ) ).toHaveValue( '' ); } ); } ); @@ -287,7 +395,7 @@ describe( 'BoxControl', () => { render( ); const valueInput = screen.getByRole( 'textbox', { - name: 'Box Control', + name: 'All sides', } ); const unitSelect = screen.getByRole( 'combobox', { name: 'Select unit', diff --git a/packages/components/src/box-control/types.ts b/packages/components/src/box-control/types.ts index 0eba0e58fd33c..12524559564ab 100644 --- a/packages/components/src/box-control/types.ts +++ b/packages/components/src/box-control/types.ts @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import type { useHover } from '@use-gesture/react'; - /** * Internal dependencies */ @@ -16,6 +11,10 @@ export type BoxControlValue = { left?: string; }; +export type CustomValueUnits = { + [ key: string ]: { max: number; step: number }; +}; + type UnitControlPassthroughProps = Omit< UnitControlProps, 'label' | 'onChange' | 'onFocus' | 'onMouseOver' | 'onMouseOut' | 'units' @@ -92,22 +91,6 @@ export type BoxControlInputControlProps = UnitControlPassthroughProps & { values: BoxControlValue; }; -export type BoxUnitControlProps = UnitControlPassthroughProps & - Pick< UnitControlProps, 'onChange' | 'onFocus' > & { - isFirst?: boolean; - isLast?: boolean; - isOnly?: boolean; - label?: string; - onHoverOff?: ( - event: ReturnType< typeof useHover >[ 'event' ], - state: Omit< ReturnType< typeof useHover >, 'event' > - ) => void; - onHoverOn?: ( - event: ReturnType< typeof useHover >[ 'event' ], - state: Omit< ReturnType< typeof useHover >, 'event' > - ) => void; - }; - export type BoxControlIconProps = { /** * @default 24 diff --git a/packages/components/src/box-control/unit-control.tsx b/packages/components/src/box-control/unit-control.tsx deleted file mode 100644 index 24d71cf0d6cd3..0000000000000 --- a/packages/components/src/box-control/unit-control.tsx +++ /dev/null @@ -1,74 +0,0 @@ -/** - * External dependencies - */ -import { useHover } from '@use-gesture/react'; - -/** - * Internal dependencies - */ -import BaseTooltip from '../tooltip'; -import { UnitControlWrapper, UnitControl } from './styles/box-control-styles'; -import type { BoxUnitControlProps } from './types'; - -const noop = () => {}; - -export default function BoxUnitControl( { - isFirst, - isLast, - isOnly, - onHoverOn = noop, - onHoverOff = noop, - label, - value, - ...props -}: BoxUnitControlProps ) { - const bindHoverGesture = useHover( ( { event, ...state } ) => { - if ( state.hovering ) { - onHoverOn( event, state ); - } else { - onHoverOff( event, state ); - } - } ); - - return ( - - - - - - ); -} - -function Tooltip( { - children, - text, -}: { - children: JSX.Element; - text?: string; -} ) { - if ( ! text ) return children; - - /** - * Wrapping the children in a `
` as Tooltip as it attempts - * to render the . Using a plain `
` appears to - * resolve this issue. - * - * Originally discovered and referenced here: - * https://github.com/WordPress/gutenberg/pull/24966#issuecomment-685875026 - */ - return ( - -
{ children }
-
- ); -} diff --git a/packages/components/src/box-control/utils.ts b/packages/components/src/box-control/utils.ts index 6614342d3da6d..e480c9a9f4674 100644 --- a/packages/components/src/box-control/utils.ts +++ b/packages/components/src/box-control/utils.ts @@ -7,17 +7,52 @@ import { __ } from '@wordpress/i18n'; * Internal dependencies */ import { parseQuantityAndUnitFromRawValue } from '../unit-control/utils'; -import type { BoxControlProps, BoxControlValue } from './types'; +import type { + BoxControlProps, + BoxControlValue, + CustomValueUnits, +} from './types'; + +export const CUSTOM_VALUE_SETTINGS: CustomValueUnits = { + px: { max: 300, step: 1 }, + '%': { max: 100, step: 1 }, + vw: { max: 100, step: 1 }, + vh: { max: 100, step: 1 }, + em: { max: 10, step: 0.1 }, + rm: { max: 10, step: 0.1 }, + svw: { max: 100, step: 1 }, + lvw: { max: 100, step: 1 }, + dvw: { max: 100, step: 1 }, + svh: { max: 100, step: 1 }, + lvh: { max: 100, step: 1 }, + dvh: { max: 100, step: 1 }, + vi: { max: 100, step: 1 }, + svi: { max: 100, step: 1 }, + lvi: { max: 100, step: 1 }, + dvi: { max: 100, step: 1 }, + vb: { max: 100, step: 1 }, + svb: { max: 100, step: 1 }, + lvb: { max: 100, step: 1 }, + dvb: { max: 100, step: 1 }, + vmin: { max: 100, step: 1 }, + svmin: { max: 100, step: 1 }, + lvmin: { max: 100, step: 1 }, + dvmin: { max: 100, step: 1 }, + vmax: { max: 100, step: 1 }, + svmax: { max: 100, step: 1 }, + lvmax: { max: 100, step: 1 }, + dvmax: { max: 100, step: 1 }, +}; export const LABELS = { - all: __( 'All' ), - top: __( 'Top' ), - bottom: __( 'Bottom' ), - left: __( 'Left' ), - right: __( 'Right' ), + all: __( 'All sides' ), + top: __( 'Top side' ), + bottom: __( 'Bottom side' ), + left: __( 'Left side' ), + right: __( 'Right side' ), mixed: __( 'Mixed' ), - vertical: __( 'Vertical' ), - horizontal: __( 'Horizontal' ), + vertical: __( 'Top and bottom sides' ), + horizontal: __( 'Left and right sides' ), }; export const DEFAULT_VALUES = {