@elementor/editor-controls 1.5.0 → 3.32.0-21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/CHANGELOG.md +0 -22
  2. package/dist/index.d.mts +95 -25
  3. package/dist/index.d.ts +95 -25
  4. package/dist/index.js +2045 -1041
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +1962 -964
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +18 -18
  9. package/src/components/control-toggle-button-group.tsx +78 -14
  10. package/src/components/floating-bar.tsx +45 -0
  11. package/src/components/{font-family-selector.tsx → item-selector.tsx} +62 -50
  12. package/src/components/repeater.tsx +1 -1
  13. package/src/components/restricted-link-infotip.tsx +76 -0
  14. package/src/components/size-control/size-input.tsx +8 -7
  15. package/src/components/size-control/text-field-inner-selection.tsx +60 -14
  16. package/src/components/text-field-popover.tsx +30 -7
  17. package/src/components/unstable-repeater/actions/add-item-action.tsx +50 -0
  18. package/src/components/unstable-repeater/actions/disable-item-action.tsx +39 -0
  19. package/src/components/unstable-repeater/actions/duplicate-item-action.tsx +32 -0
  20. package/src/components/unstable-repeater/actions/remove-item-action.tsx +27 -0
  21. package/src/components/unstable-repeater/context/repeater-context.tsx +137 -0
  22. package/src/components/unstable-repeater/header/header.tsx +23 -0
  23. package/src/components/unstable-repeater/index.ts +5 -0
  24. package/src/components/unstable-repeater/items/edit-item-popover.tsx +28 -0
  25. package/src/components/unstable-repeater/items/item.tsx +71 -0
  26. package/src/components/unstable-repeater/items/items-container.tsx +49 -0
  27. package/src/components/unstable-repeater/items/use-popover.tsx +26 -0
  28. package/src/{locations.ts → components/unstable-repeater/locations.ts} +9 -1
  29. package/src/components/unstable-repeater/types.ts +26 -0
  30. package/src/components/unstable-repeater/unstable-repeater.tsx +24 -0
  31. package/src/control-actions/control-actions.tsx +3 -20
  32. package/src/control-replacements.tsx +41 -0
  33. package/src/controls/background-control/background-control.tsx +1 -8
  34. package/src/controls/background-control/background-overlay/background-overlay-repeater-control.tsx +17 -16
  35. package/src/controls/equal-unequal-sizes-control.tsx +2 -9
  36. package/src/controls/filter-control/drop-shadow-item-content.tsx +4 -6
  37. package/src/controls/filter-control/drop-shadow-item-label.tsx +2 -2
  38. package/src/controls/filter-repeater-control.tsx +149 -110
  39. package/src/controls/font-family-control/font-family-control.tsx +22 -10
  40. package/src/controls/key-value-control.tsx +9 -6
  41. package/src/controls/link-control.tsx +8 -91
  42. package/src/controls/linked-dimensions-control.tsx +3 -16
  43. package/src/controls/number-control.tsx +10 -1
  44. package/src/controls/position-control.tsx +4 -16
  45. package/src/controls/repeatable-control.tsx +8 -5
  46. package/src/controls/select-control.tsx +7 -2
  47. package/src/controls/selection-size-control.tsx +74 -0
  48. package/src/controls/size-control.tsx +181 -126
  49. package/src/controls/stroke-control.tsx +2 -2
  50. package/src/controls/toggle-control.tsx +3 -2
  51. package/src/controls/transform-control/functions/axis-row.tsx +4 -2
  52. package/src/controls/transform-control/functions/move.tsx +2 -1
  53. package/src/controls/transform-control/functions/rotate.tsx +48 -0
  54. package/src/controls/transform-control/functions/scale-axis-row.tsx +32 -0
  55. package/src/controls/transform-control/functions/scale.tsx +45 -0
  56. package/src/controls/transform-control/functions/skew.tsx +43 -0
  57. package/src/controls/transform-control/transform-content.tsx +60 -23
  58. package/src/controls/transform-control/transform-icon.tsx +10 -2
  59. package/src/controls/transform-control/transform-label.tsx +39 -2
  60. package/src/controls/transform-control/transform-repeater-control.tsx +2 -10
  61. package/src/controls/transform-control/types.ts +58 -0
  62. package/src/controls/transform-control/use-transform-tabs-history.tsx +107 -0
  63. package/src/controls/transition-control/data.ts +34 -0
  64. package/src/controls/transition-control/transition-repeater-control.tsx +63 -0
  65. package/src/controls/transition-control/transition-selector.tsx +88 -0
  66. package/src/controls/unstable-transform-control/unstable-transform-repeater-control.tsx +35 -0
  67. package/src/hooks/use-filtered-items-list.ts +24 -0
  68. package/src/hooks/use-size-extended-options.ts +1 -6
  69. package/src/index.ts +13 -3
  70. package/src/utils/size-control.ts +12 -6
  71. package/src/hooks/use-filtered-font-families.ts +0 -24
