@siratik/wp-component-library 0.1.1 → 0.1.2
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/LICENSE +5 -0
- package/README.md +43 -5
- package/components/attachment-image/README.md +118 -0
- package/components/attachment-image/index.js +105 -0
- package/components/attributes-panel/README.md +92 -0
- package/components/attributes-panel/index.js +324 -0
- package/components/badge/README.md +61 -0
- package/components/badge/index.js +105 -0
- package/components/block-appender/README.md +158 -0
- package/components/block-appender/index.js +135 -0
- package/components/block-extension/index.js +214 -0
- package/components/business-hours/SpecialHoursControl.js +211 -0
- package/components/business-hours/WeeklyHoursControl.js +419 -0
- package/components/business-hours/index.js +8 -0
- package/components/button/README.md +172 -0
- package/components/button/index.js +84 -0
- package/components/captcha-placeholder/CloudflareTurnstile.js +111 -0
- package/components/captcha-placeholder/GoogleRecaptcha.js +132 -0
- package/components/captcha-placeholder/HCaptcha.js +99 -0
- package/components/captcha-placeholder/README.md +228 -0
- package/components/captcha-placeholder/index.js +39 -0
- package/components/captcha-placeholder/styles.js +33 -0
- package/components/geocoder/AddressEntryControl.js +390 -0
- package/components/geocoder/GeocoderSearchControl.js +353 -0
- package/components/geocoder/countryOptions.js +66 -0
- package/components/geocoder/geocoder.js +400 -0
- package/components/geocoder/index.js +16 -0
- package/components/icon-picker/Icon.js +53 -0
- package/components/icon-picker/IconPicker.js +70 -0
- package/components/icon-picker/IconPickerModal.js +474 -0
- package/components/icon-picker/IconPickerToolbarButton.js +60 -0
- package/components/icon-picker/InlineIconPicker.js +92 -0
- package/components/icon-picker/api.js +181 -0
- package/components/icon-picker/config-bootstrap.js +108 -0
- package/components/icon-picker/hooks.js +157 -0
- package/components/icon-picker/index.js +39 -0
- package/components/icon-picker/load-script.js +33 -0
- package/components/icon-picker/loaders.js +79 -0
- package/components/icon-picker/store.js +223 -0
- package/components/icon-picker/styles.js +232 -0
- package/components/icon-picker/utils.js +138 -0
- package/components/layout/README.md +268 -0
- package/components/link-tools-panel/README.md +49 -0
- package/components/link-tools-panel/index.js +198 -0
- package/components/link-tools-panel/styles.js +90 -0
- package/components/media/README.md +288 -0
- package/components/media/SettingsMediaUpload.js +187 -0
- package/components/media/editor-media-upload.js +65 -0
- package/components/media/image-controls.js +196 -0
- package/components/media/index.js +13 -0
- package/components/media/inspector-media-upload.js +290 -0
- package/components/media/toolbar-media-upload.js +53 -0
- package/components/media/utils/aspect-ratios.js +118 -0
- package/components/meta/MetaPanel.js +75 -0
- package/components/meta/README.md +46 -0
- package/components/meta/index.js +6 -0
- package/components/meta/meta-gated-editor.js +282 -0
- package/components/query/README.md +340 -0
- package/components/query/cardTemplatePartPanel.js +69 -0
- package/components/query/cardTemplatePartSelect.js +165 -0
- package/components/query/cardTemplatePartUtils.js +119 -0
- package/components/query/columnCountControl.js +63 -0
- package/components/query/displayTypeSelect.js +52 -0
- package/components/query/examples.js +166 -0
- package/components/query/hideCurrentPostControl.js +47 -0
- package/components/query/index.js +20 -0
- package/components/query/manualPostSelector.js +125 -0
- package/components/query/manualTermSelector.js +141 -0
- package/components/query/orderBySelect.js +95 -0
- package/components/query/postTypeSelect.js +67 -0
- package/components/query/postsPerPageControl.js +51 -0
- package/components/query/selectionModeControl.js +61 -0
- package/components/query/taxonomySelect.js +139 -0
- package/components/query/utils.js +75 -0
- package/components/repeater/README.md +159 -0
- package/components/repeater/index.js +829 -0
- package/components/segmented-bar/README.md +30 -0
- package/components/segmented-bar/index.js +68 -0
- package/components/sortable-select/README.md +153 -0
- package/components/sortable-select/custom-sortable-token.js +127 -0
- package/components/sortable-select/index.js +693 -0
- package/components/style-picker/README.md +36 -0
- package/components/style-picker/index.js +92 -0
- package/components/variable-field/README.md +299 -0
- package/components/variable-field/index.js +592 -0
- package/components/variable-field/value-to-chip-html.js +38 -0
- package/components/variable-inserter/README.md +68 -0
- package/components/variable-inserter/index.js +168 -0
- package/constants/statusColors.js +1 -1
- package/editor.js +16 -0
- package/index.js +127 -0
- package/package.json +21 -13
- package/utils/kitaris-localize.js +128 -0
- package/utils/slug-aware-color-setter.js +27 -0
- package/build/index.asset.php +0 -20
- package/build/index.js +0 -1242
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Icon Picker Modal
|
|
3
|
+
*
|
|
4
|
+
* The shared modal used by all three picker variants (inspector, toolbar, inline).
|
|
5
|
+
* Looks and behaves like the icon picker of WordPress core's Icon block (WP 7.1),
|
|
6
|
+
* which core does not export, rebuilt with @wordpress/components:
|
|
7
|
+
* - Full-screen "Icon library" modal.
|
|
8
|
+
* - Sidebar: search (debounced) and vertical tabs, "All" then one tab per
|
|
9
|
+
* registered icon set (on WP 7.1+ these are core's icon collections).
|
|
10
|
+
* - Grid of icon buttons (icon + label), rendered in batches; the selected
|
|
11
|
+
* icon is a primary button, scrolled into view on open.
|
|
12
|
+
* - Opens on the selected icon's set (like core), else on "All".
|
|
13
|
+
* - Search matches the current tab's icon names and labels.
|
|
14
|
+
*
|
|
15
|
+
* Keyboard: Arrow Up/Down (Home/End) move between tabs; the grid is one tab
|
|
16
|
+
* stop, with arrow keys (Home/End) to move between icons. Escape closes.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { Modal, SearchControl, Spinner } from "@wordpress/components";
|
|
20
|
+
import { useAsyncList, useDebounce, useInstanceId } from "@wordpress/compose";
|
|
21
|
+
import {
|
|
22
|
+
memo,
|
|
23
|
+
useCallback,
|
|
24
|
+
useLayoutEffect,
|
|
25
|
+
useMemo,
|
|
26
|
+
useRef,
|
|
27
|
+
useState,
|
|
28
|
+
} from "@wordpress/element";
|
|
29
|
+
import { __, isRTL } from "@wordpress/i18n";
|
|
30
|
+
import { chevronRight, Icon as WPIcon } from "@wordpress/icons";
|
|
31
|
+
|
|
32
|
+
import { useIconPickerGroups } from "./hooks";
|
|
33
|
+
import {
|
|
34
|
+
IconGridItem,
|
|
35
|
+
IconGridItemIcon,
|
|
36
|
+
IconGridItemTitle,
|
|
37
|
+
IconGridList,
|
|
38
|
+
PickerLoading,
|
|
39
|
+
PickerNoResults,
|
|
40
|
+
PickerPanel,
|
|
41
|
+
PickerSidebar,
|
|
42
|
+
Tab,
|
|
43
|
+
TabList,
|
|
44
|
+
} from "./styles";
|
|
45
|
+
import { ALL_TAB as ALL, isSetOf, openingTab, safeSvgSource } from "./utils";
|
|
46
|
+
|
|
47
|
+
/** Icons rendered per batch, like core. */
|
|
48
|
+
const BATCH_SIZE = 20;
|
|
49
|
+
|
|
50
|
+
/** Search delay in ms, like core. */
|
|
51
|
+
const SEARCH_DEBOUNCE = 300;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Lowercase, trimmed and accent-free, for search matching.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} input
|
|
57
|
+
* @returns {string}
|
|
58
|
+
*/
|
|
59
|
+
function normalizeSearch(input = "") {
|
|
60
|
+
return String(input)
|
|
61
|
+
.normalize("NFD")
|
|
62
|
+
.replace(/[̀-ͯ]/g, "")
|
|
63
|
+
.trim()
|
|
64
|
+
.toLowerCase();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {Object} icon
|
|
69
|
+
* @returns {string}
|
|
70
|
+
*/
|
|
71
|
+
const iconKey = (icon) => `${icon.iconSet}::${icon.name}`;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Single icon button — memoized so batches and focus moves only re-render
|
|
75
|
+
* the cells that change.
|
|
76
|
+
*/
|
|
77
|
+
// eslint-disable-next-line no-shadow -- named for React DevTools.
|
|
78
|
+
const IconGridCell = memo(function IconGridCell({
|
|
79
|
+
icon,
|
|
80
|
+
isSelected,
|
|
81
|
+
isTabStop,
|
|
82
|
+
onSelect,
|
|
83
|
+
onFocusCell,
|
|
84
|
+
cellRef,
|
|
85
|
+
}) {
|
|
86
|
+
const label = icon.label || icon.name;
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<IconGridItem
|
|
90
|
+
ref={cellRef}
|
|
91
|
+
__next40pxDefaultSize
|
|
92
|
+
variant={isSelected ? "primary" : undefined}
|
|
93
|
+
className="wpcl-icon-picker__item"
|
|
94
|
+
tabIndex={isTabStop ? 0 : -1}
|
|
95
|
+
aria-current={isSelected ? "true" : undefined}
|
|
96
|
+
data-icon-key={iconKey(icon)}
|
|
97
|
+
onClick={() => onSelect(icon)}
|
|
98
|
+
onFocus={() => onFocusCell(iconKey(icon))}
|
|
99
|
+
>
|
|
100
|
+
<IconGridItemIcon
|
|
101
|
+
aria-hidden="true"
|
|
102
|
+
dangerouslySetInnerHTML={{ __html: safeSvgSource(icon.source) }}
|
|
103
|
+
/>
|
|
104
|
+
<IconGridItemTitle>{label}</IconGridItemTitle>
|
|
105
|
+
</IconGridItem>
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The icon grid: core's layout, rendered in batches, one tab stop.
|
|
111
|
+
*
|
|
112
|
+
* @param {Object} props
|
|
113
|
+
* @param {Array} props.icons Icons to show.
|
|
114
|
+
* @param {Function} props.isSelectedIcon Whether an icon is the saved one.
|
|
115
|
+
* @param {Function} props.onSelect
|
|
116
|
+
*/
|
|
117
|
+
function IconGrid({ icons, isSelectedIcon, onSelect }) {
|
|
118
|
+
const shownIcons = useAsyncList(icons, { step: BATCH_SIZE });
|
|
119
|
+
const listRef = useRef(null);
|
|
120
|
+
const selectedRef = useRef(null);
|
|
121
|
+
const [activeKey, setActiveKey] = useState(null);
|
|
122
|
+
|
|
123
|
+
const selectedIndex = useMemo(
|
|
124
|
+
() => icons.findIndex(isSelectedIcon),
|
|
125
|
+
[icons, isSelectedIcon],
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// The roving tab stop: the last focused icon, else the selected one, else the first.
|
|
129
|
+
const tabStopKey = useMemo(() => {
|
|
130
|
+
if (activeKey && shownIcons.some((icon) => iconKey(icon) === activeKey)) {
|
|
131
|
+
return activeKey;
|
|
132
|
+
}
|
|
133
|
+
if (selectedIndex >= 0 && selectedIndex < shownIcons.length) {
|
|
134
|
+
return iconKey(icons[selectedIndex]);
|
|
135
|
+
}
|
|
136
|
+
return shownIcons[0] ? iconKey(shownIcons[0]) : null;
|
|
137
|
+
}, [activeKey, shownIcons, selectedIndex, icons]);
|
|
138
|
+
|
|
139
|
+
// Like core: once the selected icon has rendered (with a batch after it),
|
|
140
|
+
// scroll it to the middle, unless the user already scrolled.
|
|
141
|
+
const isReadyToScroll =
|
|
142
|
+
selectedIndex >= 0 &&
|
|
143
|
+
(shownIcons.length >= selectedIndex + BATCH_SIZE ||
|
|
144
|
+
shownIcons.length === icons.length);
|
|
145
|
+
|
|
146
|
+
useLayoutEffect(() => {
|
|
147
|
+
const node = selectedRef.current;
|
|
148
|
+
if (!isReadyToScroll || !node) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const scroller = node.closest(".components-modal__content");
|
|
152
|
+
if (scroller?.scrollTop) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
node.scrollIntoView({ block: "center" });
|
|
156
|
+
}, [isReadyToScroll]);
|
|
157
|
+
|
|
158
|
+
const handleKeyDown = useCallback(
|
|
159
|
+
(event) => {
|
|
160
|
+
const list = listRef.current;
|
|
161
|
+
if (!list) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const cells = Array.from(
|
|
165
|
+
list.querySelectorAll(":scope > .wpcl-icon-picker__item"),
|
|
166
|
+
);
|
|
167
|
+
const current = cells.indexOf(event.target);
|
|
168
|
+
if (current < 0) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const columns = Math.max(
|
|
173
|
+
1,
|
|
174
|
+
window
|
|
175
|
+
.getComputedStyle(list)
|
|
176
|
+
.gridTemplateColumns.split(" ")
|
|
177
|
+
.filter(Boolean).length,
|
|
178
|
+
);
|
|
179
|
+
const forward = isRTL() ? "ArrowLeft" : "ArrowRight";
|
|
180
|
+
const back = isRTL() ? "ArrowRight" : "ArrowLeft";
|
|
181
|
+
|
|
182
|
+
let next;
|
|
183
|
+
switch (event.key) {
|
|
184
|
+
case forward:
|
|
185
|
+
next = current + 1;
|
|
186
|
+
break;
|
|
187
|
+
case back:
|
|
188
|
+
next = current - 1;
|
|
189
|
+
break;
|
|
190
|
+
case "ArrowDown":
|
|
191
|
+
next = current + columns;
|
|
192
|
+
break;
|
|
193
|
+
case "ArrowUp":
|
|
194
|
+
next = current - columns;
|
|
195
|
+
break;
|
|
196
|
+
case "Home":
|
|
197
|
+
next = 0;
|
|
198
|
+
break;
|
|
199
|
+
case "End":
|
|
200
|
+
next = cells.length - 1;
|
|
201
|
+
break;
|
|
202
|
+
default:
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
event.preventDefault();
|
|
207
|
+
next = Math.min(Math.max(next, 0), cells.length - 1);
|
|
208
|
+
cells[next]?.focus();
|
|
209
|
+
},
|
|
210
|
+
[],
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
if (!icons.length) {
|
|
214
|
+
return (
|
|
215
|
+
<PickerNoResults>
|
|
216
|
+
<p>{__("No results found.", "wp-component-library")}</p>
|
|
217
|
+
</PickerNoResults>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return (
|
|
222
|
+
<IconGridList
|
|
223
|
+
ref={listRef}
|
|
224
|
+
role="group"
|
|
225
|
+
aria-label={__("Icon library", "wp-component-library")}
|
|
226
|
+
onKeyDown={handleKeyDown}
|
|
227
|
+
>
|
|
228
|
+
{shownIcons.map((icon, index) => {
|
|
229
|
+
const key = iconKey(icon);
|
|
230
|
+
return (
|
|
231
|
+
<IconGridCell
|
|
232
|
+
key={key}
|
|
233
|
+
icon={icon}
|
|
234
|
+
isSelected={index === selectedIndex}
|
|
235
|
+
isTabStop={key === tabStopKey}
|
|
236
|
+
onSelect={onSelect}
|
|
237
|
+
onFocusCell={setActiveKey}
|
|
238
|
+
cellRef={index === selectedIndex ? selectedRef : undefined}
|
|
239
|
+
/>
|
|
240
|
+
);
|
|
241
|
+
})}
|
|
242
|
+
</IconGridList>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Vertical tabs, like core's: arrow keys move and select.
|
|
248
|
+
*
|
|
249
|
+
* @param {Object} props
|
|
250
|
+
* @param {Array<{ value: string, label: string }>} props.tabs
|
|
251
|
+
* @param {string} props.value
|
|
252
|
+
* @param {Function} props.onChange
|
|
253
|
+
* @param {string} props.idBase
|
|
254
|
+
*/
|
|
255
|
+
function CollectionTabs({ tabs, value, onChange, idBase }) {
|
|
256
|
+
const refs = useRef([]);
|
|
257
|
+
|
|
258
|
+
const handleKeyDown = (event, index) => {
|
|
259
|
+
let next;
|
|
260
|
+
switch (event.key) {
|
|
261
|
+
case "ArrowDown":
|
|
262
|
+
next = (index + 1) % tabs.length;
|
|
263
|
+
break;
|
|
264
|
+
case "ArrowUp":
|
|
265
|
+
next = (index - 1 + tabs.length) % tabs.length;
|
|
266
|
+
break;
|
|
267
|
+
case "Home":
|
|
268
|
+
next = 0;
|
|
269
|
+
break;
|
|
270
|
+
case "End":
|
|
271
|
+
next = tabs.length - 1;
|
|
272
|
+
break;
|
|
273
|
+
default:
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
event.preventDefault();
|
|
277
|
+
onChange(tabs[next].value);
|
|
278
|
+
refs.current[next]?.focus();
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
return (
|
|
282
|
+
<TabList
|
|
283
|
+
role="tablist"
|
|
284
|
+
aria-orientation="vertical"
|
|
285
|
+
aria-label={__("Icon collections", "wp-component-library")}
|
|
286
|
+
>
|
|
287
|
+
{tabs.map((tab, index) => {
|
|
288
|
+
const isSelected = tab.value === value;
|
|
289
|
+
return (
|
|
290
|
+
<Tab
|
|
291
|
+
key={tab.value || "all"}
|
|
292
|
+
ref={(node) => {
|
|
293
|
+
refs.current[index] = node;
|
|
294
|
+
}}
|
|
295
|
+
type="button"
|
|
296
|
+
role="tab"
|
|
297
|
+
id={`${idBase}-tab-${index}`}
|
|
298
|
+
aria-selected={isSelected}
|
|
299
|
+
aria-controls={`${idBase}-panel`}
|
|
300
|
+
tabIndex={isSelected ? 0 : -1}
|
|
301
|
+
onClick={() => onChange(tab.value)}
|
|
302
|
+
onKeyDown={(event) => handleKeyDown(event, index)}
|
|
303
|
+
>
|
|
304
|
+
<span className="wpcl-icon-picker__tab-label">
|
|
305
|
+
{tab.label}
|
|
306
|
+
</span>
|
|
307
|
+
<WPIcon icon={chevronRight} size={24} />
|
|
308
|
+
</Tab>
|
|
309
|
+
);
|
|
310
|
+
})}
|
|
311
|
+
</TabList>
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* The modal itself.
|
|
317
|
+
*
|
|
318
|
+
* @param {Object} props
|
|
319
|
+
* @param {Object} props.value Currently selected icon ({ name, iconSet, source }).
|
|
320
|
+
* @param {Function} props.onChange Called with the new icon value on selection.
|
|
321
|
+
* @param {Function} props.onClose Called when the modal should close.
|
|
322
|
+
*/
|
|
323
|
+
export function IconPickerModal({ value, onChange, onClose }) {
|
|
324
|
+
const idBase = useInstanceId(IconPickerModal, "wpcl-icon-picker");
|
|
325
|
+
const [searchInput, setSearchInput] = useState("");
|
|
326
|
+
const [search, setSearch] = useState("");
|
|
327
|
+
const debouncedSetSearch = useDebounce(setSearch, SEARCH_DEBOUNCE);
|
|
328
|
+
const [currentTab, setCurrentTab] = useState(null);
|
|
329
|
+
|
|
330
|
+
const { groups, hasPending } = useIconPickerGroups();
|
|
331
|
+
|
|
332
|
+
const valueName = value?.name;
|
|
333
|
+
const valueSet = value?.iconSet;
|
|
334
|
+
const valueSource = value?.source;
|
|
335
|
+
|
|
336
|
+
// Whether a grid icon is the saved one: same name in the saved set (or a
|
|
337
|
+
// set aliasing it). Values without a set (some consumers pass the registry
|
|
338
|
+
// icon itself) match on name and SVG source.
|
|
339
|
+
const isSelectedIcon = useCallback(
|
|
340
|
+
(icon) => {
|
|
341
|
+
if (!valueName || icon.name !== valueName) {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
if (valueSet) {
|
|
345
|
+
const group = groups.find((item) => item.name === icon.iconSet);
|
|
346
|
+
return Boolean(group && isSetOf(group, valueSet));
|
|
347
|
+
}
|
|
348
|
+
return Boolean(valueSource) && icon.source === valueSource;
|
|
349
|
+
},
|
|
350
|
+
[groups, valueName, valueSet, valueSource],
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
const openingTabValue = useMemo(
|
|
354
|
+
() => openingTab(groups, valueSet, isSelectedIcon),
|
|
355
|
+
[groups, valueSet, isSelectedIcon],
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
const tabs = useMemo(
|
|
359
|
+
() => [
|
|
360
|
+
{ value: ALL, label: __("All", "wp-component-library") },
|
|
361
|
+
...groups.map((group) => ({
|
|
362
|
+
value: group.name,
|
|
363
|
+
label: group.label || group.name,
|
|
364
|
+
})),
|
|
365
|
+
],
|
|
366
|
+
[groups],
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
const tabValue =
|
|
370
|
+
currentTab !== null && tabs.some((tab) => tab.value === currentTab)
|
|
371
|
+
? currentTab
|
|
372
|
+
: openingTabValue;
|
|
373
|
+
const tabIndex = Math.max(
|
|
374
|
+
0,
|
|
375
|
+
tabs.findIndex((tab) => tab.value === tabValue),
|
|
376
|
+
);
|
|
377
|
+
const tabGroup =
|
|
378
|
+
tabValue === ALL ? null : groups.find((group) => group.name === tabValue);
|
|
379
|
+
|
|
380
|
+
const isTabLoading = tabGroup
|
|
381
|
+
? tabGroup.loadStatus === "idle" || tabGroup.loadStatus === "loading"
|
|
382
|
+
: hasPending;
|
|
383
|
+
|
|
384
|
+
const tabIcons = useMemo(
|
|
385
|
+
() =>
|
|
386
|
+
tabValue === ALL
|
|
387
|
+
? groups.flatMap((group) => group.icons)
|
|
388
|
+
: (groups.find((group) => group.name === tabValue)?.icons ?? []),
|
|
389
|
+
[groups, tabValue],
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
const haystack = useMemo(
|
|
393
|
+
() =>
|
|
394
|
+
tabIcons.map(
|
|
395
|
+
(icon) =>
|
|
396
|
+
`${normalizeSearch(icon.name)}\n${normalizeSearch(icon.label ?? "")}`,
|
|
397
|
+
),
|
|
398
|
+
[tabIcons],
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
const filteredIcons = useMemo(() => {
|
|
402
|
+
const term = normalizeSearch(search);
|
|
403
|
+
if (!term) {
|
|
404
|
+
return tabIcons;
|
|
405
|
+
}
|
|
406
|
+
return tabIcons.filter((icon, index) => haystack[index].includes(term));
|
|
407
|
+
}, [search, tabIcons, haystack]);
|
|
408
|
+
|
|
409
|
+
const handleSearch = useCallback(
|
|
410
|
+
(next) => {
|
|
411
|
+
setSearchInput(next);
|
|
412
|
+
debouncedSetSearch(next);
|
|
413
|
+
},
|
|
414
|
+
[debouncedSetSearch],
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
const handleSelect = useCallback(
|
|
418
|
+
(icon) => {
|
|
419
|
+
onChange({
|
|
420
|
+
name: icon.name,
|
|
421
|
+
iconSet: icon.iconSet,
|
|
422
|
+
source: icon.source,
|
|
423
|
+
label: icon.label,
|
|
424
|
+
});
|
|
425
|
+
onClose();
|
|
426
|
+
},
|
|
427
|
+
[onChange, onClose],
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
return (
|
|
431
|
+
<Modal
|
|
432
|
+
className="wpcl-icon-picker-modal"
|
|
433
|
+
title={__("Icon library", "wp-component-library")}
|
|
434
|
+
onRequestClose={onClose}
|
|
435
|
+
isFullScreen
|
|
436
|
+
>
|
|
437
|
+
<PickerSidebar>
|
|
438
|
+
<SearchControl
|
|
439
|
+
__nextHasNoMarginBottom
|
|
440
|
+
value={searchInput}
|
|
441
|
+
onChange={handleSearch}
|
|
442
|
+
/>
|
|
443
|
+
<CollectionTabs
|
|
444
|
+
tabs={tabs}
|
|
445
|
+
value={tabValue}
|
|
446
|
+
onChange={setCurrentTab}
|
|
447
|
+
idBase={idBase}
|
|
448
|
+
/>
|
|
449
|
+
</PickerSidebar>
|
|
450
|
+
<PickerPanel
|
|
451
|
+
role="tabpanel"
|
|
452
|
+
id={`${idBase}-panel`}
|
|
453
|
+
aria-labelledby={`${idBase}-tab-${tabIndex}`}
|
|
454
|
+
tabIndex={-1}
|
|
455
|
+
>
|
|
456
|
+
{isTabLoading ? (
|
|
457
|
+
<PickerLoading
|
|
458
|
+
role="status"
|
|
459
|
+
aria-label={__("Loading…", "wp-component-library")}
|
|
460
|
+
>
|
|
461
|
+
<Spinner />
|
|
462
|
+
</PickerLoading>
|
|
463
|
+
) : (
|
|
464
|
+
<IconGrid
|
|
465
|
+
key={tabValue || "all"}
|
|
466
|
+
icons={filteredIcons}
|
|
467
|
+
isSelectedIcon={isSelectedIcon}
|
|
468
|
+
onSelect={handleSelect}
|
|
469
|
+
/>
|
|
470
|
+
)}
|
|
471
|
+
</PickerPanel>
|
|
472
|
+
</Modal>
|
|
473
|
+
);
|
|
474
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IconPickerToolbarButton Component
|
|
3
|
+
*
|
|
4
|
+
* Toolbar button variant of the icon picker. Shows the selected icon
|
|
5
|
+
* (or a "Select Icon" label) in the block toolbar — clicking opens the modal.
|
|
6
|
+
*
|
|
7
|
+
* Matches the 10up IconPickerToolbarButton API:
|
|
8
|
+
* <IconPickerToolbarButton value={icon} onChange={handleChange} />
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { ToolbarButton } from "@wordpress/components";
|
|
12
|
+
import { useState, useCallback } from "@wordpress/element";
|
|
13
|
+
import { __ } from "@wordpress/i18n";
|
|
14
|
+
|
|
15
|
+
import { IconPickerModal } from "./IconPickerModal";
|
|
16
|
+
import { ToolbarIconWrap } from "./styles";
|
|
17
|
+
import { PLACEHOLDER_SVG, safeSvgSource } from "./utils";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {Object} props
|
|
21
|
+
* @param {Object} [props.value] Current icon { name, iconSet, source }.
|
|
22
|
+
* @param {Function} props.onChange Called with new icon value.
|
|
23
|
+
* @param {string} [props.buttonLabel] Override the toolbar button label.
|
|
24
|
+
* @param {string} [props.text] Visible text shown beside the icon.
|
|
25
|
+
*/
|
|
26
|
+
export function IconPickerToolbarButton({
|
|
27
|
+
value,
|
|
28
|
+
onChange,
|
|
29
|
+
buttonLabel = __("Select Icon", "wp-component-library"),
|
|
30
|
+
text,
|
|
31
|
+
}) {
|
|
32
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
33
|
+
|
|
34
|
+
const openModal = useCallback(() => setIsOpen(true), []);
|
|
35
|
+
const closeModal = useCallback(() => setIsOpen(false), []);
|
|
36
|
+
|
|
37
|
+
const svgSource = safeSvgSource(value?.source ?? PLACEHOLDER_SVG) || PLACEHOLDER_SVG;
|
|
38
|
+
|
|
39
|
+
const toolbarIcon = (
|
|
40
|
+
<ToolbarIconWrap dangerouslySetInnerHTML={{ __html: svgSource }} />
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<>
|
|
45
|
+
<ToolbarButton
|
|
46
|
+
icon={toolbarIcon}
|
|
47
|
+
label={buttonLabel}
|
|
48
|
+
text={text}
|
|
49
|
+
onClick={openModal}
|
|
50
|
+
/>
|
|
51
|
+
{isOpen && (
|
|
52
|
+
<IconPickerModal
|
|
53
|
+
value={value}
|
|
54
|
+
onChange={onChange}
|
|
55
|
+
onClose={closeModal}
|
|
56
|
+
/>
|
|
57
|
+
)}
|
|
58
|
+
</>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InlineIconPicker Component
|
|
3
|
+
*
|
|
4
|
+
* Block-canvas icon picker. The icon itself (or a placeholder) is the
|
|
5
|
+
* clickable element — clicking opens the shared modal.
|
|
6
|
+
*
|
|
7
|
+
* Matches the 10up InlineIconPicker API:
|
|
8
|
+
* <InlineIconPicker value={icon} onChange={handleChange} className="..." />
|
|
9
|
+
*
|
|
10
|
+
* Key implementation note:
|
|
11
|
+
* Uses InlinePickerWrap which renders as a div[role="button"] so that
|
|
12
|
+
* color: currentColor naturally cascades from the block context with
|
|
13
|
+
* no UA stylesheet interference (native <button> has color: ButtonText).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { useState, useCallback } from "@wordpress/element";
|
|
17
|
+
import { __ } from "@wordpress/i18n";
|
|
18
|
+
|
|
19
|
+
import { IconPickerModal } from "./IconPickerModal";
|
|
20
|
+
import { InlinePickerWrap } from "./styles";
|
|
21
|
+
import { PLACEHOLDER_SVG, safeSvgSource } from "./utils";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {Object} props
|
|
25
|
+
* @param {Object} [props.value] Current icon { name, iconSet, source }.
|
|
26
|
+
* @param {Function} props.onChange Called with new icon value.
|
|
27
|
+
* @param {string} [props.className] Extra class forwarded to the wrapper.
|
|
28
|
+
* @param {boolean} [props.isInteractive=true] When false, the icon is display-only
|
|
29
|
+
* (no click/keyboard trigger) — pair it
|
|
30
|
+
* with IconPickerToolbarButton.
|
|
31
|
+
*/
|
|
32
|
+
export function InlineIconPicker({
|
|
33
|
+
value,
|
|
34
|
+
onChange,
|
|
35
|
+
className = "",
|
|
36
|
+
isInteractive = true,
|
|
37
|
+
}) {
|
|
38
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
39
|
+
|
|
40
|
+
const openModal = useCallback(() => setIsOpen(true), []);
|
|
41
|
+
const closeModal = useCallback(() => setIsOpen(false), []);
|
|
42
|
+
|
|
43
|
+
const handleKeyDown = useCallback((e) => {
|
|
44
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
setIsOpen(true);
|
|
47
|
+
}
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
const hasIcon = value?.source;
|
|
51
|
+
const svgContent = hasIcon
|
|
52
|
+
? safeSvgSource(value.source) || PLACEHOLDER_SVG
|
|
53
|
+
: PLACEHOLDER_SVG;
|
|
54
|
+
|
|
55
|
+
if (!isInteractive) {
|
|
56
|
+
return (
|
|
57
|
+
<InlinePickerWrap
|
|
58
|
+
as="div"
|
|
59
|
+
className={className || undefined}
|
|
60
|
+
style={{ cursor: "inherit" }}
|
|
61
|
+
dangerouslySetInnerHTML={{ __html: svgContent }}
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<>
|
|
68
|
+
<InlinePickerWrap
|
|
69
|
+
as="div"
|
|
70
|
+
role="button"
|
|
71
|
+
tabIndex={0}
|
|
72
|
+
className={className || undefined}
|
|
73
|
+
onClick={openModal}
|
|
74
|
+
onKeyDown={handleKeyDown}
|
|
75
|
+
aria-label={
|
|
76
|
+
hasIcon
|
|
77
|
+
? __("Change icon", "wp-component-library")
|
|
78
|
+
: __("Choose icon", "wp-component-library")
|
|
79
|
+
}
|
|
80
|
+
dangerouslySetInnerHTML={{ __html: svgContent }}
|
|
81
|
+
/>
|
|
82
|
+
|
|
83
|
+
{isOpen && (
|
|
84
|
+
<IconPickerModal
|
|
85
|
+
value={value}
|
|
86
|
+
onChange={onChange}
|
|
87
|
+
onClose={closeModal}
|
|
88
|
+
/>
|
|
89
|
+
)}
|
|
90
|
+
</>
|
|
91
|
+
);
|
|
92
|
+
}
|