@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
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@elementor/editor-controls",
3
3
  "description": "This package contains the controls model and utils for the Elementor editor",
4
- "version": "0.36.0",
4
+ "version": "1.1.0",
5
5
  "private": false,
6
6
  "author": "Elementor Team",
7
7
  "homepage": "https://elementor.com/",
@@ -41,9 +41,10 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@elementor/editor-current-user": "0.5.0",
44
- "@elementor/editor-elements": "0.8.4",
45
- "@elementor/editor-props": "0.12.1",
46
- "@elementor/editor-ui": "0.11.0",
44
+ "@elementor/editor-elements": "0.8.6",
45
+ "@elementor/editor-props": "0.14.0",
46
+ "@elementor/editor-responsive": "0.13.5",
47
+ "@elementor/editor-ui": "0.12.0",
47
48
  "@elementor/editor-v1-adapters": "0.12.0",
48
49
  "@elementor/env": "0.3.5",
49
50
  "@elementor/http-client": "0.3.0",
@@ -51,16 +52,16 @@
51
52
  "@elementor/locations": "0.8.0",
52
53
  "@elementor/query": "0.2.4",
53
54
  "@elementor/session": "0.1.0",
54
- "@elementor/ui": "1.34.5",
55
+ "@elementor/ui": "1.35.5",
55
56
  "@elementor/utils": "0.4.0",
56
57
  "@elementor/wp-media": "0.6.0",
57
- "@tanstack/react-virtual": "3.13.3",
58
58
  "@wordpress/i18n": "^5.13.0"
59
59
  },
60
60
  "devDependencies": {
61
61
  "tsup": "^8.3.5"
62
62
  },
63
63
  "peerDependencies": {
64
- "react": "^18.3.1"
64
+ "react": "^18.3.1",
65
+ "react-dom": "^18.3.1"
65
66
  }
66
67
  }
@@ -22,7 +22,10 @@ type UseBoundProp< TValue extends PropValue > = {
22
22
  disabled?: boolean;
23
23
  };
24
24
 
25
- export function useBoundProp< T extends PropValue = PropValue >(): PropKeyContextValue< T, PropType >;
25
+ export function useBoundProp< T extends PropValue = PropValue, P extends PropType = PropType >(): PropKeyContextValue<
26
+ T,
27
+ P
28
+ >;
26
29
 
