@elementor/editor-controls 3.33.0-99 → 3.34.2

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 (94) hide show
  1. package/dist/index.d.mts +264 -74
  2. package/dist/index.d.ts +264 -74
  3. package/dist/index.js +2541 -1861
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +2344 -1660
  6. package/dist/index.mjs.map +1 -1
  7. package/package.json +31 -17
  8. package/src/bound-prop-context/prop-context.tsx +8 -1
  9. package/src/bound-prop-context/use-bound-prop.ts +19 -5
  10. package/src/components/autocomplete.tsx +34 -3
  11. package/src/components/conditional-control-infotip.tsx +64 -0
  12. package/src/components/{unstable-repeater → control-repeater}/actions/disable-item-action.tsx +2 -2
  13. package/src/components/{unstable-repeater → control-repeater}/actions/duplicate-item-action.tsx +10 -4
  14. package/src/components/{unstable-repeater → control-repeater}/actions/remove-item-action.tsx +2 -2
  15. package/src/components/control-repeater/context/item-context.tsx +8 -0
  16. package/src/components/{unstable-repeater → control-repeater}/context/repeater-context.tsx +24 -15
  17. package/src/components/control-repeater/control-repeater.tsx +29 -0
  18. package/src/components/{unstable-repeater → control-repeater}/index.ts +1 -2
  19. package/src/components/{unstable-repeater → control-repeater}/items/edit-item-popover.tsx +6 -20
  20. package/src/components/control-repeater/items/item.tsx +75 -0
  21. package/src/components/{unstable-repeater → control-repeater}/items/items-container.tsx +8 -13
  22. package/src/components/{unstable-repeater → control-repeater}/locations.ts +0 -4
  23. package/src/components/{unstable-repeater → control-repeater}/types.ts +1 -2
  24. package/src/components/control-toggle-button-group.tsx +79 -69
  25. package/src/components/enable-unfiltered-modal.tsx +1 -26
  26. package/src/components/icon-buttons/clear-icon-button.tsx +23 -0
  27. package/src/components/inline-editor-toolbar.tsx +137 -0
  28. package/src/components/inline-editor.tsx +111 -0
  29. package/src/components/item-selector.tsx +10 -4
  30. package/src/components/{unstable-repeater/header/header.tsx → repeater/repeater-header.tsx} +4 -12
  31. package/src/components/repeater/repeater-popover.tsx +19 -0
  32. package/src/components/repeater/repeater-tag.tsx +16 -0
  33. package/src/components/repeater/repeater.tsx +405 -0
  34. package/src/components/{sortable.tsx → repeater/sortable.tsx} +1 -1
  35. package/src/components/size-control/size-input.tsx +20 -14
  36. package/src/components/size-control/text-field-inner-selection.tsx +15 -2
  37. package/src/control-adornments/control-adornments-context.tsx +5 -4
  38. package/src/control-replacements.tsx +3 -43
  39. package/src/controls/background-control/background-control.tsx +43 -12
  40. package/src/controls/background-control/background-gradient-color-control.tsx +5 -8
  41. package/src/controls/background-control/background-overlay/background-image-overlay/background-image-overlay-position.tsx +18 -13
  42. package/src/controls/background-control/background-overlay/background-overlay-repeater-control.tsx +25 -16
  43. package/src/controls/box-shadow-repeater-control.tsx +38 -21
  44. package/src/controls/color-control.tsx +3 -1
  45. package/src/controls/date-time-control.tsx +108 -0
  46. package/src/controls/filter-control/drop-shadow/drop-shadow-item-content.tsx +1 -0
  47. package/src/controls/filter-control/drop-shadow/drop-shadow-item-label.tsx +10 -6
  48. package/src/controls/filter-control/filter-content.tsx +1 -1
  49. package/src/controls/filter-control/filter-repeater-control.tsx +24 -21
  50. package/src/controls/filter-control/single-size/single-size-item-content.tsx +1 -1
  51. package/src/controls/filter-control/single-size/single-size-item-label.tsx +2 -1
  52. package/src/controls/font-family-control/font-family-control.tsx +66 -55
  53. package/src/controls/html-tag-control.tsx +90 -0
  54. package/src/controls/image-media-control.tsx +2 -2
  55. package/src/controls/inline-editing-control.tsx +18 -0
  56. package/src/controls/key-value-control.tsx +8 -2
  57. package/src/controls/link-control.tsx +23 -123
  58. package/src/controls/linked-dimensions-control.tsx +71 -33
  59. package/src/controls/query-control.tsx +168 -0
  60. package/src/controls/repeatable-control.tsx +62 -27
  61. package/src/controls/select-control-wrapper.tsx +57 -0
  62. package/src/controls/select-control.tsx +9 -5
  63. package/src/controls/selection-size-control.tsx +13 -2
  64. package/src/controls/size-control.tsx +32 -59
  65. package/src/controls/svg-media-control.tsx +33 -10
  66. package/src/controls/text-area-control.tsx +5 -1
  67. package/src/controls/text-control.tsx +5 -0
  68. package/src/controls/toggle-control.tsx +11 -2
  69. package/src/controls/transform-control/functions/axis-row.tsx +1 -0
  70. package/src/controls/transform-control/transform-icon.tsx +2 -2
  71. package/src/controls/transform-control/transform-label.tsx +15 -32
  72. package/src/controls/transform-control/transform-repeater-control.tsx +42 -36
  73. package/src/controls/transform-control/{transform-base-control.tsx → transform-settings-control.tsx} +2 -2
  74. package/src/controls/transform-control/use-transform-tabs-history.tsx +1 -1
  75. package/src/controls/transition-control/data.ts +16 -1
  76. package/src/controls/transition-control/trainsition-events.ts +2 -2
  77. package/src/controls/transition-control/transition-repeater-control.tsx +137 -13
  78. package/src/controls/transition-control/transition-selector.tsx +37 -14
  79. package/src/controls/url-control.tsx +21 -16
  80. package/src/hooks/use-filtered-items-list.ts +3 -2
  81. package/src/hooks/use-repeatable-control-context.ts +3 -0
  82. package/src/hooks/use-sync-external-state.tsx +0 -1
  83. package/src/index.ts +21 -5
  84. package/src/utils/convert-toggle-options-to-atomic.tsx +33 -0
  85. package/src/utils/escape-html-attr.ts +11 -0
  86. package/src/components/css-code-editor/css-editor.styles.ts +0 -52
  87. package/src/components/css-code-editor/css-editor.tsx +0 -142
  88. package/src/components/css-code-editor/css-validation.ts +0 -75
  89. package/src/components/css-code-editor/resize-handle.tsx +0 -55
  90. package/src/components/css-code-editor/visual-content-change-protection.ts +0 -69
  91. package/src/components/repeater.tsx +0 -343
  92. package/src/components/unstable-repeater/items/item.tsx +0 -77
  93. package/src/components/unstable-repeater/unstable-repeater.tsx +0 -26
  94. /package/src/components/{unstable-repeater → control-repeater}/actions/tooltip-add-item-action.tsx +0 -0
