@elementor/editor-controls 0.36.0 → 1.1.0

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 (30) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/index.d.mts +78 -45
  3. package/dist/index.d.ts +78 -45
  4. package/dist/index.js +951 -651
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +890 -596
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +8 -7
  9. package/src/bound-prop-context/use-bound-prop.ts +4 -1
  10. package/src/components/font-family-selector.tsx +23 -164
  11. package/src/components/popover-grid-container.tsx +7 -10
  12. package/src/components/repeater.tsx +24 -10
  13. package/src/components/size-control/size-input.tsx +125 -0
  14. package/src/components/{text-field-inner-selection.tsx → size-control/text-field-inner-selection.tsx} +33 -16
  15. package/src/components/text-field-popover.tsx +47 -0
  16. package/src/controls/background-control/background-overlay/background-image-overlay/background-image-overlay-position.tsx +11 -3
  17. package/src/controls/background-control/background-overlay/background-image-overlay/background-image-overlay-size.tsx +7 -3
  18. package/src/controls/box-shadow-repeater-control.tsx +8 -6
  19. package/src/controls/equal-unequal-sizes-control.tsx +24 -14
  20. package/src/controls/gap-control.tsx +17 -6
  21. package/src/controls/key-value-control.tsx +99 -0
  22. package/src/controls/linked-dimensions-control.tsx +62 -81
  23. package/src/controls/position-control.tsx +109 -0
  24. package/src/controls/repeatable-control.tsx +89 -0
  25. package/src/controls/size-control.tsx +181 -149
  26. package/src/controls/stroke-control.tsx +9 -6
  27. package/src/hooks/use-repeatable-control-context.ts +24 -0
  28. package/src/hooks/use-size-extended-options.ts +21 -0
  29. package/src/index.ts +4 -1
  30. package/src/utils/size-control.ts +10 -0
