playbook_ui 12.25.0 → 12.26.0

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