@@ -0,0 +1,168 @@
1
+ import * as React from 'react';
2
+ import { useMemo, useState } from 'react';
3
+ import { numberPropTypeUtil, stringPropTypeUtil, urlPropTypeUtil } from '@elementor/editor-props';
4
+ import { type HttpResponse, httpService } from '@elementor/http-client';
5
+ import { SearchIcon } from '@elementor/icons';
6
+ import { debounce } from '@elementor/utils';
7
+ import { __ } from '@wordpress/i18n';
8
+
9
+ import { useBoundProp } from '../bound-prop-context';
10
+ import {
11
+ Autocomplete,
12
+ type CategorizedOption,
13
+ findMatchingOption,
14
+ type FlatOption,
15
+ isCategorizedOptionPool,
16
+ } from '../components/autocomplete';
17
+ import ControlActions from '../control-actions/control-actions';
18
+ import { createControl } from '../create-control';
19
+ import { type DestinationProp } from './link-control';
20
+
21
+ type Props = {
22
+ queryOptions: {
23
+ params: Record< string, unknown >;
24
+ url: string;
25
+ };
26
+ allowCustomValues?: boolean;
27
+ minInputLength?: number;
28
+ placeholder?: string;
29
+ onSetValue?: ( value: DestinationProp | null ) => void;
30
+ ariaLabel?: string;
31
+ };
32
+
33
+ type Response = HttpResponse< { value: FlatOption[] | CategorizedOption[] } >;
34
+
35
+ type FetchOptionsParams = Record< string, unknown > & { term: string };
36
+
37
+ export const QueryControl = createControl( ( props: Props ) => {
38
+ const { value, setValue } = useBoundProp< DestinationProp >();
39
+
40
+ const {
41
+ allowCustomValues = true,
42
+ queryOptions: { url, params = {} },
43
+ placeholder,
44
+ minInputLength = 2,
45
+ onSetValue,
46
+ ariaLabel,
47
+ } = props || {};
48
+
49
+ const normalizedPlaceholder = placeholder || __( 'Search', 'elementor' );
50
+
51
+ const [ options, setOptions ] = useState< FlatOption[] | CategorizedOption[] >(
52
+ generateFirstLoadedOption( value?.value )
53
+ );
54
+
55
+ const onOptionChange = ( newValue: number | null ) => {
56
+ if ( newValue === null ) {
57
+ setValue( null );
58
+ onSetValue?.( null );
59
+
60
+ return;
61
+ }
62
+
63
+ const valueToSave = {
64
+ $$type: 'query',
65
+ value: {
66
+ id: numberPropTypeUtil.create( newValue ),
67
+ label: stringPropTypeUtil.create( findMatchingOption( options, newValue )?.label || null ),
68
+ },
69
+ };
70
+
71
+ setValue( valueToSave );
72
+ onSetValue?.( valueToSave );
73
+ };
74
+
75
+ const onTextChange = ( newValue: string | null ) => {
76
+ if ( ! newValue ) {
77
+ setValue( null );
78
+ onSetValue?.( null );
79
+
80
+ return;
81
+ }
82
+
83
+ const newLinkValue = newValue?.trim() || '';
84
+ const valueToSave = newLinkValue ? urlPropTypeUtil.create( newLinkValue ) : null;
85
+
86
+ setValue( valueToSave );
87
+ onSetValue?.( valueToSave );
88
+ updateOptions( newValue );
89
+ };
90
+
91
+ const updateOptions = ( newValue: string | null ) => {
92
+ setOptions( [] );
93
+
94
+ if ( ! newValue || ! url || newValue.length < minInputLength ) {
95
+ return;
96
+ }
97
+
98
+ debounceFetch( { ...params, term: newValue } );
99
+ };
100
+
101
+ const debounceFetch = useMemo(
102
+ () =>
103
+ debounce(
104
+ ( queryParams: FetchOptionsParams ) =>
105
+ fetchOptions( url, queryParams ).then( ( newOptions ) => {
106
+ setOptions( formatOptions( newOptions ) );
107
+ } ),
108
+ 400
109
+ ),
110
+ [ url ]
111
+ );
112
+
113
+ return (
114
+ <ControlActions>
115
+ <Autocomplete
116
+ options={ options }
117
+ allowCustomValues={ allowCustomValues }
118
+ placeholder={ normalizedPlaceholder }
119
+ startAdornment={ <SearchIcon fontSize="tiny" /> }
120
+ value={ value?.value?.id?.value || value?.value }
121
+ onOptionChange={ onOptionChange }
122
+ onTextChange={ onTextChange }
123
+ minInputLength={ minInputLength }
124
+ disablePortal={ false }
125
+ inputProps={ {
126
+ ...( ariaLabel ? { 'aria-label': ariaLabel } : {} ),
127
+ } }
128
+ />
129
+ </ControlActions>
130
+ );
131
+ } );
132
+
133
+ async function fetchOptions( ajaxUrl: string, params: FetchOptionsParams ) {
134
+ if ( ! params || ! ajaxUrl ) {
135
+ return [];
136
+ }
137
+
138
+ try {
139
+ const { data: response } = await httpService().get< Response >( ajaxUrl, { params } );
140
+
141
+ return response.data.value;
142
+ } catch {
143
+ return [];
144
+ }
145
+ }
146
+
147
+ function formatOptions( options: FlatOption[] | CategorizedOption[] ): FlatOption[] | CategorizedOption[] {
148
+ const compareKey = isCategorizedOptionPool( options ) ? 'groupLabel' : 'label';
149
+
150
+ return options.sort( ( a, b ) =>
151
+ a[ compareKey ] && b[ compareKey ] ? a[ compareKey ].localeCompare( b[ compareKey ] ) : 0
152
+ );
153
+ }
154
+
155
+ function generateFirstLoadedOption( unionValue: DestinationProp | null ): FlatOption[] {
156
+ const value = unionValue?.id?.value;
157
+ const label = unionValue?.label?.value;
158
+ const type = unionValue?.id?.$$type || 'url';
159
+
160
+ return value && label && type === 'number'
161
+ ? [
162
+ {
163
+ id: value.toString(),
164
+ label,
165
+ },
166
+ ]
167
+ : [];
168
+ }
@@ -1,26 +1,27 @@
1
1
  import * as React from 'react';