@@ -0,0 +1,109 @@
1
+ import * as React from 'react';
2
+ import { useMemo } from 'react';
3
+ import { positionPropTypeUtil, stringPropTypeUtil } from '@elementor/editor-props';
4
+ import { MenuListItem } from '@elementor/editor-ui';
5
+ import { isExperimentActive } from '@elementor/editor-v1-adapters';
6
+ import { LetterXIcon, LetterYIcon } from '@elementor/icons';
7
+ import { Grid, Select, type SelectChangeEvent } from '@elementor/ui';
8
+ import { __ } from '@wordpress/i18n';
9
+
10
+ import { PropKeyProvider, PropProvider, useBoundProp } from '../bound-prop-context';
11
+ import { ControlFormLabel } from '../components/control-form-label';
12
+ import { SizeControl } from './size-control';
13
+
14
+ type Positions =
15
+ | 'center center'
16
+ | 'center left'
17
+ | 'center right'
18
+ | 'top center'
19
+ | 'top left'
20
+ | 'top right'
21
+ | 'bottom center'
22
+ | 'bottom left'
23
+ | 'bottom right'
24
+ | 'custom';
25
+
26
+ const positionOptions = [
27
+ { label: __( 'Center center', 'elementor' ), value: 'center center' },
28
+ { label: __( 'Center left', 'elementor' ), value: 'center left' },
29
+ { label: __( 'Center right', 'elementor' ), value: 'center right' },
30
+ { label: __( 'Top center', 'elementor' ), value: 'top center' },
31
+ { label: __( 'Top left', 'elementor' ), value: 'top left' },
32
+ { label: __( 'Top right', 'elementor' ), value: 'top right' },
33
+ { label: __( 'Bottom center', 'elementor' ), value: 'bottom center' },
34
+ { label: __( 'Bottom left', 'elementor' ), value: 'bottom left' },
35
+ { label: __( 'Bottom right', 'elementor' ), value: 'bottom right' },
36
+ ];
37
+
38
+ export const PositionControl = () => {
39
+ const positionContext = useBoundProp( positionPropTypeUtil );
40
+ const stringPropContext = useBoundProp( stringPropTypeUtil );
41
+
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 ] );
54
+
55
+ const handlePositionChange = ( event: SelectChangeEvent< Positions > ) => {
56
+ const value = event.target.value || null;
57
+
58
+ if ( value === 'custom' && isVersion331Active ) {
59
+ positionContext.setValue( { x: null, y: null } );
60
+ } else {
61
+ stringPropContext.setValue( value );
62
+ }
63
+ };
64
+
65
+ return (
66
+ <Grid container spacing={ 1.5 }>
67
+ <Grid item xs={ 12 }>
68
+ <Grid container gap={ 2 } alignItems="center" flexWrap="nowrap">
69
+ <Grid item xs={ 6 }>
70
+ <ControlFormLabel>{ __( 'Object position', 'elementor' ) }</ControlFormLabel>
71
+ </Grid>
72
+ <Grid item xs={ 6 } sx={ { overflow: 'hidden' } }>
73
+ <Select
74
+ size="tiny"
75
+ disabled={ stringPropContext.disabled }
76
+ value={ ( positionContext.value ? 'custom' : stringPropContext.value ) ?? '' }
77
+ onChange={ handlePositionChange }
78
+ fullWidth
79
+ >
80
+ { availablePositionOptions.map( ( { label, value } ) => (
81
+ <MenuListItem key={ value } value={ value ?? '' }>
82
+ { label }
83
+ </MenuListItem>
84
+ ) ) }
85
+ </Select>
86
+ </Grid>
87
+ </Grid>
88
+ </Grid>
89
+ { isCustom && (
90
+ <PropProvider { ...positionContext }>
91
+ <Grid item xs={ 12 }>
92
+ <Grid container spacing={ 1.5 }>
93
+ <Grid item xs={ 6 }>
94
+ <PropKeyProvider bind={ 'x' }>
95
+ <SizeControl startIcon={ <LetterXIcon fontSize={ 'tiny' } /> } />
96
+ </PropKeyProvider>
97
+ </Grid>
98
+ <Grid item xs={ 6 }>
99
+ <PropKeyProvider bind={ 'y' }>
100
+ <SizeControl startIcon={ <LetterYIcon fontSize={ 'tiny' } /> } />
101
+ </PropKeyProvider>
102
+ </Grid>
103
+ </Grid>
104
+ </Grid>
105
+ </PropProvider>
106
+ ) }
107
+ </Grid>
108
+ );
109
+ };
@@ -0,0 +1,89 @@
1
+ import * as React from 'react';
2
+ import { useMemo } from 'react';
3
+ import { createArrayPropUtils, type PropKey } from '@elementor/editor-props';
4
+ import { __ } from '@wordpress/i18n';
5
+
6
+ import { PropKeyProvider, PropProvider, useBoundProp } from '../bound-prop-context';
7
+ import { PopoverContent } from '../components/popover-content';
8
+ import { PopoverGridContainer } from '../components/popover-grid-container';
9
+ import { Repeater } from '../components/repeater';
10
+ import { createControl } from '../create-control';
11
+ import {
12
+ type ChildControlConfig,
13
+ RepeatableControlContext,
14
+ useRepeatableControlContext,
15
+ } from '../hooks/use-repeatable-control-context';
16
+
17
+ type RepeatableControlProps = {
18
+ label: string;
19
+ childControlConfig: ChildControlConfig;
20
+ showDuplicate?: boolean;
21
+ showToggle?: boolean;
22
+ };
23
+
24
+ export const RepeatableControl = createControl(
25
+ ( { label, childControlConfig, showDuplicate, showToggle }: RepeatableControlProps ) => {
26
+ const { propTypeUtil: childPropTypeUtil } = childControlConfig;
27
+
28
+ if ( ! childPropTypeUtil ) {
29
+ return null;
30
+ }
31
+
32
+ const childArrayPropTypeUtil = useMemo(
33
+ () => createArrayPropUtils( childPropTypeUtil.key, childPropTypeUtil.schema ),
34
+ [ childPropTypeUtil.key, childPropTypeUtil.schema ]
35
+ );
36
+
37
+ const { propType, value, setValue } = useBoundProp( childArrayPropTypeUtil );
38
+
39
+ return (
40
+ <PropProvider propType={ propType } value={ value } setValue={ setValue }>
41
+ <RepeatableControlContext.Provider value={ childControlConfig }>
42
+ <Repeater
43
+ openOnAdd
44
+ values={ value ?? [] }
45
+ setValues={ setValue }
46
+ label={ label }
47
+ itemSettings={ {
48
+ Icon: ItemIcon,
49
+ Label: ItemLabel,
50
+ Content: ItemContent,
51
+ initialValues: childPropTypeUtil.create( null ),
52
+ } }
53
+ showDuplicate={ showDuplicate }
54
+ showToggle={ showToggle }
55
+ />
56
+ </RepeatableControlContext.Provider>
57
+ </PropProvider>
58
+ );
59
+ }
60
+ );
61
+
62
+ const ItemContent = ( { bind }: { bind: PropKey } ) => {
63
+ return (
64
+ <PropKeyProvider bind={ bind }>
65
+ <Content />
66
+ </PropKeyProvider>
67
+ );
68
+ };
69
+
70
+ // TODO: Configurable icon probably can be somehow part of the injected control and bubbled up to the repeater
71
+ const ItemIcon = () => <></>;
72
+
73
+ const Content = () => {
74
+ const { component: ChildControl, props = {} } = useRepeatableControlContext();
75
+
76
+ return (
77
+ <PopoverContent p={ 1.5 }>
78
+ <PopoverGridContainer>
79
+ <ChildControl { ...props } />
80
+ </PopoverGridContainer>
81
+ </PopoverContent>
82
+ );
83
+ };
84
+
85
+ const ItemLabel = () => {
86
+ const { label = __( 'Empty', 'elementor' ) } = useRepeatableControlContext();
87
+
88
+ return <span>{ label }</span>;
89
+ };
@@ -1,183 +1,215 @@
1
1
  import * as React from 'react';