@@ -1,7 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import { type RefObject, useRef } from 'react';
3
3
  import { dimensionsPropTypeUtil, type PropKey, sizePropTypeUtil } from '@elementor/editor-props';
4
- import { isExperimentActive } from '@elementor/editor-v1-adapters';
5
4
  import { DetachIcon, LinkIcon, SideBottomIcon, SideLeftIcon, SideRightIcon, SideTopIcon } from '@elementor/icons';
6
5
  import { Grid, Stack, ToggleButton, Tooltip } from '@elementor/ui';
7
6
  import { __ } from '@wordpress/i18n';
@@ -35,8 +34,6 @@ export const LinkedDimensionsControl = createControl(
35
34
 
36
35
  const isLinked = ! dimensionsValue && ! sizeValue ? true : !! sizeValue;
37
36
 
38
- const isUsingNestedProps = isExperimentActive( 'e_v_3_30' );
39
-
40
37
  const onLinkToggle = () => {
41
38
  if ( ! isLinked ) {
42
39
  setSizeValue( dimensionsValue[ 'block-start' ]?.value ?? null );
@@ -71,11 +68,7 @@ export const LinkedDimensionsControl = createControl(
71
68
  isDisabled={ () => disabled }
72
69
  >
73
70
  <Stack direction="row" gap={ 2 } flexWrap="nowrap">
74
- { isUsingNestedProps ? (
75
- <ControlFormLabel>{ label }</ControlFormLabel>
76
- ) : (
77
- <ControlLabel>{ label }</ControlLabel>
78
- ) }
71
+ <ControlFormLabel>{ label }</ControlFormLabel>
79
72
  <Tooltip title={ isLinked ? unlinkedLabel : linkedLabel } placement="top">
80
73
  <ToggleButton
81
74
  aria-label={ isLinked ? unlinkedLabel : linkedLabel }
@@ -91,7 +84,7 @@ export const LinkedDimensionsControl = createControl(
91
84
  </Tooltip>
92
85
  </Stack>
93
86
 
94
- { getCssMarginProps( isSiteRtl ).map( ( row, index ) => (
87
+ { getCssDimensionProps( isSiteRtl ).map( ( row, index ) => (
95
88
  <Stack direction="row" gap={ 2 } flexWrap="nowrap" key={ index } ref={ gridRowRefs[ index ] }>
96
89
  { row.map( ( { icon, ...props } ) => (
97
90
  <Grid container gap={ 0.75 } alignItems="center" key={ props.bind }>
@@ -141,12 +134,6 @@ const Control = ( {
141
134
  };
142
135
 
143
136
  const Label = ( { label, bind }: { label: string; bind: PropKey } ) => {
144
- const isUsingNestedProps = isExperimentActive( 'e_v_3_30' );
145
-
146
- if ( ! isUsingNestedProps ) {
147
- return <ControlFormLabel>{ label }</ControlFormLabel>;
148
- }
149
-
150
137
  return (
151
138
  <PropKeyProvider bind={ bind }>
152
139
  <ControlLabel>{ label }</ControlLabel>
@@ -154,7 +141,7 @@ const Label = ( { label, bind }: { label: string; bind: PropKey } ) => {
154
141
  );
155
142
  };
156
143
 
157
- function getCssMarginProps( isSiteRtl: boolean ) {
144
+ function getCssDimensionProps( isSiteRtl: boolean ) {
158
145
  return [
159
146
  [
160
147
  {
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import { numberPropTypeUtil } from '@elementor/editor-props';
3
- import { TextField } from '@elementor/ui';
3
+ import { InputAdornment, TextField } from '@elementor/ui';
4
4
 
5
5
  import { useBoundProp } from '../bound-prop-context';
6
6
  import ControlActions from '../control-actions/control-actions';
@@ -18,12 +18,14 @@ export const NumberControl = createControl(
18
18
  min = -Number.MAX_VALUE,
19
19
  step = 1,
20
20
  shouldForceInt = false,
21
+ startIcon,
21
22
  }: {
22
23
  placeholder?: string;
23
24
  max?: number;
24
25
  min?: number;
25
26
  step?: number;
26
27
  shouldForceInt?: boolean;
28
+ startIcon?: React.ReactNode;
27
29
  } ) => {
28
30
  const { value, setValue, placeholder, disabled } = useBoundProp( numberPropTypeUtil );
29
31
 
@@ -52,6 +54,13 @@ export const NumberControl = createControl(
52
54
  onChange={ handleChange }
53
55
  placeholder={ labelPlaceholder ?? ( placeholder ? String( placeholder ) : '' ) }
54
56
  inputProps={ { step } }
57
+ InputProps={ {
58
+ startAdornment: startIcon ? (
59
+ <InputAdornment position="start" disabled={ disabled }>
60
+ { startIcon }
61
+ </InputAdornment>
62
+ ) : undefined,
63
+ } }
55
64
  onKeyDown={ ( event: KeyboardEvent ) => {
56
65
  if ( RESTRICTED_INPUT_KEYS.includes( event.key ) ) {
57
66
  event.preventDefault();
@@ -1,8 +1,6 @@
1
1
  import * as React from 'react';
2
- import { useMemo } from 'react';
3
2
  import { positionPropTypeUtil, stringPropTypeUtil } from '@elementor/editor-props';
4
3
  import { MenuListItem } from '@elementor/editor-ui';
5
- import { isExperimentActive } from '@elementor/editor-v1-adapters';
6
4
  import { LetterXIcon, LetterYIcon } from '@elementor/icons';
7
5
  import { Grid, Select, type SelectChangeEvent } from '@elementor/ui';
8
6
  import { __ } from '@wordpress/i18n';
@@ -33,29 +31,19 @@ const positionOptions = [
33
31
  { label: __( 'Bottom center', 'elementor' ), value: 'bottom center' },
34
32
  { label: __( 'Bottom left', 'elementor' ), value: 'bottom left' },
35
33
  { label: __( 'Bottom right', 'elementor' ), value: 'bottom right' },
34
+ { label: __( 'Custom', 'elementor' ), value: 'custom' },
36
35
  ];
37
36
 
38
37
  export const PositionControl = () => {
39
38
  const positionContext = useBoundProp( positionPropTypeUtil );
40
39
  const stringPropContext = useBoundProp( stringPropTypeUtil );
41
40
 
42
- const isVersion331Active = isExperimentActive( 'e_v_3_31' );
43
- const isCustom = !! positionContext.value && isVersion331Active;
44
-
45
- const availablePositionOptions = useMemo( () => {
46
- const options = [ ...positionOptions ];
47
-
48
- if ( isVersion331Active ) {
49
- options.push( { label: __( 'Custom', 'elementor' ), value: 'custom' } );
50
- }
51
-
52
- return options;
53
- }, [ isVersion331Active ] );
41
+ const isCustom = !! positionContext.value;
54
42
 
55
43
  const handlePositionChange = ( event: SelectChangeEvent< Positions > ) => {
56
44
  const value = event.target.value || null;
57
45
 
58
- if ( value === 'custom' && isVersion331Active ) {
46
+ if ( value === 'custom' ) {
59
47
  positionContext.setValue( { x: null, y: null } );
60
48
  } else {
61
49
  stringPropContext.setValue( value );
@@ -77,7 +65,7 @@ export const PositionControl = () => {
77
65
  onChange={ handlePositionChange }
78
66
  fullWidth
79
67
  >
80
- { availablePositionOptions.map( ( { label, value } ) => (
68
+ { positionOptions.map( ( { label, value } ) => (
81
69
  <MenuListItem key={ value } value={ value ?? '' }>
82
70
  { label }
83
71
  </MenuListItem>
@@ -23,6 +23,7 @@ type RepeatableControlProps = {
23
23
  initialValues?: object;
24
24
  patternLabel?: string;
25
25
  placeholder?: string;
26
+ propKey?: string;
26
27
  };
27
28
 
28
29
  const PLACEHOLDER_REGEX = /\$\{([^}]+)\}/g;
@@ -36,6 +37,7 @@ export const RepeatableControl = createControl(
36
37
  initialValues,
37
38
  patternLabel,
38
39
  placeholder,
40
+ propKey,
39
41
  }: RepeatableControlProps ) => {
40
42
  const { propTypeUtil: childPropTypeUtil } = childControlConfig;
41
43
 
@@ -44,8 +46,8 @@ export const RepeatableControl = createControl(
44
46
  }
45
47
 
46
48
  const childArrayPropTypeUtil = useMemo(
47
- () => createArrayPropUtils( childPropTypeUtil.key, childPropTypeUtil.schema ),
48
- [ childPropTypeUtil.key, childPropTypeUtil.schema ]
49
+ () => createArrayPropUtils( childPropTypeUtil.key, childPropTypeUtil.schema, propKey ),
50
+ [ childPropTypeUtil.key, childPropTypeUtil.schema, propKey ]
49
51
  );
50
52
 
51
53
  const contextValue = useMemo(
@@ -181,11 +183,12 @@ const shouldShowPlaceholder = ( pattern: string, data: Record< string, unknown >
181
183
 
182
184
  const ItemLabel = ( { value }: { value: Record< string, unknown > } ) => {
183
185
  const { placeholder, patternLabel } = useRepeatableControlContext();
184
-
185
- const label = shouldShowPlaceholder( patternLabel, value ) ? placeholder : interpolate( patternLabel, value );
186
+ const showPlaceholder = shouldShowPlaceholder( patternLabel, value );
187
+ const label = showPlaceholder ? placeholder : interpolate( patternLabel, value );
188
+ const color = showPlaceholder ? 'text.tertiary' : 'text.primary';
186
189
 
187
190
  return (
188
- <Box component="span" color="text.tertiary">
191
+ <Box component="span" color={ color }>
189
192
  { label }
190
193
  </Box>
191
194
  );
@@ -7,14 +7,19 @@ import { useBoundProp } from '../bound-prop-context';
7
7
  import ControlActions from '../control-actions/control-actions';
8
8
  import { createControl } from '../create-control';
9
9
 
10
+ export type SelectOption = {
11
+ label: string;
12
+ value: StringPropValue[ 'value' ];
13
+ disabled?: boolean;
14
+ };
15
+
10
16
  type Props = {
11
- options: Array< { label: string; value: StringPropValue[ 'value' ]; disabled?: boolean } >;
17
+ options: SelectOption[];
12
18
  onChange?: ( newValue: string | null, previousValue: string | null | undefined ) => void;
13
19
  };
14
20
 
15
21
  export const SelectControl = createControl( ( { options, onChange }: Props ) => {
16
22
  const { value, setValue, disabled, placeholder } = useBoundProp( stringPropTypeUtil );
17
-
18
23
  const handleChange = ( event: SelectChangeEvent< StringPropValue[ 'value' ] > ) => {
19
24
  const newValue = event.target.value || null;
20
25
 
@@ -0,0 +1,74 @@
1
+ import * as React from 'react';
2
+ import { useMemo, useRef } from 'react';
3
+ import { selectionSizePropTypeUtil } from '@elementor/editor-props';
4
+ import { Grid } from '@elementor/ui';
5
+
6
+ import { PropKeyProvider, PropProvider, useBoundProp } from '../bound-prop-context';
7
+ import { ControlFormLabel } from '../components/control-form-label';
8
+ import { createControl } from '../create-control';
9
+ import { SizeControl, type SizeControlProps } from './size-control';
10
+
11
+ type SelectionComponentConfig = {
12
+ component: React.ComponentType< Record< string, unknown > >;
13
+ props: Record< string, unknown >;
14
+ };
15
+
16
+ type SizeControlConfig = Pick< SizeControlProps, 'variant' | 'units' | 'defaultUnit' >;
17
+
18
+ type SelectionSizeControlProps = {
19
+ selectionLabel: string;
20
+ sizeLabel: string;
21
+ selectionConfig: SelectionComponentConfig;
22
+ sizeConfigMap: Record< string, SizeControlConfig >;
23
+ };
24
+
25
+ export const SelectionSizeControl = createControl(
26
+ ( { selectionLabel, sizeLabel, selectionConfig, sizeConfigMap }: SelectionSizeControlProps ) => {
27
+ const { value, setValue, propType } = useBoundProp( selectionSizePropTypeUtil );
28
+ const rowRef = useRef< HTMLDivElement >( null );
29
+
30
+ const currentSizeConfig = useMemo( () => {
31
+ switch ( value.selection.$$type ) {
32
+ case 'key-value':
33
+ return sizeConfigMap[ value?.selection?.value.value.value || '' ];
34
+ case 'string':
35
+ return sizeConfigMap[ value?.selection?.value || '' ];
36
+ default:
37
+ return null;
38
+ }
39
+ }, [ value, sizeConfigMap ] );
40
+ const SelectionComponent = selectionConfig.component;
41
+
42
+ return (
43
+ <PropProvider value={ value } setValue={ setValue } propType={ propType }>
44
+ <Grid container spacing={ 1.5 } ref={ rowRef }>
45
+ <Grid item xs={ 6 } sx={ { display: 'flex', alignItems: 'center' } }>
46
+ <ControlFormLabel>{ selectionLabel }</ControlFormLabel>
47
+ </Grid>
48
+ <Grid item xs={ 6 }>
49
+ <PropKeyProvider bind="selection">
50
+ <SelectionComponent { ...selectionConfig.props } />
51
+ </PropKeyProvider>
52
+ </Grid>
53
+ { currentSizeConfig && (
54
+ <>
55
+ <Grid item xs={ 6 } sx={ { display: 'flex', alignItems: 'center' } }>
56
+ <ControlFormLabel>{ sizeLabel }</ControlFormLabel>
57
+ </Grid>
58
+ <Grid item xs={ 6 }>
59
+ <PropKeyProvider bind="size">
60
+ <SizeControl
61
+ anchorRef={ rowRef }
62
+ variant={ currentSizeConfig.variant }
63
+ units={ currentSizeConfig.units }
64
+ defaultUnit={ currentSizeConfig.defaultUnit }
65
+ />
66
+ </PropKeyProvider>
67
+ </Grid>
68
+ </>
69
+ ) }
70
+ </Grid>
71
+ </PropProvider>
72
+ );
73
+ }
74
+ );
@@ -11,165 +11,218 @@ import { createControl } from '../create-control';
11
11
  import { useSizeExtendedOptions } from '../hooks/use-size-extended-options';
12
12
  import { useSyncExternalState } from '../hooks/use-sync-external-state';
13
13
  import {
14
- defaultUnits,
15
- type DegreeUnit,
14
+ type AngleUnit,
15
+ angleUnits,
16
+ DEFAULT_SIZE,
17
+ DEFAULT_UNIT,
16
18
  type ExtendedOption,
17
19
  isUnitExtendedOption,
20
+ type LengthUnit,
21
+ lengthUnits,
22
+ type TimeUnit,
23
+ timeUnits,
18
24
  type Unit,
19
25
  } from '../utils/size-control';
20
26
 
21
- const DEFAULT_UNIT = 'px';
22
- const DEFAULT_SIZE = NaN;
23
-
24
27
  type SizeValue = SizePropValue[ 'value' ];
25
28
 
26
- type SizeControlProps = {
29
+ type SizeVariant = 'length' | 'angle' | 'time';
30
+
31
+ type UnitProps< T extends readonly Unit[] > = {
32
+ units?: T;
33
+ defaultUnit?: T[ number ];
34
+ };
35
+
36
+ type BaseSizeControlProps = {
27
37
  placeholder?: string;
28
38
  startIcon?: React.ReactNode;
29
- units?: ( Unit | DegreeUnit )[];
30
39
  extendedOptions?: ExtendedOption[];
31
40
  disableCustom?: boolean;
32
41
  anchorRef?: RefObject< HTMLDivElement | null >;
33
- defaultUnit?: Unit | DegreeUnit;
34
42
  };
35
43
 
44
+ type LengthSizeControlProps = BaseSizeControlProps &
45
+ UnitProps< LengthUnit[] > & {
46
+ variant: 'length';
47
+ };
48
+
49
+ type AngleSizeControlProps = BaseSizeControlProps &
50
+ UnitProps< AngleUnit[] > & {
51
+ variant: 'angle';
52
+ };
53
+
54
+ type TimeSizeControlProps = BaseSizeControlProps &
55
+ UnitProps< TimeUnit[] > & {
56
+ variant: 'time';
57
+ };
58
+
59
+ export type SizeControlProps = LengthSizeControlProps | AngleSizeControlProps | TimeSizeControlProps;
60
+
36
61
  type State = {
37
62
  numeric: number;
38
63
  custom: string;
39
- unit: Unit | DegreeUnit | ExtendedOption;
64
+ unit: Unit | ExtendedOption;
40
65
  };
41
66
 
42
- export const SizeControl = createControl( ( props: SizeControlProps ) => {
43
- const defaultUnit = props.defaultUnit ?? DEFAULT_UNIT;
44
- const { units = [ ...defaultUnits ], placeholder, startIcon, anchorRef } = props;
45
- const { value: sizeValue, setValue: setSizeValue, disabled, restoreValue } = useBoundProp( sizePropTypeUtil );
46
- const [ internalState, setInternalState ] = useState( createStateFromSizeProp( sizeValue, defaultUnit ) );
47
- const activeBreakpoint = useActiveBreakpoint();
48
-
49
- const extendedOptions = useSizeExtendedOptions( props.extendedOptions || [], props.disableCustom ?? false );
50
- const popupState = usePopupState( { variant: 'popover' } );
51
-
52
- const [ state, setState ] = useSyncExternalState( {
53
- external: internalState,
54
- setExternal: ( newState: State | null ) => setSizeValue( extractValueFromState( newState ) ),
55
- persistWhen: ( newState ) => {
56
- if ( ! newState?.unit ) {
57
- return false;
58
- }
59
-
60
- if ( isUnitExtendedOption( newState.unit ) ) {
61
- return newState.unit === 'auto' ? true : !! newState.custom;
67
+ const defaultSelectedUnit: Record< SizeControlProps[ 'variant' ], Unit > = {
68
+ length: 'px',
69
+ angle: 'deg',
70
+ time: 'ms',
71
+ } as const;
72
+
73
+ const defaultUnits: Record< SizeControlProps[ 'variant' ], Unit[] > = {
74
+ length: [ ...lengthUnits ] as LengthUnit[],
75
+ angle: [ ...angleUnits ] as AngleUnit[],
76
+ time: [ ...timeUnits ] as TimeUnit[],
77
+ } as const;
78
+
79
+ export const SizeControl = createControl(
80
+ ( {
81
+ variant = 'length' as SizeControlProps[ 'variant' ],
82
+ defaultUnit,
83
+ units,
84
+ placeholder,
85
+ startIcon,
86
+ anchorRef,
87
+ extendedOptions,
88
+ disableCustom,
89
+ }: Omit< SizeControlProps, 'variant' > & { variant?: SizeVariant } ) => {
90
+ const {
91
+ value: sizeValue,
92
+ setValue: setSizeValue,
93
+ disabled,
94
+ restoreValue,
95
+ placeholder: externalPlaceholder,
96
+ } = useBoundProp( sizePropTypeUtil );
97
+ const actualDefaultUnit = defaultUnit ?? externalPlaceholder?.unit ?? defaultSelectedUnit[ variant ];
98
+ const actualUnits = units ?? [ ...defaultUnits[ variant ] ];
99
+ const [ internalState, setInternalState ] = useState( createStateFromSizeProp( sizeValue, actualDefaultUnit ) );
100
+ const activeBreakpoint = useActiveBreakpoint();
101
+
102
+ const actualExtendedOptions = useSizeExtendedOptions( extendedOptions || [], disableCustom ?? false );
103
+ const popupState = usePopupState( { variant: 'popover' } );
104
+
105
+ const [ state, setState ] = useSyncExternalState( {
106
+ external: internalState,
107
+ setExternal: ( newState: State | null ) => setSizeValue( extractValueFromState( newState ) ),
108
+ persistWhen: ( newState ) => {
109
+ if ( ! newState?.unit ) {
110
+ return false;
111
+ }
112
+
113
+ if ( isUnitExtendedOption( newState.unit ) ) {
114
+ return newState.unit === 'auto' ? true : !! newState.custom;
115
+ }
116
+
117
+ return !! newState?.numeric || newState?.numeric === 0;
118
+ },
119
+ fallback: ( newState ) => ( {
120
+ unit: newState?.unit ?? actualDefaultUnit,
121
+ numeric: newState?.numeric ?? DEFAULT_SIZE,
122
+ custom: newState?.custom ?? '',
123
+ } ),
124
+ } );
125
+
126
+ const { size: controlSize = DEFAULT_SIZE, unit: controlUnit = actualDefaultUnit } =
127
+ extractValueFromState( state ) || {};
128
+
129
+ const handleUnitChange = ( newUnit: Unit | ExtendedOption ) => {
130
+ if ( newUnit === 'custom' ) {
131
+ popupState.open( anchorRef?.current );
62
132
  }
63
133
 
64
- return !! newState?.numeric || newState?.numeric === 0;
65
- },
66
- fallback: ( newState ) => ( {
67
- unit: newState?.unit ?? defaultUnit,
68
- numeric: newState?.numeric ?? DEFAULT_SIZE,
69
- custom: newState?.custom ?? '',
70
- } ),
71
- } );
72
-
73
- const { size: controlSize = DEFAULT_SIZE, unit: controlUnit = defaultUnit } = extractValueFromState( state ) || {};
74
-
75
- const handleUnitChange = ( newUnit: Unit | DegreeUnit | ExtendedOption ) => {
76
- if ( newUnit === 'custom' ) {
77
- popupState.open( anchorRef?.current );
78
- }
79
-
80
- setState( ( prev ) => ( { ...prev, unit: newUnit } ) );
81
- };
82
-
83
- const handleSizeChange = ( event: React.ChangeEvent< HTMLInputElement > ) => {
84
- const { value: size } = event.target;
85
-
86
- if ( controlUnit === 'auto' ) {
87
- setState( ( prev ) => ( { ...prev, unit: controlUnit } ) );
134
+ setState( ( prev ) => ( { ...prev, unit: newUnit } ) );
135
+ };
88
136
 
89
- return;
90
- }
137
+ const handleSizeChange = ( event: React.ChangeEvent< HTMLInputElement > ) => {
138
+ const { value: size } = event.target;
91
139
 
92
- setState( ( prev ) => ( {
93
- ...prev,
94
- [ controlUnit === 'custom' ? 'custom' : 'numeric' ]: formatSize( size, controlUnit ),
95
- unit: controlUnit,
96
- } ) );
97
- };
140
+ if ( controlUnit === 'auto' ) {
141
+ setState( ( prev ) => ( { ...prev, unit: controlUnit } ) );
98
142
 
99
- const onInputFocus = ( event: React.FocusEvent< HTMLInputElement > ) => {
100
- if ( isUnitExtendedOption( state.unit ) ) {
101
- ( event.target as HTMLElement )?.blur();
102
- }
103
- };
143
+ return;
144
+ }
104
145
 
105
- const onInputClick = ( event: React.MouseEvent ) => {
106
- if ( ( event.target as HTMLElement ).closest( 'input' ) && 'custom' === state.unit ) {
107
- popupState.open( anchorRef?.current );
108
- }
109
- };
146
+ setState( ( prev ) => ( {
147
+ ...prev,
148
+ [ controlUnit === 'custom' ? 'custom' : 'numeric' ]: formatSize( size, controlUnit ),
149
+ unit: controlUnit,
150
+ } ) );
151
+ };
110
152
 
111
- useEffect( () => {
112
- const newState = createStateFromSizeProp( sizeValue, state.unit === 'custom' ? state.unit : defaultUnit );
113
- const currentUnitType = isUnitExtendedOption( state.unit ) ? 'custom' : 'numeric';
114
- const mergedStates = {
115
- ...state,
116
- unit: newState.unit ?? state.unit,
117
- [ currentUnitType ]: newState[ currentUnitType ],
153
+ const onInputClick = ( event: React.MouseEvent ) => {
154
+ if ( ( event.target as HTMLElement ).closest( 'input' ) && 'custom' === state.unit ) {
155
+ popupState.open( anchorRef?.current );
156
+ }
118
157
  };
119
158
 
120
- if ( mergedStates.unit !== 'auto' && areStatesEqual( state, mergedStates ) ) {
121
- return;
122
- }
159
+ useEffect( () => {
160
+ const newState = createStateFromSizeProp(
161
+ sizeValue,
162
+ state.unit === 'custom' ? state.unit : actualDefaultUnit,
163
+ '',
164
+ state.custom
165
+ );
166
+ const currentUnitType = isUnitExtendedOption( state.unit ) ? 'custom' : 'numeric';
167
+ const mergedStates = {
168
+ ...state,
169
+ unit: newState.unit ?? state.unit,
170
+ [ currentUnitType ]: newState[ currentUnitType ],
171
+ };
172
+
173
+ if ( mergedStates.unit !== 'auto' && areStatesEqual( state, mergedStates ) ) {
174
+ return;
175
+ }
123
176
 
124
- if ( state.unit === newState.unit ) {
125
- setInternalState( mergedStates );
177
+ if ( state.unit === newState.unit ) {
178
+ setInternalState( mergedStates );
126
179
 
127
- return;
128
- }
180
+ return;
181
+ }
129
182
 
130
- setState( newState );
131
- // eslint-disable-next-line react-hooks/exhaustive-deps
132
- }, [ sizeValue ] );
183
+ setState( newState );
184
+ // eslint-disable-next-line react-hooks/exhaustive-deps
185
+ }, [ sizeValue ] );
133
186
 
134
- useEffect( () => {
135
- const newState = createStateFromSizeProp( sizeValue, defaultUnit );
187
+ useEffect( () => {
188
+ const newState = createStateFromSizeProp( sizeValue, actualDefaultUnit, '', state.custom );
136
189
 
137
- if ( activeBreakpoint && ! areStatesEqual( newState, state ) ) {
138
- setState( newState );
139
- }
140
- // eslint-disable-next-line react-hooks/exhaustive-deps
141
- }, [ activeBreakpoint ] );
142
-
143
- return (
144
- <>
145
- <SizeInput
146
- disabled={ disabled }
147
- size={ controlSize }
148
- unit={ controlUnit }
149
- units={ [ ...units, ...( extendedOptions || [] ) ] }
150
- placeholder={ placeholder }
151
- startIcon={ startIcon }
152
- handleSizeChange={ handleSizeChange }
153
- handleUnitChange={ handleUnitChange }
154
- onBlur={ restoreValue }
155
- onFocus={ onInputFocus }
156
- onClick={ onInputClick }
157
- popupState={ popupState }
158
- />
159
- { anchorRef?.current && (
160
- <TextFieldPopover
190
+ if ( activeBreakpoint && ! areStatesEqual( newState, state ) ) {
191
+ setState( newState );
192
+ }
193
+ // eslint-disable-next-line react-hooks/exhaustive-deps
194
+ }, [ activeBreakpoint ] );
195
+
196
+ return (
197
+ <>
198
+ <SizeInput
199
+ disabled={ disabled }
200
+ size={ controlSize }
201
+ unit={ controlUnit }
202
+ units={ [ ...actualUnits, ...( actualExtendedOptions || [] ) ] }
203
+ placeholder={ placeholder }
204
+ startIcon={ startIcon }
205
+ handleSizeChange={ handleSizeChange }
206
+ handleUnitChange={ handleUnitChange }
207
+ onBlur={ restoreValue }
208
+ onClick={ onInputClick }
161
209
  popupState={ popupState }
162
- anchorRef={ anchorRef }
163
- restoreValue={ restoreValue }
164
- value={ controlSize as string }
165
- onChange={ handleSizeChange }
166
210
  />
167
- ) }
168
- </>
169
- );
170
- } );
211
+ { anchorRef?.current && (
212
+ <TextFieldPopover
213
+ popupState={ popupState }
214
+ anchorRef={ anchorRef }
215
+ restoreValue={ restoreValue }
216
+ value={ controlSize as string }
217
+ onChange={ handleSizeChange }
218
+ />
219
+ ) }
220
+ </>
221
+ );
222
+ }
223
+ );
171
224
 
172
- function formatSize< TSize extends string | number >( size: TSize, unit: Unit | DegreeUnit | ExtendedOption ): TSize {
225
+ function formatSize< TSize extends string | number >( size: TSize, unit: Unit | ExtendedOption ): TSize {
173
226
  if ( isUnitExtendedOption( unit ) ) {
174
227
  return unit === 'auto' ? ( '' as TSize ) : ( String( size ?? '' ) as TSize );
175
228
  }
@@ -179,17 +232,19 @@ function formatSize< TSize extends string | number >( size: TSize, unit: Unit |
179
232
 
180
233
  function createStateFromSizeProp(
181
234
  sizeValue: SizeValue | null,
182
- defaultUnit: Unit | DegreeUnit | ExtendedOption
235
+ defaultUnit: Unit | ExtendedOption,
236
+ defaultSize: string | number = '',
237
+ customState: string = ''
183
238
  ): State {
184
239
  const unit = sizeValue?.unit ?? defaultUnit;
185
- const size = sizeValue?.size ?? '';
240
+ const size = sizeValue?.size ?? defaultSize;
186
241
 
187
242
  return {
188
243
  numeric:
189
244
  ! isUnitExtendedOption( unit ) && ! isNaN( Number( size ) ) && ( size || size === 0 )
190
245
  ? Number( size )
191
246
  : DEFAULT_SIZE,
192
- custom: unit === 'custom' ? String( size ) : '',
247
+ custom: unit === 'custom' ? String( size ) : customState,
193
248
  unit,
194
249
  };
195
250
  }
@@ -8,7 +8,7 @@ import { PropKeyProvider, PropProvider, useBoundProp } from '../bound-prop-conte
8
8
  import { ControlFormLabel } from '../components/control-form-label';
9
9
  import { SectionContent } from '../components/section-content';
10
10
  import { createControl } from '../create-control';
11
- import { type Unit } from '../utils/size-control';
11
+ import { type LengthUnit } from '../utils/size-control';
12
12
  import { ColorControl } from './color-control';
13
13
  import { SizeControl } from './size-control';
14
14
 
@@ -18,7 +18,7 @@ type StrokeProps = {
18
18
  children: React.ReactNode;
19
19
  };
20
20
 
21
- const units: Unit[] = [ 'px', 'em', 'rem' ];
21
+ const units: LengthUnit[] = [ 'px', 'em', 'rem' ];
22
22
 
23
23
  export const StrokeControl = createControl( () => {
24
24
  const propContext = useBoundProp( strokePropTypeUtil );