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