2
- import { useRef } from 'react';
3
- import { sizePropTypeUtil, stringPropTypeUtil } from '@elementor/editor-props';
4
- import { Box, InputAdornment } from '@elementor/ui';
2
+ import { type MutableRefObject, useEffect, useState } from 'react';
3
+ import { sizePropTypeUtil, type SizePropValue } from '@elementor/editor-props';
4
+ import { useActiveBreakpoint } from '@elementor/editor-responsive';
5
+ import { usePopupState } from '@elementor/ui';
5
6
 
6
7
  import { useBoundProp } from '../bound-prop-context';
7
- import { SelectionEndAdornment, TextFieldInnerSelection } from '../components/text-field-inner-selection';
8
- import ControlActions from '../control-actions/control-actions';
8
+ import { SizeInput } from '../components/size-control/size-input';
9
+ import { TextFieldPopover } from '../components/text-field-popover';
9
10
  import { createControl } from '../create-control';
11
+ import { useSizeExtendedOptions } from '../hooks/use-size-extended-options';
10
12
  import { useSyncExternalState } from '../hooks/use-sync-external-state';
13
+ import { defaultUnits, type ExtendedOption, isUnitExtendedOption, type Unit } from '../utils/size-control';
11
14
 
12
- export type ExtendedValue = 'auto';
13
- export type Unit = 'px' | '%' | 'em' | 'rem' | 'vw' | 'vh';
15
+ const DEFAULT_UNIT = 'px';
16
+ const DEFAULT_SIZE = NaN;
14
17
 
15
- const defaultUnits: Unit[] = [ 'px', '%', 'em', 'rem', 'vw', 'vh' ];
16
-
17
- const defaultUnit = 'px';
18
- const defaultSize = NaN;
18
+ type SizeValue = SizePropValue[ 'value' ];
19
19
 
