Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
e531d9a
docs: add icon-picker-control refactor spec and implementation plan
YvetteNikolov Apr 13, 2026
6db01e2
feat(icon-picker-control): add REST API helpers for wp-icons endpoints
YvetteNikolov Apr 13, 2026
1fb934f
fix(icon-picker-control): url-encode name param in getIconSvg, mark s…
YvetteNikolov Apr 13, 2026
3c7afea
feat(icon-picker-control): add module-level SVG cache utility
YvetteNikolov Apr 13, 2026
3c86439
fix(icon-picker-control): add clearCache export and fix test isolatio…
YvetteNikolov Apr 13, 2026
ca45c43
feat(icon-picker-control): add useIconSets hook for API detection
YvetteNikolov Apr 13, 2026
20135a3
fix(icon-picker-control): add AbortController cleanup to useIconSets,…
YvetteNikolov Apr 13, 2026
3c4cf43
fix(icon-picker-control): guard useIconSets state updates with isMoun…
YvetteNikolov Apr 13, 2026
de5bc6c
feat(icon-picker-control): extract FontAwesomeIconPicker component
YvetteNikolov Apr 13, 2026
665803e
feat(icon-picker-control): add SvgIconResults grid component
YvetteNikolov Apr 13, 2026
cb72a0e
fix(icon-picker-control): sanitize SVG content, use stable result key…
YvetteNikolov Apr 13, 2026
70057bf
feat(icon-picker-control): add SvgIconPicker component with debounced…
YvetteNikolov Apr 13, 2026
f147590
fix(icon-picker-control): extract sanitizeSvg utility, fix race condi…
YvetteNikolov Apr 13, 2026
bf541d9
feat(icon-picker-control): replace index.jsx with detection wrapper, …
YvetteNikolov Apr 13, 2026
9e20f01
feat(icon-picker-control): add SVG picker styles and update README
YvetteNikolov Apr 13, 2026
4327bf2
fix(icon-picker-control): harden SVG sanitizer, add detection timeout…
YvetteNikolov Apr 13, 2026
0aa9ef0
chore: remove superpower docs
YvetteNikolov Apr 13, 2026
63afe33
chore: update icon component with svg and svg-icon-picker values
YvetteNikolov Apr 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 65 additions & 17 deletions packages/components/src/icon-picker-control/README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,72 @@
# Icon picker control
# Icon Picker Control

The IconPickerControl use the Font Awesome API to search for icons. More information found here: <https://fontawesome.com/docs/apis/>.
Picks an icon in the block editor. Supports two modes:

## Hooks
- **FontAwesome mode** (default, legacy): searches the FontAwesome GraphQL API and returns a CSS class string (e.g. `fa-solid fa-house`). Stored in the `icon` block attribute.
- **SVG mode** (new): uses the `wp-icons` REST API (`/yard/icons`) to search sets of SVG icons and returns raw SVG markup. Stored in the `iconSVG` block attribute.

By default all the styles from Font Awesome are loaded. Use this filter to change which styles are needed. In this example all styles are loaded:
Mode is selected automatically. If `/yard/icons` is reachable **and** the consumer provides an `onChangeSVG` callback, SVG mode is used. Otherwise FontAwesome mode is used. Existing consumers that only provide `onChange` are unaffected.

```JS
## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| `onChange` | `Function` | — | Called with FA class string (FontAwesome mode). |
| `icon` | `string` | — | Current FA class string, used for the legacy icon preview. |
| `displayIconPreview` | `boolean` | `true` | Show a preview of the selected icon above the search field. |
| `displayAsPopover` | `boolean` | `true` | Show search results in a popover (vs. inline). |
| `displayDeleteIcon` | `boolean` | `false` | Show a delete button below the search field. |
| `handleRemove` | `Function` | — | Called when the delete button is clicked. |
| `onChangeSVG` | `Function` | — | Called with raw SVG string (SVG mode). Opts the consumer into SVG mode when provided. |
| `iconSVG` | `string` | — | Current SVG markup, used for the SVG icon preview. |

## New consumer example

```jsx
<IconPickerControlInspector
icon={ icon }
iconSVG={ iconSVG }
onChange={ ( v ) => setAttributes({ icon: v }) }
onChangeSVG={ ( v ) => setAttributes({ iconSVG: v }) }
/>
```

### Block attribute registration

```js
attributes: {
icon: { type: 'string', default: '' },
iconSVG: { type: 'string', default: '' },
}
```

### Frontend rendering (handle both old and new saved content)

```jsx
{ iconSVG
? <span dangerouslySetInnerHTML={{ __html: iconSVG }} />
: icon && <i className={ icon } />
}
```

## FontAwesome mode — filter

To limit which FontAwesome family/style combinations appear, use this WordPress filter:

```js
import { addFilter } from '@wordpress/hooks';

