@elementor/editor-controls 0.35.0 → 1.0.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 (33) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/index.d.mts +24 -13
  3. package/dist/index.d.ts +24 -13
  4. package/dist/index.js +809 -558
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +748 -491
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +5 -4
  9. package/src/bound-prop-context/use-bound-prop.ts +4 -1
  10. package/src/components/control-form-label.tsx +3 -3
  11. package/src/components/control-label.tsx +1 -1
  12. package/src/components/font-family-selector.tsx +8 -10
  13. package/src/components/popover-grid-container.tsx +7 -10
  14. package/src/components/repeater.tsx +2 -4
  15. package/src/components/size-control/size-input.tsx +125 -0
  16. package/src/components/{text-field-inner-selection.tsx → size-control/text-field-inner-selection.tsx} +33 -16
  17. package/src/components/sortable.tsx +4 -2
  18. package/src/components/text-field-popover.tsx +47 -0
  19. package/src/controls/background-control/background-overlay/background-image-overlay/background-image-overlay-position.tsx +14 -5
  20. package/src/controls/background-control/background-overlay/background-image-overlay/background-image-overlay-size.tsx +9 -4
  21. package/src/controls/background-control/background-overlay/background-overlay-repeater-control.tsx +1 -1
  22. package/src/controls/box-shadow-repeater-control.tsx +11 -9
  23. package/src/controls/equal-unequal-sizes-control.tsx +38 -18
  24. package/src/controls/font-family-control/font-family-control.tsx +3 -1
  25. package/src/controls/gap-control.tsx +20 -7
  26. package/src/controls/image-control.tsx +2 -2
  27. package/src/controls/link-control.tsx +1 -1
  28. package/src/controls/linked-dimensions-control.tsx +71 -83
  29. package/src/controls/size-control.tsx +179 -149
  30. package/src/controls/stroke-control.tsx +9 -6
  31. package/src/hooks/use-size-extended-options.ts +21 -0
  32. package/src/index.ts +2 -1
  33. package/src/utils/size-control.ts +10 -0
@@ -1,183 +1,213 @@
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 >;
25
27
  };
26
28
 
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
- );
29
+ type State = {
30
+ numeric: number;
31
+ custom: string;
32
+ unit: Unit | ExtendedOption;
33
+ };
72
34
 