20
20
  type SizeControlProps = {
21
21
  placeholder?: string;
22
22
  startIcon?: React.ReactNode;
23
23
  units?: Unit[];
24
- extendedValues?: ExtendedValue[];
24
+ extendedOptions?: ExtendedOption[];
25
+ disableCustom?: boolean;
26
+ anchorRef?: MutableRefObject< HTMLElement | undefined >;
27
+ defaultUnit?: Unit;
25
28
  };
26
29
 
27
- export const SizeControl = createControl(
28
- ( { units = defaultUnits, extendedValues = [], placeholder, startIcon }: SizeControlProps ) => {
29
- const { value: sizeValue, setValue: setSizeValue, restoreValue, disabled } = useBoundProp( sizePropTypeUtil );
30
-
31
- const [ state, setState ] = useSyncExternalState( {
32
- external: sizeValue,
33
- setExternal: setSizeValue,
34
- persistWhen: ( controlValue ) => !! controlValue?.size || controlValue?.size === 0,
35
- fallback: ( controlValue ) => ( { unit: controlValue?.unit || defaultUnit, size: defaultSize } ),
36
- } );
37
-
38
- const handleUnitChange = ( unit: Unit ) => {
39
- setState( ( prev ) => ( {
40
- size: prev?.size ?? defaultSize,
41
- unit,
42
- } ) );
43
- };
44
-
45
- const handleSizeChange = ( event: React.ChangeEvent< HTMLInputElement > ) => {
46
- const { value: size } = event.target;
47
-
48
- setState( ( prev ) => ( {
49
- ...prev,
50
- size: size || size === '0' ? parseFloat( size ) : defaultSize,
51
- } ) );
52
- };
53
-
54
- const Input = extendedValues?.length ? ExtendedSizeInput : SizeInput;
55
-
56
- return (
57
- <Input
58
- disabled={ disabled }
59
- size={ state.size }
60
- unit={ state.unit }
61
- placeholder={ placeholder }
62
- startIcon={ startIcon }
63
- units={ units }
64
- extendedValues={ extendedValues }
65
- handleSizeChange={ handleSizeChange }
66
- handleUnitChange={ handleUnitChange }
67
- onBlur={ restoreValue }
68
- />
69
- );
70
- }
71
- );
30
+ type State = {
31
+ numeric: number;
32
+ custom: string;
33
+ unit: Unit | ExtendedOption;
34
+ };
72
35
 
