playbook_ui 12.25.0.pre.alpha.play822bolddefaultfortitle3764 → 12.25.0.pre.alpha.play824786

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 (55) 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/pb_phone_number_input/_phone_number_input.tsx +109 -44
  33. data/app/pb_kits/playbook/pb_phone_number_input/docs/_phone_number_input_validation.html.erb +14 -0
  34. data/app/pb_kits/playbook/pb_phone_number_input/docs/_phone_number_input_validation.jsx +60 -0
  35. data/app/pb_kits/playbook/pb_phone_number_input/docs/example.yml +3 -1
  36. data/app/pb_kits/playbook/pb_phone_number_input/docs/index.js +1 -0
  37. data/app/pb_kits/playbook/pb_phone_number_input/phone_number_input.rb +6 -0
  38. data/app/pb_kits/playbook/pb_phone_number_input/phone_number_input.test.js +110 -62
  39. data/app/pb_kits/playbook/pb_title/_title.scss +1 -1
  40. data/app/pb_kits/playbook/pb_title/_title.tsx +3 -2
  41. data/app/pb_kits/playbook/pb_title/docs/_title_light_weight.html.erb +0 -1
  42. data/app/pb_kits/playbook/pb_title/docs/_title_light_weight.jsx +0 -6
  43. data/app/pb_kits/playbook/pb_title/docs/_title_light_weight.md +2 -1
  44. data/app/pb_kits/playbook/pb_title/title.rb +10 -3
  45. data/app/pb_kits/playbook/pb_title/title.test.js +3 -3
  46. data/app/pb_kits/playbook/playbook-doc.js +2 -0
  47. data/app/pb_kits/playbook/utilities/object.ts +3 -0
  48. data/dist/menu.yml +1 -0
  49. data/dist/playbook-rails.js +7 -7
  50. data/lib/playbook/forms/builder/intl_telephone_field.rb +12 -0
  51. data/lib/playbook/forms/builder.rb +1 -0
  52. data/lib/playbook/version.rb +1 -1
  53. metadata +26 -4
  54. data/app/pb_kits/playbook/pb_multi_level_select/_multi_select_helper.tsx +0 -31
  55. 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
  },