playbook_ui 16.1.0.pre.alpha.play276813969 → 16.1.0.pre.alpha.play277814027

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/pb_advanced_table/Components/RegularTableView.tsx +12 -2
  3. data/app/pb_kits/playbook/pb_advanced_table/advanced_table.test.jsx +33 -0
  4. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_column_styling_background_custom.jsx +71 -0
  5. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_column_styling_background_custom.md +4 -0
  6. data/app/pb_kits/playbook/pb_advanced_table/docs/example.yml +1 -0
  7. data/app/pb_kits/playbook/pb_advanced_table/docs/index.js +2 -1
  8. data/app/pb_kits/playbook/pb_date_picker/_date_picker.tsx +14 -5
  9. data/app/pb_kits/playbook/pb_date_picker/docs/_date_picker_default.md +1 -0
  10. data/app/pb_kits/playbook/pb_dropdown/_dropdown.tsx +11 -46
  11. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_label.html.erb +3 -6
  12. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_label.jsx +0 -1
  13. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_label.md +1 -3
  14. data/app/pb_kits/playbook/pb_dropdown/dropdown.html.erb +4 -10
  15. data/app/pb_kits/playbook/pb_dropdown/dropdown.rb +0 -9
  16. data/app/pb_kits/playbook/pb_dropdown/dropdown_trigger.html.erb +2 -7
  17. data/app/pb_kits/playbook/pb_dropdown/dropdown_trigger.rb +0 -4
  18. data/app/pb_kits/playbook/pb_dropdown/index.js +73 -125
  19. data/app/pb_kits/playbook/pb_dropdown/subcomponents/DropdownTrigger.tsx +0 -16
  20. data/app/pb_kits/playbook/pb_dropdown/utilities/clickOutsideHelper.tsx +0 -6
  21. data/app/pb_kits/playbook/pb_form/docs/_form_with_required_indicator.html.erb +1 -0
  22. data/app/pb_kits/playbook/pb_form/pb_form_validation.js +9 -2
  23. data/app/pb_kits/playbook/pb_multi_level_select/_multi_level_select.scss +0 -7
  24. data/app/pb_kits/playbook/pb_multi_level_select/_multi_level_select.tsx +549 -638
  25. data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_label.html.erb +3 -3
  26. data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_label.jsx +7 -4
  27. data/app/pb_kits/playbook/pb_multi_level_select/multi_level_select.test.jsx +4 -4
  28. data/app/pb_kits/playbook/pb_textarea/_textarea.tsx +10 -0
  29. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_default.html.erb +3 -3
  30. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_default.jsx +3 -0
  31. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_default.md +1 -0
  32. data/app/pb_kits/playbook/pb_textarea/textarea.html.erb +25 -9
  33. data/app/pb_kits/playbook/pb_textarea/textarea.rb +7 -1
  34. data/app/pb_kits/playbook/pb_time_picker/_time_picker.tsx +97 -11
  35. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_on_handler.jsx +5 -2
  36. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_required_indicator.html.erb +6 -0
  37. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_required_indicator.jsx +16 -0
  38. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_required_indicator.md +3 -0
  39. data/app/pb_kits/playbook/pb_time_picker/docs/example.yml +2 -0
  40. data/app/pb_kits/playbook/pb_time_picker/docs/index.js +1 -0
  41. data/app/pb_kits/playbook/pb_time_picker/time_picker.rb +3 -0
  42. data/app/pb_kits/playbook/pb_time_picker/time_picker.test.jsx +47 -1
  43. data/dist/chunks/_typeahead-CWA5wlah.js +1 -0
  44. data/dist/chunks/vendor.js +3 -3
  45. data/dist/menu.yml +2 -2
  46. data/dist/playbook-rails-react-bindings.js +1 -1
  47. data/dist/playbook-rails.js +1 -1
  48. data/dist/playbook.css +1 -1
  49. data/lib/playbook/version.rb +1 -1
  50. metadata +10 -4
  51. data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_label.md +0 -3
  52. data/dist/chunks/_typeahead-C4YsbA48.js +0 -1
@@ -26,688 +26,599 @@ import {
26
26
  getExpandedItems,
27
27
  } from "./_helper_functions";
28
28
 
