@elementor/editor-controls 0.34.2 → 0.36.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;
@@ -41,31 +17,20 @@ type FontFamilyControlProps = {
41
17
  fontFamilies: FontCategory[];
42
18
  };
43
19
 
20
+ const SIZE = 'tiny';
21
+
44
22
  export const FontFamilyControl = createControl( ( { fontFamilies }: FontFamilyControlProps ) => {
45
- const [ searchValue, setSearchValue ] = useState( '' );
46
23
  const { value: fontFamily, setValue: setFontFamily, disabled } = useBoundProp( stringPropTypeUtil );
47
24
 
48
25
  const popoverState = usePopupState( { variant: 'popover' } );
49
26
 
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
27
  return (
63
28
  <>
64
29
  <ControlActions>
65
30
  <UnstableTag
66
31
  variant="outlined"
67
32
  label={ fontFamily }
68
- endIcon={ <ChevronDownIcon fontSize="tiny" /> }
33
+ endIcon={ <ChevronDownIcon fontSize={ SIZE } /> }
69
34
  { ...bindTrigger( popoverState ) }
70
35
  fullWidth
71
36
  disabled={ disabled }
@@ -76,240 +41,14 @@ export const FontFamilyControl = createControl( ( { fontFamilies }: FontFamilyCo
76
41
  disableScrollLock
77
42
  anchorOrigin={ { vertical: 'bottom', horizontal: 'left' } }
78
43
  { ...bindPopover( popoverState ) }
79
- onClose={ handleClose }
80
44
  >
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>
45
+ <FontFamilySelector
46
+ fontFamilies={ fontFamilies }
47
+ fontFamily={ fontFamily }
48
+ onFontFamilyChange={ setFontFamily }
49
+ onClose={ popoverState.close }
50
+ />
157
51
  </Popover>
158
52
  </>
159
53
  );
160
54
  } );
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
- };
@@ -44,6 +44,8 @@ export const GapControl = createControl( ( { label }: { label: string } ) => {
44
44
  // translators: %s: Tooltip title.
45
45
  const unlinkedLabel = __( 'Unlink %s', 'elementor' ).replace( '%s', tooltipLabel );
46
46
 
47
+ const disabled = sizeDisabled || directionDisabled;
48
+
47
49
  return (
48
50
  <PropProvider propType={ propType } value={ directionValue } setValue={ setDirectionValue }>
49
51
  <Stack direction="row" gap={ 2 } flexWrap="nowrap">
@@ -56,7 +58,7 @@ export const GapControl = createControl( ( { label }: { label: string } ) => {
56
58
  selected={ isLinked }
57
59
  sx={ { marginLeft: 'auto' } }
58
60
  onChange={ onLinkToggle }
59
- disabled={ sizeDisabled || directionDisabled }
61
+ disabled={ disabled }
60
62
  >
61
63
  <LinkedIcon fontSize={ 'tiny' } />
62
64
  </ToggleButton>
@@ -29,7 +29,7 @@ export const ImageControl = createControl(
29
29
  <Stack gap={ 1.5 }>
30
30
  { [ 'all', 'media' ].includes( showMode ) ? (
31
31
  <PropKeyProvider bind={ 'src' }>
32
- <ControlFormLabel> { __( 'Image', 'elementor' ) } </ControlFormLabel>
32
+ <ControlFormLabel>{ __( 'Image', 'elementor' ) }</ControlFormLabel>
33
33
  <ImageMediaControl mediaTypes={ mediaTypes } />
34
34
  </PropKeyProvider>
35
35
  ) : null }
@@ -37,7 +37,7 @@ export const ImageControl = createControl(
37
37
  <PropKeyProvider bind={ 'size' }>
38
38
  <Grid container gap={ 1.5 } alignItems="center" flexWrap="nowrap">
39
39
  <Grid item xs={ 6 }>
40
- <ControlFormLabel> { resolutionLabel } </ControlFormLabel>
40
+ <ControlFormLabel>{ resolutionLabel }</ControlFormLabel>
41
41
  </Grid>
42
42
  <Grid item xs={ 6 } sx={ { overflow: 'hidden' } }>
43
43
  <SelectControl options={ sizes } />
@@ -189,7 +189,7 @@ export const LinkControl = createControl( ( props: Props ) => {
189
189
  </ControlActions>
190
190
  </PropKeyProvider>
191
191
  <PropKeyProvider bind={ 'isTargetBlank' }>
192
- <SwitchControl disabled={ ! value } />
192
+ <SwitchControl disabled={ propContext.disabled || ! value } />
193
193
  </PropKeyProvider>
194
194
  </Stack>
195
195
  </Collapse>
@@ -58,8 +58,15 @@ export const LinkedDimensionsControl = createControl(
58
58
  // translators: %s: Tooltip title.
59
59
  const unlinkedLabel = __( 'Unlink %s', 'elementor' ).replace( '%s', tooltipLabel );
60
60
 
61
+ const disabled = sizeDisabled || dimensionsDisabled;
62
+
61
63
  return (
62
- <PropProvider propType={ propType } value={ dimensionsValue } setValue={ setDimensionsValue }>
64
+ <PropProvider
65
+ propType={ propType }
66
+ value={ dimensionsValue }
67
+ setValue={ setDimensionsValue }
68
+ disabled={ disabled }
69
+ >
63
70
  <Stack direction="row" gap={ 2 } flexWrap="nowrap">
64
71
  { isUsingNestedProps ? (
65
72
  <ControlFormLabel>{ label }</ControlFormLabel>
@@ -74,7 +81,7 @@ export const LinkedDimensionsControl = createControl(
74
81
  selected={ isLinked }
75
82
  sx={ { marginLeft: 'auto' } }
76
83
  onChange={ onLinkToggle }
77
- disabled={ sizeDisabled || dimensionsDisabled }
84
+ disabled={ disabled }
78
85
  >
79
86
  <LinkedIcon fontSize={ 'tiny' } />
80
87
  </ToggleButton>
@@ -141,6 +148,8 @@ export const LinkedDimensionsControl = createControl(
141
148
  <Grid item xs={ 12 }>
142
149
  <Control
143
150
  bind={ 'inline-start' }
151
+ isLinked={ isLinked }
152
+ extendedValues={ extendedValues }
144
153
  startIcon={
145
154
  isSiteRtl ? (
146
155
  <SideRightIcon fontSize={ 'tiny' } />
@@ -148,8 +157,6 @@ export const LinkedDimensionsControl = createControl(
148
157
  <SideLeftIcon fontSize={ 'tiny' } />
149
158
  )
150
159
  }
151
- isLinked={ isLinked }
152
- extendedValues={ extendedValues }
153
160
  />
154
161
  </Grid>
155
162
  </Grid>
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';