PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.5
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.5
4.9.5 4.9.4 4.9.3 4.9.2 4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 All 203 releases
betterdocs / react-src / admin / faq-builder / woocommerce / GroupMultiSelect.js

GroupMultiSelect.js in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.9.5, at react-src/admin/faq-builder/woocommerce/GroupMultiSelect.js

123 lines 5.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import React, { useLayoutEffect, useRef, useState } from "react";
2 import Select from "react-select";
3 import AsyncSelect from "react-select/async";
4 import { __ } from "@wordpress/i18n";
5
6 // Multi-select where the control keeps showing the placeholder and the chosen
7 // items render as removable chips *below* the field (design-system style).
8 // Pass `options` for a static list, or `loadOptions` for async search.
9 const GroupMultiSelect = ({
10 options,
11 loadOptions,
12 value,
13 onChange,
14 isDisabled = false,
15 // `loading` + a non-zero `loadingCount` (the saved-selection size) mean the
16 // group already has assignments still being resolved — show a single
17 // placeholder chip so the row is reserved while they load. The count only
18 // gates the skeleton; it no longer maps to the number of placeholder chips.
19 loading = false,
20 loadingCount = 0,
21 placeholder = __("Select FAQ groups…", "betterdocs"),
22 noOptionsMessage = () => __("No FAQ groups found", "betterdocs"),
23 }) => {
24 const removeOne = (val) =>
25 onChange((value || []).filter((opt) => opt.value !== val));
26
27 // Animate the chip area's height so the row eases between states — empty →
28 // skeleton → real chips — and between any number of selected chips (which
29 // may wrap to a different number of rows than the skeleton reserved),
30 // instead of snapping/jumping. We measure the natural content height of the
31 // inner (unconstrained) wrapper and transition the outer wrapper's height to
32 // it; the outer is `overflow: hidden`, so clipping it never feeds back into
33 // the measured height (no ResizeObserver loop).
34 const measureRef = useRef(null);
35 const [areaHeight, setAreaHeight] = useState(null);
36
37 useLayoutEffect(() => {
38 const el = measureRef.current;
39 if (!el || typeof ResizeObserver === "undefined") return;
40 // Seed the height before the first paint so the modal opens at the right
41 // size (no initial expand animation), then keep it in sync as chips,
42 // skeletons, or wrapping change.
43 const sync = () => setAreaHeight(el.scrollHeight);
44 sync();
45 const ro = new ResizeObserver(sync);
46 ro.observe(el);
47 return () => ro.disconnect();
48 }, []);
49
50 const sharedProps = {
51 isMulti: true,
52 controlShouldRenderValue: false,
53 hideSelectedOptions: false,
54 closeMenuOnSelect: false,
55 isClearable: false,
56 className: "bd-faq-select bd-woo-assign-select",
57 classNamePrefix: "bd-faq-select",
58 value,
59 isDisabled,
60 onChange: (selected) => onChange(selected || []),
61 menuPortalTarget:
62 typeof document !== "undefined" ? document.body : null,
63 menuPlacement: "auto",
64 styles: { menuPortal: (base) => ({ ...base, zIndex: 9999 }) },
65 placeholder,
66 noOptionsMessage,
67 };
68
69 return (
70 <div className="bd-faq-multiselect">
71 {loadOptions ? (
72 <AsyncSelect
73 {...sharedProps}
74 cacheOptions
75 defaultOptions
76 loadOptions={loadOptions}
77 />
78 ) : (
79 <Select {...sharedProps} options={options} />
80 )}
81 <div
82 className="bd-faq-chips-anim"
83 style={areaHeight == null ? undefined : { height: areaHeight }}
84 >
85 <div className="bd-faq-chips-measure" ref={measureRef}>
86 {loading && loadingCount > 0 ? (
87 // A single placeholder chip — one row only. The exact
88 // saved count is irrelevant here; the height animation
89 // eases up to however many rows the real chips need.
90 <div className="bd-faq-selected-chips" aria-hidden="true">
91 <span className="bd-faq-chip bd-faq-chip--skeleton" />
92 </div>
93 ) : value && value.length > 0 ? (
94 <div className="bd-faq-selected-chips">
95 {value.map((opt, i) => (
96 <span
97 className={`bd-faq-chip bd-faq-chip--${(i % 6) + 1}`}
98 key={opt.value}
99 >
100 <span className="bd-faq-chip__label">
101 {opt.label}
102 </span>
103 <button
104 type="button"
105 className="bd-faq-chip__remove"
106 aria-label={__("Remove", "betterdocs")}
107 disabled={isDisabled}
108 onClick={() => removeOne(opt.value)}
109 >
110 ×
111 </button>
112 </span>
113 ))}
114 </div>
115 ) : null}
116 </div>
117 </div>
118 </div>
119 );
120 };
121
122 export default GroupMultiSelect;
123