73
- const ExtendedSizeInput = ( props: SizeInputProps ) => {
74
- const { value: stringValue, setValue: setStringValue } = useBoundProp( stringPropTypeUtil );
75
- const { extendedValues = [] } = props;
36
+ export const SizeControl = createControl( ( props: SizeControlProps ) => {
37
+ const defaultUnit = props.defaultUnit ?? DEFAULT_UNIT;
38
+ const { units = [ ...defaultUnits ], placeholder, startIcon, anchorRef } = props;
39
+ const { value: sizeValue, setValue: setSizeValue, disabled, restoreValue } = useBoundProp( sizePropTypeUtil );
40
+ const [ internalState, setInternalState ] = useState( createStateFromSizeProp( sizeValue, defaultUnit ) );
41
+ const activeBreakpoint = useActiveBreakpoint();
42
+
43
+ const extendedOptions = useSizeExtendedOptions( props.extendedOptions || [], props.disableCustom ?? false );
44
+ const popupState = usePopupState( { variant: 'popover' } );
45
+
46
+ const [ state, setState ] = useSyncExternalState( {
47
+ external: internalState,
48
+ setExternal: ( newState: State | null ) => setSizeValue( extractValueFromState( newState ) ),
49
+ persistWhen: ( newState ) => {
50
+ if ( ! newState?.unit ) {
51
+ return false;
52
+ }
53
+
54
+ if ( isUnitExtendedOption( newState.unit ) ) {
55
+ return newState.unit === 'auto' ? true : !! newState.custom;
56
+ }
57
+
58
+ return !! newState?.numeric || newState?.numeric === 0;
59
+ },
60
+ fallback: ( newState ) => ( {
61
+ unit: newState?.unit ?? props.defaultUnit ?? DEFAULT_UNIT,
62
+ numeric: newState?.numeric ?? DEFAULT_SIZE,
63
+ custom: newState?.custom ?? '',
64
+ } ),
65
+ } );
66
+
67
+ const { size: controlSize = DEFAULT_SIZE, unit: controlUnit = DEFAULT_UNIT } = extractValueFromState( state ) || {};
68
+
69
+ const handleUnitChange = ( newUnit: Unit | ExtendedOption ) => {
70
+ if ( newUnit === 'custom' ) {
71
+ popupState.open( anchorRef?.current );
72
+ }
76
73
 
77
- const unit = ( stringValue ?? props.unit ) as Unit;
74
+ setState( ( prev ) => ( { ...prev, unit: newUnit } ) );
75
+ };
78
76
 
79
- const handleUnitChange = ( newUnit: Unit ) => {
80
- if ( extendedValues.includes( newUnit as ExtendedValue ) ) {
81
- setStringValue( newUnit );
82
- } else {
83
- props.handleUnitChange( newUnit );
77
+ const handleSizeChange = ( event: React.ChangeEvent< HTMLInputElement > ) => {
78
+ const { value: size } = event.target;
79
+
80
+ if ( controlUnit === 'auto' ) {
81
+ setState( ( prev ) => ( { ...prev, unit: controlUnit } ) );
82
+
83
+ return;
84
84
  }
85
+
86
+ setState( ( prev ) => ( {
87
+ ...prev,
88
+ [ controlUnit === 'custom' ? 'custom' : 'numeric' ]: formatSize( size, controlUnit ),
89
+ unit: controlUnit,
90
+ } ) );
85
91
  };
86
92
 
87
- return (
88
- <SizeInput
89
- { ...props }
90
- units={ [ ...props.units, ...( extendedValues as unknown as Unit[] ) ] }
91
- handleUnitChange={ handleUnitChange }
92
- unit={ unit }
93
- />
94
- );
95
- };
93
+ const onInputFocus = ( event: React.FocusEvent< HTMLInputElement > ) => {
94
+ if ( isUnitExtendedOption( state.unit ) ) {
95
+ ( event.target as HTMLElement )?.blur();
96
+ }
97
+ };
96
98
 
97
- type SizeInputProps = {
98
- unit: Unit;
99
- size: number;
100
- placeholder?: string;
101
- startIcon?: React.ReactNode;
102
- units: Unit[];
103
- extendedValues?: ExtendedValue[];
104
- onBlur?: ( event: React.FocusEvent< HTMLInputElement > ) => void;
105
- handleUnitChange: ( unit: Unit ) => void;
106
- handleSizeChange: ( event: React.ChangeEvent< HTMLInputElement > ) => void;
107
- disabled?: boolean;
108
- };
99
+ const onInputClick = ( event: React.MouseEvent ) => {
100
+ if ( ( event.target as HTMLElement ).closest( 'input' ) && 'custom' === state.unit ) {
101
+ popupState.open( anchorRef?.current );
102
+ }
103
+ };
109
104
 
110
- const RESTRICTED_INPUT_KEYS = [ 'e', 'E', '+', '-' ];
111
-
112
- const SizeInput = ( {
113
- units,
114
- handleUnitChange,
115
- handleSizeChange,
116
- placeholder,
117
- startIcon,
118
- onBlur,
119
- size,
120
- unit,
121
- disabled,
122
- }: SizeInputProps ) => {
123
- const unitInputBufferRef = useRef( '' );
124
-
125
- const handleKeyUp = ( event: React.KeyboardEvent< HTMLInputElement > ) => {
126
- const { key } = event;
127
-
128
- if ( ! /^[a-zA-Z%]$/.test( key ) ) {
105
+ useEffect( () => {
106
+ const newState = createStateFromSizeProp( sizeValue, defaultUnit );
107
+ const currentUnit = isUnitExtendedOption( state.unit ) ? 'custom' : 'numeric';
108
+ const mergedStates = { ...state, [ currentUnit ]: newState[ currentUnit ] };
109
+
110
+ if ( mergedStates.unit !== 'auto' && areStatesEqual( state, mergedStates ) ) {
129
111
  return;
130
112
  }
131
113
 
132
- event.preventDefault();
114
+ if ( state.unit === newState.unit ) {
115
+ setInternalState( mergedStates );
116
+
117
+ return;
118
+ }
133
119
 
134
- const newChar = key.toLowerCase();
135
- const updatedBuffer = ( unitInputBufferRef.current + newChar ).slice( -3 );
136
- unitInputBufferRef.current = updatedBuffer;
120
+ setState( newState );
121
+ // eslint-disable-next-line react-hooks/exhaustive-deps
122
+ }, [ sizeValue ] );
137
123
 
138
- const matchedUnit =
139
- units.find( ( u ) => u.includes( updatedBuffer ) ) ||
140
- units.find( ( u ) => u.startsWith( newChar ) ) ||
141
- units.find( ( u ) => u.includes( newChar ) );
124
+ useEffect( () => {
125
+ const newState = createStateFromSizeProp( sizeValue, defaultUnit );
142
126
 
143
- if ( matchedUnit ) {
144
- handleUnitChange( matchedUnit );
127
+ if ( activeBreakpoint && ! areStatesEqual( newState, state ) ) {
128
+ setState( newState );
145
129
  }
146
- };
130
+ // eslint-disable-next-line react-hooks/exhaustive-deps
131
+ }, [ activeBreakpoint ] );
147
132
 
148
133
  return (
149
- <ControlActions>
150
- <Box>
151
- <TextFieldInnerSelection
152
- disabled={ disabled }
153
- endAdornment={
154
- <SelectionEndAdornment
155
- disabled={ disabled }
156
- options={ units }
157
- onClick={ handleUnitChange }
158
- value={ unit ?? defaultUnit }
159
- />
160
- }
161
- placeholder={ placeholder }
162
- startAdornment={
163
- startIcon ? (
164
- <InputAdornment position="start" disabled={ disabled }>
165
- { startIcon }
166
- </InputAdornment>
167
- ) : undefined
168
- }
169
- type="number"
170
- value={ Number.isNaN( size ) ? '' : size }
134
+ <>
135
+ <SizeInput
136
+ disabled={ disabled }
137
+ size={ controlSize }
138
+ unit={ controlUnit }
139
+ units={ [ ...units, ...( extendedOptions || [] ) ] }
140
+ placeholder={ placeholder }
141
+ startIcon={ startIcon }
142
+ handleSizeChange={ handleSizeChange }
143
+ handleUnitChange={ handleUnitChange }
144
+ onBlur={ restoreValue }
145
+ onFocus={ onInputFocus }
146
+ onClick={ onInputClick }
147
+ popupState={ popupState }
148
+ />
149
+ { anchorRef?.current && (
150
+ <TextFieldPopover
151
+ popupState={ popupState }
152
+ anchorRef={ anchorRef as MutableRefObject< HTMLElement > }
153
+ restoreValue={ restoreValue }
154
+ value={ controlSize as string }
171
155
  onChange={ handleSizeChange }
172
- onBlur={ onBlur }
173
- onKeyDown={ ( event ) => {
174
- if ( RESTRICTED_INPUT_KEYS.includes( event.key ) ) {
175
- event.preventDefault();
176
- }
177
- } }
178
- onKeyUp={ handleKeyUp }
179
156
  />
180
- </Box>
181
- </ControlActions>
157
+ ) }
158
+ </>
182
159
  );
183
- };
160
+ } );
161
+
162
+ function formatSize< TSize extends string | number >( size: TSize, unit: Unit | ExtendedOption ): TSize {
163
+ if ( isUnitExtendedOption( unit ) ) {
164
+ return unit === 'auto' ? ( '' as TSize ) : ( String( size ?? '' ) as TSize );
165
+ }
166
+
167
+ return size || size === 0 ? ( Number( size ) as TSize ) : ( NaN as TSize );
168
+ }
169
+
170
+ function createStateFromSizeProp( sizeValue: SizeValue | null, defaultUnit: Unit ): State {
171
+ const unit = sizeValue?.unit ?? defaultUnit;
172
+ const size = sizeValue?.size ?? '';
173
+
174
+ return {
175
+ numeric:
176
+ ! isUnitExtendedOption( unit ) && ! isNaN( Number( size ) ) && ( size || size === 0 )
177
+ ? Number( size )
178
+ : DEFAULT_SIZE,
179
+ custom: unit === 'custom' ? String( size ) : '',
180
+ unit,
181
+ };
182
+ }
183
+
184
+ function extractValueFromState( state: State | null ): SizeValue | null {
185
+ if ( ! state ) {
186
+ return null;
187
+ }
188
+
189
+ if ( ! state?.unit ) {
190
+ return { size: DEFAULT_SIZE, unit: DEFAULT_UNIT };
191
+ }
192
+
193
+ const { unit } = state;
194
+
195
+ if ( unit === 'auto' ) {
196
+ return { size: '', unit };
197
+ }
198
+
199
+ return {
200
+ size: state[ unit === 'custom' ? 'custom' : 'numeric' ],
201
+ unit,
202
+ } as SizeValue;
203
+ }
204
+
205
+ function areStatesEqual( state1: State, state2: State ): boolean {
206
+ if ( state1.unit !== state2.unit || state1.custom !== state2.custom ) {
207
+ return false;
208
+ }
209
+
210
+ if ( isUnitExtendedOption( state1.unit ) ) {
211
+ return state1.custom === state2.custom;
212
+ }
213
+
214
+ return state1.numeric === state2.numeric || ( isNaN( state1.numeric ) && isNaN( state2.numeric ) );
215
+ }
@@ -1,4 +1,5 @@
1
1
  import * as React from 'react';
2
+ import { forwardRef, type MutableRefObject, useRef } from 'react';
2
3
  import { strokePropTypeUtil } from '@elementor/editor-props';
3
4
  import { Grid } from '@elementor/ui';
4
5
  import { __ } from '@wordpress/i18n';
@@ -7,8 +8,9 @@ import { PropKeyProvider, PropProvider, useBoundProp } from '../bound-prop-conte
7
8
  import { ControlFormLabel } from '../components/control-form-label';
8
9
  import { SectionContent } from '../components/section-content';
9
10
  import { createControl } from '../create-control';
11
+ import { type Unit } from '../utils/size-control';
10
12
  import { ColorControl } from './color-control';
11
- import { SizeControl, type Unit } from './size-control';
13
+ import { SizeControl } from './size-control';
12
14
 
13
15
  type StrokeProps = {
14
16
  bind: string;
@@ -20,12 +22,13 @@ const units: Unit[] = [ 'px', 'em', 'rem' ];
20
22
 
21
23
  export const StrokeControl = createControl( () => {
22
24
  const propContext = useBoundProp( strokePropTypeUtil );
25
+ const rowRef: MutableRefObject< HTMLElement | undefined > = useRef();
23
26
 
24
27
  return (
25
28
  <PropProvider { ...propContext }>
26
29
  <SectionContent>
27
- <Control bind="width" label={ __( 'Stroke width', 'elementor' ) }>
28
- <SizeControl units={ units } />
30
+ <Control bind="width" label={ __( 'Stroke width', 'elementor' ) } ref={ rowRef }>
31
+ <SizeControl units={ units } anchorRef={ rowRef } />
29
32
  </Control>
30
33
  <Control bind="color" label={ __( 'Stroke color', 'elementor' ) }>
31
34
  <ColorControl />
@@ -35,9 +38,9 @@ export const StrokeControl = createControl( () => {
35
38
  );
36
39
  } );
37
40
 
38
- const Control = ( { bind, label, children }: StrokeProps ) => (
41
+ const Control = forwardRef( ( { bind, label, children }: StrokeProps, ref ) => (
39
42
  <PropKeyProvider bind={ bind }>
40
- <Grid container gap={ 2 } alignItems="center" flexWrap="nowrap">
43
+ <Grid container gap={ 2 } alignItems="center" flexWrap="nowrap" ref={ ref }>
41
44
  <Grid item xs={ 6 }>
42
45
  <ControlFormLabel>{ label }</ControlFormLabel>
43
46
  </Grid>
@@ -46,4 +49,4 @@ const Control = ( { bind, label, children }: StrokeProps ) => (
46
49
  </Grid>
47
50
  </Grid>
48
51
  </PropKeyProvider>
49
- );
52
+ ) );
@@ -0,0 +1,24 @@
1
+ import { createContext, useContext } from 'react';
2
+ import { type PropTypeUtil } from '@elementor/editor-props';
3
+
4
+ export type ChildControlConfig = {
5
+ component: React.ComponentType;
6
+ props?: Record< string, unknown >;
7
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8
+ propTypeUtil: PropTypeUtil< string, any >;
9
+ label?: string;
10
+ };
11
+
12
+ const RepeatableControlContext = createContext< ChildControlConfig | undefined >( undefined );
13
+
14
+ const useRepeatableControlContext = () => {
15
+ const context = useContext( RepeatableControlContext );
16
+
17
+ if ( ! context ) {
18
+ throw new Error( 'useRepeatableControlContext must be used within RepeatableControl' );
19
+ }
20
+
21
+ return context;
22
+ };
23
+
24
+ export { RepeatableControlContext, useRepeatableControlContext };
@@ -0,0 +1,21 @@
1
+ import { useMemo } from 'react';
2
+ import { type ExtendedOption } from '@elementor/editor-controls';
3
+ import { isExperimentActive } from '@elementor/editor-v1-adapters';
4
+
5
+ const EXPERIMENT_ID = 'e_v_3_30';
6
+
7
+ export function useSizeExtendedOptions( options: ExtendedOption[], disableCustom: boolean ) {
8
+ return useMemo( () => {
9
+ const isVersion330Active = isExperimentActive( EXPERIMENT_ID );
10
+ const shouldDisableCustom = ! isVersion330Active || disableCustom;
11
+ const extendedOptions = [ ...options ];
12
+
13
+ if ( ! shouldDisableCustom && ! extendedOptions.includes( 'custom' ) ) {
14
+ extendedOptions.push( 'custom' );
15
+ } else if ( options.includes( 'custom' ) ) {
16
+ extendedOptions.splice( extendedOptions.indexOf( 'custom' ), 1 );
17
+ }
18
+
19
+ return extendedOptions;
20
+ }, [ options, disableCustom ] );
21
+ }
package/src/index.ts CHANGED
@@ -19,6 +19,9 @@ export { AspectRatioControl } from './controls/aspect-ratio-control';
19
19
  export { SvgMediaControl } from './controls/svg-media-control';
20
20
  export { BackgroundControl } from './controls/background-control/background-control';
21
21
  export { SwitchControl } from './controls/switch-control';
22
+ export { RepeatableControl } from './controls/repeatable-control';
23
+ export { KeyValueControl } from './controls/key-value-control';
24
+ export { PositionControl } from './controls/position-control';
22
25
 
23
26
  // components
24
27
  export { ControlFormLabel } from './components/control-form-label';
@@ -32,7 +35,7 @@ export type { EqualUnequalItems } from './controls/equal-unequal-sizes-control';
32
35
  export type { ControlActionsItems } from './control-actions/control-actions-context';
33
36
  export type { PropProviderProps } from './bound-prop-context';
34
37
  export type { SetValue } from './bound-prop-context/prop-context';
35
- export type { ExtendedValue } from './controls/size-control';
38
+ export type { ExtendedOption } from './utils/size-control';
36
39
  export type { ToggleControlProps } from './controls/toggle-control';
37
40
  export type { FontCategory } from './controls/font-family-control/font-family-control';
38
41