playbook_ui 14.20.0.pre.rc.2 → 14.21.0.pre.alpha.PLAY2167advtablenitrorowborderdoubling8097

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 (115) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/pb_advanced_table/Components/CustomCell.tsx +1 -1
  3. data/app/pb_kits/playbook/pb_advanced_table/Components/RegularTableView.tsx +116 -49
  4. data/app/pb_kits/playbook/pb_advanced_table/Components/TableActionBar.tsx +61 -35
  5. data/app/pb_kits/playbook/pb_advanced_table/Components/TableHeaderCell.tsx +37 -23
  6. data/app/pb_kits/playbook/pb_advanced_table/Context/AdvancedTableContext.tsx +58 -2
  7. data/app/pb_kits/playbook/pb_advanced_table/Hooks/useTableActions.ts +1 -1
  8. data/app/pb_kits/playbook/pb_advanced_table/Hooks/useTableState.ts +16 -4
  9. data/app/pb_kits/playbook/pb_advanced_table/SubKits/TableHeader.tsx +7 -3
  10. data/app/pb_kits/playbook/pb_advanced_table/_advanced_table.scss +46 -21
  11. data/app/pb_kits/playbook/pb_advanced_table/_advanced_table.tsx +13 -3
  12. data/app/pb_kits/playbook/pb_advanced_table/advanced_table.html.erb +16 -8
  13. data/app/pb_kits/playbook/pb_advanced_table/advanced_table.rb +16 -1
  14. data/app/pb_kits/playbook/pb_advanced_table/advanced_table.test.jsx +61 -0
  15. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_beta.md +6 -2
  16. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_column_visibility_with_state.jsx +1 -0
  17. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_column_visibility_with_state.md +3 -1
  18. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_default.md +1 -1
  19. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_no_subrows.html.erb +33 -0
  20. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_no_subrows.jsx +0 -1
  21. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_pinned_rows.jsx +57 -0
  22. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_pinned_rows_react.md +5 -0
  23. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_scrollbar_none.html.erb +33 -0
  24. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_scrollbar_none.jsx +53 -0
  25. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_selectable_rows_actions_rails.html.erb +137 -0
  26. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_selectable_rows_actions_rails.md +3 -0
  27. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_selectable_rows_header_rails.html.erb +40 -0
  28. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_selectable_rows_header_rails.md +1 -0
  29. data/app/pb_kits/playbook/pb_advanced_table/docs/example.yml +8 -2
  30. data/app/pb_kits/playbook/pb_advanced_table/docs/index.js +3 -1
  31. data/app/pb_kits/playbook/pb_advanced_table/index.js +157 -12
  32. data/app/pb_kits/playbook/pb_advanced_table/table_action_bar.html.erb +23 -0
  33. data/app/pb_kits/playbook/pb_advanced_table/table_action_bar.rb +19 -0
  34. data/app/pb_kits/playbook/pb_advanced_table/table_header.rb +4 -0
  35. data/app/pb_kits/playbook/pb_checkbox/checkbox.html.erb +4 -11
  36. data/app/pb_kits/playbook/pb_checkbox/checkbox.rb +10 -6
  37. data/app/pb_kits/playbook/pb_checkbox/docs/_checkbox_indeterminate.html.erb +2 -48
  38. data/app/pb_kits/playbook/pb_checkbox/docs/_checkbox_indeterminate_rails.md +1 -0
  39. data/app/pb_kits/playbook/pb_checkbox/index.js +56 -0
  40. data/app/pb_kits/playbook/pb_date_picker/docs/_date_picker_quick_pick_date_display.html.erb +13 -0
  41. data/app/pb_kits/playbook/pb_draggable/context/index.tsx +17 -58
  42. data/app/pb_kits/playbook/pb_dropdown/_dropdown.scss +1 -1
  43. data/app/pb_kits/playbook/pb_dropdown/_dropdown.tsx +86 -19
  44. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_close_on_select.jsx +42 -0
  45. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_close_on_select.md +1 -0
  46. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_default_rails.html.erb +31 -0
  47. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_default_rails.md +5 -0
  48. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_multi_select.jsx +56 -0
  49. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_multi_select.md +3 -0
  50. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_multi_select_display.jsx +58 -0
  51. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_multi_select_display.md +3 -0
  52. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_multi_select_display_rails.html.erb +20 -0
  53. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_multi_select_display_rails.md +1 -0
  54. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_multi_select_rails.html.erb +19 -0
  55. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_multi_select_rails.md +3 -0
  56. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_multi_select_with_autocomplete.html.erb +20 -0
  57. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_multi_select_with_autocomplete.jsx +57 -0
  58. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_multi_select_with_autocomplete.md +1 -0
  59. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_multi_select_with_custom_options.html.erb +50 -0
  60. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_multi_select_with_custom_options.jsx +105 -0
  61. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_multi_select_with_default.html.erb +22 -0
  62. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_multi_select_with_default.jsx +67 -0
  63. data/app/pb_kits/playbook/pb_dropdown/docs/example.yml +13 -1
  64. data/app/pb_kits/playbook/pb_dropdown/docs/index.js +6 -0
  65. data/app/pb_kits/playbook/pb_dropdown/dropdown.html.erb +3 -3
  66. data/app/pb_kits/playbook/pb_dropdown/dropdown.rb +16 -2
  67. data/app/pb_kits/playbook/pb_dropdown/dropdown.test.jsx +108 -2
  68. data/app/pb_kits/playbook/pb_dropdown/dropdown_trigger.html.erb +34 -13
  69. data/app/pb_kits/playbook/pb_dropdown/dropdown_trigger.rb +3 -1
  70. data/app/pb_kits/playbook/pb_dropdown/hooks/useHandleOnKeydown.tsx +0 -6
  71. data/app/pb_kits/playbook/pb_dropdown/index.js +357 -40
  72. data/app/pb_kits/playbook/pb_dropdown/keyboard_accessibility.js +39 -12
  73. data/app/pb_kits/playbook/pb_dropdown/subcomponents/DropdownOption.tsx +26 -18
  74. data/app/pb_kits/playbook/pb_dropdown/subcomponents/DropdownTrigger.tsx +96 -19
  75. data/app/pb_kits/playbook/pb_dropdown/subcomponents/MultiSelectTriggerDisplay.tsx +58 -0
  76. data/app/pb_kits/playbook/pb_form/docs/_form_form_with.html.erb +1 -0
  77. data/app/pb_kits/playbook/pb_form/docs/_form_form_with_validate.html.erb +1 -0
  78. data/app/pb_kits/playbook/pb_phone_number_input/_phone_number_input.tsx +4 -0
  79. data/app/pb_kits/playbook/pb_phone_number_input/docs/_phone_number_input_exclude_countries.html.erb +4 -0
  80. data/app/pb_kits/playbook/pb_phone_number_input/docs/_phone_number_input_exclude_countries.jsx +15 -0
  81. data/app/pb_kits/playbook/pb_phone_number_input/docs/_phone_number_input_exclude_countries.md +1 -0
  82. data/app/pb_kits/playbook/pb_phone_number_input/docs/_phone_number_input_only_countries.jsx +1 -1
  83. data/app/pb_kits/playbook/pb_phone_number_input/docs/example.yml +4 -3
  84. data/app/pb_kits/playbook/pb_phone_number_input/docs/index.js +1 -0
  85. data/app/pb_kits/playbook/pb_phone_number_input/phone_number_input.rb +3 -0
  86. data/app/pb_kits/playbook/pb_popover/index.ts +9 -4
  87. data/app/pb_kits/playbook/pb_select/docs/_select_custom_select_subheaders.html.erb +12 -0
  88. data/app/pb_kits/playbook/pb_select/docs/_select_custom_select_subheaders.jsx +31 -0
  89. data/app/pb_kits/playbook/pb_select/docs/_select_custom_select_subheaders.md +1 -0
  90. data/app/pb_kits/playbook/pb_select/docs/example.yml +2 -0
  91. data/app/pb_kits/playbook/pb_select/docs/index.js +1 -0
  92. data/app/pb_kits/playbook/pb_table/docs/_table_with_selectable_rows.html.erb +3 -51
  93. data/app/pb_kits/playbook/pb_table/styles/_mobile_collapse.scss +1 -1
  94. data/app/pb_kits/playbook/pb_typeahead/_typeahead.tsx +73 -3
  95. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_preserve_input.jsx +23 -0
  96. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_preserve_input.md +1 -0
  97. data/app/pb_kits/playbook/pb_typeahead/docs/example.yml +1 -0
  98. data/app/pb_kits/playbook/pb_typeahead/docs/index.js +1 -0
  99. data/dist/chunks/_typeahead-CoOpeYom.js +22 -0
  100. data/dist/chunks/_weekday_stacked-B_jpa2Rz.js +45 -0
  101. data/dist/chunks/lib-D7Va7yqa.js +29 -0
  102. data/dist/chunks/{pb_form_validation-WWvUXPKD.js → pb_form_validation-DSkdRDMf.js} +1 -1
  103. data/dist/chunks/vendor.js +1 -1
  104. data/dist/menu.yml +1 -1
  105. data/dist/playbook-doc.js +2 -2
  106. data/dist/playbook-rails-react-bindings.js +1 -1
  107. data/dist/playbook-rails.js +1 -1
  108. data/dist/playbook.css +1 -1
  109. data/lib/playbook/kit_base.rb +3 -3
  110. data/lib/playbook/version.rb +2 -2
  111. metadata +48 -7
  112. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_default.html.erb +0 -10
  113. data/dist/chunks/_typeahead-B9-s4j4U.js +0 -22
  114. data/dist/chunks/_weekday_stacked-CvzpmXD5.js +0 -45
  115. data/dist/chunks/lib-B20MXZcW.js +0 -29