73
- const ExtendedSizeInput = ( props: SizeInputProps ) => {
74
- const { value: stringValue, setValue: setStringValue } = useBoundProp( stringPropTypeUtil );
75
- const { extendedValues = [] } = props;
35
+ export const SizeControl = createControl( ( props: SizeControlProps ) => {
36
+ const { units = [ ...defaultUnits ], placeholder, startIcon, anchorRef } = props;
37
+ const { value: sizeValue, setValue: setSizeValue, disabled, restoreValue } = useBoundProp( sizePropTypeUtil );
38
+ const [ internalState, setInternalState ] = useState( createStateFromSizeProp( sizeValue ) );
39
+ const activeBreakpoint = useActiveBreakpoint();
40
+
41
+ const extendedOptions = useSizeExtendedOptions( props.extendedOptions || [], props.disableCustom ?? false );
42
+ const popupState = usePopupState( { variant: 'popover' } );
43
+
44
+ const [ state, setState ] = useSyncExternalState( {
45
+ external: internalState,
46
+ setExternal: ( newState: State | null ) => setSizeValue( extractValueFromState( newState ) ),
47
+ persistWhen: ( newState ) => {
48
+ if ( ! newState?.unit ) {
49
+ return false;
50
+ }
51
+
52
+ if ( isUnitExtendedOption( newState.unit ) ) {
53
+ return newState.unit === 'auto' ? true : !! newState.custom;
54
+ }
55
+
56
+ return !! newState?.numeric || newState?.numeric === 0;
57
+ },
58
+ fallback: ( newState ) => ( {
59
+ unit: newState?.unit ?? DEFAULT_UNIT,
60
+ numeric: newState?.numeric ?? DEFAULT_SIZE,
61
+ custom: newState?.custom ?? '',
62
+ } ),
63
+ } );
64
+
65
+ const { size: controlSize = DEFAULT_SIZE, unit: controlUnit = DEFAULT_UNIT } = extractValueFromState( state ) || {};
66
+
67
+ const handleUnitChange = ( newUnit: Unit | ExtendedOption ) => {
68
+ if ( newUnit === 'custom' ) {
69
+ popupState.open( anchorRef?.current );
70
+ }
76
71
 
77
- const unit = ( stringValue ?? props.unit ) as Unit;
72
+ setState( ( prev ) => ( { ...prev, unit: newUnit } ) );
73
+ };
78
74
 
79
- const handleUnitChange = ( newUnit: Unit ) => {
80
- if ( extendedValues.includes( newUnit as ExtendedValue ) ) {
81
- setStringValue( newUnit );
82
- } else {
83
- props.handleUnitChange( newUnit );
75
+ const handleSizeChange = ( event: React.ChangeEvent< HTMLInputElement > ) => {
76
+ const { value: size } = event.target;
77
+
78
+ if ( controlUnit === 'auto' ) {
79
+ setState( ( prev ) => ( { ...prev, unit: controlUnit } ) );
80
+
81
+ return;
84
82
  }
83
+
84
+ setState( ( prev ) => ( {
85
+ ...prev,
86
+ [ controlUnit === 'custom' ? 'custom' : 'numeric' ]: formatSize( size, controlUnit ),
87
+ unit: controlUnit,
88
+ } ) );
85
89
  };
86
90
 
87
- return (
88
- <SizeInput
89
- { ...props }
90
- units={ [ ...props.units, ...( extendedValues as unknown as Unit[] ) ] }
91
- handleUnitChange={ handleUnitChange }
92
- unit={ unit }
93
- />
94
- );
95
- };
91
+ const onInputFocus = ( event: React.FocusEvent< HTMLInputElement > ) => {
92
+ if ( isUnitExtendedOption( state.unit ) ) {
93
+ ( event.target as HTMLElement )?.blur();
94
+ }
95
+ };
96
96
 
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
- };
97
+ const onInputClick = ( event: React.MouseEvent ) => {
98
+ if ( ( event.target as HTMLElement ).closest( 'input' ) && 'custom' === state.unit ) {
99
+ popupState.open( anchorRef?.current );
100
+ }
101
+ };
109
102
 
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 ) ) {
103
+ useEffect( () => {
104
+ const newState = createStateFromSizeProp( sizeValue );
105
+ const currentUnit = isUnitExtendedOption( state.unit ) ? 'custom' : 'numeric';
106
+ const mergedStates = { ...state, [ currentUnit ]: newState[ currentUnit ] };
107
+
108
+ if ( mergedStates.unit !== 'auto' && areStatesEqual( state, mergedStates ) ) {
129
109
  return;
130
110
  }
131
111
 
132
- event.preventDefault();
112
+ if ( state.unit === newState.unit ) {
113
+ setInternalState( mergedStates );
114
+
115
+ return;
116
+ }
133
117
 
134
- const newChar = key.toLowerCase();
135
- const updatedBuffer = ( unitInputBufferRef.current + newChar ).slice( -3 );
136
- unitInputBufferRef.current = updatedBuffer;
118
+ setState( newState );
119
+ // eslint-disable-next-line react-hooks/exhaustive-deps
120
+ }, [ sizeValue ] );
137
121
 
138
- const matchedUnit =
139
- units.find( ( u ) => u.includes( updatedBuffer ) ) ||
140
- units.find( ( u ) => u.startsWith( newChar ) ) ||
141
- units.find( ( u ) => u.includes( newChar ) );
122
+ useEffect( () => {
123
+ const newState = createStateFromSizeProp( sizeValue );
142
124
 
143
- if ( matchedUnit ) {
144
- handleUnitChange( matchedUnit );
125
+ if ( activeBreakpoint && ! areStatesEqual( newState, state ) ) {
126
+ setState( newState );
145
127
  }
146
- };
128
+ // eslint-disable-next-line react-hooks/exhaustive-deps
129
+ }, [ activeBreakpoint ] );
147
130
 
148
131
  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 }
132
+ <>
133
+ <SizeInput
134
+ disabled={ disabled }
135
+ size={ controlSize }
136
+ unit={ controlUnit }
137
+ units={ [ ...units, ...( extendedOptions || [] ) ] }
138
+ placeholder={ placeholder }
139
+ startIcon={ startIcon }
140
+ handleSizeChange={ handleSizeChange }
141
+ handleUnitChange={ handleUnitChange }
142
+ onBlur={ restoreValue }
143
+ onFocus={ onInputFocus }
144
+ onClick={ onInputClick }
145
+ popupState={ popupState }
146
+ />
147
+ { anchorRef?.current && (
148
+ <TextFieldPopover
149
+ popupState={ popupState }
150
+ anchorRef={ anchorRef as MutableRefObject< HTMLElement > }
151
+ restoreValue={ restoreValue }
152
+ value={ controlSize as string }
171
153
  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
154
  />
180
- </Box>
181
- </ControlActions>
155
+ ) }
156
+ </>
182
157
  );