29
- interface MultiLevelSelectComponent extends React.ForwardRefExoticComponent<
30
- MultiLevelSelectProps & React.RefAttributes<HTMLInputElement>
31
- > {
29
+ interface MultiLevelSelectComponent
30
+ extends React.ForwardRefExoticComponent<
31
+ MultiLevelSelectProps & React.RefAttributes<HTMLInputElement>
32
+ > {
32
33
  Options: typeof MultiLevelSelectOptions;
33
34
  }
34
35
 
35
36
  type MultiLevelSelectProps = {
36
- aria?: { [key: string]: string };
37
- className?: string;
38
- data?: { [key: string]: string };
39
- disabled?: boolean;
40
- error?: string;
41
- htmlOptions?: { [key: string]: string | number | boolean | (() => void) };
42
- id?: string;
43
- inputDisplay?: "pills" | "none";
44
- inputName?: string;
45
- label?: string;
46
- name?: string;
47
- required?: boolean;
48
- returnAllSelected?: boolean;
49
- showCheckedChildren?: boolean;
50
- treeData?: { [key: string]: string }[] | any;
51
- onChange?: (event: { target: { name?: string; value: any } }) => void;
52
- onSelect?: (prop: { [key: string]: any }) => void;
53
- selectedIds?: string[] | any;
54
- variant?: "multi" | "single";
55
- wrapped?: boolean;
56
- pillColor?:
57
- | "primary"
58
- | "neutral"
59
- | "success"
60
- | "warning"
61
- | "error"
62
- | "info"
63
- | "data_1"
64
- | "data_2"
65
- | "data_3"
66
- | "data_4"
67
- | "data_5"
68
- | "data_6"
69
- | "data_7"
70
- | "data_8"
71
- | "windows"
72
- | "siding"
73
- | "roofing"
74
- | "doors"
75
- | "gutters"
76
- | "solar"
77
- | "insulation"
78
- | "accessories";
79
- } & GlobalProps;
80
-
81
- const MultiLevelSelect = forwardRef<HTMLInputElement, MultiLevelSelectProps>(
82
- (props) => {
83
- const {
84
- aria = {},
85
- className,
86
- data = {},
87
- disabled = false,
88
- error,
89
- htmlOptions = {},
90
- id,
91
- inputDisplay = "pills",
92
- inputName,
93
- name,
94
- label,
95
- required = false,
96
- returnAllSelected = false,
97
- showCheckedChildren = true,
98
- treeData,
99
- onChange = () => null,
100
- onSelect = () => null,
101
- selectedIds,
102
- variant = "multi",
103
- wrapped,
104
- pillColor = "primary",
105
- } = props;
106
-
107
- const ariaProps = buildAriaProps(aria);
108
- const dataProps = buildDataProps(data);
109
- const htmlProps = buildHtmlProps(htmlOptions);
110
- const classes = classnames(
111
- buildCss("pb_multi_level_select"),
112
- error && "error",
113
- globalProps(props),
114
- className
115
- );
37
+ aria?: { [key: string]: string }
38
+ className?: string
39
+ data?: { [key: string]: string }
40
+ disabled?: boolean
41
+ error?: string
42
+ htmlOptions?: {[key: string]: string | number | boolean | (() => void)},
43
+ id?: string
44
+ inputDisplay?: "pills" | "none"
45
+ inputName?: string
46
+ label?: string
47
+ name?: string
48
+ required?: boolean
49
+ returnAllSelected?: boolean
50
+ showCheckedChildren?: boolean
51
+ treeData?: { [key: string]: string; }[] | any
52
+ onChange?: (event: { target: { name?: string; value: any } }) => void
53
+ onSelect?: (prop: { [key: string]: any }) => void
54
+ selectedIds?: string[] | any
55
+ variant?: "multi" | "single"
56
+ wrapped?: boolean
57
+ pillColor?: "primary" | "neutral" | "success" | "warning" | "error" | "info" | "data_1" | "data_2" | "data_3" | "data_4" | "data_5" | "data_6" | "data_7" | "data_8" | "windows" | "siding" | "roofing" | "doors" | "gutters" | "solar" | "insulation" | "accessories",
58
+ } & GlobalProps
59
+
60
+ const MultiLevelSelect = forwardRef<HTMLInputElement, MultiLevelSelectProps>((props) => {
61
+ const {
62
+ aria = {},
63
+ className,
64
+ data = {},
65
+ disabled = false,
66
+ error,
67
+ htmlOptions = {},
68
+ id,
69
+ inputDisplay = "pills",
70
+ inputName,
71
+ name,
72
+ label,
73
+ required = false,
74
+ returnAllSelected = false,
75
+ showCheckedChildren = true,
76
+ treeData,
77
+ onChange = () => null,
78
+ onSelect = () => null,
79
+ selectedIds,
80
+ variant = "multi",
81
+ children,
82
+ wrapped,
83
+ pillColor = "primary"
84
+ } = props
85
+
86
+ const ariaProps = buildAriaProps(aria);
87
+ const dataProps = buildDataProps(data);
88
+ const htmlProps = buildHtmlProps(htmlOptions);
89
+ const classes = classnames(
90
+ buildCss("pb_multi_level_select"),
91
+ error && "error",
92
+ globalProps(props),
93
+ className
94
+ );
95
+
96
+ const dropdownRef = useRef(null);
97
+
98
+ // State for whether dropdown is open or closed
99
+ const [isDropdownClosed, setIsDropdownClosed] = useState(true);
100
+ // State from onChange for textinput, to use for filtering to create typeahead
101
+ const [filterItem, setFilterItem] = useState("");
102
+ // FormattedData with checked and parent_id added
103
+ const [formattedData, setFormattedData] = useState([]);
104
+ // State for the return of returnAllSelected
105
+ const [returnedArray, setReturnedArray] = useState([]);
106
+ // State for default return
107
+ const [defaultReturn, setDefaultReturn] = useState([]);
108
+ // Get expanded items from treeData
109
+ const initialExpandedItems = getExpandedItems(treeData, selectedIds, showCheckedChildren);
110
+ // Initialize state with expanded items
111
+ const [expanded, setExpanded] = useState(initialExpandedItems);
112
+
113
+ // Single Select specific state
114
+ const [singleSelectedItem, setSingleSelectedItem] = useState({
115
+ id: [],
116
+ value: "",
117
+ item: [],
118
+ });
119
+
120
+ const arrowDownElementId = `arrow_down_${id}`
121
+ const arrowUpElementId = `arrow_up_${id}`
122
+
123
+ const modifyRecursive = (tree: { [key: string]: any }[], check: boolean) => {
124
+ if (!Array.isArray(tree)) {
125
+ return;
126
+ }
127
+ return tree.map((item: { [key: string]: any }) => {
128
+ if (!item.disabled) {
129
+ item.checked = check;
130
+ }
131
+ item.children = modifyRecursive(item.children, check);
132
+ return item;
133
+ });
134
+ };
135
+
136
+ // Function to map over data and add parent_id + depth property to each item
137
+ const addCheckedAndParentProperty = (
138
+ treeData: { [key: string]: any }[],
139
+ selectedIds: string[],
140
+ parent_id: string | null = null,
141
+ depth = 0,
142
+ parentDisabled = false
143
+ ) => {
144
+ if (!Array.isArray(treeData)) {
145
+ return;
146
+ }
147
+ return treeData.map((item: { [key: string]: any } | any) => {
148
+ // An item is disabled if it is explicitly set as disabled or if its parent is disabled
149
+ const isDisabled = item.disabled || (parentDisabled && !returnAllSelected);
150
+
151
+ const newItem = {
152
+ ...item,
153
+ checked: Boolean(
154
+ selectedIds && selectedIds.length && selectedIds.includes(item.id)
155
+ ),
156
+ parent_id,
157
+ depth,
158
+ disabled: isDisabled,
159
+ };
160
+ if (newItem.children && newItem.children.length > 0) {
161
+ const children =
162
+ item.checked && !returnAllSelected
163
+ ? modifyRecursive(item.children, true)
164
+ : item.children;
165
+ newItem.children = addCheckedAndParentProperty(
166
+ children,
167
+ selectedIds,
168
+ newItem.id,
169
+ depth + 1,
170
+ isDisabled
171
+ );
172
+ }
173
+ return newItem;
174
+ });
175
+ };
116
176
 
117
- const dropdownRef = useRef(null);
118
-
119
- // State for whether dropdown is open or closed
120
- const [isDropdownClosed, setIsDropdownClosed] = useState(true);
121
- // State from onChange for textinput, to use for filtering to create typeahead
122
- const [filterItem, setFilterItem] = useState("");
123
- // FormattedData with checked and parent_id added
124
- const [formattedData, setFormattedData] = useState([]);
125
- // State for the return of returnAllSelected
126
- const [returnedArray, setReturnedArray] = useState([]);
127
- // State for default return
128
- const [defaultReturn, setDefaultReturn] = useState([]);
129
- // Get expanded items from treeData
130
- const initialExpandedItems = getExpandedItems(
177
+ useEffect(() => {
178
+ const formattedData = addCheckedAndParentProperty(
131
179
  treeData,
132
- selectedIds,
133
- showCheckedChildren,
180
+ variant === "single" ? [selectedIds?.[0]] : selectedIds
134
181
  );
135
- // Initialize state with expanded items
136
- const [expanded, setExpanded] = useState(initialExpandedItems);
137
-
138
- // Single Select specific state
139
- const [singleSelectedItem, setSingleSelectedItem] = useState({
140
- id: [],
141
- value: "",
142
- item: [],
143
- });
144
182
 
145
- const arrowDownElementId = `arrow_down_${id}`;
146
- const arrowUpElementId = `arrow_up_${id}`;
147
- // Control id for label htmlFor: use suffix to avoid conflict with outer div's id
148
- const sanitizeForId = (str: string) =>
149
- str.toLowerCase().replace(/\s+/g, "_").replace(/[^a-z0-9_]/g, "");
150
- const labelForId = id
151
- ? `${id}_input`
152
- : (name ? sanitizeForId(name) : null) ||
153
- (label ? sanitizeForId(label) : null) ||
154
- "multiselect_input";
155
- const errorId = error ? `${labelForId}-error` : undefined;
156
-
157
- const modifyRecursive = (
158
- tree: { [key: string]: any }[],
159
- check: boolean,
160
- ) => {
161
- if (!Array.isArray(tree)) {
162
- return;
163
- }
164
- return tree.map((item: { [key: string]: any }) => {
165
- if (!item.disabled) {
166
- item.checked = check;
167
- }
168
- item.children = modifyRecursive(item.children, check);
169
- return item;
170
- });
171
- };
183
+ setFormattedData(formattedData);
172
184
 
173
- // Function to map over data and add parent_id + depth property to each item
174
- const addCheckedAndParentProperty = (
175
- treeData: { [key: string]: any }[],
176
- selectedIds: string[],
177
- parent_id: string | null = null,
178
- depth = 0,
179
- parentDisabled = false,
180
- ) => {
181
- if (!Array.isArray(treeData)) {
182
- return;
183
- }
184
- return treeData.map((item: { [key: string]: any } | any) => {
185
- // An item is disabled if it is explicitly set as disabled or if its parent is disabled
186
- const isDisabled =
187
- item.disabled || (parentDisabled && !returnAllSelected);
188
-
189
- const newItem = {
190
- ...item,
191
- checked: Boolean(
192
- selectedIds && selectedIds.length && selectedIds.includes(item.id),
193
- ),
194
- parent_id,
195
- depth,
196
- disabled: isDisabled,
197
- };
198
- if (newItem.children && newItem.children.length > 0) {
199
- const children =
200
- item.checked && !returnAllSelected
201
- ? modifyRecursive(item.children, true)
202
- : item.children;
203
- newItem.children = addCheckedAndParentProperty(
204
- children,
205
- selectedIds,
206
- newItem.id,
207
- depth + 1,
208
- isDisabled,
185
+ if (variant === "single") {
186
+ // No selectedIds, reset state
187
+ if (selectedIds?.length === 0 || !selectedIds?.length) {
188
+ setSingleSelectedItem({ id: [], value: "", item: [] });
189
+ } else {
190
+ // If there is a selectedId but no current item, set the selectedItem
191
+ if (selectedIds?.length !== 0 && !singleSelectedItem.value) {
192
+ const selectedItem = filterFormattedDataById(
193
+ formattedData,
194
+ selectedIds[0]
209
195
  );
210
- }
211
- return newItem;
212
- });
213
- };
214
-
215
- useEffect(() => {
216
- const formattedData = addCheckedAndParentProperty(
217
- treeData,
218
- variant === "single" ? [selectedIds?.[0]] : selectedIds,
219
- );
220
-
221
- setFormattedData(formattedData);
222
-
223
- if (variant === "single") {
224
- // No selectedIds, reset state
225
- if (selectedIds?.length === 0 || !selectedIds?.length) {
226
- setSingleSelectedItem({ id: [], value: "", item: [] });
227
- } else {
228
- // If there is a selectedId but no current item, set the selectedItem
229
- if (selectedIds?.length !== 0 && !singleSelectedItem.value) {
230
- const selectedItem = filterFormattedDataById(
231
- formattedData,
232
- selectedIds[0],
233
- );
234
-
235
- if (!selectedItem.length) {
236
- setSingleSelectedItem({ id: [], value: "", item: [] });
237
- } else {
238
- const { id, label } = selectedItem[0];
239
- setSingleSelectedItem({
240
- id: [id],
241
- value: label,
242
- item: selectedItem,
243
- });
244
- }
245
- }
246
- }
247
- }
248
- }, [treeData, selectedIds]);
249
196
 
250
- useEffect(() => {
251
- if (returnAllSelected) {
252
- setReturnedArray(getCheckedItems(formattedData));
253
- } else if (variant === "single") {
254
- setDefaultReturn(singleSelectedItem.item);
255
- } else {
256
- setDefaultReturn(getDefaultCheckedItems(formattedData));
257
- }
258
- }, [formattedData]);
259
-
260
- useEffect(() => {
261
- // Function to handle clicks outside the dropdown
262
- const handleClickOutside = (event: any) => {
263
- // Don't close if clicking on the associated label
264
- const labelEl = document.querySelector(`label[for="${labelForId}"]`);
265
- if (labelEl?.contains(event.target)) return;
266
-
267
- if (
268
- dropdownRef.current &&
269
- !dropdownRef.current.contains(event.target) &&
270
- event.target.id !== arrowDownElementId &&
271
- event.target.id !== arrowUpElementId
272
- ) {
273
- setIsDropdownClosed(true);
274
- }
275
- };
276
- // Attach the event listener
277
- window.addEventListener("click", handleClickOutside);
278
- // Clean up the event listener on unmount
279
- return () => {
280
- window.removeEventListener("click", handleClickOutside);
281
- };
282
- }, [labelForId]);
283
-
284
- useEffect(() => {
285
- if (id) {
286
- // Attach the clear function to the window, scoped by the id
287
- (window as any)[`clearMultiLevelSelect_${id}`] = () => {
288
- const resetData = modifyRecursive(formattedData, false);
289
- setFormattedData(resetData);
290
- setReturnedArray([]);
291
- setDefaultReturn([]);
292
- setSingleSelectedItem({ id: [], value: "", item: [] });
293
- onSelect([]);
294
- };
295
- return () => {
296
- delete (window as any)[`clearMultiLevelSelect_${id}`];
297
- };
298
- }
299
- }, [formattedData, id, onSelect]);
300
-
301
- // Iterate over tree, find item and set checked or unchecked
302
- const modifyValue = (
303
- id: string,
304
- tree: { [key: string]: any }[],
305
- check: boolean,
306
- ) => {
307
- if (!Array.isArray(tree)) {
308
- return;
309
- }
310
- return tree.map((item: any) => {
311
- if (item.id != id)
312
- item.children = modifyValue(id, item.children, check);
313
- else {
314
- if (!item.disabled) {
315
- item.checked = check;
316
- }
317
- if (variant === "single") {
318
- // Single select: no children should be checked
319
- item.children = modifyRecursive(item.children, !check);
197
+ if (!selectedItem.length) {
198
+ setSingleSelectedItem({ id: [], value: "", item: [] });
320
199
  } else {
321
- item.children = modifyRecursive(item.children, check);
200
+ const { id, label } = selectedItem[0];
201
+ setSingleSelectedItem({ id: [id], value: label, item: selectedItem });
322
202
  }
323
203
  }
324
-
325
- return item;
326
- });
327
- };
328
-
329
- // Clone tree, check items + children
330
- const checkItem = (item: { [key: string]: any }) => {
331
- const tree = cloneDeep(formattedData);
332
- if (returnAllSelected) {
333
- return modifyValue(item.id, tree, true);
334
- } else {
335
- const checkedTree = modifyValue(item.id, tree, true);
336
- return recursiveCheckParent(item, checkedTree);
337
- }
338
- };
339
-
340
- // Clone tree, uncheck items + children
341
- const unCheckItem = (item: { [key: string]: any }) => {
342
- const tree = cloneDeep(formattedData);
343
- if (returnAllSelected) {
344
- return modifyValue(item.id, tree, false);
345
- } else {
346
- const uncheckedTree = modifyValue(item.id, tree, false);
347
- return getAncestorsOfUnchecked(uncheckedTree, item);
348
- }
349
- };
350
-
351
- // setFormattedData with proper properties
352
- const changeItem = (item: { [key: string]: any }, check: boolean) => {
353
- const tree = check ? checkItem(item) : unCheckItem(item);
354
- setFormattedData(tree);
355
-
356
- return tree;
357
- };
358
-
359
- // Click event for x on form pill
360
- const handlePillClose = (
361
- event: any,
362
- clickedItem: { [key: string]: any },
363
- ) => {
364
- // Prevents the dropdown from closing when clicking on the pill
365
- event.stopPropagation();
366
- const updatedTree = changeItem(clickedItem, false);
367
- // Logic for removing items from returnArray or defaultReturn when pills clicked
368
- if (returnAllSelected) {
369
- onSelect(getCheckedItems(updatedTree));
370
- onChange({ target: { name, value: getCheckedItems(updatedTree) } });
371
- } else {
372
- onSelect(getDefaultCheckedItems(updatedTree));
373
- onChange({
374
- target: { name, value: getDefaultCheckedItems(updatedTree) },
375
- });
376
204
  }
377
- };
378
-
379
- // Handle click on label - focus input and open dropdown
380
- const handleLabelClick = (e: React.MouseEvent) => {
381
- e.stopPropagation();
382
- const input = document.getElementById(labelForId);
383
- if (input) input.focus();
384
- setIsDropdownClosed(false);
385
- };
386
-
387
- // Handle click on input wrapper(entire div with pills, typeahead, etc) so it doesn't close when input or form pill is clicked
388
- const handleInputWrapperClick = (e: any) => {
205
+ }
206
+ }, [treeData, selectedIds]);
207
+
208
+ useEffect(() => {
209
+ if (returnAllSelected) {
210
+ setReturnedArray(getCheckedItems(formattedData));
211
+ } else if (variant === "single") {
212
+ setDefaultReturn(singleSelectedItem.item);
213
+ } else {
214
+ setDefaultReturn(getDefaultCheckedItems(formattedData));
215
+ }
216
+ }, [formattedData]);
217
+
218
+ useEffect(() => {
219
+ // Function to handle clicks outside the dropdown
220
+ const handleClickOutside = (event: any) => {
389
221
  if (
390
- e.target.id === labelForId ||
391
- e.target.classList.contains("pb_form_pill_tag") ||
392
- disabled
222
+ dropdownRef.current &&
223
+ !dropdownRef.current.contains(event.target) &&
224
+ event.target.id !== arrowDownElementId &&
225
+ event.target.id !== arrowUpElementId
393
226
  ) {
394
- return;
227
+ setIsDropdownClosed(true)
395
228
  }
396
- setIsDropdownClosed(!isDropdownClosed);
397
229
  };
398
-
399
- // Main function to handle any click inside dropdown
400
- const handledropdownItemClick = (e: any, check: boolean) => {
401
- const clickedItem = e.target.parentNode.id;
402
- // Setting filterItem to "" will clear textinput and clear typeahead
403
- setFilterItem("");
404
-
405
- const filtered = filterFormattedDataById(formattedData, clickedItem);
406
- const updatedTree = changeItem(filtered[0], check);
407
- if (returnAllSelected) {
408
- onSelect(getCheckedItems(updatedTree));
409
- onChange({ target: { name, value: getCheckedItems(updatedTree) } });
410
- } else {
411
- onSelect(getDefaultCheckedItems(updatedTree));
412
- onChange({
413
- target: { name, value: getDefaultCheckedItems(updatedTree) },
414
- });
415
- }
230
+ // Attach the event listener
231
+ window.addEventListener("click", handleClickOutside);
232
+ // Clean up the event listener on unmount
233
+ return () => {
234
+ window.removeEventListener("click", handleClickOutside);
416
235
  };
417
-
418
- // Single select
419
- const handleRadioButtonClick = (e: React.ChangeEvent<HTMLInputElement>) => {
420
- const { id, value: inputText } = e.target;
421
- // The radio button needs a unique ID, this grabs the ID before the hyphen
422
- const selectedItemID = id.match(/^[^-]*/)[0];
423
-
424
- // Check if the item is disabled - if so, don't allow selection (safety check in addition to native disabled attribute)
425
- const clickedItem = filterFormattedDataById(
426
- formattedData,
427
- selectedItemID,
428
- );
429
- if (clickedItem.length > 0 && clickedItem[0].disabled) {
430
- return;
236
+ }, []);
237
+
238
+ useEffect(() => {
239
+ if (id) {
240
+ // Attach the clear function to the window, scoped by the id
241
+ (window as any)[`clearMultiLevelSelect_${id}`] = () => {
242
+ const resetData = modifyRecursive(formattedData, false);
243
+ setFormattedData(resetData);
244
+ setReturnedArray([]);
245
+ setDefaultReturn([]);
246
+ setSingleSelectedItem({ id: [], value: "", item: [] });
247
+ onSelect([]);
248
+ };
249
+ return () => {
250
+ delete (window as any)[`clearMultiLevelSelect_${id}`];
251
+ };
252
+ }
253
+ }, [formattedData, id, onSelect]);
254
+
255
+ // Iterate over tree, find item and set checked or unchecked
256
+ const modifyValue = (
257
+ id: string,
258
+ tree: { [key: string]: any }[],
259
+ check: boolean
260
+ ) => {
261
+ if (!Array.isArray(tree)) {
262
+ return;
263
+ }
264
+ return tree.map((item: any) => {
265
+ if (item.id != id) item.children = modifyValue(id, item.children, check);
266
+ else {
267
+ if (!item.disabled) {
268
+ item.checked = check;
269
+ }
270
+ if (variant === "single") {
271
+ // Single select: no children should be checked
272
+ item.children = modifyRecursive(item.children, !check);
273
+ } else {
274
+ item.children = modifyRecursive(item.children, check);
275
+ }
431
276
  }
432
277
 
433
- // Reset tree checked state, triggering useEffect
434
- const treeWithNoSelections = modifyRecursive(formattedData, false);
435
- // Update tree with single selection
436
- const treeWithSelectedItem = modifyValue(
437
- selectedItemID,
438
- treeWithNoSelections,
439
- true,
440
- );
441
- const selectedItem = filterFormattedDataById(
442
- treeWithSelectedItem,
443
- selectedItemID,
444
- );
445
-
446
- setFormattedData(treeWithSelectedItem);
447
- setSingleSelectedItem({
448
- id: [selectedItemID],
449
- value: inputText,
450
- item: selectedItem,
451
- });
452
- // Reset the filter to always display dropdown options on click
453
- setFilterItem("");
454
- setIsDropdownClosed(true);
455
-
456
- onSelect(selectedItem);
457
- onChange({ target: { name, value: selectedItem } });
458
- };
459
-
460
- // Single select: reset the tree state upon typing
461
- const handleRadioInputChange = (inputText: string) => {
462
- modifyRecursive(formattedData, false);
463
- setDefaultReturn([]);
464
- setSingleSelectedItem({ id: [], value: inputText, item: [] });
465
- setFilterItem(inputText);
466
- };
467
-
468
- const isTreeRowExpanded = (item: any) => expanded.indexOf(item.id) > -1;
469
-
470
- // Handle click on chevron toggles in dropdown
471
- const handleToggleClick = (id: string, event: React.MouseEvent) => {
472
- event.stopPropagation();
473
- const clickedItem = filterFormattedDataById(formattedData, id);
474
- if (clickedItem) {
475
- let expandedArray = [...expanded];
476
- const itemExpanded = isTreeRowExpanded(clickedItem[0]);
477
-
478
- if (itemExpanded)
479
- expandedArray = expandedArray.filter((i) => i != clickedItem[0].id);
480
- else expandedArray.push(clickedItem[0].id);
481
-
482
- setExpanded(expandedArray);
483
- }
484
- };
278
+ return item;
279
+ });
280
+ };
281
+
282
+ // Clone tree, check items + children
283
+ const checkItem = (item: { [key: string]: any }) => {
284
+ const tree = cloneDeep(formattedData);
285
+ if (returnAllSelected) {
286
+ return modifyValue(item.id, tree, true);
287
+ } else {
288
+ const checkedTree = modifyValue(item.id, tree, true);
289
+ return recursiveCheckParent(item, checkedTree);
290
+ }
291
+ };
292
+
293
+ // Clone tree, uncheck items + children
294
+ const unCheckItem = (item: { [key: string]: any }) => {
295
+ const tree = cloneDeep(formattedData);
296
+ if (returnAllSelected) {
297
+ return modifyValue(item.id, tree, false);
298
+ } else {
299
+ const uncheckedTree = modifyValue(item.id, tree, false);
300
+ return getAncestorsOfUnchecked(uncheckedTree, item);
301
+ }
302
+ };
303
+
304
+ // setFormattedData with proper properties
305
+ const changeItem = (item: { [key: string]: any }, check: boolean) => {
306
+ const tree = check ? checkItem(item) : unCheckItem(item);
307
+ setFormattedData(tree);
308
+
309
+ return tree;
310
+ };
311
+
312
+ // Click event for x on form pill
313
+ const handlePillClose = (event: any, clickedItem: { [key: string]: any }) => {
314
+ // Prevents the dropdown from closing when clicking on the pill
315
+ event.stopPropagation();
316
+ const updatedTree = changeItem(clickedItem, false);
317
+ // Logic for removing items from returnArray or defaultReturn when pills clicked
318
+ if (returnAllSelected) {
319
+ onSelect(getCheckedItems(updatedTree));
320
+ onChange({ target: { name, value: getCheckedItems(updatedTree) } });
321
+ } else {
322
+ onSelect(getDefaultCheckedItems(updatedTree));
323
+ onChange({ target: { name, value: getDefaultCheckedItems(updatedTree) } });
324
+ }
325
+ };
326
+
327
+ // Handle click on input wrapper(entire div with pills, typeahead, etc) so it doesn't close when input or form pill is clicked
328
+ const handleInputWrapperClick = (e: any) => {
329
+ if (
330
+ e.target.id === "multiselect_input" ||
331
+ e.target.classList.contains("pb_form_pill_tag") ||
332
+ disabled
333
+ ) {
334
+ return;
335
+ }
336
+ setIsDropdownClosed(!isDropdownClosed);
337
+ };
338
+
339
+ // Main function to handle any click inside dropdown
340
+ const handledropdownItemClick = (e: any, check: boolean) => {
341
+ const clickedItem = e.target.parentNode.id;
342
+ // Setting filterItem to "" will clear textinput and clear typeahead
343
+ setFilterItem("");
344
+
345
+ const filtered = filterFormattedDataById(formattedData, clickedItem);
346
+ const updatedTree = changeItem(filtered[0], check);
347
+ if (returnAllSelected) {
348
+ onSelect(getCheckedItems(updatedTree));
349
+ onChange({ target: { name, value: getCheckedItems(updatedTree) } });
350
+ } else {
351
+ onSelect(getDefaultCheckedItems(updatedTree));
352
+ onChange({ target: { name, value: getDefaultCheckedItems(updatedTree) } });
353
+ }
354
+ };
355
+
356
+ // Single select
357
+ const handleRadioButtonClick = (e: React.ChangeEvent<HTMLInputElement>) => {
358
+ const { id, value: inputText } = e.target;
359
+ // The radio button needs a unique ID, this grabs the ID before the hyphen
360
+ const selectedItemID = id.match(/^[^-]*/)[0];
361
+
362
+ // Check if the item is disabled - if so, don't allow selection (safety check in addition to native disabled attribute)
363
+ const clickedItem = filterFormattedDataById(formattedData, selectedItemID);
364
+ if (clickedItem.length > 0 && clickedItem[0].disabled) {
365
+ return;
366
+ }
367
+
368
+ // Reset tree checked state, triggering useEffect
369
+ const treeWithNoSelections = modifyRecursive(formattedData, false);
370
+ // Update tree with single selection
371
+ const treeWithSelectedItem = modifyValue(
372
+ selectedItemID,
373
+ treeWithNoSelections,
374
+ true
375
+ );
376
+ const selectedItem = filterFormattedDataById(
377
+ treeWithSelectedItem,
378
+ selectedItemID
379
+ );
485
380
 
486
- const itemsSelectedLength = () => {
487
- let items;
488
- if (returnAllSelected && returnedArray && returnedArray.length) {
489
- items = returnedArray.length;
490
- } else if (!returnAllSelected && defaultReturn && defaultReturn.length) {
491
- items = defaultReturn.length;
492
- }
493
- return items;
494
- };
381
+ setFormattedData(treeWithSelectedItem);
382
+ setSingleSelectedItem({
383
+ id: [selectedItemID],
384
+ value: inputText,
385
+ item: selectedItem,
386
+ });
387
+ // Reset the filter to always display dropdown options on click
388
+ setFilterItem("");
389
+ setIsDropdownClosed(true);
390
+
391
+ onSelect(selectedItem);
392
+ onChange({ target: { name, value: selectedItem } });
393
+ };
394
+
395
+ // Single select: reset the tree state upon typing
396
+ const handleRadioInputChange = (inputText: string) => {
397
+ modifyRecursive(formattedData, false);
398
+ setDefaultReturn([]);
399
+ setSingleSelectedItem({ id: [], value: inputText, item: [] });
400
+ setFilterItem(inputText);
401
+ };
402
+
403
+ const isTreeRowExpanded = (item: any) => expanded.indexOf(item.id) > -1;
404
+
405
+ // Handle click on chevron toggles in dropdown
406
+ const handleToggleClick = (id: string, event: React.MouseEvent) => {
407
+ event.stopPropagation();
408
+ const clickedItem = filterFormattedDataById(formattedData, id);
409
+ if (clickedItem) {
410
+ let expandedArray = [...expanded];
411
+ const itemExpanded = isTreeRowExpanded(clickedItem[0]);
412
+
413
+ if (itemExpanded)
414
+ expandedArray = expandedArray.filter((i) => i != clickedItem[0].id);
415
+ else expandedArray.push(clickedItem[0].id);
416
+
417
+ setExpanded(expandedArray);
418
+ }
419
+ };
420
+
421
+ const itemsSelectedLength = () => {
422
+ let items;
423
+ if (returnAllSelected && returnedArray && returnedArray.length) {
424
+ items = returnedArray.length;
425
+ } else if (!returnAllSelected && defaultReturn && defaultReturn.length) {
426
+ items = defaultReturn.length;
427
+ }
428
+ return items;
429
+ };
430
+
431
+ // Rendering formattedData to UI based on typeahead
432
+ const renderNestedOptions = (items: { [key: string]: string; }[] | any ) => {
433
+ const hasOptionsChild = React.Children.toArray(props.children).some(
434
+ (child) => React.isValidElement(child) && child.type === MultiLevelSelect.Options
435
+ );
495
436
 
496
- // Rendering formattedData to UI based on typeahead
497
- const renderNestedOptions = (items: { [key: string]: string }[] | any) => {
498
- const hasOptionsChild = React.Children.toArray(props.children).some(
499
- (child) =>
500
- React.isValidElement(child) &&
501
- child.type === MultiLevelSelect.Options,
437
+ if (hasOptionsChild) {
438
+ return React.Children.map(props.children, (child) => {
439
+ if (React.isValidElement(child) && child.type === MultiLevelSelect.Options) {
440
+ return React.cloneElement(child, { items });
441
+ }
442
+ return null;
443
+ });
444
+ } else {
445
+ // If no children, use the default rendering
446
+ return (
447
+ <MultiLevelSelectOptions items={items} />
502
448
  );
503
-
504
- if (hasOptionsChild) {
505
- return React.Children.map(props.children, (child) => {
506
- if (
507
- React.isValidElement(child) &&
508
- child.type === MultiLevelSelect.Options
509
- ) {
510
- return React.cloneElement(child, { items });
511
- }
512
- return null;
513
- });
514
- } else {
515
- // If no children, use the default rendering
516
- return <MultiLevelSelectOptions items={items} />;
449
+ }
450
+ };
451
+
452
+
453
+ return (
454
+ <div
455
+ {...ariaProps}
456
+ {...dataProps}
457
+ {...htmlProps}
458
+ className={classes}
459
+ id={id}
460
+ >
461
+ {label &&
462
+ <Caption
463
+ marginBottom="xs"
464
+ text={label}
465
+ />
517
466
  }
518
- };
519
-
520
- return (
521
- <div
522
- {...ariaProps}
523
- {...dataProps}
524
- {...htmlProps}
525
- className={classes}
526
- id={id}
467
+ <MultiLevelSelectContext.Provider value={{
468
+ variant,
469
+ inputName,
470
+ renderNestedOptions,
471
+ isTreeRowExpanded,
472
+ handleToggleClick,
473
+ handleRadioButtonClick,
474
+ handledropdownItemClick,
475
+ filterItem,
476
+ }}>
477
+ <div className="wrapper"
478
+ ref={dropdownRef}
527
479
  >
528
- {label && (
529
- <label htmlFor={labelForId}
530
- onClick={handleLabelClick}
531
- >
532
- <Caption
533
- className="pb_multi_level_select_kit_label"
534
- marginBottom="xs"
535
- text={label}
536
- />
537
- </label>
538
- )}
539
- <MultiLevelSelectContext.Provider
540
- value={{
541
- variant,
542
- inputName,
543
- renderNestedOptions,
544
- isTreeRowExpanded,
545
- handleToggleClick,
546
- handleRadioButtonClick,
547
- handledropdownItemClick,
548
- filterItem,
549
- }}
480
+ <div className="input_wrapper"
481
+ onClick={handleInputWrapperClick}
550
482
  >
551
- <div className="wrapper"
552
- ref={dropdownRef}
553
- >
554
- <div className="input_wrapper"
555
- onClick={handleInputWrapperClick}
556
- >
557
- <div className="input_inner_container">
558
- {variant === "single" && defaultReturn.length !== 0
559
- ? defaultReturn.map((selectedItem) => (
483
+ <div className="input_inner_container">
484
+ {variant === "single" && defaultReturn.length !== 0
485
+ ? defaultReturn.map((selectedItem) => (
486
+ <input
487
+ disabled={disabled}
488
+ key={selectedItem.id}
489
+ name={`${name}[]`}
490
+ required={required}
491
+ type="hidden"
492
+ value={selectedItem.id}
493
+ />
494
+ ))
495
+ : null}
496
+
497
+ {variant !== "single" && (
498
+ <>
499
+ {returnAllSelected && returnedArray.length !== 0
500
+ ? returnedArray.map((item) => (
560
501
  <input
561
502
  disabled={disabled}
562
- key={selectedItem.id}
503
+ key={item.id}
563
504
  name={`${name}[]`}
564
505
  required={required}
565
506
  type="hidden"
566
- value={selectedItem.id}
507
+ value={item.id}
567
508
  />
568
509
  ))
569
510
  : null}
570
511
 
571
- {variant !== "single" && (
572
- <>
573
- {returnAllSelected && returnedArray.length !== 0
574
- ? returnedArray.map((item) => (
575
- <input
576
- disabled={disabled}
577
- key={item.id}
578
- name={`${name}[]`}
579
- required={required}
580
- type="hidden"
581
- value={item.id}
582
- />
583
- ))
584
- : null}
585
-
586
- {!returnAllSelected
587
- ? defaultReturn.map((item) => (
588
- <input
589
- disabled={disabled}
590
- key={item.id}
591
- name={`${name}[]`}
592
- required={required}
593
- type="hidden"
594
- value={item.id}
595
- />
596
- ))
597
- : null}
598
-
599
- {returnAllSelected &&
600
- returnedArray.length !== 0 &&
601
- inputDisplay === "pills"
602
- ? returnedArray.map((item, index) => (
603
- <FormPill
604
- color={pillColor}
605
- key={index}
606
- onClick={(event: any) =>
607
- handlePillClose(event, item)
608
- }
609
- text={item.label}
610
- wrapped={wrapped}
611
- />
612
- ))
613
- : null}
614
-
615
- {!returnAllSelected &&
616
- defaultReturn.length !== 0 &&
617
- inputDisplay === "pills"
618
- ? defaultReturn.map((item, index) => (
619
- <FormPill
620
- color={pillColor}
621
- key={index}
622
- onClick={(event: any) =>
623
- handlePillClose(event, item)
624
- }
625
- text={item.label}
626
- wrapped={wrapped}
627
- />
628
- ))
629
- : null}
630
-
631
- {returnAllSelected &&
632
- returnedArray.length !== 0 &&
633
- inputDisplay === "pills" && <br />}
634
-
635
- {!returnAllSelected &&
636
- defaultReturn.length !== 0 &&
637
- inputDisplay === "pills" && <br />}
638
- </>
639
- )}
640
-
641
- <input
642
- aria-describedby={errorId}
643
- aria-invalid={!!error}
644
- disabled={disabled}
645
- id={labelForId}
646
- onChange={(e) => {
647
- variant === "single"
648
- ? handleRadioInputChange(e.target.value)
649
- : setFilterItem(e.target.value);
650
- }}
651
- onClick={() => setIsDropdownClosed(false)}
652
- onFocus={() => !disabled && setIsDropdownClosed(false)}
653
- placeholder={
654
- inputDisplay === "none" && itemsSelectedLength()
655
- ? `${itemsSelectedLength()} ${
656
- itemsSelectedLength() === 1 ? "item" : "items"
657
- } selected`
658
- : "Start typing..."
659
- }
660
- required={required}
661
- value={singleSelectedItem.value || filterItem}
662
- />
663
- </div>
664
-
665
- {isDropdownClosed ? (
666
- <div id={arrowDownElementId}
667
- key="chevron-down"
668
- >
669
- <Icon icon="chevron-down"
670
- id={arrowDownElementId}
671
- size="xs"
672
- />
673
- </div>
674
- ) : (
675
- <div id={arrowUpElementId}
676
- key="chevron-up"
677
- >
678
- <Icon icon="chevron-up"
679
- id={arrowUpElementId}
680
- size="xs"
681
- />
682
- </div>
683
- )}
684
- </div>
512
+ {!returnAllSelected
513
+ ? defaultReturn.map((item) => (
514
+ <input
515
+ disabled={disabled}
516
+ key={item.id}
517
+ name={`${name}[]`}
518
+ required={required}
519
+ type="hidden"
520
+ value={item.id}
521
+ />
522
+ ))
523
+ : null}
685
524
 
686
- <div
687
- className={`dropdown_menu ${isDropdownClosed ? "close" : "open"}`}
688
- >
689
- {renderNestedOptions(
690
- filterItem
691
- ? findByFilter(formattedData, filterItem)
692
- : formattedData,
693
- )}
694
- </div>
525
+ {returnAllSelected &&
526
+ returnedArray.length !== 0 &&
527
+ inputDisplay === "pills"
528
+ ? returnedArray.map((item, index) => (
529
+ <FormPill
530
+ color={pillColor}
531
+ key={index}
532
+ onClick={(event: any) => handlePillClose(event, item)}
533
+ text={item.label}
534
+ wrapped={wrapped}
535
+ />
536
+ ))
537
+ : null}
538
+
539
+ {!returnAllSelected &&
540
+ defaultReturn.length !== 0 &&
541
+ inputDisplay === "pills"
542
+ ? defaultReturn.map((item, index) => (
543
+ <FormPill
544
+ color={pillColor}
545
+ key={index}
546
+ onClick={(event: any) => handlePillClose(event, item)}
547
+ text={item.label}
548
+ wrapped={wrapped}
549
+ />
550
+ ))
551
+ : null}
552
+
553
+ {returnAllSelected &&
554
+ returnedArray.length !== 0 &&
555
+ inputDisplay === "pills" && <br />}
556
+
557
+ {!returnAllSelected &&
558
+ defaultReturn.length !== 0 &&
559
+ inputDisplay === "pills" && <br />}
560
+ </>
561
+ )}
562
+
563
+ <input
564
+ disabled={disabled}
565
+ id="multiselect_input"
566
+ onChange={(e) => {
567
+ variant === "single"
568
+ ? handleRadioInputChange(e.target.value)
569
+ : setFilterItem(e.target.value);
570
+ }}
571
+ onClick={() => setIsDropdownClosed(false)}
572
+ placeholder={
573
+ inputDisplay === "none" && itemsSelectedLength()
574
+ ? `${itemsSelectedLength()} ${
575
+ itemsSelectedLength() === 1 ? "item" : "items"
576
+ } selected`
577
+ : "Start typing..."
578
+ }
579
+ required={required}
580
+ value={singleSelectedItem.value || filterItem}
581
+ />
695
582
  </div>
696
- </MultiLevelSelectContext.Provider>
697
- {error && (
583
+
584
+ {isDropdownClosed ? (
585
+ <div id={arrowDownElementId}
586
+ key="chevron-down">
587
+ <Icon
588
+ icon="chevron-down"
589
+ id={arrowDownElementId}
590
+ size="xs"
591
+ />
592
+ </div>
593
+ ) : (
594
+ <div id={arrowUpElementId}
595
+ key="chevron-up">
596
+ <Icon
597
+ icon="chevron-up"
598
+ id={arrowUpElementId}
599
+ size="xs"
600
+ />
601
+ </div>
602
+ )}
603
+ </div>
604
+
605
+ <div className={`dropdown_menu ${isDropdownClosed ? "close" : "open"}`}>
606
+ {renderNestedOptions(
607
+ filterItem ? findByFilter(formattedData, filterItem) : formattedData
608
+ )}
609
+ </div>
610
+ </div>
611
+ </MultiLevelSelectContext.Provider>
612
+ {error &&
698
613
  <Body
699
- aria={{ atomic: "true", live: "polite" }}
700
614
  dark={props.dark}
701
- htmlOptions={{ role: "alert" }}
702
- id={errorId}
703
615
  status="negative"
704
616
  text={error}
705
617
  />
706
- )}
707
- </div>
708
- );
709
- },
710
- ) as MultiLevelSelectComponent;
618
+ }
619
+ </div>
620
+ );
621
+ }) as MultiLevelSelectComponent;
711
622
 
712
623
  MultiLevelSelect.displayName = "MultiLevelSelect";
713
624
  MultiLevelSelect.Options = MultiLevelSelectOptions;