@@ -22,12 +22,6 @@ const {
22
22
  }
23
23
 
24
24
  switch (e.key) {
25
- case "Backspace":
26
- case "Delete":
27
- if (autocomplete) {
28
- handleBackspace();
29
- }
30
- break;
31
25
  case "ArrowDown": {
32
26
  e.preventDefault();
33
27
  setIsDropDownClosed(false);
@@ -13,6 +13,7 @@ const DROPDOWN_PLACEHOLDER = "[data-dropdown-placeholder]";
13
13
  const DROPDOWN_INPUT = "#dropdown-selected-option";
14
14
  const SEARCH_INPUT_SELECTOR = "[data-dropdown-autocomplete]";
15
15
  const SEARCH_BAR_SELECTOR = "[data-dropdown-search]";
16
+ const CLEAR_ICON_SELECTOR = "#dropdown_clear_icon";
16
17
 
17
18
  export default class PbDropdown extends PbEnhancedElement {
18
19
  static get selector() {
@@ -20,11 +21,18 @@ export default class PbDropdown extends PbEnhancedElement {
20
21
  }
21
22
 
22
23
  get target() {
23
- return this.element.parentNode.querySelector(CONTAINER_SELECTOR);
24
+ return this.element.querySelector(CONTAINER_SELECTOR);
24
25
  }
25
26
 
27
+ selectedOptions = new Set();
28
+ clearBtn = null;
29
+
26
30
  connect() {
27
31
  this.keyboardHandler = new PbDropdownKeyboard(this);
32
+ this.isMultiSelect = this.element.dataset.pbDropdownMultiSelect === "true";
33
+ this.formPillProps = this.element.dataset.formPillProps
34
+ ? JSON.parse(this.element.dataset.formPillProps)
35
+ : {};
28
36
  this.setDefaultValue();
29
37
  this.bindEventListeners();
30
38
  this.bindSearchInput();
@@ -32,6 +40,26 @@ export default class PbDropdown extends PbEnhancedElement {
32
40
  this.handleFormValidation();
33
41
  this.handleFormReset();
34
42
  this.bindSearchBar();
43
+ this.updatePills();
44
+
45
+ this.clearBtn = this.element.querySelector(CLEAR_ICON_SELECTOR);
46
+ if (this.clearBtn) {
47
+ this.clearBtn.style.display = "none";
48
+ this.clearBtn.addEventListener("click", (e) => {
49
+ e.stopPropagation();
50
+ this.clearSelection();
51
+ });
52
+ }
53
+ this.updateClearButton();
54
+ }
55
+
56
+ updateClearButton() {
57
+ if (!this.clearBtn) return;
58
+ const hasSelection = this.isMultiSelect
59
+ ? this.selectedOptions.size > 0
60
+ : Boolean(this.element.querySelector(DROPDOWN_INPUT).value);
61
+
62
+ this.clearBtn.style.display = hasSelection ? "" : "none";
35
63
  }
36
64
 
37
65
  bindEventListeners() {
@@ -52,7 +80,7 @@ export default class PbDropdown extends PbEnhancedElement {
52
80
  bindSearchBar() {
53
81
  this.searchBar = this.element.querySelector(SEARCH_BAR_SELECTOR);
54
82
  if (!this.searchBar) return;
55
-
83
+
56
84
  this.searchBar.addEventListener("input", (e) =>
57
85
  this.handleSearch(e.target.value)
58
86
  );
@@ -73,26 +101,59 @@ export default class PbDropdown extends PbEnhancedElement {
73
101
  );
74
102
  }
75
103
 
104
+ adjustDropdownHeight() {
105
+ if (this.target.classList.contains("open")) {
106
+ const el = this.target;
107
+ el.style.height = "auto";
108
+ requestAnimationFrame(() => {
109
+ const newHeight = el.scrollHeight + "px";
110
+ el.offsetHeight; // force reflow
111
+ el.style.height = newHeight;
112
+ });
113
+ }
114
+ }
115
+
76
116
  handleSearch(term = "") {
77
117
  const lcTerm = term.toLowerCase();
118
+ let hasMatch = false
78
119
  this.element.querySelectorAll(OPTION_SELECTOR).forEach((opt) => {
79
- const label = JSON.parse(opt.dataset.dropdownOptionLabel).label
80
- .toString()
120
+ //make it so that if the option is selected, it will not show up in the search results
121
+ if (this.isMultiSelect && this.selectedOptions.has(opt.dataset.dropdownOptionLabel)) {
122
+ opt.style.display = "none";
123
+ return;
124
+ }
125
+ const label = JSON.parse(opt.dataset.dropdownOptionLabel)
126
+ .label.toString()
81
127
  .toLowerCase();
82
128
 
83
129
  // hide or show option
84
130
  const match = label.includes(lcTerm);
85
131
  opt.style.display = match ? "" : "none";
132
+ if (match) hasMatch = true
86
133
  });
87
134
 
88
- if (this.target.classList.contains("open")) {
89
- const el = this.target;
90
- el.style.height = "auto";
91
- requestAnimationFrame(() => {
92
- const newHeight = el.scrollHeight + "px";
93
- el.offsetHeight; // force reflow
94
- el.style.height = newHeight;
95
- });
135
+ this.adjustDropdownHeight();
136
+
137
+ this.removeNoOptionsMessage()
138
+ if (!hasMatch) {
139
+ this.showNoOptionsMessage()
140
+ }
141
+ }
142
+
143
+ showNoOptionsMessage() {
144
+ if (this.element.querySelector(".dropdown_no_options")) return;
145
+
146
+ const noOptionElement = document.createElement("div");
147
+ noOptionElement.className = "pb_body_kit_light dropdown_no_options pb_item_kit p_xs display_flex justify_content_center";
148
+ noOptionElement.textContent = "no option";
149
+
150
+ this.target.appendChild(noOptionElement);
151
+ }
152
+
153
+ removeNoOptionsMessage() {
154
+ const existing = this.element.querySelector(".dropdown_no_options");
155
+ if (existing) {
156
+ existing.remove();
96
157
  }
97
158
  }
98
159
 
@@ -102,10 +163,26 @@ export default class PbDropdown extends PbEnhancedElement {
102
163
 
103
164
  if (option) {
104
165
  const value = option.dataset.dropdownOptionLabel;
105
- hiddenInput.value = JSON.parse(value).id;
106
- this.clearFormValidation(hiddenInput);
166
+ if (this.isMultiSelect) {
167
+ const alreadySelected = this.selectedOptions.has(value);
168
+ if (alreadySelected) {
169
+ this.selectedOptions.delete(value);
170
+ } else {
171
+ this.selectedOptions.add(value);
172
+ }
173
+ this.updatePills();
174
+ this.syncHiddenInputs();
175
+ if (this.searchInput && this.isMultiSelect) {
176
+ this.searchInput.value = "";
177
+ this.handleBackspaceClear();
178
+ }
179
+ } else {
180
+ hiddenInput.value = JSON.parse(value).id;
181
+ }
107
182
 
183
+ this.clearFormValidation(hiddenInput);
108
184
  this.onOptionSelected(value, option);
185
+ this.updateClearButton();
109
186
  }
110
187
  }
111
188
 
@@ -137,6 +214,31 @@ export default class PbDropdown extends PbEnhancedElement {
137
214
  }
138
215
  }
139
216
 
217
+ emitSelectionChange() {
218
+ let detail;
219
+
220
+ if (this.isMultiSelect) {
221
+ detail = Array.from(this.selectedOptions).map(JSON.parse);
222
+ } else {
223
+ const hiddenInput = this.element.querySelector(DROPDOWN_INPUT);
224
+ detail = hiddenInput.value
225
+ ? JSON.parse(
226
+ this.element.querySelector(
227
+ OPTION_SELECTOR +
228
+ `[data-dropdown-option-label*='"id":"${hiddenInput.value}"']`
229
+ ).dataset.dropdownOptionLabel
230
+ )
231
+ : null;
232
+ }
233
+ this.element.setAttribute("data-option-selected", JSON.stringify(detail));
234
+ this.element.dispatchEvent(
235
+ new CustomEvent("pb:dropdown:selected", {
236
+ detail,
237
+ bubbles: true,
238
+ })
239
+ );
240
+ }
241
+
140
242
  onOptionSelected(value, selectedOption) {
141
243
  const triggerElement = this.element.querySelector(DROPDOWN_TRIGGER_DISPLAY);
142
244
  const customDisplayElement = this.element.querySelector(
@@ -144,29 +246,22 @@ export default class PbDropdown extends PbEnhancedElement {
144
246
  );
145
247
 
146
248
  if (triggerElement) {
147
- const selectedLabel = JSON.parse(value).label;
148
- if (customDisplayElement) {
149
- triggerElement.textContent = ""
150
- this.element.setAttribute("data-option-selected", value);
151
- const selectedObj = JSON.parse(value);
152
- this.element.dispatchEvent(
153
- new CustomEvent("pb:dropdown:selected", {
154
- detail: selectedObj,
155
- bubbles: true,
156
- })
157
- );
158
- } else {
159
- triggerElement.textContent = selectedLabel
249
+ if (!this.isMultiSelect) {
250
+ const selectedLabel = JSON.parse(value).label;
251
+ triggerElement.textContent = selectedLabel;
252
+ this.emitSelectionChange();
160
253
  }
161
254
  if (customDisplayElement) {
255
+ triggerElement.textContent = "";
162
256
  customDisplayElement.style.display = "block";
163
257
  customDisplayElement.style.paddingRight = "8px";
164
258
  }
165
259
  }
166
260
 
167
261
  const autocompleteInput = this.element.querySelector(SEARCH_INPUT_SELECTOR);
168
- if (autocompleteInput){
262
+ if (autocompleteInput && !this.isMultiSelect) {
169
263
  autocompleteInput.value = JSON.parse(value).label;
264
+ this.emitSelectionChange();
170
265
  }
171
266
 
172
267
  const customTrigger = this.element.querySelector(CUSTOM_DISPLAY_SELECTOR);
@@ -178,10 +273,24 @@ export default class PbDropdown extends PbEnhancedElement {
178
273
  }
179
274
 
180
275
  const options = this.element.querySelectorAll(OPTION_SELECTOR);
181
- options.forEach((option) => {
182
- option.classList.remove("pb_dropdown_option_selected");
183
- });
184
- selectedOption.classList.add("pb_dropdown_option_selected");
276
+ if (this.isMultiSelect) {
277
+ this.emitSelectionChange();
278
+ Array.from(this.selectedOptions).map((option) => {
279
+ if (
280
+ JSON.parse(option).id ===
281
+ JSON.parse(selectedOption.dataset.dropdownOptionLabel).id
282
+ ) {
283
+ selectedOption.style.display = "none";
284
+ this.adjustDropdownHeight();
285
+ }
286
+ });
287
+ } else {
288
+ options.forEach((option) => {
289
+ option.classList.remove("pb_dropdown_option_selected");
290
+ });
291
+ selectedOption.classList.add("pb_dropdown_option_selected");
292
+ }
293
+ this.updateClearButton();
185
294
  }
186
295
 
187
296
  showElement(elem) {
@@ -255,21 +364,66 @@ export default class PbDropdown extends PbEnhancedElement {
255
364
  errorLabelElement.remove();
256
365
  }
257
366
  }
367
+ if (this.isMultiSelect) {
368
+ if (this.selectedOptions.size > 0) {
369
+ const dropdownWrapperElement = input.closest(".dropdown_wrapper");
370
+ dropdownWrapperElement.classList.remove("error");
371
+ const errorLabelElement = dropdownWrapperElement.querySelector(
372
+ ".pb_body_kit_negative"
373
+ );
374
+ if (errorLabelElement) {
375
+ errorLabelElement.remove();
376
+ }
377
+ }
378
+ }
258
379
  }
259
380
 
260
381
  setDefaultValue() {
261
382
  const hiddenInput = this.element.querySelector(DROPDOWN_INPUT);
262
- const options = this.element.querySelectorAll(OPTION_SELECTOR);
263
-
383
+ const optionEls = Array.from(
384
+ this.element.querySelectorAll(OPTION_SELECTOR)
385
+ );
264
386
  const defaultValue = hiddenInput.dataset.defaultValue || "";
265
- hiddenInput.value = defaultValue;
387
+ if (!defaultValue) return;
388
+
389
+ if (this.isMultiSelect) {
390
+ const ids = defaultValue.split(",");
391
+ ids.forEach((id) => {
392
+ const selectedOption = optionEls.find((opt) => {
393
+ try {
394
+ return JSON.parse(opt.dataset.dropdownOptionLabel).id === id;
395
+ } catch {
396
+ return false;
397
+ }
398
+ });
399
+ if (!selectedOption) {
400
+ console.warn(`Dropdown default ID ${id} not found`);
401
+ return;
402
+ }
266
403
 
267
- if (defaultValue) {
268
- const selectedOption = Array.from(options).find((option) => {
269
- return (
270
- JSON.parse(option.dataset.dropdownOptionLabel).id === defaultValue
271
- );
404
+ const raw = selectedOption.dataset.dropdownOptionLabel;
405
+ this.selectedOptions.add(raw);
406
+
407
+ selectedOption.style.display = "none";
272
408
  });
409
+
410
+ this.updatePills();
411
+ this.updateClearButton();
412
+ this.adjustDropdownHeight();
413
+ this.syncHiddenInputs();
414
+ } else {
415
+ hiddenInput.value = defaultValue;
416
+ const selectedOption = optionEls.find((opt) => {
417
+ try {
418
+ return (
419
+ JSON.parse(opt.dataset.dropdownOptionLabel).id === defaultValue
420
+ );
421
+ } catch {
422
+ return false;
423
+ }
424
+ });
425
+ if (!selectedOption) return;
426
+
273
427
  selectedOption.classList.add("pb_dropdown_option_selected");
274
428
  this.setTriggerElementText(
275
429
  JSON.parse(selectedOption.dataset.dropdownOptionLabel).label
@@ -292,12 +446,32 @@ export default class PbDropdown extends PbEnhancedElement {
292
446
  const options = this.element.querySelectorAll(OPTION_SELECTOR);
293
447
  options.forEach((option) => {
294
448
  option.classList.remove("pb_dropdown_option_selected");
449
+ option.style.display = "";
295
450
  });
296
451
 
297
452
  hiddenInput.value = "";
298
453
 
299
454
  const defaultPlaceholder = this.element.querySelector(DROPDOWN_PLACEHOLDER);
300
455
  this.setTriggerElementText(defaultPlaceholder.dataset.dropdownPlaceholder);
456
+
457
+ if (this.searchInput) {
458
+ this.searchInput.value = "";
459
+ if (this.target.classList.contains("open")) {
460
+ const el = this.target;
461
+ el.style.height = "auto";
462
+ requestAnimationFrame(() => {
463
+ const newHeight = el.scrollHeight + "px";
464
+ el.offsetHeight; // force reflow
465
+ el.style.height = newHeight;
466
+ });
467
+ }
468
+ }
469
+ if (this.isMultiSelect) {
470
+ this.selectedOptions.clear();
471
+ this.updatePills();
472
+ this.updateClearButton();
473
+ this.syncHiddenInputs();
474
+ }
301
475
  }
302
476
 
303
477
  setTriggerElementText(text) {
@@ -306,4 +480,147 @@ export default class PbDropdown extends PbEnhancedElement {
306
480
  triggerElement.textContent = text;
307
481
  }
308
482
  }
483
+
484
+ updatePills() {
485
+ if (!this.isMultiSelect) return;
486
+
487
+ const wrapper = this.element.querySelector("#dropdown_pills_wrapper");
488
+ const placeholder = this.element.querySelector(
489
+ "#dropdown_trigger_display_multi_select"
490
+ );
491
+ if (!wrapper) return;
492
+
493
+ wrapper.innerHTML = "";
494
+ // Show or hide the placeholder based on selected options
495
+ if (placeholder) {
496
+ if (this.selectedOptions.size > 0) {
497
+ placeholder.style.display = "none";
498
+ } else {
499
+ placeholder.style.display = "";
500
+ }
501
+ }
502
+
503
+ Array.from(this.selectedOptions).map((option) => {
504
+ // Create a form pill for each selected option
505
+ const pill = document.createElement("div");
506
+ const color = this.formPillProps.color || "primary";
507
+ pill.classList.add(`pb_form_pill_kit_${color}`, "mr_xs");
508
+ if (this.formPillProps.size === "small") {
509
+ pill.classList.add("small");
510
+ }
511
+ pill.tabIndex = 0;
512
+ pill.dataset.pillId = JSON.parse(option).id;
513
+ const innerDiv = document.createElement("h3");
514
+ innerDiv.className = "pb_title_kit_size_4 pb_form_pill_text";
515
+ innerDiv.textContent = JSON.parse(option).label;
516
+ pill.appendChild(innerDiv);
517
+
518
+ const closeIcon = document.createElement("div");
519
+ closeIcon.className = "pb_form_pill_close";
520
+ closeIcon.innerHTML = `<svg class="pb_custom_icon svg-inline--fa svg_${
521
+ this.formPillProps.size === "small" ? "xs" : "sm"
522
+ } svg_fw" xmlns="http://www.w3.org/2000/svg" width="auto" height="auto" viewBox="0 0 31 25"><path fill="currentColor" d="M23.0762 6.77734L17.4512 12.4023L23.0293 17.9805C23.498 18.4023 23.498 19.1055 23.0293 19.5273C22.6074 19.9961 21.9043 19.9961 21.4824 19.5273L15.8574 13.9492L10.2793 19.5273C9.85742 19.9961 9.1543 19.9961 8.73242 19.5273C8.26367 19.1055 8.26367 18.4023 8.73242 17.9336L14.3105 12.3555L8.73242 6.77734C8.26367 6.35547 8.26367 5.65234 8.73242 5.18359C9.1543 4.76172 9.85742 4.76172 10.3262 5.18359L15.9043 10.8086L21.4824 5.23047C21.9043 4.76172 22.6074 4.76172 23.0762 5.23047C23.498 5.65234 23.498 6.35547 23.0762 6.77734Z"/></svg>`;
523
+ pill.appendChild(closeIcon);
524
+
525
+ closeIcon.addEventListener("click", (e) => {
526
+ e.stopPropagation();
527
+ const id = pill.dataset.pillId;
528
+ this.selectedOptions.delete(option);
529
+
530
+ const optEl = this.element.querySelector(
531
+ `${OPTION_SELECTOR}[data-dropdown-option-label*='"id":${JSON.stringify(
532
+ id
533
+ )}']`
534
+ );
535
+ if (optEl) {
536
+ optEl.style.display = "";
537
+ if (this.target.classList.contains("open")) {
538
+ this.showElement(this.target);
539
+ }
540
+ }
541
+
542
+ this.updatePills();
543
+ this.updateClearButton();
544
+ this.emitSelectionChange();
545
+ this.syncHiddenInputs();
546
+ });
547
+ wrapper.appendChild(pill);
548
+ });
549
+ }
550
+
551
+ clearSelection() {
552
+ if (this.isMultiSelect) {
553
+ this.selectedOptions.clear();
554
+ this.element.querySelectorAll(OPTION_SELECTOR).forEach((opt) => {
555
+ opt.style.display = "";
556
+ });
557
+ if (this.target.classList.contains("open")) {
558
+ this.showElement(this.target);
559
+ }
560
+ }
561
+ const customDisplay = this.element.querySelector(
562
+ "#dropdown_trigger_custom_display"
563
+ );
564
+ if (customDisplay) {
565
+ customDisplay.style.display = "none";
566
+ }
567
+ this.resetDropdownValue();
568
+ this.updatePills();
569
+ this.updateClearButton();
570
+ this.syncHiddenInputs();
571
+ this.emitSelectionChange();
572
+ }
573
+
574
+ syncHiddenInputs() {
575
+ if (!this.isMultiSelect) return;
576
+ this.element
577
+ .querySelectorAll('input[data-generated="true"]')
578
+ .forEach((n) => n.remove());
579
+
580
+ const baseInput = this.element.querySelector(DROPDOWN_INPUT);
581
+ if (!baseInput) return;
582
+ // for multi_select, for each selectedOption, create a hidden input
583
+ const name = baseInput.getAttribute("name");
584
+ this.selectedOptions.forEach((raw) => {
585
+ const id = JSON.parse(raw).id;
586
+ const inp = document.createElement("input");
587
+ inp.type = "hidden";
588
+ inp.name = name;
589
+ inp.value = id;
590
+ inp.dataset.generated = "true";
591
+ baseInput.insertAdjacentElement("afterend", inp);
592
+ });
593
+ baseInput.value = "";
594
+ }
595
+
596
+ handleBackspaceClear() {
597
+ if (!this.isMultiSelect) {
598
+ this.element.querySelectorAll(OPTION_SELECTOR).forEach((opt) => {
599
+ opt.classList.remove("pb_dropdown_option_selected");
600
+ opt.style.display = "";
601
+ this.adjustDropdownHeight();
602
+ });
603
+
604
+ const hiddenInput = this.element.querySelector(DROPDOWN_INPUT);
605
+ if (hiddenInput) hiddenInput.value = "";
606
+
607
+ const placeholder = this.element.querySelector(DROPDOWN_PLACEHOLDER);
608
+ if (placeholder)
609
+ this.setTriggerElementText(placeholder.dataset.dropdownPlaceholder);
610
+ }
611
+ if (this.isMultiSelect) {
612
+ this.element.querySelectorAll(OPTION_SELECTOR).forEach((opt) => {
613
+ const optValue = opt.dataset.dropdownOptionLabel;
614
+ if (
615
+ this.selectedOptions.size > 0 &&
616
+ this.selectedOptions.has(optValue)
617
+ ) {
618
+ opt.style.display = "none";
619
+ } else {
620
+ opt.style.display = "";
621
+ }
622
+ this.adjustDropdownHeight();
623
+ });
624
+ }
625
+ }
309
626
  }
@@ -27,6 +27,13 @@ export class PbDropdownKeyboard {
27
27
  }
28
28
  }
29
29
 
30
+ getVisibleOptions() {
31
+ // We only want to return the options that are visible
32
+ return Array.from(
33
+ this.dropdownElement.querySelectorAll(OPTION_SELECTOR)
34
+ ).filter((opt) => opt.style.display !== "none");
35
+ }
36
+
30
37
  openDropdownIfClosed() {
31
38
  if (!this.dropdown.target.classList.contains("open")) {
32
39
  this.dropdown.showElement(this.dropdown.target);
@@ -71,7 +78,7 @@ export class PbDropdownKeyboard {
71
78
  if (this.searchInput) {
72
79
  setTimeout(() => {
73
80
  if (this.searchInput.value.trim() === "") {
74
- this.dropdown.resetDropdownValue();
81
+ this.dropdown.handleBackspaceClear();
75
82
  }
76
83
  }, 0);
77
84
  }
@@ -81,23 +88,43 @@ export class PbDropdownKeyboard {
81
88
  }
82
89
  }
83
90
 
84
- moveFocus(direction) {
91
+ moveFocus(direction) {
92
+ const allOptions = Array.from(
93
+ this.dropdownElement.querySelectorAll(OPTION_SELECTOR)
94
+ );
95
+ const visible = this.getVisibleOptions();
96
+ if (!visible.length) return;
97
+
85
98
  if (this.focusedOptionIndex !== -1) {
86
- this.options[this.focusedOptionIndex].classList.remove(
99
+ allOptions[this.focusedOptionIndex].classList.remove(
87
100
  "pb_dropdown_option_focused"
88
101
  );
89
102
  }
90
- this.focusedOptionIndex =
91
- (this.focusedOptionIndex + direction + this.options.length) %
92
- this.options.length;
93
- this.options[this.focusedOptionIndex].classList.add(
94
- "pb_dropdown_option_focused"
95
- );
103
+
104
+ const prevVisibleIndex =
105
+ this.focusedOptionIndex === -1
106
+ ? -1
107
+ : visible.indexOf(allOptions[this.focusedOptionIndex]);
108
+
109
+ const nextVisibleIndex =
110
+ (prevVisibleIndex + direction + visible.length) % visible.length;
111
+
112
+ const nextEl = visible[nextVisibleIndex];
113
+ nextEl.classList.add("pb_dropdown_option_focused");
114
+
115
+ this.focusedOptionIndex = allOptions.indexOf(nextEl);
96
116
  }
97
117
 
118
+
98
119
  selectOption() {
99
- const option = this.options[this.focusedOptionIndex];
100
- this.dropdown.onOptionSelected(option.dataset.dropdownOptionLabel, option);
101
- this.dropdown.hideElement(this.dropdown.target);
120
+ const allOptions = Array.from(
121
+ this.dropdownElement.querySelectorAll(OPTION_SELECTOR)
122
+ );
123
+ if (this.focusedOptionIndex < 0) return;
124
+
125
+ const optionEl = allOptions[this.focusedOptionIndex];
126
+ this.dropdown.handleOptionClick({ target: optionEl });
127
+ this.dropdown.toggleElement(this.dropdown.target);
128
+ this.dropdown.updateClearButton();
102
129
  }
103
130
  }
@@ -25,7 +25,7 @@ type DropdownOptionProps = {
25
25
  key?: string | number;
26
26
  option?: GenericObject;
27
27
  padding?: string;
28
- } & GlobalProps;
28
+ } & GlobalProps;
29
29
 
30
30
  const DropdownOption = (props: DropdownOptionProps) => {
31
31
  const {
@@ -45,27 +45,32 @@ const DropdownOption = (props: DropdownOptionProps) => {
45
45
  filterItem,
46
46
  focusedOptionIndex,
47
47
  handleOptionClick,
48
+ multiSelect,
48
49
  selected,
49
50
  } = useContext(DropdownContext);
50
51
 
51
- const isItemMatchingFilter = (option: GenericObject) => {
52
- const label = typeof option.label === 'string' ? option.label.toLowerCase() : option.label;
52
+ const isItemMatchingFilter = (option: GenericObject | undefined) => {
53
+ const label = typeof option?.label === 'string' ? option.label.toLowerCase() : option?.label;
53
54
  return String(label).toLowerCase().includes(filterItem.toLowerCase());
54
55
  }
55
56
 
56
- if (!isItemMatchingFilter(option)) {
57
+ // When multiSelect, then if an option is selected, remove from dropdown
58
+ const isSelected = Array.isArray(selected)
59
+ ? selected.some((item) => item.label === option?.label)
60
+ : (selected as GenericObject)?.label === option?.label;
61
+
62
+ if (!isItemMatchingFilter(option) || (multiSelect && isSelected)) {
57
63
  return null;
58
64
  }
65
+
59
66
  const isFocused =
60
67
  focusedOptionIndex >= 0 &&
61
- filteredOptions[focusedOptionIndex].label === option.label;
68
+ filteredOptions[focusedOptionIndex].label === option?.label;
69
+
62
70
  const focusedClass = isFocused && "focused";
63
71
 
64
- const selectedClass = `${
65
- selected?.label === option.label
66
- ? "selected"
67
- : "list"
68
- }`;
72
+ const selectedClass = isSelected ? "selected" : "list";
73
+
69
74
  const ariaProps = buildAriaProps(aria);
70
75
  const dataProps = buildDataProps(data);
71
76
  const htmlProps = buildHtmlProps(htmlOptions);
@@ -87,21 +92,24 @@ const DropdownOption = (props: DropdownOptionProps) => {
87
92
  className={classes}
88
93
  id={id}
89
94
  key={key}
90
- onClick= {() => handleOptionClick(option)}
95
+ onClick={(e) => {
96
+ e.stopPropagation();
97
+ handleOptionClick(option);
98
+ }}
91
99
  >
92
100
  <ListItem
93
101
  cursor="pointer"
94
102
  dark={dark}
95
- data-name={option.value}
96
- key={option.label}
103
+ data-name={option?.value}
104
+ key={option?.label}
97
105
  padding="none"
98
106
  >
99
- {children ?
107
+ {children ?
100
108
  <div className="dropdown_option_wrapper">{children}</div> :
101
- <Body dark={dark}
102
- text={option.label}
103
- />
104
- }
109
+ <Body dark={dark}
110
+ text={option?.label}
111
+ />
112
+ }
105
113
  </ListItem>
106
114
  </div>
107
115
  );