addFilter('yard.fontawesome-family-styles', 'yard', () => [
{ family: 'classic', style: 'solid' },
{ family: 'classic', style: 'regular' },
{ family: 'classic', style: 'light' },
{ family: 'classic', style: 'thin' },
{ family: 'classic', style: 'brands' },
{ family: 'duotone', style: 'solid' },
{ family: 'sharp', style: 'solid' },
{ family: 'sharp', style: 'regular' },
{ family: 'sharp', style: 'light' },
{ family: 'sharp', style: 'thin' },
]);
addFilter( 'yard.fontawesome-family-styles', 'yard', () => [
{ family: 'classic', style: 'solid' },
{ family: 'classic', style: 'regular' },
] );
```

## SVG mode — how it works

1. On mount, fetches `/yard/icons` to get available icon sets.
2. Renders a set selector (`SelectControl`) populated with the returned sets.
3. Search fires after 3 characters, debounced by 300 ms.
4. Shows the first 10 results; fetches each icon's SVG eagerly (with skeleton placeholders while loading).
5. SVGs are cached in a module-level `Map` for the page session — the same icon is never fetched twice.
6. Selecting an icon calls `onChangeSVG` with the raw SVG string.
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/**
* WordPress dependencies
*/
import { Popover, SearchControl } from '@wordpress/components';
import { useDispatch } from '@wordpress/data';
import { useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { applyFilters } from '@wordpress/hooks';
import { store as noticesStore } from '@wordpress/notices';

/**
* Internal dependencies
*/
import DeleteIcon from './delete-icon.jsx';
import IconResults from './icon-results.jsx';
import { getFontAwesomeIcons } from '../utils/api';
import { convertResponseToClassnames } from '../utils/helpers';

const FontAwesomeIconPicker = ( {
onChange,
icon,
displayIconPreview = true,
displayAsPopover = true,
displayDeleteIcon = false,
handleRemove,
} ) => {
const [ isOpen, setOpen ] = useState( false );
const [ searchInput, setSearchInput ] = useState( '' );
const [ searchResults, setSearchResults ] = useState( [] );
const [ popoverAnchor, setPopoverAnchor ] = useState();

const { createNotice } = useDispatch( noticesStore );

const allowedFamilyStyles = applyFilters(
'yard.fontawesome-family-styles',
[
{ family: 'classic', style: 'solid' },
{ family: 'classic', style: 'regular' },
{ family: 'classic', style: 'light' },
{ family: 'classic', style: 'thin' },
{ family: 'classic', style: 'brands' },
{ family: 'duotone', style: 'solid' },
{ family: 'sharp', style: 'solid' },
{ family: 'sharp', style: 'regular' },
{ family: 'sharp', style: 'light' },
{ family: 'sharp', style: 'thin' },
]
);

const searchFontAwesomeIcons = async ( searchValue ) => {
try {
const response = await getFontAwesomeIcons( searchValue );
if ( ! response ) return;

const result = response?.data?.search.reduce(
( iconResults, iconData ) => {
convertResponseToClassnames(
iconData,
allowedFamilyStyles
).forEach( ( value ) => {
iconResults.push( value );
} );

return iconResults;
},
[]
);
if ( ! result ) return;

setSearchResults( result );
setOpen( true );
} catch ( err ) {
return showErrorNotice();
}
};

const showErrorNotice = () => {
createNotice(
'error',
__(
'Momenteel kunnen er geen iconen worden opgehaald, probeer het later nog een keer.'
),
{
isDismissible: true,
type: 'snackbar',
id: 'icon-picker-control-error',
}
);
};

const handleIconClick = ( clickedIcon ) => {
onChange( clickedIcon );
setSearchInput( () => '' );
setOpen( () => false );
};

return (
<>
{ displayIconPreview && icon && (
<i className={ icon + ' icon-picker-control-preview-icon' }></i>
) }

<SearchControl
placeholder={ __( 'Zoek een icoon' ) }
value={ searchInput }
help={ __( 'Gebruik Engelse termen om een icoon te zoeken.' ) }
onChange={ ( searchValue ) => {
setSearchInput( searchValue );
searchFontAwesomeIcons( searchValue );
} }
ref={ setPopoverAnchor }
/>

{ displayAsPopover && searchInput && isOpen && (
<Popover
anchor={ popoverAnchor }
title={ __( 'Kies een icoon' ) }
onClose={ () => setOpen( false ) }
focusOnMount={ false }
>
<IconResults
searchResults={ searchResults }
handleIconClick={ handleIconClick }
/>
</Popover>
) }

{ ! displayAsPopover && searchInput && (
<IconResults
searchResults={ searchResults }
handleIconClick={ handleIconClick }
/>
) }

{ displayDeleteIcon && icon && (
<DeleteIcon handleRemove={ handleRemove } />
) }
</>
);
};

export default FontAwesomeIconPicker;
Loading
Loading