2
2
  import { useMemo } from 'react';
3
- import { createArrayPropUtils } from '@elementor/editor-props';
3
+ import { createArrayPropUtils, type SizePropValue } from '@elementor/editor-props';
4
4
  import { Box } from '@elementor/ui';
5
5
 
6
6
  import { PropProvider, useBoundProp } from '../bound-prop-context';
7
+ import { ControlRepeater, Item, TooltipAddItemAction } from '../components/control-repeater';
8
+ import { DisableItemAction } from '../components/control-repeater/actions/disable-item-action';
9
+ import { DuplicateItemAction } from '../components/control-repeater/actions/duplicate-item-action';
10
+ import { RemoveItemAction } from '../components/control-repeater/actions/remove-item-action';
11
+ import { type TooltipAddItemActionProps } from '../components/control-repeater/actions/tooltip-add-item-action';
12
+ import { EditItemPopover } from '../components/control-repeater/items/edit-item-popover';
13
+ import { ItemsContainer } from '../components/control-repeater/items/items-container';
14
+ import { type CollectionPropUtil, type RepeatablePropValue } from '../components/control-repeater/types';
7
15
  import { PopoverContent } from '../components/popover-content';
8
16
  import { PopoverGridContainer } from '../components/popover-grid-container';
9
- import { type CollectionPropUtil } from '../components/repeater';
10
- import { Header, Item, TooltipAddItemAction, UnstableRepeater } from '../components/unstable-repeater';
11
- import { DisableItemAction } from '../components/unstable-repeater/actions/disable-item-action';
12
- import { DuplicateItemAction } from '../components/unstable-repeater/actions/duplicate-item-action';
13
- import { RemoveItemAction } from '../components/unstable-repeater/actions/remove-item-action';
14
- import { type TooltipAddItemActionProps } from '../components/unstable-repeater/actions/tooltip-add-item-action';
15
- import { EditItemPopover } from '../components/unstable-repeater/items/edit-item-popover';
16
- import { ItemsContainer } from '../components/unstable-repeater/items/items-container';
17
- import { type RepeatablePropValue } from '../components/unstable-repeater/types';
17
+ import { RepeaterHeader } from '../components/repeater/repeater-header';
18
18
  import { createControl } from '../create-control';
