@elementor/editor-controls 0.34.2 → 0.35.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.
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.34.2",
4
+ "version": "0.35.0",
5
5
  "private": false,
6
6
  "author": "Elementor Team",
7
7
  "homepage": "https://elementor.com/",
@@ -0,0 +1,284 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import * as React from 'react';
3
+ import { type FontCategory } from '@elementor/editor-controls';
4
+ import { SearchIcon, TextIcon, XIcon } from '@elementor/icons';
5
+ import {
6
+ Box,
7
+ Divider,
8
+ IconButton,
9
+ InputAdornment,
10
+ Link,
11
+ MenuList,
12
+ MenuSubheader,
13
+ Stack,
14
+ styled,
15
+ TextField,
16
+ Typography,
17
+ } from '@elementor/ui';
18
+ import { debounce } from '@elementor/utils';
19
+ import { useVirtualizer } from '@tanstack/react-virtual';
20
+ import { __ } from '@wordpress/i18n';
21
+
22
+ import { enqueueFont } from '../controls/font-family-control/enqueue-font';
23
+ import { type FontListItem, useFilteredFontFamilies } from '../hooks/use-filtered-font-families';
24
+
25
+ const SIZE = 'tiny';
26
+
27
+ type FontFamilySelectorProps = {
28
+ fontFamilies: FontCategory[];
29
+ fontFamily: string | null;
30
+ onFontFamilyChange: ( fontFamily: string ) => void;
31
+ onClose: () => void;
32
+ };
33
+
34
+ export const FontFamilySelector = ( {
35
+ fontFamilies,
36
+ fontFamily,
37
+ onFontFamilyChange,
38
+ onClose,
39
+ }: FontFamilySelectorProps ) => {
40
+ const [ searchValue, setSearchValue ] = useState( '' );
41
+
42
+ const filteredFontFamilies = useFilteredFontFamilies( fontFamilies, searchValue );
43
+
44
+ const handleSearch = ( event: React.ChangeEvent< HTMLInputElement > ) => {
45
+ setSearchValue( event.target.value );
46
+ };
47
+
48
+ const handleClose = () => {
49
+ setSearchValue( '' );
50
+ onClose();
51
+ };
52
+
53
+ return (
54
+ <Stack>
55
+ <Stack direction="row" alignItems="center" pl={ 1.5 } pr={ 0.5 } py={ 1.5 }>
56
+ <TextIcon fontSize={ SIZE } sx={ { mr: 0.5 } } />
57
+ <Typography variant="subtitle2">{ __( 'Font Family', 'elementor' ) }</Typography>
58
+ <IconButton size={ SIZE } sx={ { ml: 'auto' } } onClick={ handleClose }>
59
+ <XIcon fontSize={ SIZE } />
60
+ </IconButton>
61
+ </Stack>
62
+
63
+ <Box px={ 1.5 } pb={ 1 }>
64
+ <TextField
65
+ // eslint-disable-next-line jsx-a11y/no-autofocus
66
+ autoFocus
67
+ fullWidth
68
+ size={ SIZE }
69
+ value={ searchValue }
70
+ placeholder={ __( 'Search', 'elementor' ) }
71
+ onChange={ handleSearch }
72
+ InputProps={ {
73
+ startAdornment: (
74
+ <InputAdornment position="start">
75
+ <SearchIcon fontSize={ SIZE } />
76
+ </InputAdornment>
77
+ ),
78
+ } }
79
+ />
80
+ </Box>
81
+ <Divider />
82
+ { filteredFontFamilies.length > 0 ? (
83
+ <FontList
84
+ fontListItems={ filteredFontFamilies }
85
+ setFontFamily={ onFontFamilyChange }
86
+ handleClose={ handleClose }
87
+ fontFamily={ fontFamily }
88
+ />
89
+ ) : (
90
+ <Box sx={ { overflowY: 'auto', height: 260, width: 220 } }>
91
+ <Stack alignItems="center" p={ 2.5 } gap={ 1.5 } overflow={ 'hidden' }>
92
+ <TextIcon fontSize="large" />
93
+ <Box sx={ { maxWidth: 160, overflow: 'hidden' } }>
94
+ <Typography align="center" variant="subtitle2" color="text.secondary">
95
+ { __( 'Sorry, nothing matched', 'elementor' ) }
96
+ </Typography>
97
+ <Typography
98
+ variant="subtitle2"
99
+ color="text.secondary"
100
+ sx={ {
101
+ display: 'flex',
102
+ width: '100%',
103
+ justifyContent: 'center',
104
+ } }
105
+ >
106
+ <span>&ldquo;</span>
107
+ <span style={ { maxWidth: '80%', overflow: 'hidden', textOverflow: 'ellipsis' } }>
108
+ { searchValue }
109
+ </span>
110
+ <span>&rdquo;.</span>
111
+ </Typography>
112
+ </Box>
113
+ <Typography align="center" variant="caption" color="text.secondary">
114
+ { __( 'Try something else.', 'elementor' ) }
115
+ <Link
116
+ color="secondary"
117
+ variant="caption"
118
+ component="button"
119
+ onClick={ () => setSearchValue( '' ) }
120
+ >
121
+ { __( 'Clear & try again', 'elementor' ) }
122
+ </Link>
123
+ </Typography>
124
+ </Stack>
125
+ </Box>
126
+ ) }
127
+ </Stack>
128
+ );
129
+ };
130
+
131
+ type FontListProps = {
132
+ fontListItems: FontListItem[];
133
+ setFontFamily: ( fontFamily: string ) => void;
134
+ handleClose: () => void;
135
+ fontFamily: string | null;
136
+ };
137
+
138
+ const LIST_ITEM_HEIGHT = 36;
139
+ const LIST_ITEMS_BUFFER = 6;
140
+
141
+ const FontList = ( { fontListItems, setFontFamily, handleClose, fontFamily }: FontListProps ) => {
142
+ const containerRef = useRef< HTMLDivElement >( null );
143
+ const selectedItem = fontListItems.find( ( item ) => item.value === fontFamily );
144
+
145
+ const debouncedVirtualizeChange = useDebounce( ( { getVirtualIndexes }: { getVirtualIndexes: () => number[] } ) => {
146
+ getVirtualIndexes().forEach( ( index ) => {
147
+ const item = fontListItems[ index ];
148
+ if ( item && item.type === 'font' ) {
149
+ enqueueFont( item.value );
150
+ }
151
+ } );
152
+ }, 100 );
153
+
154
+ const virtualizer = useVirtualizer( {
155
+ count: fontListItems.length,
156
+ getScrollElement: () => containerRef.current,
157
+ estimateSize: () => LIST_ITEM_HEIGHT,
158
+ overscan: LIST_ITEMS_BUFFER,
159
+ onChange: debouncedVirtualizeChange,
160
+ } );
161
+
162
+ useEffect(
163
+ () => {
164
+ virtualizer.scrollToIndex( fontListItems.findIndex( ( item ) => item.value === fontFamily ) );
165
+ },
166
+ // eslint-disable-next-line react-compiler/react-compiler
167
+ // eslint-disable-next-line react-hooks/exhaustive-deps
168
+ [ fontFamily ]
169
+ );
170
+
171
+ return (
172
+ <Box
173
+ ref={ containerRef }
174
+ sx={ {
175
+ overflowY: 'auto',
176
+ height: 260,
177
+ width: 220,
178
+ } }
179
+ >
180
+ <StyledMenuList
181
+ role="listbox"
182
+ style={ {
183
+ height: `${ virtualizer.getTotalSize() }px`,
184
+ } }
185
+ data-testid="font-list"
186
+ >
187
+ { virtualizer.getVirtualItems().map( ( virtualRow ) => {
188
+ const item = fontListItems[ virtualRow.index ];
189
+ const isLast = virtualRow.index === fontListItems.length - 1;
190
+ // Ignore the first item, which is a category, and use the second item instead.
191
+ const isFirst = virtualRow.index === 1;
192
+ const isSelected = selectedItem?.value === item.value;
193
+
194
+ // If no item is selected, the first item should be focused.
195
+ const tabIndexFallback = ! selectedItem ? 0 : -1;
196
+
197
+ if ( item.type === 'category' ) {
198
+ return (
199
+ <MenuSubheader
200
+ key={ virtualRow.key }
201
+ style={ {
202
+ transform: `translateY(${ virtualRow.start }px)`,
203
+ } }
204
+ >
205
+ { item.value }
206
+ </MenuSubheader>
207
+ );
208
+ }
209
+
210
+ return (
211
+ <li
212
+ key={ virtualRow.key }
213
+ role="option"
214
+ aria-selected={ isSelected }
215
+ onClick={ () => {
216
+ setFontFamily( item.value );
217
+ handleClose();
218
+ } }
219
+ onKeyDown={ ( event ) => {
220
+ if ( event.key === 'Enter' ) {
221
+ setFontFamily( item.value );
222
+ handleClose();
223
+ }
224
+
225
+ if ( event.key === 'ArrowDown' && isLast ) {
226
+ event.preventDefault();
227
+ event.stopPropagation();
228
+ }
229
+
230
+ if ( event.key === 'ArrowUp' && isFirst ) {
231
+ event.preventDefault();
232
+ event.stopPropagation();
233
+ }
234
+ } }
235
+ tabIndex={ isSelected ? 0 : tabIndexFallback }
236
+ style={ {
237
+ transform: `translateY(${ virtualRow.start }px)`,
238
+ fontFamily: item.value,
239
+ } }
240
+ >
241
+ { item.value }
242
+ </li>
243
+ );
244
+ } ) }
245
+ </StyledMenuList>
246
+ </Box>
247
+ );
248
+ };
249
+
250
+ const StyledMenuList = styled( MenuList )( ( { theme } ) => ( {
251
+ '& > li': {
252
+ height: LIST_ITEM_HEIGHT,
253
+ position: 'absolute',
254
+ top: 0,
255
+ left: 0,
256
+ width: '100%',
257
+ display: 'flex',
258
+ alignItems: 'center',
259
+ },
260
+ '& > [role="option"]': {
261
+ ...theme.typography.caption,
262
+ lineHeight: 'inherit',
263
+ padding: theme.spacing( 0.75, 2, 0.75, 4 ),
264
+ '&:hover, &:focus': {
265
+ backgroundColor: theme.palette.action.hover,
266
+ },
267
+ '&[aria-selected="true"]': {
268
+ backgroundColor: theme.palette.action.selected,
269
+ },
270
+ cursor: 'pointer',
271
+ textOverflow: 'ellipsis',
272
+ },
273
+ width: '100%',
274
+ position: 'relative',
275
+ } ) );
276
+
277
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
278
+ const useDebounce = < TArgs extends any[] >( fn: ( ...args: TArgs ) => void, delay: number ) => {
279
+ const [ debouncedFn ] = useState( () => debounce( fn, delay ) );
280
+
281
+ useEffect( () => () => debouncedFn.cancel(), [ debouncedFn ] );
282
+
283
+ return debouncedFn;
284
+ };
@@ -8,6 +8,7 @@ import { __ } from '@wordpress/i18n';
8
8
 
