@elementor/editor-controls 4.3.0-beta1 → 4.3.0-beta3

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.
@@ -0,0 +1,344 @@
1
+ import * as React from 'react';
2
+ import { useLayoutEffect, useRef, useState } from 'react';
3
+ import { Box, Tooltip, useTheme } from '@elementor/ui';
4
+ import { useVirtualizer } from '@tanstack/react-virtual';
5
+ import { __ } from '@wordpress/i18n';
6
+
7
+ import { type FontAwesome7Icon } from './font-awesome-7-catalog';
8
+ import { FontAwesomeGlyph } from './font-awesome-glyph';
9
+
10
+ export const ICON_LIBRARY_GRID_FALLBACK_COLUMN_COUNT = 4;
11
+ export const ICON_LIBRARY_GRID_MIN_CELL_SIZE = 52;
12
+ export const ICON_LIBRARY_GRID_TOOLTIP_ENTER_DELAY = 1000;
13
+
14
+ const ICON_GLYPH_SIZE = 20;
15
+ const GRID_OVERSCAN = 6;
16
+ const GRID_COLUMN_GAP = 1;
17
+ const GRID_HORIZONTAL_PADDING = 1;
18
+ const HOME_END_KEYS = new Set( [ 'Home', 'End' ] );
19
+
20
+ const getGridMetrics = ( containerWidth: number, columnGap: number, inlinePadding: number ) => {
21
+ const availableWidth = containerWidth - inlinePadding;
22
+
23
+ if ( availableWidth <= 0 ) {
24
+ return {
25
+ columnCount: ICON_LIBRARY_GRID_FALLBACK_COLUMN_COUNT,
26
+ cellSize: ICON_LIBRARY_GRID_MIN_CELL_SIZE,
27
+ };
28
+ }
29
+
30
+ const columnCount = Math.max(
31
+ 1,
32
+ Math.floor( ( availableWidth + columnGap ) / ( ICON_LIBRARY_GRID_MIN_CELL_SIZE + columnGap ) )
33
+ );
34
+ const cellSize = Math.floor( ( availableWidth - columnGap * ( columnCount - 1 ) ) / columnCount );
35
+
36
+ return {
37
+ columnCount,
38
+ cellSize: Math.max( cellSize, 1 ),
39
+ };
40
+ };
41
+
42
+ type IconLibraryGridItem = FontAwesome7Icon & {
43
+ id: string;
44
+ value: string;
45
+ };
46
+
47
+ type IconLibraryGridProps = {
48
+ items: IconLibraryGridItem[];
49
+ selectedValue?: string;
50
+ onSelect: ( id: string ) => void;
51
+ onClose: () => void;
52
+ noResultsComponent?: React.ReactNode;
53
+ };
54
+
55
+ export const IconLibraryGrid = ( {
56
+ items,
57
+ selectedValue,
58
+ onSelect,
59
+ onClose,
60
+ noResultsComponent,
61
+ }: IconLibraryGridProps ) => {
62
+ const theme = useTheme();
63
+ const containerRef = useRef< HTMLDivElement >( null );
64
+ const cellRefs = useRef( new Map< string, HTMLButtonElement >() );
65
+ const shouldRestoreFocusRef = useRef( false );
66
+ const selectedIndex = items.findIndex( ( item ) => item.id === selectedValue );
67
+ const [ focusedIndex, setFocusedIndex ] = useState( selectedIndex >= 0 ? selectedIndex : 0 );
68
+ const [ { columnCount, cellSize }, setGridMetrics ] = useState( () => getGridMetrics( 0, 0, 0 ) );
69
+ const rowCount = Math.ceil( items.length / columnCount );
70
+ const virtualizer = useVirtualizer( {
71
+ count: rowCount,
72
+ getScrollElement: () => containerRef.current,
73
+ estimateSize: () => cellSize,
74
+ overscan: GRID_OVERSCAN,
75
+ } );
76
+ const focusedItem = items[ focusedIndex ];
77
+ const visibleRowIndexes = virtualizer.getVirtualIndexes().join( ',' );
78
+
79
+ useLayoutEffect( () => {
80
+ setFocusedIndex( ( current ) => {
81
+ if ( items.length === 0 ) {
82
+ return 0;
83
+ }
84
+
85
+ if ( selectedIndex >= 0 ) {
86
+ return selectedIndex;
87
+ }
88
+
89
+ return Math.min( current, items.length - 1 );
90
+ } );
91
+
92
+ if ( selectedIndex >= 0 ) {
93
+ virtualizer.scrollToIndex( Math.floor( selectedIndex / columnCount ) );
94
+ }
95
+ // eslint-disable-next-line react-hooks/exhaustive-deps
96
+ }, [ columnCount, items, selectedIndex, selectedValue ] );
97
+
98
+ useLayoutEffect( () => {
99
+ if ( ! shouldRestoreFocusRef.current || ! focusedItem ) {
100
+ return;
101
+ }
102
+
103
+ const cell = cellRefs.current.get( focusedItem.id );
104
+
105
+ if ( ! cell ) {
106
+ return;
107
+ }
108
+
109
+ cell.focus();
110
+ shouldRestoreFocusRef.current = false;
111
+ }, [ focusedItem, visibleRowIndexes ] );
112
+
113
+ useLayoutEffect( () => {
114
+ const container = containerRef.current;
115
+
116
+ if ( ! container ) {
117
+ return;
118
+ }
119
+
120
+ const measureGrid = () => {
121
+ const columnGap = Number.parseFloat( theme.spacing( GRID_COLUMN_GAP ) );
122
+ const inlinePadding = Number.parseFloat( theme.spacing( GRID_HORIZONTAL_PADDING ) ) * 2;
123
+ setGridMetrics( getGridMetrics( container.clientWidth, columnGap, inlinePadding ) );
124
+ };
125
+
126
+ measureGrid();
127
+ const resizeObserver = new ResizeObserver( measureGrid );
128
+ resizeObserver.observe( container );
129
+
130
+ return () => {
131
+ resizeObserver.disconnect();
132
+ };
133
+ }, [ theme, items.length ] );
134
+
135
+ useLayoutEffect( () => {
136
+ virtualizer.measure();
137
+ // eslint-disable-next-line react-hooks/exhaustive-deps
138
+ }, [ cellSize, columnCount ] );
139
+
140
+ const isRtl = theme.direction === 'rtl' || document.documentElement.dir === 'rtl';
141
+ const horizontalOffset = isRtl ? -1 : 1;
142
+
143
+ const moveFocus = ( nextIndex: number ) => {
144
+ if ( nextIndex < 0 || nextIndex >= items.length ) {
145
+ return;
146
+ }
147
+
148
+ shouldRestoreFocusRef.current = true;
149
+ setFocusedIndex( nextIndex );
150
+ virtualizer.scrollToIndex( Math.floor( nextIndex / columnCount ) );
151
+ };
152
+
153
+ const handleKeyDown = ( event: React.KeyboardEvent< HTMLButtonElement >, index: number ) => {
154
+ if ( event.key === 'Enter' || event.key === ' ' ) {
155
+ event.preventDefault();
156
+ onSelect( items[ index ].id );
157
+ onClose();
158
+ return;
159
+ }
160
+
161
+ if ( event.key === 'ArrowRight' ) {
162
+ event.preventDefault();
163
+ moveFocus( index + horizontalOffset );
164
+ return;
165
+ }
166
+
167
+ if ( event.key === 'ArrowLeft' ) {
168
+ event.preventDefault();
169
+ moveFocus( index - horizontalOffset );
170
+ return;
171
+ }
172
+
173
+ if ( event.key === 'ArrowDown' ) {
174
+ event.preventDefault();
175
+ moveFocus( index + columnCount );
176
+ return;
177
+ }
178
+
179
+ if ( event.key === 'ArrowUp' ) {
180
+ event.preventDefault();
181
+ moveFocus( index - columnCount );
182
+ return;
183
+ }
184
+
185
+ if ( HOME_END_KEYS.has( event.key ) ) {
186
+ event.preventDefault();
187
+
188
+ if ( event.ctrlKey ) {
189
+ moveFocus( event.key === 'Home' ? 0 : items.length - 1 );
190
+ return;
191
+ }
192
+
193
+ const rowStart = Math.floor( index / columnCount ) * columnCount;
194
+ const rowEnd = Math.min( rowStart + columnCount - 1, items.length - 1 );
195
+
196
+ moveFocus( event.key === 'Home' ? rowStart : rowEnd );
197
+ }
198
+ };
199
+
200
+ return (
201
+ <Box
202
+ ref={ containerRef }
203
+ sx={ {
204
+ width: '100%',
205
+ height: '100%',
206
+ minWidth: 0,
207
+ overflowX: 'hidden',
208
+ overflowY: 'auto',
209
+ } }
210
+ >
211
+ { items.length === 0 && noResultsComponent ? (
212
+ noResultsComponent
213
+ ) : (
214
+ <Box
215
+ role="grid"
216
+ aria-label={ __( 'Icons', 'elementor' ) }
217
+ aria-rowcount={ rowCount }
218
+ aria-colcount={ columnCount }
219
+ data-testid="icon-library-grid"
220
+ sx={ {
221
+ width: '100%',
222
+ minWidth: 0,
223
+ height: virtualizer.getTotalSize(),
224
+ position: 'relative',
225
+ } }
226
+ >
227
+ { virtualizer.getVirtualItems().map( ( virtualRow ) => {
228
+ const startIndex = virtualRow.index * columnCount;
229
+ const rowItems = items.slice( startIndex, startIndex + columnCount );
230
+
231
+ return (
232
+ <Box
233
+ key={ virtualRow.key }
234
+ role="row"
235
+ aria-rowindex={ virtualRow.index + 1 }
236
+ sx={ {
237
+ position: 'absolute',
238
+ top: 0,
239
+ left: 0,
240
+ width: '100%',
241
+ height: virtualRow.size,
242
+ transform: `translateY(${ virtualRow.start }px)`,
243
+ display: 'grid',
244
+ gridTemplateColumns: `repeat(${ columnCount }, minmax(0, 1fr))`,
245
+ gap: GRID_COLUMN_GAP,
246
+ px: GRID_HORIZONTAL_PADDING,
247
+ boxSizing: 'border-box',
248
+ } }
249
+ >
250
+ { rowItems.map( ( item, columnIndex ) => {
251
+ const index = startIndex + columnIndex;
252
+ const isSelected = selectedValue === item.id;
253
+ const tabIndex = focusedIndex === index ? 0 : -1;
254
+
255
+ return (
256
+ <Box
257
+ key={ item.id }
258
+ role="presentation"
259
+ sx={ { minWidth: 0, minHeight: 0, width: '100%', height: '100%' } }
260
+ >
261
+ <Tooltip
262
+ title={ item.label }
263
+ placement="top"
264
+ enterDelay={ ICON_LIBRARY_GRID_TOOLTIP_ENTER_DELAY }
265
+ enterNextDelay={ ICON_LIBRARY_GRID_TOOLTIP_ENTER_DELAY }
266
+ disableInteractive
267
+ disableFocusListener
268
+ >
269
+ <Box
270
+ component="button"
271
+ type="button"
272
+ role="gridcell"
273
+ aria-colindex={ columnIndex + 1 }
274
+ aria-label={ item.label }
275
+ aria-selected={ isSelected }
276
+ tabIndex={ tabIndex }
277
+ ref={ ( node: HTMLButtonElement | null ) => {
278
+ if ( node ) {
279
+ cellRefs.current.set( item.id, node );
280
+ } else {
281
+ cellRefs.current.delete( item.id );
282
+ }
283
+ } }
284
+ onClick={ () => {
285
+ onSelect( item.id );
286
+ onClose();
287
+ } }
288
+ onFocus={ () => setFocusedIndex( index ) }
289
+ onKeyDown={ ( event: React.KeyboardEvent< HTMLButtonElement > ) =>
290
+ handleKeyDown( event, index )
291
+ }
292
+ sx={ {
293
+ boxSizing: 'border-box',
294
+ appearance: 'none',
295
+ m: 0,
296
+ width: '100%',
297
+ height: '100%',
298
+ minWidth: 0,
299
+ minHeight: 0,
300
+ display: 'flex',
301
+ alignItems: 'center',
302
+ justifyContent: 'center',
303
+ border: '1px solid',
304
+ borderColor: 'divider',
305
+ borderRadius: 1,
306
+ color: 'text.tertiary',
307
+ bgcolor: 'transparent',
308
+ p: 0,
309
+ cursor: 'pointer',
310
+ font: 'inherit',
311
+ lineHeight: 0,
312
+ overflow: 'hidden',
313
+ '&:hover, &:focus': {
314
+ bgcolor: 'action.hover',
315
+ },
316
+ '&[aria-selected="true"]': {
317
+ bgcolor: 'action.selected',
318
+ },
319
+ '&[aria-selected="true"]:hover, &[aria-selected="true"]:focus':
320
+ {
321
+ bgcolor: 'action.selected',
322
+ },
323
+ } }
324
+ >
325
+ { item.paths.length > 0 ? (
326
+ <FontAwesomeGlyph
327
+ icon={ item }
328
+ size={ ICON_GLYPH_SIZE }
329
+ color="currentColor"
330
+ />
331
+ ) : null }
332
+ </Box>
333
+ </Tooltip>
334
+ </Box>
335
+ );
336
+ } ) }
337
+ </Box>
338
+ );
339
+ } ) }
340
+ </Box>
341
+ ) }
342
+ </Box>
343
+ );
344
+ };
@@ -0,0 +1,305 @@
1
+ import * as React from 'react';
2
+ import { useMemo, useState } from 'react';
3
+ import {
4
+ PopoverBody,
5
+ PopoverHeader,
6
+ PopoverMenuList,
7
+ SearchField,
8
+ StyledMenuList,
9
+ type VirtualizedItem,
10
+ } from '@elementor/editor-ui';
11
+ import { ComponentsIcon } from '@elementor/icons';
12
+ import { useSessionStorage } from '@elementor/session';
13
+ import { Box, CircularProgress, Divider, Link, Stack, styled, Typography } from '@elementor/ui';
14
+ import { useDebounceState } from '@elementor/utils';
15
+ import { __ } from '@wordpress/i18n';
16
+
17
+ import {
18
+ createIconSelectionValue,
19
+ filterFontAwesome7Icons,
20
+ findFontAwesome7Icon,
21
+ type FontAwesome7Icon,
22
+ type FontAwesome7LibraryFilter,
23
+ } from './font-awesome-7-catalog';
24
+ import { FontAwesomeGlyph } from './font-awesome-glyph';
25
+ import { IconLibraryFilter } from './icon-library-filter';
26
+ import { IconLibraryGrid } from './icon-library-grid';
27
+ import { type IconLibraryView, IconLibraryViewToggle } from './icon-library-view-toggle';
28
+ import { useFontAwesome7Catalog } from './use-font-awesome-7-catalog';
29
+
30
+ export const ICON_LIBRARY_POPOVER_WIDTH = 300;
31
+ export const ICON_LIBRARY_ROW_HEIGHT = 48;
32
+ export const ICON_LIBRARY_SEARCH_DEBOUNCE_DELAY = 300;
33
+ export const ICON_LIBRARY_VIEW_STORAGE_KEY = 'icon-library-view';
34
+ export const ICON_LIBRARY_VIEW_STORAGE_PREFIX = 'editor-controls';
35
+ const DEFAULT_ICON_LIBRARY_VIEW: IconLibraryView = 'list';
36
+
37
+ const isIconLibraryView = ( value: unknown ): value is IconLibraryView => {
38
+ return value === 'grid' || value === 'list';
39
+ };
40
+ const ICON_TILE_SIZE = 40;
41
+ const ICON_GLYPH_SIZE = 20;
42
+ const ICON_LIBRARY_INLINE_SPACING = 1;
43
+
44
+ const CompactIconLibraryMenuList = styled( StyledMenuList )( ( { theme } ) => ( {
45
+ '& > [role="option"]': {
46
+ padding: theme.spacing( 0.75, ICON_LIBRARY_INLINE_SPACING ),
47
+ },
48
+ } ) );
49
+
50
+ type IconLibraryItem = VirtualizedItem< 'item', string > & Omit< FontAwesome7Icon, 'value' >;
51
+
52
+ type IconLibraryPopoverProps = {
53
+ open: boolean;
54
+ selectedIconClass: string | null;
55
+ selectedIconLibrary: string | null;
56
+ onSelect: ( icon: { value: string; library: string } ) => void;
57
+ onClose: () => void;
58
+ width?: number;
59
+ };
60
+
61
+ export const IconLibraryPopover = ( {
62
+ open,
63
+ selectedIconClass,
64
+ selectedIconLibrary,
65
+ onSelect,
66
+ onClose,
67
+ width = ICON_LIBRARY_POPOVER_WIDTH,
68
+ }: IconLibraryPopoverProps ) => {
69
+ const {
70
+ debouncedValue: searchValue,
71
+ inputValue: searchInputValue,
72
+ handleChange: handleSearchChange,
73
+ setImmediateValue: setSearchValue,
74
+ } = useDebounceState( { delay: ICON_LIBRARY_SEARCH_DEBOUNCE_DELAY } );
75
+ const [ activeLibraries, setActiveLibraries ] = useState< FontAwesome7LibraryFilter >( [] );
76
+ const [ storedView, setStoredView ] = useSessionStorage< IconLibraryView >(
77
+ ICON_LIBRARY_VIEW_STORAGE_KEY,
78
+ ICON_LIBRARY_VIEW_STORAGE_PREFIX
79
+ );
80
+ const view = isIconLibraryView( storedView ) ? storedView : DEFAULT_ICON_LIBRARY_VIEW;
81
+ const { data: icons = [], isLoading } = useFontAwesome7Catalog( open );
82
+
83
+ const items = useMemo(
84
+ () => createIconLibraryItems( icons, searchValue, activeLibraries ),
85
+ [ activeLibraries, icons, searchValue ]
86
+ );
87
+ const selectedValue = useMemo(
88
+ () => findFontAwesome7Icon( icons, selectedIconClass, selectedIconLibrary )?.id,
89
+ [ icons, selectedIconClass, selectedIconLibrary ]
90
+ );
91
+
92
+ const handleClose = () => {
93
+ setSearchValue( '' );
94
+ setActiveLibraries( [] );
95
+ onClose();
96
+ };
97
+
98
+ const handleSelect = ( id: string ) => {
99
+ const icon = items.find( ( item ) => item.id === id );
100
+
101
+ if ( ! icon ) {
102
+ return;
103
+ }
104
+
105
+ onSelect( {
106
+ value: createIconSelectionValue( icon.library, icon.name ),
107
+ library: icon.library,
108
+ } );
109
+ };
110
+
111
+ const handleClearSearch = () => {
112
+ setSearchValue( '' );
113
+ };
114
+
115
+ return (
116
+ <PopoverBody width={ width } fillWidth id="icon-library">
117
+ <PopoverHeader
118
+ title={ __( 'Icon library', 'elementor' ) }
119
+ onClose={ handleClose }
120
+ icon={ <ComponentsIcon fontSize="tiny" /> }
121
+ actions={ [ <IconLibraryViewToggle key="view" value={ view } onChange={ setStoredView } /> ] }
122
+ sx={ { pl: ICON_LIBRARY_INLINE_SPACING, pr: 0.5 } }
123
+ />
124
+ <Stack direction="row" alignItems="center" gap={ 1 } sx={ { px: ICON_LIBRARY_INLINE_SPACING, pb: 1 } }>
125
+ <SearchField
126
+ value={ searchInputValue }
127
+ onSearch={ handleSearchChange }
128
+ placeholder={ __( 'Search', 'elementor' ) }
129
+ id="icon-library-search"
130
+ sx={ { flex: 1, px: 0, pb: 0 } }
131
+ />
132
+ <IconLibraryFilter value={ activeLibraries } onChange={ setActiveLibraries } />
133
+ </Stack>
134
+ <Divider />
135
+ <Box sx={ { flex: 1, overflow: 'hidden', minHeight: 0, minWidth: 0 } }>
136
+ <IconLibraryContent
137
+ isLoading={ isLoading }
138
+ items={ items }
139
+ selectedValue={ selectedValue }
140
+ searchValue={ searchValue }
141
+ isCatalogAvailable={ icons.length > 0 }
142
+ view={ view }
143
+ onSelect={ handleSelect }
144
+ onClose={ handleClose }
145
+ onClearSearch={ handleClearSearch }
146
+ />
147
+ </Box>
148
+ </PopoverBody>
149
+ );
150
+ };
151
+
152
+ type IconLibraryContentProps = {
153
+ isLoading: boolean;
154
+ items: IconLibraryItem[];
155
+ selectedValue: string | undefined;
156
+ searchValue: string;
157
+ isCatalogAvailable: boolean;
158
+ view: IconLibraryView;
159
+ onSelect: ( id: string ) => void;
160
+ onClose: () => void;
161
+ onClearSearch: () => void;
162
+ };
163
+
164
+ const IconLibraryContent = ( {
165
+ isLoading,
166
+ items,
167
+ selectedValue,
168
+ searchValue,
169
+ isCatalogAvailable,
170
+ view,
171
+ onSelect,
172
+ onClose,
173
+ onClearSearch,
174
+ }: IconLibraryContentProps ) => {
175
+ if ( isLoading ) {
176
+ return <IconLibraryLoadingState />;
177
+ }
178
+
179
+ const emptyState = (
180
+ <IconLibraryEmptyState
181
+ searchValue={ searchValue }
182
+ isCatalogAvailable={ isCatalogAvailable }
183
+ onClear={ onClearSearch }
184
+ />
185
+ );
186
+
187
+ if ( view === 'grid' ) {
188
+ return (
189
+ <Box sx={ { width: '100%', height: '100%', minWidth: 0, minHeight: 0 } }>
190
+ <IconLibraryGrid
191
+ items={ items }
192
+ selectedValue={ selectedValue }
193
+ onSelect={ onSelect }
194
+ onClose={ onClose }
195
+ noResultsComponent={ emptyState }
196
+ />
197
+ </Box>
198
+ );
199
+ }
200
+
201
+ return (
202
+ <PopoverMenuList
203
+ items={ items }
204
+ selectedValue={ selectedValue }
205
+ menuListTemplate={ CompactIconLibraryMenuList }
206
+ onSelect={ onSelect }
207
+ onClose={ onClose }
208
+ itemHeight={ ICON_LIBRARY_ROW_HEIGHT }
209
+ menuItemContentTemplate={ IconLibraryRow }
210
+ noResultsComponent={ emptyState }
211
+ data-testid="icon-library-list"
212
+ />
213
+ );
214
+ };
215
+
216
+ const IconLibraryLoadingState = () => (
217
+ <Stack alignItems="center" justifyContent="center" height="100%">
218
+ <CircularProgress role="progressbar" size={ 24 } />
219
+ </Stack>
220
+ );
221
+
222
+ const IconLibraryEmptyState = ( {
223
+ searchValue,
224
+ isCatalogAvailable,
225
+ onClear,
226
+ }: {
227
+ searchValue: string;
228
+ isCatalogAvailable: boolean;
229
+ onClear: () => void;
230
+ } ) => {
231
+ if ( ! isCatalogAvailable ) {
232
+ return <CatalogUnavailable />;
233
+ }
234
+
235
+ return <NoResults searchValue={ searchValue } onClear={ onClear } />;
236
+ };
237
+
238
+ const IconLibraryRow = ( item: VirtualizedItem< string, string > ) => {
239
+ const icon = item as IconLibraryItem;
240
+
241
+ return (
242
+ <Stack direction="row" alignItems="center" gap={ 1 } sx={ { width: '100%' } }>
243
+ <Box
244
+ sx={ {
245
+ width: ICON_TILE_SIZE,
246
+ height: ICON_TILE_SIZE,
247
+ display: 'flex',
248
+ alignItems: 'center',
249
+ justifyContent: 'center',
250
+ border: 1,
251
+ borderColor: 'divider',
252
+ borderRadius: 1,
253
+ color: 'text.tertiary',
254
+ flexShrink: 0,
255
+ } }
256
+ >
257
+ { icon.paths.length > 0 ? (
258
+ <FontAwesomeGlyph icon={ icon } size={ ICON_GLYPH_SIZE } color="currentColor" />
259
+ ) : null }
260
+ </Box>
261
+ <Typography variant="caption" color="text.primary" noWrap>
262
+ { icon.label }
263
+ </Typography>
264
+ </Stack>
265
+ );
266
+ };
267
+
268
+ const CatalogUnavailable = () => (
269
+ <Stack alignItems="center" justifyContent="center" height="100%" p={ 2.5 } gap={ 1.5 }>
270
+ <ComponentsIcon fontSize="large" />
271
+ <Typography align="center" variant="subtitle2" color="text.secondary">
272
+ { __( "Icons couldn't be loaded.", 'elementor' ) }
273
+ </Typography>
274
+ </Stack>
275
+ );
276
+
277
+ const NoResults = ( { searchValue, onClear }: { searchValue: string; onClear: () => void } ) => (
278
+ <Stack alignItems="center" justifyContent="center" height="100%" p={ 2.5 } gap={ 1.5 }>
279
+ <ComponentsIcon fontSize="large" />
280
+ <Typography align="center" variant="subtitle2" color="text.secondary">
281
+ { __( 'Sorry, nothing matched', 'elementor' ) }
282
+ </Typography>
283
+ { searchValue ? (
284
+ <>
285
+ <Typography align="center" variant="subtitle2" color="text.secondary" noWrap sx={ { maxWidth: '80%' } }>
286
+ &ldquo;{ searchValue }&rdquo;.
287
+ </Typography>
288
+ <Link color="secondary" variant="caption" component="button" type="button" onClick={ onClear }>
289
+ { __( 'Clear & try again', 'elementor' ) }
290
+ </Link>
291
+ </>
292
+ ) : null }
293
+ </Stack>
294
+ );
295
+
296
+ const createIconLibraryItems = (
297
+ icons: FontAwesome7Icon[],
298
+ searchValue: string,
299
+ libraries: FontAwesome7LibraryFilter
300
+ ): IconLibraryItem[] =>
301
+ filterFontAwesome7Icons( icons, searchValue, libraries ).map( ( icon ) => ( {
302
+ ...icon,
303
+ type: 'item',
304
+ value: icon.id,
305
+ } ) );
@@ -0,0 +1 @@
1
+ export const ICON_LIBRARY_ACTION_TOOLTIP_ENTER_DELAY = 0;