19
19
  import {
20
20
  type ChildControlConfig,
21
21
  RepeatableControlContext,
22
22
  useRepeatableControlContext,
23
23
  } from '../hooks/use-repeatable-control-context';
24
+ import { CUSTOM_SIZE_LABEL } from './size-control';
24
25
 
25
26
  type RepeatableControlProps = {
26
27
  label: string;
@@ -49,7 +50,7 @@ export const RepeatableControl = createControl(
49
50
  propKey,
50
51
  addItemTooltipProps,
51
52
  }: RepeatableControlProps ) => {
52
- const { propTypeUtil: childPropTypeUtil } = childControlConfig;
53
+ const { propTypeUtil: childPropTypeUtil, isItemDisabled } = childControlConfig;
53
54
 
54
55
  if ( ! childPropTypeUtil ) {
55
56
  return null;
@@ -74,29 +75,35 @@ export const RepeatableControl = createControl(
74
75
  return (
75
76
  <PropProvider propType={ propType } value={ value } setValue={ setValue }>
76
77
  <RepeatableControlContext.Provider value={ contextValue }>
77
- <UnstableRepeater
78
+ <ControlRepeater
78
79
  initial={ childPropTypeUtil.create( initialValues || null ) }
79
80
  propTypeUtil={ childArrayPropTypeUtil as CollectionPropUtil< RepeatablePropValue > }
81
+ isItemDisabled={ isItemDisabled }
80
82
  >
81
- <Header label={ repeaterLabel }>
83
+ <RepeaterHeader label={ repeaterLabel }>
82
84
  <TooltipAddItemAction
83
85
  { ...addItemTooltipProps }
84
86
  newItemIndex={ 0 }
85
87
  ariaLabel={ repeaterLabel }
86
88
  />
87
- </Header>
88
- <ItemsContainer
89
- isSortable={ false }
90
- itemTemplate={ <Item Icon={ ItemIcon } Label={ ItemLabel } /> }
91
- >
92
- { showDuplicate && <DuplicateItemAction /> }
93
- { showToggle && <DisableItemAction /> }
94
- <RemoveItemAction />
89
+ </RepeaterHeader>
90
+ <ItemsContainer isSortable={ false }>
91
+ <Item
92
+ Icon={ ItemIcon }
93
+ Label={ ItemLabel }
94
+ actions={
95
+ <>
96
+ { showDuplicate && <DuplicateItemAction /> }
97
+ { showToggle && <DisableItemAction /> }
98
+ <RemoveItemAction />
99
+ </>
100
+ }
101
+ />
95
102
  </ItemsContainer>
96
103
  <EditItemPopover>
97
104
  <Content />
98
105
  </EditItemPopover>
99
- </UnstableRepeater>
106
+ </ControlRepeater>
100
107
  </RepeatableControlContext.Provider>
101
108
  </PropProvider>
102
109
  );
@@ -126,7 +133,7 @@ const interpolate = ( template: string, data: Record< string, unknown > ) => {
126
133
  const value = getNestedValue( data, path );
127
134
 
128
135
  if ( typeof value === 'object' && value !== null && ! Array.isArray( value ) ) {
129
- if ( value.name ) {
136
+ if ( 'name' in value && value.name ) {
130
137
  return value.name as string;
131
138
  }
132
139
 
@@ -142,12 +149,32 @@ const interpolate = ( template: string, data: Record< string, unknown > ) => {
142
149
  };
143
150
 
144
151
  const getNestedValue = ( obj: Record< string, unknown >, path: string ) => {
145
- return path.split( '.' ).reduce( ( current: Record< string, unknown >, key ) => {
152
+ let parentObj: Record< string, unknown > = {};
153
+ const pathKeys = path.split( '.' );
154
+ const key = pathKeys.slice( -1 )[ 0 ];
155
+
156
+ let value: unknown = pathKeys.reduce( ( current: Record< string, unknown >, currentKey, currentIndex ) => {
157
+ if ( currentIndex === pathKeys.length - 2 ) {
158
+ parentObj = current;
159
+ }
160
+
146
161
  if ( current && typeof current === 'object' ) {
147
- return current[ key ] as Record< string, unknown >;
162
+ return current[ currentKey ] as Record< string, unknown >;
148
163
  }
164
+
149
165
  return {};
150
166
  }, obj );
167
+
168
+ value = !! value ? value : '';
169
+ const propType = parentObj?.$$type;
170
+ const propValue = parentObj?.value as SizePropValue[ 'value' ];
171
+ const doesValueRepresentCustomSize = key === 'unit' && propType === 'size' && propValue?.unit === 'custom';
172
+
173
+ if ( ! doesValueRepresentCustomSize ) {
174
+ return value;
175
+ }
176
+
177
+ return propValue?.size ? '' : CUSTOM_SIZE_LABEL;
151
178
  };
152
179
 
153
180
  const isEmptyValue = ( val: unknown ) => {
@@ -190,11 +217,19 @@ const shouldShowPlaceholder = ( pattern: string, data: Record< string, unknown >
190
217
  return false;
191
218
  };
192
219
 
220
+ const getTextColor = ( isReadOnly: boolean, showPlaceholder: boolean ): string => {
221
+ if ( isReadOnly ) {
222
+ return 'text.disabled';
223
+ }
224
+ return showPlaceholder ? 'text.tertiary' : 'text.primary';
225
+ };
226
+
193
227
  const ItemLabel = ( { value }: { value: Record< string, unknown > } ) => {
194
- const { placeholder, patternLabel } = useRepeatableControlContext();
228
+ const { placeholder, patternLabel, props: childProps } = useRepeatableControlContext();
195
229
  const showPlaceholder = shouldShowPlaceholder( patternLabel, value );
196
230
  const label = showPlaceholder ? placeholder : interpolate( patternLabel, value );
197
- const color = showPlaceholder ? 'text.tertiary' : 'text.primary';
231
+ const isReadOnly = !! childProps?.readOnly;
232
+ const color = getTextColor( isReadOnly, showPlaceholder );
198
233
 
199
234
  return (
200
235
  <Box component="span" color={ color }>
@@ -0,0 +1,57 @@
1
+ import * as React from 'react';
2
+ import { useEffect, useState } from 'react';
3
+
4
+ import { createControl } from '../create-control';
5
+ import { SelectControl, type SelectOption } from './select-control';
6
+
7
+ type ExtendedWindow = Window & {
8
+ elementor: { $previewContents: [ HTMLIFrameElement ]; config: { document: { id: string } } };
9
+ };
10
+
11
+ const getOffCanvasElements = () => {
12
+ const extendedWindow = window as unknown as ExtendedWindow;
13
+ const documentId = extendedWindow.elementor.config.document.id;
14
+ const offCanvasElements = extendedWindow.elementor.$previewContents[ 0 ].querySelectorAll(
15
+ `[data-elementor-id="${ documentId }"] .elementor-widget-off-canvas.elementor-element-edit-mode`
16
+ );
17
+
18
+ return Array.from( offCanvasElements as unknown as HTMLElement[] ).map( ( offCanvasElement ) => {
19
+ return {
20
+ label: offCanvasElement.querySelector( '.e-off-canvas' )?.getAttribute( 'aria-label' ) ?? '',
21
+ value: offCanvasElement.dataset.id,
22
+ } as SelectOption;
23
+ } );
24
+ };
25
+
26
+ const collectionMethods = {
27
+ 'off-canvas': getOffCanvasElements,
28
+ } as const;
29
+
30
+ type SelectControlWrapperProps = Parameters< typeof SelectControl >[ 0 ] & {
31
+ collectionId?: keyof typeof collectionMethods;
32
+ };
33
+
34
+ const useDynamicOptions = (
35
+ collectionId?: keyof typeof collectionMethods,
36
+ initialOptions?: SelectControlWrapperProps[ 'options' ]
37
+ ) => {
38
+ const [ options, setOptions ] = useState< SelectControlWrapperProps[ 'options' ] >( initialOptions ?? [] );
39
+
40
+ useEffect( () => {
41
+ if ( ! collectionId || ! collectionMethods[ collectionId ] ) {
42
+ setOptions( initialOptions ?? [] );
43
+ return;
44
+ }
45
+ setOptions( collectionMethods[ collectionId ]() );
46
+ }, [ collectionId, initialOptions ] );
47
+
48
+ return options;
49
+ };
50
+
51
+ export const SelectControlWrapper = createControl(
52
+ ( { collectionId, options, ...props }: SelectControlWrapperProps ) => {
53
+ const actualOptions = useDynamicOptions( collectionId, options );
54
+
55
+ return <SelectControl options={ actualOptions } { ...props } />;
56
+ }
57
+ );
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import { stringPropTypeUtil, type StringPropValue } from '@elementor/editor-props';
3
3
  import { MenuListItem } from '@elementor/editor-ui';
4
- import { Select, type SelectChangeEvent, Typography } from '@elementor/ui';
4
+ import { Select, type SelectChangeEvent, type SelectProps, Typography } from '@elementor/ui';
5
5
 
6
6
  import { useBoundProp } from '../bound-prop-context';
7
7
  import ControlActions from '../control-actions/control-actions';
@@ -13,12 +13,13 @@ export type SelectOption = {
13
13
  disabled?: boolean;
14
14
  };
15
15
 
16
- type Props = {
16
+ type SelectControlProps = {
17
17
  options: SelectOption[];
18
18
  onChange?: ( newValue: string | null, previousValue: string | null | undefined ) => void;
19
+ MenuProps?: SelectProps[ 'MenuProps' ];
20
+ ariaLabel?: string;
19
21
  };
20
-
21
- export const SelectControl = createControl( ( { options, onChange }: Props ) => {
22
+ export const SelectControl = createControl( ( { options, onChange, MenuProps, ariaLabel }: SelectControlProps ) => {
22
23
  const { value, setValue, disabled, placeholder } = useBoundProp( stringPropTypeUtil );
23
24
  const handleChange = ( event: SelectChangeEvent< StringPropValue[ 'value' ] > ) => {
24
25
  const newValue = event.target.value || null;
@@ -26,6 +27,7 @@ export const SelectControl = createControl( ( { options, onChange }: Props ) =>
26
27
  onChange?.( newValue, value );
27
28
  setValue( newValue );
28
29
  };
30
+ const isDisabled = disabled || options.length === 0;
29
31
 
30
32
  return (
31
33
  <ControlActions>
@@ -33,6 +35,8 @@ export const SelectControl = createControl( ( { options, onChange }: Props ) =>
33
35
  sx={ { overflow: 'hidden' } }
34
36
  displayEmpty
35
37
  size="tiny"
38
+ MenuProps={ MenuProps }
39
+ aria-label={ ariaLabel || placeholder }
36
40
  renderValue={ ( selectedValue: string | null ) => {
37
41
  const findOptionByValue = ( searchValue: string | null ) =>
38
42
  options.find( ( opt ) => opt.value === searchValue );
@@ -55,7 +59,7 @@ export const SelectControl = createControl( ( { options, onChange }: Props ) =>
55
59
  } }
56
60
  value={ value ?? '' }
57
61
  onChange={ handleChange }
58
- disabled={ disabled }
62
+ disabled={ isDisabled }
59
63
  fullWidth
60
64
  >
61
65
  { options.map( ( { label, ...props } ) => (
@@ -20,13 +20,22 @@ type SelectionSizeControlProps = {
20
20
  sizeLabel: string;
21
21
  selectionConfig: SelectionComponentConfig;
22
22
  sizeConfigMap: Record< string, SizeControlConfig >;
23
+ isRepeaterControl?: boolean;
23
24
  };
24
25
 
25
26
  export const SelectionSizeControl = createControl(
26
- ( { selectionLabel, sizeLabel, selectionConfig, sizeConfigMap }: SelectionSizeControlProps ) => {
27
+ ( {
28
+ selectionLabel,
29
+ sizeLabel,
30
+ selectionConfig,
31
+ sizeConfigMap,
32
+ isRepeaterControl = false,
33
+ }: SelectionSizeControlProps ) => {
27
34
  const { value, setValue, propType } = useBoundProp( selectionSizePropTypeUtil );
28
35
  const rowRef = useRef< HTMLDivElement >( null );
29
36
 
37
+ const sizeFieldId = sizeLabel.replace( /\s+/g, '-' ).toLowerCase();
38
+
30
39
  const currentSizeConfig = useMemo( () => {
31
40
  switch ( value.selection.$$type ) {
32
41
  case 'key-value':
@@ -53,7 +62,7 @@ export const SelectionSizeControl = createControl(
53
62
  { currentSizeConfig && (
54
63
  <>
55
64
  <Grid item xs={ 6 } sx={ { display: 'flex', alignItems: 'center' } }>
56
- <ControlFormLabel>{ sizeLabel }</ControlFormLabel>
65
+ <ControlFormLabel htmlFor={ sizeFieldId }>{ sizeLabel }</ControlFormLabel>
57
66
  </Grid>
58
67
  <Grid item xs={ 6 }>
59
68
  <PropKeyProvider bind="size">
@@ -62,6 +71,8 @@ export const SelectionSizeControl = createControl(
62
71
  variant={ currentSizeConfig.variant }
63
72
  units={ currentSizeConfig.units }
64
73
  defaultUnit={ currentSizeConfig.defaultUnit }
74
+ id={ sizeFieldId }
75
+ isRepeaterControl={ isRepeaterControl }
65
76
  />
66
77
  </PropKeyProvider>
67
78
  </Grid>
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { type RefObject, useEffect, useState } from 'react';
2
+ import { type RefObject, useEffect, useMemo } from 'react';
3
3
  import { type PropType, sizePropTypeUtil, type SizePropValue } from '@elementor/editor-props';
4
4
  import { useActiveBreakpoint } from '@elementor/editor-responsive';
5
5
  import { usePopupState } from '@elementor/ui';
@@ -42,6 +42,8 @@ type BaseSizeControlProps = {
42
42
  min?: number;
43
43
  enablePropTypeUnits?: boolean;
44
44
  id?: string;
45
+ ariaLabel?: string;
46
+ isRepeaterControl?: boolean;
45
47
  };
46
48
 
47
49
  type LengthSizeControlProps = BaseSizeControlProps &
@@ -79,6 +81,8 @@ const defaultUnits: Record< SizeControlProps[ 'variant' ], Unit[] > = {
79
81
  time: [ ...timeUnits ] as TimeUnit[],
80
82
  } as const;
81
83
 
84
+ export const CUSTOM_SIZE_LABEL = 'fx';
85
+
82
86
  export const SizeControl = createControl(
83
87
  ( {
84
88
  variant = 'length' as SizeControlProps[ 'variant' ],
@@ -92,6 +96,7 @@ export const SizeControl = createControl(
92
96
  min = 0,
93
97
  enablePropTypeUnits = false,
94
98
  id,
99
+ ariaLabel,
95
100
  }: Omit< SizeControlProps, 'variant' > & { variant?: SizeVariant } ) => {
96
101
  const {
97
102
  value: sizeValue,
@@ -101,29 +106,24 @@ export const SizeControl = createControl(
101
106
  placeholder: externalPlaceholder,
102
107
  propType,
103
108
  } = useBoundProp( sizePropTypeUtil );
109
+
104
110
  const actualDefaultUnit = defaultUnit ?? externalPlaceholder?.unit ?? defaultSelectedUnit[ variant ];
105
- const [ internalState, setInternalState ] = useState( createStateFromSizeProp( sizeValue, actualDefaultUnit ) );
106
111
  const activeBreakpoint = useActiveBreakpoint();
107
112
  const actualUnits = resolveUnits( propType, enablePropTypeUnits, variant, units );
108
113
 
109
114
  const actualExtendedOptions = useSizeExtendedOptions( extendedOptions || [], disableCustom ?? false );
110
115
  const popupState = usePopupState( { variant: 'popover' } );
111
116
 
117
+ const memorizedExternalState = useMemo(
118
+ () => createStateFromSizeProp( sizeValue, actualDefaultUnit ),
119
+ [ sizeValue, actualDefaultUnit ]
120
+ );
121
+
112
122
  const [ state, setState ] = useSyncExternalState( {
113
- external: internalState,
123
+ external: memorizedExternalState,
114
124
  setExternal: ( newState: State | null, options, meta ) =>
115
125
  setSizeValue( extractValueFromState( newState ), options, meta ),
116
- persistWhen: ( newState ) => {
117
- if ( ! newState?.unit ) {
118
- return false;
119
- }
120
-
121
- if ( isUnitExtendedOption( newState.unit ) ) {
122
- return newState.unit === 'auto' ? true : !! newState.custom;
123
- }
124
-
125
- return !! newState?.numeric || newState?.numeric === 0;
126
- },
126
+ persistWhen: ( newState ) => !! extractValueFromState( newState ),
127
127
  fallback: ( newState ) => ( {
128
128
  unit: newState?.unit ?? actualDefaultUnit,
129
129
  numeric: newState?.numeric ?? DEFAULT_SIZE,
@@ -132,7 +132,7 @@ export const SizeControl = createControl(
132
132
  } );
133
133
 
134
134
  const { size: controlSize = DEFAULT_SIZE, unit: controlUnit = actualDefaultUnit } =
135
- extractValueFromState( state ) || {};
135
+ extractValueFromState( state, true ) || {};
136
136
 
137
137
  const handleUnitChange = ( newUnit: Unit | ExtendedOption ) => {
138
138
  if ( newUnit === 'custom' ) {
@@ -169,40 +169,14 @@ export const SizeControl = createControl(
169
169
  }
170
170
  };
171
171
 
172
- useEffect( () => {
173
- const newState = createStateFromSizeProp(
174
- sizeValue,
175
- state.unit === 'custom' ? state.unit : actualDefaultUnit,
176
- '',
177
- state.custom
178
- );
179
- const currentUnitType = isUnitExtendedOption( state.unit ) ? 'custom' : 'numeric';
180
- const mergedStates = {
181
- ...state,
182
- unit: newState.unit ?? state.unit,
183
- [ currentUnitType ]: newState[ currentUnitType ],
184
- };
185
-
186
- if ( mergedStates.unit !== 'auto' && areStatesEqual( state, mergedStates ) ) {
187
- return;
188
- }
189
-
190
- if ( state.unit === newState.unit ) {
191
- setInternalState( mergedStates );
192
-
193
- return;
172
+ const maybeClosePopup = React.useCallback( () => {
173
+ if ( popupState && popupState.isOpen ) {
174
+ popupState.close();
194
175
  }
195
-
196
- setState( newState );
197
- // eslint-disable-next-line react-hooks/exhaustive-deps
198
- }, [ sizeValue ] );
176
+ }, [ popupState ] );
199
177
 
200
178
  useEffect( () => {
201
- const newState = createStateFromSizeProp( sizeValue, actualDefaultUnit, '', state.custom );
202
-
203
- if ( activeBreakpoint && ! areStatesEqual( newState, state ) ) {
204
- setState( newState );
205
- }
179
+ maybeClosePopup();
206
180
  // eslint-disable-next-line react-hooks/exhaustive-deps
207
181
  }, [ activeBreakpoint ] );
208
182
 
@@ -222,6 +196,7 @@ export const SizeControl = createControl(
222
196
  popupState={ popupState }
223
197
  min={ min }
224
198
  id={ id }
199
+ ariaLabel={ ariaLabel }
225
200
  />
226
201
  { anchorRef?.current && popupState.isOpen && (
227
202
  <TextFieldPopover
@@ -279,7 +254,7 @@ function createStateFromSizeProp(
279
254
  };
280
255
  }
281
256
 
282
- function extractValueFromState( state: State | null ): SizeValue | null {
257
+ function extractValueFromState( state: State | null, allowEmpty: boolean = false ): SizeValue | null {
283
258
  if ( ! state ) {
284
259
  return null;
285
260
  }
@@ -294,20 +269,18 @@ function extractValueFromState( state: State | null ): SizeValue | null {
294
269
  return { size: '', unit };
295
270
  }
296
271
 
297
- return {
298
- size: state[ unit === 'custom' ? 'custom' : 'numeric' ],
299
- unit,
300
- } as SizeValue;
301
- }
302
-
303
- function areStatesEqual( state1: State, state2: State ): boolean {
304
- if ( state1.unit !== state2.unit || state1.custom !== state2.custom ) {
305
- return false;
272
+ if ( unit === 'custom' ) {
273
+ return { size: state.custom ?? '', unit: 'custom' };
306
274
  }
307
275
 
308
- if ( isUnitExtendedOption( state1.unit ) ) {
309
- return state1.custom === state2.custom;
276
+ const numeric = state.numeric;
277
+
278
+ if ( ! allowEmpty && ( numeric === undefined || numeric === null || Number.isNaN( numeric ) ) ) {
279
+ return null;
310
280
  }
311
281
 
312
- return state1.numeric === state2.numeric || ( isNaN( state1.numeric ) && isNaN( state2.numeric ) );
282
+ return {
283
+ size: numeric,
284
+ unit,
285
+ };
313
286
  }