9
9
  import { useBoundProp } from '../bound-prop-context';
10
10
  import { ControlLabel } from '../components/control-label';
11
+ import ControlActions from '../control-actions/control-actions';
11
12
  import { createControl } from '../create-control';
12
13
 
13
14
  const RATIO_OPTIONS = [
@@ -69,61 +70,63 @@ export const AspectRatioControl = createControl( ( { label }: { label: string }
69
70
  };
70
71
 
71
72
  return (
72
- <Stack direction="column" pt={ 2 } gap={ 2 }>
73
- <Grid container gap={ 2 } alignItems="center" flexWrap="nowrap">
74
- <Grid item xs={ 6 }>
75
- <ControlLabel>{ label }</ControlLabel>
76
- </Grid>
77
- <Grid item xs={ 6 }>
78
- <Select
79
- size="tiny"
80
- displayEmpty
81
- sx={ { overflow: 'hidden' } }
82
- disabled={ disabled }
83
- value={ selectedValue }
84
- onChange={ handleSelectChange }
85
- fullWidth
86
- >
87
- { [ ...RATIO_OPTIONS, { label: __( 'Custom', 'elementor' ), value: CUSTOM_RATIO } ].map(
88
- ( { label: optionLabel, ...props } ) => (
89
- <MenuListItem key={ props.value } { ...props } value={ props.value ?? '' }>
90
- { optionLabel }
91
- </MenuListItem>
92
- )
93
- ) }
94
- </Select>
95
- </Grid>
96
- </Grid>
97
- { isCustom && (
73
+ <ControlActions>
74
+ <Stack direction="column" pt={ 2 } gap={ 2 }>
98
75
  <Grid container gap={ 2 } alignItems="center" flexWrap="nowrap">
99
76
  <Grid item xs={ 6 }>
100
- <TextField
101
- size="tiny"
102
- type="number"
103
- fullWidth
104
- disabled={ disabled }
105
- value={ customWidth }
106
- onChange={ handleCustomWidthChange }
107
- InputProps={ {
108
- startAdornment: <ArrowsMoveHorizontalIcon fontSize="tiny" />,
109
- } }
110
- />
77
+ <ControlLabel>{ label }</ControlLabel>
111
78
  </Grid>
112
79
  <Grid item xs={ 6 }>
113
- <TextField
80
+ <Select
114
81
  size="tiny"
115
- type="number"
116
- fullWidth
82
+ displayEmpty
83
+ sx={ { overflow: 'hidden' } }
117
84
  disabled={ disabled }
118
- value={ customHeight }
119
- onChange={ handleCustomHeightChange }
120
- InputProps={ {
121
- startAdornment: <ArrowsMoveVerticalIcon fontSize="tiny" />,
122
- } }
123
- />
85
+ value={ selectedValue }
86
+ onChange={ handleSelectChange }
87
+ fullWidth
88
+ >
89
+ { [ ...RATIO_OPTIONS, { label: __( 'Custom', 'elementor' ), value: CUSTOM_RATIO } ].map(
90
+ ( { label: optionLabel, ...props } ) => (
91
+ <MenuListItem key={ props.value } { ...props } value={ props.value ?? '' }>
92
+ { optionLabel }
93
+ </MenuListItem>
94
+ )
95
+ ) }
96
+ </Select>
124
97
  </Grid>
125
98
  </Grid>
126
- ) }
127
- </Stack>
99
+ { isCustom && (
100
+ <Grid container gap={ 2 } alignItems="center" flexWrap="nowrap">
101
+ <Grid item xs={ 6 }>
102
+ <TextField
103
+ size="tiny"
104
+ type="number"
105
+ fullWidth
106
+ disabled={ disabled }
107
+ value={ customWidth }
108
+ onChange={ handleCustomWidthChange }
109
+ InputProps={ {
110
+ startAdornment: <ArrowsMoveHorizontalIcon fontSize="tiny" />,
111
+ } }
112
+ />
113
+ </Grid>
114
+ <Grid item xs={ 6 }>
115
+ <TextField
116
+ size="tiny"
117
+ type="number"
118
+ fullWidth
119
+ disabled={ disabled }
120
+ value={ customHeight }
121
+ onChange={ handleCustomHeightChange }
122
+ InputProps={ {
123
+ startAdornment: <ArrowsMoveVerticalIcon fontSize="tiny" />,
124
+ } }
125
+ />
126
+ </Grid>
127
+ </Grid>
128
+ ) }
129
+ </Stack>
130
+ </ControlActions>
128
131
  );
129
132
  } );