27
30
  export function useBoundProp< TKey extends string, TValue extends PropValue >(
28
31
  propTypeUtil: PropTypeUtil< TKey, TValue >
@@ -1,21 +1,9 @@
1
- import { useEffect, useRef, useState } from 'react';
2
1
  import * as React from 'react';
3
- import { PopoverHeader } from '@elementor/editor-ui';
4
- import { SearchIcon, TextIcon } from '@elementor/icons';
5
- import {
6
- Box,
7
- Divider,
8
- InputAdornment,
9
- Link,
10
- MenuList,
11
- MenuSubheader,
12
- Stack,
13
- styled,
14
- TextField,
15
- Typography,
16
- } from '@elementor/ui';
2
+ import { useEffect, useState } from 'react';
3
+ import { PopoverHeader, PopoverMenuList, PopoverScrollableContent, PopoverSearch } from '@elementor/editor-ui';
4
+ import { TextIcon } from '@elementor/icons';
5
+ import { Box, Divider, Link, Stack, Typography } from '@elementor/ui';
17
6
  import { debounce } from '@elementor/utils';
18
- import { useVirtualizer } from '@tanstack/react-virtual';
19
7
  import { __ } from '@wordpress/i18n';
20
8
 
21
9
  import { enqueueFont } from '../controls/font-family-control/enqueue-font';
@@ -41,8 +29,8 @@ export const FontFamilySelector = ( {
41
29
 
42
30
  const filteredFontFamilies = useFilteredFontFamilies( fontFamilies, searchValue );
43
31
 
44
- const handleSearch = ( event: React.ChangeEvent< HTMLInputElement > ) => {
45
- setSearchValue( event.target.value );
32
+ const handleSearch = ( value: string ) => {
33
+ setSearchValue( value );
46
34
  };
47
35
 
48
36
  const handleClose = () => {
@@ -57,25 +45,11 @@ export const FontFamilySelector = ( {
57
45
  onClose={ handleClose }
58
46
  icon={ <TextIcon fontSize={ SIZE } /> }
59
47
  />
60
-
61
- <Box px={ 1.5 } pb={ 1 }>
62
- <TextField
63
- // eslint-disable-next-line jsx-a11y/no-autofocus
64
- autoFocus
65
- fullWidth
66
- size={ SIZE }
67
- value={ searchValue }
68
- placeholder={ __( 'Search', 'elementor' ) }
69
- onChange={ handleSearch }
70
- InputProps={ {
71
- startAdornment: (
72
- <InputAdornment position="start">
73
- <SearchIcon fontSize={ SIZE } />
74
- </InputAdornment>
75
- ),
76
- } }
77
- />
78
- </Box>
48
+ <PopoverSearch
49
+ value={ searchValue }
50
+ onSearch={ handleSearch }
51
+ placeholder={ __( 'Search', 'elementor' ) }
52
+ />
79
53
  <Divider />
80
54
  { filteredFontFamilies.length > 0 ? (
81
55
  <FontList
@@ -85,7 +59,7 @@ export const FontFamilySelector = ( {
85
59
  fontFamily={ fontFamily }
86
60
  />
87
61
  ) : (
88
- <Box sx={ { overflowY: 'auto', height: 260, width: 220 } }>
62
+ <PopoverScrollableContent>
89
63
  <Stack alignItems="center" p={ 2.5 } gap={ 1.5 } overflow={ 'hidden' }>
90
64
  <TextIcon fontSize="large" />
91
65
  <Box sx={ { maxWidth: 160, overflow: 'hidden' } }>
@@ -120,7 +94,7 @@ export const FontFamilySelector = ( {
120
94
  </Link>
121
95
  </Typography>
122
96
  </Stack>
123
- </Box>
97
+ </PopoverScrollableContent>
124
98
  ) }
125
99
  </Stack>
126
100
  );
@@ -133,11 +107,7 @@ type FontListProps = {
133
107
  fontFamily: string | null;
134
108
  };
135
109
 
136
- const LIST_ITEM_HEIGHT = 36;
137
- const LIST_ITEMS_BUFFER = 6;
138
-
139
110
  const FontList = ( { fontListItems, setFontFamily, handleClose, fontFamily }: FontListProps ) => {
140
- const containerRef = useRef< HTMLDivElement >( null );
141
111
  const selectedItem = fontListItems.find( ( item ) => item.value === fontFamily );
142
112
 
143
113
  const debouncedVirtualizeChange = useDebounce( ( { getVirtualIndexes }: { getVirtualIndexes: () => number[] } ) => {
@@ -149,131 +119,20 @@ const FontList = ( { fontListItems, setFontFamily, handleClose, fontFamily }: Fo
149
119
  } );
150
120
  }, 100 );
151
121
 
152
- const virtualizer = useVirtualizer( {
153
- count: fontListItems.length,
154
- getScrollElement: () => containerRef.current,
155
- estimateSize: () => LIST_ITEM_HEIGHT,
156
- overscan: LIST_ITEMS_BUFFER,
157
- onChange: debouncedVirtualizeChange,
158
- } );
159
-
160
- useEffect(
161
- () => {
162
- virtualizer.scrollToIndex( fontListItems.findIndex( ( item ) => item.value === fontFamily ) );
163
- },
164
- // eslint-disable-next-line react-compiler/react-compiler
165
- // eslint-disable-next-line react-hooks/exhaustive-deps
166
- [ fontFamily ]
167
- );
168
-
169
122
  return (
170
- <Box
171
- ref={ containerRef }
172
- sx={ {
173
- overflowY: 'auto',
174
- height: 260,
175
- width: 220,
176
- } }
177
- >
178
- <StyledMenuList
179
- role="listbox"
180
- style={ {
181
- height: `${ virtualizer.getTotalSize() }px`,
182
- } }
183
- data-testid="font-list"
184
- >
185
- { virtualizer.getVirtualItems().map( ( virtualRow ) => {
186
- const item = fontListItems[ virtualRow.index ];
187
- const isLast = virtualRow.index === fontListItems.length - 1;
188
- // Ignore the first item, which is a category, and use the second item instead.
189
- const isFirst = virtualRow.index === 1;
190
- const isSelected = selectedItem?.value === item.value;
191
-
192
- // If no item is selected, the first item should be focused.
193
- const tabIndexFallback = ! selectedItem ? 0 : -1;
194
-
195
- if ( item.type === 'category' ) {
196
- return (
197
- <MenuSubheader
198
- key={ virtualRow.key }
199
- style={ {
200
- transform: `translateY(${ virtualRow.start }px)`,
201
- } }
202
- >
203
- { item.value }
204
- </MenuSubheader>
205
- );
206
- }
207
-
208
- return (
209
- <li
210
- key={ virtualRow.key }
211
- role="option"
212
- aria-selected={ isSelected }
213
- onClick={ () => {
214
- setFontFamily( item.value );
215
- handleClose();
216
- } }
217
- onKeyDown={ ( event ) => {
218
- if ( event.key === 'Enter' ) {
219
- setFontFamily( item.value );
220
- handleClose();
221
- }
222
-
223
- if ( event.key === 'ArrowDown' && isLast ) {
224
- event.preventDefault();
225
- event.stopPropagation();
226
- }
227
-
228
- if ( event.key === 'ArrowUp' && isFirst ) {
229
- event.preventDefault();
230
- event.stopPropagation();
231
- }
232
- } }
233
- tabIndex={ isSelected ? 0 : tabIndexFallback }
234
- style={ {
235
- transform: `translateY(${ virtualRow.start }px)`,
236
- fontFamily: item.value,
237
- } }
238
- >
239
- { item.value }
240
- </li>
241
- );
242
- } ) }
243
- </StyledMenuList>
244
- </Box>
123
+ <PopoverMenuList
124
+ items={ fontListItems }
125
+ selectedValue={ selectedItem?.value }
126
+ onChange={ debouncedVirtualizeChange }
127
+ onSelect={ setFontFamily }
128
+ onClose={ handleClose }
129
+ itemStyle={ ( item ) => ( { fontFamily: item.value } ) }
130
+ data-testid="font-list"
131
+ />
245
132
  );
246
133
  };
247
134
 
248
- const StyledMenuList = styled( MenuList )( ( { theme } ) => ( {
249
- '& > li': {
250
- height: LIST_ITEM_HEIGHT,
251
- position: 'absolute',
252
- top: 0,
253
- left: 0,
254
- width: '100%',
255
- display: 'flex',
256
- alignItems: 'center',
257
- },
258
- '& > [role="option"]': {
259
- ...theme.typography.caption,
260
- lineHeight: 'inherit',
261
- padding: theme.spacing( 0.75, 2, 0.75, 4 ),
262
- '&:hover, &:focus': {
263
- backgroundColor: theme.palette.action.hover,
264
- },
265
- '&[aria-selected="true"]': {
266
- backgroundColor: theme.palette.action.selected,
267
- },
268
- cursor: 'pointer',
269
- textOverflow: 'ellipsis',
270
- },
271
- width: '100%',
272
- position: 'relative',
273
- } ) );
274
-
275
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
276
- const useDebounce = < TArgs extends any[] >( fn: ( ...args: TArgs ) => void, delay: number ) => {
135
+ const useDebounce = < TArgs extends unknown[] >( fn: ( ...args: TArgs ) => void, delay: number ) => {
277
136
  const [ debouncedFn ] = useState( () => debounce( fn, delay ) );
278
137
 
279
138
  useEffect( () => () => debouncedFn.cancel(), [ debouncedFn ] );
@@ -1,4 +1,4 @@
1
- import { type FC, type PropsWithChildren } from 'react';
1
+ import { forwardRef, type PropsWithChildren } from 'react';
2
2
  import * as React from 'react';
3
3
  import { Grid } from '@elementor/ui';
4
4
 
@@ -8,13 +8,10 @@ type PopoverGridContainerProps = PropsWithChildren< {
8
8
  flexWrap?: React.ComponentProps< typeof Grid >[ 'flexWrap' ];
9
9
  } >;
10
10
 
11
- export const PopoverGridContainer: FC< PopoverGridContainerProps > = ( {
12
- gap = 1.5,
13
- alignItems = 'center',
14
- flexWrap = 'nowrap',
15
- children,
16
- } ) => (
17
- <Grid container gap={ gap } alignItems={ alignItems } flexWrap={ flexWrap }>
18
- { children }
19
- </Grid>
11
+ export const PopoverGridContainer = forwardRef(
12
+ ( { gap = 1.5, alignItems = 'center', flexWrap = 'nowrap', children }: PopoverGridContainerProps, ref ) => (
13
+ <Grid container gap={ gap } alignItems={ alignItems } flexWrap={ flexWrap } ref={ ref }>
14
+ { children }
15
+ </Grid>
16
+ )
20
17
  );
@@ -48,6 +48,8 @@ type RepeaterProps< T > = {
48
48
  value: T;
49
49
  } >;
50
50
  };
51
+ showDuplicate?: boolean;
52
+ showToggle?: boolean;
51
53
  };
52
54
 
53
55
  const EMPTY_OPEN_ITEM = -1;
@@ -60,6 +62,8 @@ export const Repeater = < T, >( {
60
62
  addToBottom = false,
61
63
  values: repeaterValues = [],
62
64
  setValues: setRepeaterValues,
65
+ showDuplicate = true,
66
+ showToggle = true,
63
67
  }: RepeaterProps< Item< T > > ) => {
64
68
  const [ openItem, setOpenItem ] = useState( EMPTY_OPEN_ITEM );
65
69
 
@@ -195,6 +199,8 @@ export const Repeater = < T, >( {
195
199
  toggleDisableItem={ () => toggleDisableRepeaterItem( index ) }
196
200
  openOnMount={ openOnAdd && openItem === key }
197
201
  onOpen={ () => setOpenItem( EMPTY_OPEN_ITEM ) }
202
+ showDuplicate={ showDuplicate }
203
+ showToggle={ showToggle }
198
204
  >
199
205
  { ( props ) => (
200
206
  <itemSettings.Content { ...props } value={ value } bind={ String( index ) } />
@@ -219,6 +225,8 @@ type RepeaterItemProps = {
219
225
  children: ( { anchorEl }: { anchorEl: AnchorEl } ) => React.ReactNode;
220
226
  openOnMount: boolean;
221
227
  onOpen: () => void;
228
+ showDuplicate: boolean;
229
+ showToggle: boolean;
222
230
  disabled?: boolean;
223
231
  };
224
232
 
@@ -232,6 +240,8 @@ const RepeaterItem = ( {
232
240
  toggleDisableItem,
233
241
  openOnMount,
234
242
  onOpen,
243
+ showDuplicate,
244
+ showToggle,
235
245
  disabled,
236
246
  }: RepeaterItemProps ) => {
237
247
  const [ anchorEl, setAnchorEl ] = useState< AnchorEl >( null );
@@ -255,16 +265,20 @@ const RepeaterItem = ( {
255
265
  startIcon={ startIcon }
256
266
  actions={
257
267
  <>
258
- <Tooltip title={ duplicateLabel } placement="top">
259
- <IconButton size={ SIZE } onClick={ duplicateItem } aria-label={ duplicateLabel }>
260
- <CopyIcon fontSize={ SIZE } />
261
- </IconButton>
262
- </Tooltip>
263
- <Tooltip title={ toggleLabel } placement="top">
264
- <IconButton size={ SIZE } onClick={ toggleDisableItem } aria-label={ toggleLabel }>
265
- { propDisabled ? <EyeOffIcon fontSize={ SIZE } /> : <EyeIcon fontSize={ SIZE } /> }
266
- </IconButton>
267
- </Tooltip>
268
+ { showDuplicate && (
269
+ <Tooltip title={ duplicateLabel } placement="top">
270
+ <IconButton size={ SIZE } onClick={ duplicateItem } aria-label={ duplicateLabel }>
271
+ <CopyIcon fontSize={ SIZE } />
272
+ </IconButton>
273
+ </Tooltip>
274
+ ) }
275
+ { showToggle && (
276
+ <Tooltip title={ toggleLabel } placement="top">
277
+ <IconButton size={ SIZE } onClick={ toggleDisableItem } aria-label={ toggleLabel }>
278
+ { propDisabled ? <EyeOffIcon fontSize={ SIZE } /> : <EyeIcon fontSize={ SIZE } /> }
279
+ </IconButton>
280
+ </Tooltip>
281
+ ) }
268
282
  <Tooltip title={ removeLabel } placement="top">
269
283
  <IconButton size={ SIZE } onClick={ removeItem } aria-label={ removeLabel }>
270
284
  <XIcon fontSize={ SIZE } />
@@ -0,0 +1,125 @@
1
+ import * as React from 'react';
2
+ import { useRef } from 'react';
3
+ import { PencilIcon } from '@elementor/icons';
4
+ import { Box, InputAdornment, type PopupState } from '@elementor/ui';
5
+
6
+ import ControlActions from '../../control-actions/control-actions';
7
+ import { type ExtendedOption, isUnitExtendedOption, type Unit } from '../../utils/size-control';
8
+ import { SelectionEndAdornment, TextFieldInnerSelection } from '../size-control/text-field-inner-selection';
9
+
10
+ type SizeInputProps = {
11
+ unit: Unit | ExtendedOption;
12
+ size: number | string;
13
+ placeholder?: string;
14
+ startIcon?: React.ReactNode;
15
+ units: ( Unit | ExtendedOption )[];
16
+ onBlur?: ( event: React.FocusEvent< HTMLInputElement > ) => void;
17
+ onFocus?: ( event: React.FocusEvent< HTMLInputElement > ) => void;
18
+ onClick?: ( event: React.MouseEvent< HTMLInputElement > ) => void;
19
+ handleUnitChange: ( unit: Unit | ExtendedOption ) => void;
20
+ handleSizeChange: ( event: React.ChangeEvent< HTMLInputElement > ) => void;
21
+ popupState: PopupState;
22
+ disabled?: boolean;
23
+ };
24
+
25
+ const RESTRICTED_INPUT_KEYS = [ 'e', 'E', '+', '-' ];
26
+
27
+ export const SizeInput = ( {
28
+ units,
29
+ handleUnitChange,
30
+ handleSizeChange,
31
+ placeholder,
32
+ startIcon,
33
+ onBlur,
34
+ onFocus,
35
+ onClick,
36
+ size,
37
+ unit,
38
+ popupState,
39
+ disabled,
40
+ }: SizeInputProps ) => {
41
+ const unitInputBufferRef = useRef( '' );
42
+ const inputType = isUnitExtendedOption( unit ) ? 'text' : 'number';
43
+ const inputValue = ! isUnitExtendedOption( unit ) && Number.isNaN( size ) ? '' : size ?? '';
44
+
45
+ const handleKeyUp = ( event: React.KeyboardEvent< HTMLInputElement > ) => {
46
+ const { key } = event;
47
+
48
+ if ( ! /^[a-zA-Z%]$/.test( key ) ) {
49
+ return;
50
+ }
51
+
52
+ event.preventDefault();
53
+
54
+ const newChar = key.toLowerCase();
55
+ const updatedBuffer = ( unitInputBufferRef.current + newChar ).slice( -3 );
56
+ unitInputBufferRef.current = updatedBuffer;
57
+
58
+ const matchedUnit =
59
+ units.find( ( u ) => u.includes( updatedBuffer ) ) ||
60
+ units.find( ( u ) => u.startsWith( newChar ) ) ||
61
+ units.find( ( u ) => u.includes( newChar ) );
62
+
63
+ if ( matchedUnit ) {
64
+ handleUnitChange( matchedUnit );
65
+ }
66
+ };
67
+
68
+ const popupAttributes = {
69
+ 'aria-controls': popupState.isOpen ? popupState.popupId : undefined,
70
+ 'aria-haspopup': true,
71
+ };
72
+
73
+ const inputProps = {
74
+ ...popupAttributes,
75
+ autoComplete: 'off',
76
+ onClick,
77
+ onFocus,
78
+ startAdornment: startIcon ? (
79
+ <InputAdornment position="start" disabled={ disabled }>
80
+ { startIcon }
81
+ </InputAdornment>
82
+ ) : undefined,
83
+ endAdornment: (
84
+ <SelectionEndAdornment
85
+ disabled={ disabled }
86
+ options={ units }
87
+ onClick={ handleUnitChange }
88
+ value={ unit }
89
+ alternativeOptionLabels={ {
90
+ custom: <PencilIcon fontSize="small" />,
91
+ } }
92
+ menuItemsAttributes={
93
+ units.includes( 'custom' )
94
+ ? {
95
+ custom: popupAttributes,
96
+ }
97
+ : undefined
98
+ }
99
+ />
100
+ ),
101
+ };
102
+
103
+ return (
104
+ <ControlActions>
105
+ <Box>
106
+ <TextFieldInnerSelection
107
+ disabled={ disabled }
108
+ placeholder={ placeholder }
109
+ type={ inputType }
110
+ value={ inputValue }
111
+ onChange={ handleSizeChange }
112
+ onKeyDown={ ( event ) => {
113
+ if ( RESTRICTED_INPUT_KEYS.includes( event.key ) ) {
114
+ event.preventDefault();
115
+ }
116
+ } }
117
+ onKeyUp={ handleKeyUp }
118
+ onBlur={ onBlur }
119
+ shouldBlockInput={ isUnitExtendedOption( unit ) }
120
+ inputProps={ inputProps }
121
+ />
122
+ </Box>
123
+ </ControlActions>
124
+ );
125
+ };
@@ -2,7 +2,16 @@ import * as React from 'react';
2
2
  import { forwardRef, useId } from 'react';
3
3
  import { type PropValue } from '@elementor/editor-props';
4
4
  import { MenuListItem } from '@elementor/editor-ui';
5
- import { bindMenu, bindTrigger, Button, InputAdornment, Menu, TextField, usePopupState } from '@elementor/ui';
5
+ import {
6
+ bindMenu,
7
+ bindTrigger,
8
+ Button,
9
+ InputAdornment,
10
+ Menu,
11
+ TextField,
12
+ type TextFieldProps,
13
+ usePopupState,
14
+ } from '@elementor/ui';
6
15
 
7
16
  type TextFieldInnerSelectionProps = {
8
17
  placeholder?: string;
@@ -12,8 +21,10 @@ type TextFieldInnerSelectionProps = {
12
21
  onBlur?: ( event: React.FocusEvent< HTMLInputElement > ) => void;
13
22
  onKeyDown?: ( event: React.KeyboardEvent< HTMLInputElement > ) => void;
14
23
  onKeyUp?: ( event: React.KeyboardEvent< HTMLInputElement > ) => void;
15
- endAdornment: React.ReactNode;
16
- startAdornment?: React.ReactNode;
24
+ shouldBlockInput?: boolean;
25
+ inputProps: TextFieldProps[ 'InputProps' ] & {
26
+ endAdornment: React.JSX.Element;
27
+ };
17
28
  disabled?: boolean;
18
29
  };
19
30
 
@@ -27,8 +38,8 @@ export const TextFieldInnerSelection = forwardRef(
27
38
  onBlur,
28
39
  onKeyDown,
29
40
  onKeyUp,
30
- endAdornment,
31
- startAdornment,
41
+ shouldBlockInput = false,
42
+ inputProps,
32
43
  disabled,
33
44
  }: TextFieldInnerSelectionProps,
34
45
  ref
@@ -36,20 +47,18 @@ export const TextFieldInnerSelection = forwardRef(
36
47
  return (
37
48
  <TextField
38
49
  ref={ ref }
50
+ sx={ { input: { cursor: shouldBlockInput ? 'default !important' : undefined } } }
39
51
  size="tiny"
40
52
  fullWidth
41
- type={ type }
53
+ type={ shouldBlockInput ? undefined : type }
42
54
  value={ value }
55
+ onChange={ shouldBlockInput ? undefined : onChange }
56
+ onKeyDown={ shouldBlockInput ? undefined : onKeyDown }
57
+ onKeyUp={ shouldBlockInput ? undefined : onKeyUp }
43
58
  disabled={ disabled }
44
- onChange={ onChange }
45
- onKeyDown={ onKeyDown }
46
- onKeyUp={ onKeyUp }
47
59
  onBlur={ onBlur }
48
60
  placeholder={ placeholder }
49
- InputProps={ {
50
- endAdornment,
51
- startAdornment,
52
- } }
61
+ InputProps={ inputProps }
53
62
  />
54
63
  );
55
64
  }
@@ -59,13 +68,17 @@ type SelectionEndAdornmentProps< T extends string > = {
59
68
  options: T[];
60
69
  onClick: ( value: T ) => void;
61
70
  value: T;
71
+ alternativeOptionLabels?: { [ key in T ]?: React.ReactNode };
72
+ menuItemsAttributes?: { [ key in T ]?: Record< string, unknown > };
62
73
  disabled?: boolean;
63
74
  };
64
75
 
65
76
  export const SelectionEndAdornment = < T extends string >( {
66
77
  options,
78
+ alternativeOptionLabels = {} as Record< T, React.ReactNode >,
67
79
  onClick,
68
80
  value,
81
+ menuItemsAttributes = {},
69
82
  disabled,
70
83
  }: SelectionEndAdornmentProps< T > ) => {
71
84
  const popupState = usePopupState( {
@@ -87,13 +100,17 @@ export const SelectionEndAdornment = < T extends string >( {
87
100
  sx={ { font: 'inherit', minWidth: 'initial', textTransform: 'uppercase' } }
88
101
  { ...bindTrigger( popupState ) }
89
102
  >
90
- { value }
103
+ { alternativeOptionLabels[ value ] ?? value }
91
104
  </Button>
92
105
 
93
106
  <Menu MenuListProps={ { dense: true } } { ...bindMenu( popupState ) }>
94
107
  { options.map( ( option, index ) => (
95
- <MenuListItem key={ option } onClick={ () => handleMenuItemClick( index ) }>
96
- { option.toUpperCase() }
108
+ <MenuListItem
109
+ key={ option }
110
+ onClick={ () => handleMenuItemClick( index ) }
111
+ { ...menuItemsAttributes?.[ option ] }
112
+ >
113
+ { alternativeOptionLabels[ option ] ?? option.toUpperCase() }
97
114
  </MenuListItem>
98
115
  ) ) }
99
116
  </Menu>
@@ -0,0 +1,47 @@
1
+ import * as React from 'react';
2
+ import { type MutableRefObject } from 'react';
3
+ import { bindPopover, Paper, Popover, type PopupState, TextField } from '@elementor/ui';
4
+
5
+ type Props = {
6
+ popupState: PopupState;
7
+ anchorRef: MutableRefObject< HTMLElement >;
8
+ restoreValue: () => void;
9
+ value: string;
10
+ onChange: ( event: React.ChangeEvent< HTMLInputElement > ) => void;
11
+ };
12
+
13
+ export const TextFieldPopover = ( props: Props ) => {
14
+ const { popupState, restoreValue, anchorRef, value, onChange } = props;
15
+
16
+ return (
17
+ <Popover
18
+ disablePortal
19
+ { ...bindPopover( popupState ) }
20
+ anchorOrigin={ { vertical: 'bottom', horizontal: 'center' } }
21
+ transformOrigin={ { vertical: 'top', horizontal: 'center' } }
22
+ onClose={ () => {
23
+ restoreValue();
24
+ popupState.close();
25
+ } }
26
+ >
27
+ <Paper
28
+ sx={ {
29
+ width: anchorRef.current.offsetWidth + 'px',
30
+ borderRadius: 2,
31
+ p: 1.5,
32
+ } }
33
+ >
34
+ <TextField
35
+ value={ value }
36
+ onChange={ onChange }
37
+ size="tiny"
38
+ type="text"
39
+ fullWidth
40
+ inputProps={ {
41
+ autoFocus: true,
42
+ } }
43
+ />
44
+ </Paper>
45
+ </Popover>
46
+ );
47
+ };