playbook_ui 12.25.0 → 12.26.0.pre.alpha.multiselectfixes795

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/_playbook.scss +1 -0
  3. data/app/pb_kits/playbook/index.js +1 -0
  4. data/app/pb_kits/playbook/pb_avatar/docs/_avatar_swift.md +82 -1
  5. data/app/pb_kits/playbook/pb_detail/_detail.scss +44 -0
  6. data/app/pb_kits/playbook/pb_detail/_detail.tsx +55 -0
  7. data/app/pb_kits/playbook/pb_detail/_detail_mixins.scss +29 -0
  8. data/app/pb_kits/playbook/pb_detail/detail.html.erb +7 -0
  9. data/app/pb_kits/playbook/pb_detail/detail.rb +31 -0
  10. data/app/pb_kits/playbook/pb_detail/detail.test.jsx +46 -0
  11. data/app/pb_kits/playbook/pb_detail/docs/_description.md +1 -0
  12. data/app/pb_kits/playbook/pb_detail/docs/_detail_bold.html.erb +34 -0
  13. data/app/pb_kits/playbook/pb_detail/docs/_detail_bold.jsx +49 -0
  14. data/app/pb_kits/playbook/pb_detail/docs/_detail_bold.md +1 -0
  15. data/app/pb_kits/playbook/pb_detail/docs/_detail_colors.html.erb +24 -0
  16. data/app/pb_kits/playbook/pb_detail/docs/_detail_colors.jsx +38 -0
  17. data/app/pb_kits/playbook/pb_detail/docs/_detail_colors.md +6 -0
  18. data/app/pb_kits/playbook/pb_detail/docs/_detail_default.html.erb +3 -0
  19. data/app/pb_kits/playbook/pb_detail/docs/_detail_default.jsx +13 -0
  20. data/app/pb_kits/playbook/pb_detail/docs/_detail_styled.html.erb +22 -0
  21. data/app/pb_kits/playbook/pb_detail/docs/_detail_styled.jsx +32 -0
  22. data/app/pb_kits/playbook/pb_detail/docs/example.yml +11 -0
  23. data/app/pb_kits/playbook/pb_detail/docs/index.js +4 -0
  24. data/app/pb_kits/playbook/pb_docs/kit_example.html.erb +14 -13
  25. data/app/pb_kits/playbook/pb_form_pill/_form_pill.tsx +3 -2
  26. data/app/pb_kits/playbook/pb_multi_level_select/_helper_functions.tsx +228 -0
  27. data/app/pb_kits/playbook/pb_multi_level_select/_multi_level_select.scss +58 -98
  28. data/app/pb_kits/playbook/pb_multi_level_select/_multi_level_select.tsx +344 -86
  29. data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_default.jsx +727 -61
  30. data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_default.md +1 -1
  31. data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_return_all_selected.html.erb +1 -0
  32. data/app/pb_kits/playbook/pb_multi_level_select/multi_level_select.test.jsx +1 -1
  33. data/app/pb_kits/playbook/playbook-doc.js +2 -0
  34. data/dist/menu.yml +1 -0
  35. data/dist/playbook-rails.js +7 -7
  36. data/lib/playbook/version.rb +2 -2
  37. metadata +27 -9
  38. data/app/pb_kits/playbook/pb_multi_level_select/_multi_select_helper.tsx +0 -31
  39. data/app/pb_kits/playbook/pb_multi_level_select/helper_functions.ts +0 -87
@@ -1,9 +1,25 @@
1
- import React, { useState, useEffect, useMemo } from "react";
1
+ import React, { useState, useEffect, useRef } from "react";
2
2
  import classnames from "classnames";
3
3
  import { buildAriaProps, buildCss, buildDataProps } from "../utilities/props";
4
- import { globalProps } from "../utilities/globalProps";
5
- import { findItemById, checkIt, unCheckIt, getParentAndAncestorsIds } from "./helper_functions";
6
- import MultiSelectHelper from "./_multi_select_helper";
4
+ import { globalProps, GlobalProps } from "../utilities/globalProps";
5
+ import Icon from "../pb_icon/_icon";
6
+ import Checkbox from "../pb_checkbox/_checkbox";
7
+ import FormPill from "../pb_form_pill/_form_pill";
8
+ import CircleIconButton from "../pb_circle_icon_button/_circle_icon_button";
9
+ import {
10
+ unCheckIt,
11
+ getAncestorsOfUnchecked,
12
+ unCheckedRecursive,
13
+ checkedRecursive,
14
+ filterFormattedDataById,
15
+ findByFilter,
16
+ getCheckedItems,
17
+ updateReturnItems,
18
+ recursiveReturnOnlyParent,
19
+ removeChildrenIfParentChecked,
20
+ getChildIds,
21
+ areAllCheckedFalse
22
+ } from "./_helper_functions";
7
23
 