183
- };
158
+ } );
159
+
160
+ function formatSize< TSize extends string | number >( size: TSize, unit: Unit | ExtendedOption ): TSize {
161
+ if ( isUnitExtendedOption( unit ) ) {
162
+ return unit === 'auto' ? ( '' as TSize ) : ( String( size ?? '' ) as TSize );
163
+ }
164
+
165
+ return size || size === 0 ? ( Number( size ) as TSize ) : ( NaN as TSize );
166
+ }
167
+
168
+ function createStateFromSizeProp( sizeValue: SizeValue | null ): State {
169
+ const unit = sizeValue?.unit ?? DEFAULT_UNIT;
170
+ const size = sizeValue?.size ?? '';
171
+
172
+ return {
173
+ numeric:
174
+ ! isUnitExtendedOption( unit ) && ! isNaN( Number( size ) ) && ( size || size === 0 )
175
+ ? Number( size )
176
+ : DEFAULT_SIZE,
177
+ custom: unit === 'custom' ? String( size ) : '',
178
+ unit,
179
+ };
180
+ }
181
+
182
+ function extractValueFromState( state: State | null ): SizeValue | null {
183
+ if ( ! state ) {
184
+ return null;
185
+ }
186
+
187
+ if ( ! state?.unit ) {
188
+ return { size: DEFAULT_SIZE, unit: DEFAULT_UNIT };
189
+ }
190
+
191
+ const { unit } = state;
192
+
193
+ if ( unit === 'auto' ) {
194
+ return { size: '', unit };
195
+ }
196
+
197
+ return {
198
+ size: state[ unit === 'custom' ? 'custom' : 'numeric' ],
199
+ unit,
200
+ } as SizeValue;
201
+ }
202
+
203
+ function areStatesEqual( state1: State, state2: State ): boolean {
204
+ if ( state1.unit !== state2.unit || state1.custom !== state2.custom ) {
205
+ return false;
206
+ }
207
+
208
+ if ( isUnitExtendedOption( state1.unit ) ) {
209
+ return state1.custom === state2.custom;
210
+ }
211
+
212
+ return state1.numeric === state2.numeric || ( isNaN( state1.numeric ) && isNaN( state2.numeric ) );
213
+ }
@@ -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,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
@@ -23,6 +23,7 @@ export { SwitchControl } from './controls/switch-control';
23
23
  // components
24
24
  export { ControlFormLabel } from './components/control-form-label';
25
25
  export { ControlToggleButtonGroup } from './components/control-toggle-button-group';
26
+ export { FontFamilySelector } from './components/font-family-selector';
26
27
 
27
28
  // types
28
29
  export type { ControlComponent } from './create-control';
@@ -31,7 +32,7 @@ export type { EqualUnequalItems } from './controls/equal-unequal-sizes-control';
31
32
  export type { ControlActionsItems } from './control-actions/control-actions-context';
32
33
  export type { PropProviderProps } from './bound-prop-context';
33
34
  export type { SetValue } from './bound-prop-context/prop-context';
34
- export type { ExtendedValue } from './controls/size-control';
35
+ export type { ExtendedOption } from './utils/size-control';
35
36
  export type { ToggleControlProps } from './controls/toggle-control';
36
37
  export type { FontCategory } from './controls/font-family-control/font-family-control';
37
38
 
@@ -0,0 +1,10 @@
1
+ export const defaultUnits = [ 'px', '%', 'em', 'rem', 'vw', 'vh' ] as const;
2
+ const defaultExtendedOptions = [ 'auto', 'custom' ] as const;
3
+
4
+ export type Unit = ( typeof defaultUnits )[ number ];
5
+
6
+ export type ExtendedOption = ( typeof defaultExtendedOptions )[ number ];
7
+
8
+ export function isUnitExtendedOption( unit: Unit | ExtendedOption ): unit is ExtendedOption {
9
+ return defaultExtendedOptions.includes( unit as ExtendedOption );
10
+ }