@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.
Files changed (96) hide show
  1. package/LICENSE +5 -0
  2. package/README.md +43 -5
  3. package/components/attachment-image/README.md +118 -0
  4. package/components/attachment-image/index.js +105 -0
  5. package/components/attributes-panel/README.md +92 -0
  6. package/components/attributes-panel/index.js +324 -0
  7. package/components/badge/README.md +61 -0
  8. package/components/badge/index.js +105 -0
  9. package/components/block-appender/README.md +158 -0
  10. package/components/block-appender/index.js +135 -0
  11. package/components/block-extension/index.js +214 -0
  12. package/components/business-hours/SpecialHoursControl.js +211 -0
  13. package/components/business-hours/WeeklyHoursControl.js +419 -0
  14. package/components/business-hours/index.js +8 -0
  15. package/components/button/README.md +172 -0
  16. package/components/button/index.js +84 -0
  17. package/components/captcha-placeholder/CloudflareTurnstile.js +111 -0
  18. package/components/captcha-placeholder/GoogleRecaptcha.js +132 -0
  19. package/components/captcha-placeholder/HCaptcha.js +99 -0
  20. package/components/captcha-placeholder/README.md +228 -0
  21. package/components/captcha-placeholder/index.js +39 -0
  22. package/components/captcha-placeholder/styles.js +33 -0
  23. package/components/geocoder/AddressEntryControl.js +390 -0
  24. package/components/geocoder/GeocoderSearchControl.js +353 -0
  25. package/components/geocoder/countryOptions.js +66 -0
  26. package/components/geocoder/geocoder.js +400 -0
  27. package/components/geocoder/index.js +16 -0
  28. package/components/icon-picker/Icon.js +53 -0
  29. package/components/icon-picker/IconPicker.js +70 -0
  30. package/components/icon-picker/IconPickerModal.js +474 -0
  31. package/components/icon-picker/IconPickerToolbarButton.js +60 -0
  32. package/components/icon-picker/InlineIconPicker.js +92 -0
  33. package/components/icon-picker/api.js +181 -0
  34. package/components/icon-picker/config-bootstrap.js +108 -0
  35. package/components/icon-picker/hooks.js +157 -0
  36. package/components/icon-picker/index.js +39 -0
  37. package/components/icon-picker/load-script.js +33 -0
  38. package/components/icon-picker/loaders.js +79 -0
  39. package/components/icon-picker/store.js +223 -0
  40. package/components/icon-picker/styles.js +232 -0
  41. package/components/icon-picker/utils.js +138 -0
  42. package/components/layout/README.md +268 -0
  43. package/components/link-tools-panel/README.md +49 -0
  44. package/components/link-tools-panel/index.js +198 -0
  45. package/components/link-tools-panel/styles.js +90 -0
  46. package/components/media/README.md +288 -0
  47. package/components/media/SettingsMediaUpload.js +187 -0
  48. package/components/media/editor-media-upload.js +65 -0
  49. package/components/media/image-controls.js +196 -0
  50. package/components/media/index.js +13 -0
  51. package/components/media/inspector-media-upload.js +290 -0
  52. package/components/media/toolbar-media-upload.js +53 -0
  53. package/components/media/utils/aspect-ratios.js +118 -0
  54. package/components/meta/MetaPanel.js +75 -0
  55. package/components/meta/README.md +46 -0
  56. package/components/meta/index.js +6 -0
  57. package/components/meta/meta-gated-editor.js +282 -0
  58. package/components/query/README.md +340 -0
  59. package/components/query/cardTemplatePartPanel.js +69 -0
  60. package/components/query/cardTemplatePartSelect.js +165 -0
  61. package/components/query/cardTemplatePartUtils.js +119 -0
  62. package/components/query/columnCountControl.js +63 -0
  63. package/components/query/displayTypeSelect.js +52 -0
  64. package/components/query/examples.js +166 -0
  65. package/components/query/hideCurrentPostControl.js +47 -0
  66. package/components/query/index.js +20 -0
  67. package/components/query/manualPostSelector.js +125 -0
  68. package/components/query/manualTermSelector.js +141 -0
  69. package/components/query/orderBySelect.js +95 -0
  70. package/components/query/postTypeSelect.js +67 -0
  71. package/components/query/postsPerPageControl.js +51 -0
  72. package/components/query/selectionModeControl.js +61 -0
  73. package/components/query/taxonomySelect.js +139 -0
  74. package/components/query/utils.js +75 -0
  75. package/components/repeater/README.md +159 -0
  76. package/components/repeater/index.js +829 -0
  77. package/components/segmented-bar/README.md +30 -0
  78. package/components/segmented-bar/index.js +68 -0
  79. package/components/sortable-select/README.md +153 -0
  80. package/components/sortable-select/custom-sortable-token.js +127 -0
  81. package/components/sortable-select/index.js +693 -0
  82. package/components/style-picker/README.md +36 -0
  83. package/components/style-picker/index.js +92 -0
  84. package/components/variable-field/README.md +299 -0
  85. package/components/variable-field/index.js +592 -0
  86. package/components/variable-field/value-to-chip-html.js +38 -0
  87. package/components/variable-inserter/README.md +68 -0
  88. package/components/variable-inserter/index.js +168 -0
  89. package/constants/statusColors.js +1 -1
  90. package/editor.js +16 -0
  91. package/index.js +127 -0
  92. package/package.json +21 -13
  93. package/utils/kitaris-localize.js +128 -0
  94. package/utils/slug-aware-color-setter.js +27 -0
  95. package/build/index.asset.php +0 -20
  96. 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
+ }