8
24
  type MultiLevelSelectProps = {
9
25
  aria?: { [key: string]: string };
@@ -13,7 +29,7 @@ type MultiLevelSelectProps = {
13
29
  returnAllSelected?: boolean;
14
30
  treeData?: { [key: string]: string }[];
15
31
  onSelect?: (prop: { [key: string]: any }) => void;
16
- };
32
+ } & GlobalProps;
17
33
 
18
34
  const MultiLevelSelect = (props: MultiLevelSelectProps) => {
19
35
  const {
@@ -34,104 +50,346 @@ const MultiLevelSelect = (props: MultiLevelSelectProps) => {
34
50
  className
35
51
  );
36
52
 
53
+ const dropdownRef = useRef(null);
54
+
55
+ //state for whether dropdown is open or closed
56
+ const [isClosed, setIsClosed] = useState(true);
57
+ //state from onchange for textinput, to use for filtering to create typeahead
58
+ const [filterItem, setFilterItem] = useState("");
59
+ //this is essentially the return that the user will get when they use the kit
60
+ const [returnedArray, setReturnedArray] = useState([]);
61
+ //formattedData with checked and parent_id added
37
62
  const [formattedData, setFormattedData] = useState(treeData);
38
- const [selectedItems, setSelectedItems] = useState([]);
39
- const [checkedData, setCheckedData] = useState([]);
40
-
41
- const onChange = (currentNode: { [key: string]: any }) => {
42
- const updatedData = formattedData.map((item: any) => {
43
- if (item.id === currentNode._id) {
44
- if (currentNode.checked) {
45
- checkIt(item, selectedItems, setSelectedItems, false);
46
- } else {
47
- unCheckIt(item, selectedItems, setSelectedItems, false);
48
- }
49
- } else if (item.children) {
50
- const foundItem = findItemById(item.children, currentNode._id);
51
- if (foundItem) {
52
- if (currentNode.checked) {
53
- checkIt(foundItem, selectedItems, setSelectedItems, false);
54
- if (currentNode._parent) {
55
- const parents = getParentAndAncestorsIds(currentNode._parent, formattedData)
56
- parents.forEach((item:string) => {
57
- const ancestor = findItemById(formattedData,item)
58
- ancestor.expanded = true
59
- });
60
- }
61
- } else {
62
- unCheckIt(foundItem, selectedItems, setSelectedItems, false);
63
- if (currentNode._parent) {
64
- const parents = getParentAndAncestorsIds(currentNode._parent, formattedData)
65
- parents.forEach((item:string) => {
66
- const ancestor = findItemById(formattedData,item)
67
- ancestor.expanded = true
68
- });
69
- }
70
- }
71
- }
63
+ //toggle chevron in dropdown
64
+ //@ts-ignore
65
+ const [isToggled, setIsToggled] = useState<{ [id: number]: boolean }>({});
66
+ //state for return for default
67
+ const [defaultReturn, setDefaultReturn] = useState([]);
68
+
69
+ useEffect(() => {
70
+ let el = document.getElementById(`pb_data_wrapper_${id}`);
71
+ if (el) {
72
+ el.setAttribute(
73
+ "data-tree",
74
+ JSON.stringify(returnAllSelected ? returnedArray : defaultReturn)
75
+ );
76
+ }
77
+ returnAllSelected
78
+ ? onSelect(returnedArray)
79
+ : onSelect(
80
+ defaultReturn.filter(
81
+ (item, index, self) =>
82
+ index === self.findIndex((obj) => obj.id === item.id)
83
+ )
84
+ );
85
+ }, [returnedArray, defaultReturn]);
86
+
87
+ useEffect(() => {
88
+ //Create new formattedData array for use
89
+ setFormattedData(addCheckedAndParentProperty(treeData));
90
+ // Function to handle clicks outside the dropdown
91
+ const handleClickOutside = (event: any) => {
92
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
93
+ setIsClosed(true);
72
94
  }
95
+ };
96
+ //if any items already checked in first render, set return accordingly
97
+ const initialChecked = getCheckedItems(treeData)
98
+ const initialUnchecked = areAllCheckedFalse(treeData)
99
+ initialChecked && returnAllSelected && setReturnedArray(initialChecked)
100
+ initialChecked && !returnAllSelected && setDefaultReturn(initialChecked)
101
+ initialUnchecked && returnAllSelected && setReturnedArray([])
102
+ initialUnchecked && !returnAllSelected && setDefaultReturn([])
73
103
 
74
- return item;
75
- });
104
+ // Attach the event listener
105
+ window.addEventListener("click", handleClickOutside);
106
+ // Clean up the event listener on unmount
107
+ return () => {
108
+ window.removeEventListener("click", handleClickOutside);
109
+ };
110
+ }, []);
76
111
 
77
- setFormattedData(updatedData);
112
+ //function to map over data and add parent_id + depth property to each item
113
+ const addCheckedAndParentProperty = (
114
+ treeData: { [key: string]: any }[],
115
+ parent_id: string = null,
116
+ depth: number = 0,
117
+ ) => {
118
+ if (!Array.isArray(treeData)) {
119
+ return;
120
+ }
121
+ return treeData.map((item: { [key: string]: any } | any) => {
122
+ const newItem = {
123
+ ...item,
124
+ parent_id,
125
+ depth,
126
+ };
127
+ if (newItem.children && newItem.children.length > 0) {
128
+ newItem.children = addCheckedAndParentProperty(
129
+ newItem.children,
130
+ newItem.id,
131
+ depth + 1,
132
+ );
133
+ }
134
+ return newItem;
135
+ });
78
136
  };
79
137
 
80
- useEffect(() => {
138
+ //click event for x on form pill
139
+ const handlePillClose = (event: any, clickedItem: { [key: string]: any }) => {
140
+ // prevents the dropdown from closing when clicking on the pill
141
+ event.stopPropagation();
142
+ //logic for removing items from returnArray or defaultReturn when pills clicked
81
143
  if (returnAllSelected) {
82
- const selected = selectedItems.filter(
83
- (item: { [key: string]: any }) => item.checked
84
- );
85
- //filter to remove duplicate items
86
- const uniqueSelected = selected.filter(
87
- (obj, index, self) => index === self.findIndex((t) => t.id === obj.id)
88
- );
89
- setCheckedData(uniqueSelected);
144
+ if (returnedArray.includes(clickedItem)) {
145
+ if (clickedItem.children && clickedItem.children.length > 0) {
146
+ const childrenOfChecked = getChildIds(clickedItem, returnedArray);
147
+ const updatedFiltered = returnedArray
148
+ .filter((item) => item !== clickedItem)
149
+ .filter((item) => !childrenOfChecked.includes(item.id));
150
+ setReturnedArray(updatedFiltered);
151
+ } else {
152
+ const updatedFiltered = returnedArray.filter(
153
+ (item) => item !== clickedItem
154
+ );
155
+ setReturnedArray(updatedFiltered);
156
+ }
157
+ }
158
+ } else {
159
+ if (defaultReturn.includes(clickedItem)) {
160
+ getAncestorsOfUnchecked(formattedData, clickedItem);
161
+ const newChecked = getCheckedItems(formattedData);
162
+ const filteredReturn = updateReturnItems(newChecked).filter(
163
+ (item) => item.id !== clickedItem.id
164
+ );
165
+ setDefaultReturn(filteredReturn);
166
+ }
167
+ }
168
+ if (clickedItem.children && clickedItem.children.length > 0) {
169
+ unCheckedRecursive(clickedItem);
90
170
  }
91
- }, [selectedItems]);
171
+ //logic to uncheck clickedItem in formattedData
172
+ unCheckIt(formattedData, clickedItem.id);
173
+ };
92
174
 
93
- useEffect(() => {
94
- let el = document.getElementById(`pb_data_wrapper_${id}`);
95
- if (el) {
96
- el.setAttribute("data-tree", JSON.stringify(checkedData));
175
+ //handle click on input wrapper(entire div with pills, typeahead, etc) so it doesn't close when input or form pill is clicked
176
+ const handleInputWrapperClick = (e: any) => {
177
+ e.stopPropagation();
178
+ if (
179
+ e.target.id === "multiselect_input" ||
180
+ e.target.classList.contains("pb_form_pill_tag")
181
+ ) {
182
+ return;
97
183
  }
98
- if (returnAllSelected) {
99
- onSelect(checkedData);
184
+ setIsClosed(!isClosed);
185
+ };
186
+
187
+ //Main function to handle any click inside dropdown
188
+ const handledropdownItemClick = (e: any) => {
189
+ const clickedItem = e.target.parentNode.id;
190
+ //setting filterItem to "" will clear textinput and clear typeahead
191
+ setFilterItem("");
192
+
193
+ const filtered = filterFormattedDataById(formattedData, clickedItem);
194
+ //check and uncheck all children of checked/unchecked parent item
195
+ if (filtered[0].children && filtered[0].children.length > 0) {
196
+ if (filtered[0].checked) {
197
+ filtered[0].children.forEach((item: { [key: string]: any }) => {
198
+ checkedRecursive(item);
199
+ });
200
+ } else if (!filtered[0].checked) {
201
+ filtered[0].children.forEach((item: { [key: string]: any }) => {
202
+ unCheckedRecursive(item);
203
+ });
204
+ }
205
+ }
206
+
207
+ const checkedItems = getCheckedItems(formattedData);
208
+
209
+ //checking and unchecking items for returnAllSelected variant
210
+ if (returnedArray.includes(filtered[0])) {
211
+ if (!filtered[0].checked) {
212
+ if (filtered[0].children && filtered[0].children.length > 0) {
213
+ const childrenOfChecked = getChildIds(filtered[0], returnedArray);
214
+ const updatedFiltered = returnedArray
215
+ .filter((item) => item !== filtered[0])
216
+ .filter((item) => !childrenOfChecked.includes(item.id));
217
+
218
+ setReturnedArray(updatedFiltered);
219
+ } else {
220
+ const updatedFiltered = returnedArray.filter(
221
+ (item) => item !== filtered[0]
222
+ );
223
+ setReturnedArray(updatedFiltered);
224
+ }
225
+ }
226
+ } else {
227
+ setReturnedArray(checkedItems);
100
228
  }
101
- }, [checkedData]);
102
229
 
103
- const DropDownSelectComponent = useMemo(() => {
230
+ //when item is unchecked for default variant
231
+ if (!filtered[0].checked && !returnAllSelected) {
232
+ //uncheck parent and grandparent if any child unchecked
233
+ getAncestorsOfUnchecked(formattedData, filtered[0]);
234
+
235
+ const newChecked = getCheckedItems(formattedData);
236
+ //get all checked items, and filter to check if all children checked, if yes return only parent
237
+ const filteredReturn = updateReturnItems(newChecked);
238
+ setDefaultReturn(filteredReturn);
239
+ }
240
+
241
+ //when item is checked for default variant
242
+ if (!returnAllSelected && filtered[0].checked) {
243
+ //if checked item has children
244
+ if (filtered[0].children && filtered[0].children.length > 0) {
245
+ removeChildrenIfParentChecked(
246
+ filtered[0],
247
+ defaultReturn,
248
+ setDefaultReturn
249
+ );
250
+ }
251
+
252
+ //if clicked item has parent_id, find parent and check if all children checked or not
253
+ if (filtered[0].parent_id !== null) {
254
+ recursiveReturnOnlyParent(
255
+ filtered[0],
256
+ formattedData,
257
+ defaultReturn,
258
+ setDefaultReturn
259
+ );
260
+ } else {
261
+ setDefaultReturn([filtered[0]]);
262
+ }
263
+ }
264
+ };
265
+
266
+ //handle click on chevron toggles in dropdown
267
+ const handleToggleClick = (id: string, event: React.MouseEvent) => {
268
+ event.stopPropagation();
269
+ setIsToggled((prevState: { [id: string]: boolean }) => ({
270
+ ...prevState,
271
+ [id]: !prevState[id],
272
+ }));
273
+ const clickedItem = filterFormattedDataById(formattedData, id);
274
+
275
+ if (clickedItem) {
276
+ clickedItem[0].expanded = !clickedItem[0].expanded;
277
+ }
278
+ };
279
+
280
+ //rendering formattedData to UI based on typeahead
281
+ const renderNestedOptions = (items: { [key: string]: any }[]) => {
104
282
  return (
105
- <MultiSelectHelper
106
- treeData={formattedData}
107
- onChange={(
108
- // @ts-ignore
109
- selectedNodes: { [key: string]: any }[],
110
- currentNode: { [key: string]: any }[]
111
- ) => {
112
- setCheckedData(currentNode);
113
- onSelect(currentNode);
114
-
115
- }}
116
- id={id}
117
- {...props}
118
- />
283
+ <ul>
284
+ {Array.isArray(items) &&
285
+ items.map((item: { [key: string]: any }) => {
286
+ return (
287
+ <>
288
+ <li
289
+ key={item.id}
290
+ className="dropdown_item"
291
+ data-name={item.id}
292
+ >
293
+ <div className="dropdown_item_checkbox_row">
294
+ <div
295
+ key={
296
+ item.expanded ? "chevron-down" : "chevron-right"
297
+ }
298
+ >
299
+ <CircleIconButton
300
+ icon={
301
+ item.expanded ? "chevron-down" : "chevron-right"
302
+ }
303
+ className={item.children && item.children.length > 0 ? "" : "toggle_icon"}
304
+ onClick={(event) => handleToggleClick(item.id, event)}
305
+ variant="link"
306
+ />
307
+ </div>
308
+ <Checkbox text={item.label} id={item.id}>
309
+ <input
310
+ checked={item.checked}
311
+ type="checkbox"
312
+ name={item.label}
313
+ value={item.label}
314
+ onChange={(e) => {
315
+ item.checked = !item.checked;
316
+ handledropdownItemClick(e);
317
+ }}
318
+ />
319
+ </Checkbox>
320
+ </div>
321
+ {item.expanded &&
322
+ item.children &&
323
+ item.children.length > 0 &&
324
+ !filterItem && ( // Show children if expanded is true
325
+ <div>{renderNestedOptions(item.children)}</div>
326
+ )}
327
+ </li>
328
+ </>
329
+ );
330
+ })}
331
+ </ul>
119
332
  );
120
- }, [formattedData])
333
+ };
121
334
 
122
335
  return (
123
336
  <div {...ariaProps} {...dataProps} className={classes} id={id}>
124
- {returnAllSelected ? (
125
- <MultiSelectHelper
126
- treeData={formattedData}
127
- treeMode={returnAllSelected}
128
- id={id}
129
- onChange={onChange}
130
- {...props}
131
- />
132
- ) : (
133
- <>{DropDownSelectComponent}</>
134
- )}
337
+ <div ref={dropdownRef} className="wrapper">
338
+ <div className="input_wrapper" onClick={handleInputWrapperClick}>
339
+ <div className="input_inner_container">
340
+ {returnedArray.length !== 0 && returnAllSelected
341
+ ? returnedArray.map((item, index) => (
342
+ <FormPill
343
+ key={index}
344
+ text={item.label}
345
+ size="small"
346
+ onClick={(event) => handlePillClose(event, item)}
347
+ />
348
+ ))
349
+ : null}
350
+ {!returnAllSelected &&
351
+ defaultReturn.length !== 0 &&
352
+ defaultReturn
353
+ .filter(
354
+ (item, index, self) =>
355
+ index === self.findIndex((obj) => obj.id === item.id)
356
+ )
357
+ .map((item, index) => (
358
+ <FormPill
359
+ key={index}
360
+ text={item.label}
361
+ size="small"
362
+ onClick={(event) => handlePillClose(event, item)}
363
+ />
364
+ ))}
365
+ {returnedArray.length !== 0 && returnAllSelected && <br />}
366
+ {defaultReturn.length !== 0 && !returnAllSelected && <br />}
367
+ <input
368
+ id="multiselect_input"
369
+ onChange={(e) => {
370
+ setFilterItem(e.target.value);
371
+ }}
372
+ placeholder="Start typing..."
373
+ value={filterItem}
374
+ onClick={() => setIsClosed(false)}
375
+ />
376
+ </div>
377
+ {isClosed ? (
378
+ <div key="chevron-down">
379
+ <Icon icon="chevron-down" />
380
+ </div>
381
+ ) : (
382
+ <div key="chevron-up">
383
+ <Icon icon="chevron-up" />
384
+ </div>
385
+ )}
386
+ </div>
387
+ <div className={`dropdown_menu ${isClosed ? "close" : "open"}`}>
388
+ {renderNestedOptions(
389
+ filterItem ? findByFilter(formattedData, filterItem) : formattedData
390
+ )}
391
+ </div>
392
+ </div>
135
393
  </div>
136
394
  );
137
395
  };