playbook_ui 16.1.0.pre.alpha.play264213818 → 16.1.0.pre.alpha.play276813969

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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/pb_card/docs/_card_light.html.erb +3 -35
  3. data/app/pb_kits/playbook/pb_dialog/_dialog.scss +8 -6
  4. data/app/pb_kits/playbook/pb_dropdown/_dropdown.scss +6 -0
  5. data/app/pb_kits/playbook/pb_dropdown/_dropdown.tsx +83 -13
  6. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_blank_selection_rails.md +3 -0
  7. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_blank_selection_react.md +3 -0
  8. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_clearable.html.erb +52 -0
  9. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_clearable.jsx +72 -0
  10. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_clearable.md +5 -0
  11. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_constrain_height.jsx +33 -0
  12. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_constrain_height_rails.html.erb +20 -0
  13. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_constrain_height_rails.md +8 -0
  14. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_constrain_height_react.md +8 -0
  15. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_label.html.erb +6 -3
  16. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_label.jsx +1 -0
  17. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_label.md +3 -1
  18. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_placeholder.html.erb +9 -0
  19. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_placeholder.jsx +33 -0
  20. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_placeholder.md +3 -0
  21. data/app/pb_kits/playbook/pb_dropdown/docs/example.yml +6 -0
  22. data/app/pb_kits/playbook/pb_dropdown/docs/index.js +4 -1
  23. data/app/pb_kits/playbook/pb_dropdown/dropdown.html.erb +11 -5
  24. data/app/pb_kits/playbook/pb_dropdown/dropdown.rb +15 -0
  25. data/app/pb_kits/playbook/pb_dropdown/dropdown.test.jsx +94 -0
  26. data/app/pb_kits/playbook/pb_dropdown/dropdown_container.rb +5 -1
  27. data/app/pb_kits/playbook/pb_dropdown/dropdown_trigger.html.erb +7 -2
  28. data/app/pb_kits/playbook/pb_dropdown/dropdown_trigger.rb +4 -0
  29. data/app/pb_kits/playbook/pb_dropdown/index.js +184 -77
  30. data/app/pb_kits/playbook/pb_dropdown/subcomponents/DropdownContainer.tsx +3 -0
  31. data/app/pb_kits/playbook/pb_dropdown/subcomponents/DropdownTrigger.tsx +18 -1
  32. data/app/pb_kits/playbook/pb_dropdown/utilities/clickOutsideHelper.tsx +6 -0
  33. data/app/pb_kits/playbook/pb_filter/Filter/SortMenu.tsx +1 -1
  34. data/app/pb_kits/playbook/pb_filter/docs/_filter_default.html.erb +2 -2
  35. data/app/pb_kits/playbook/pb_filter/docs/_filter_default.jsx +16 -9
  36. data/app/pb_kits/playbook/pb_filter/filter.rb +2 -2
  37. data/app/pb_kits/playbook/pb_form/docs/_form_with_required_indicator.html.erb +1 -0
  38. data/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_truncated_text.html.erb +5 -5
  39. data/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_truncated_text.jsx +4 -4
  40. data/app/pb_kits/playbook/pb_form_pill/form_pill.rb +4 -0
  41. data/app/pb_kits/playbook/pb_multi_level_select/_multi_level_select.scss +7 -0
  42. data/app/pb_kits/playbook/pb_multi_level_select/_multi_level_select.tsx +638 -549
  43. data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_label.html.erb +3 -3
  44. data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_label.jsx +4 -7
  45. data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_label.md +3 -0
  46. data/app/pb_kits/playbook/pb_multi_level_select/multi_level_select.test.jsx +4 -4
  47. data/app/pb_kits/playbook/pb_passphrase/_passphrase.tsx +20 -5
  48. data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_meter_settings.jsx +1 -0
  49. data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_required_indicator.html.erb +7 -0
  50. data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_required_indicator.jsx +24 -0
  51. data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_required_indicator.md +3 -0
  52. data/app/pb_kits/playbook/pb_passphrase/docs/example.yml +2 -0
  53. data/app/pb_kits/playbook/pb_passphrase/docs/index.js +1 -0
  54. data/app/pb_kits/playbook/pb_passphrase/passphrase.rb +2 -0
  55. data/app/pb_kits/playbook/pb_passphrase/passphrase.test.jsx +30 -1
  56. data/app/pb_kits/playbook/pb_phone_number_input/_phone_number_input.tsx +3 -0
  57. data/app/pb_kits/playbook/pb_phone_number_input/docs/_phone_number_input_required_indicator.html.erb +5 -0
  58. data/app/pb_kits/playbook/pb_phone_number_input/docs/_phone_number_input_required_indicator.jsx +14 -0
  59. data/app/pb_kits/playbook/pb_phone_number_input/docs/_phone_number_input_required_indicator.md +3 -0
  60. data/app/pb_kits/playbook/pb_phone_number_input/docs/example.yml +2 -0
  61. data/app/pb_kits/playbook/pb_phone_number_input/docs/index.js +1 -0
  62. data/app/pb_kits/playbook/pb_phone_number_input/phone_number_input.rb +3 -0
  63. data/app/pb_kits/playbook/pb_phone_number_input/phone_number_input.test.js +34 -3
  64. data/app/pb_kits/playbook/pb_typeahead/_typeahead.test.jsx +24 -1
  65. data/app/pb_kits/playbook/pb_typeahead/_typeahead.tsx +2 -1
  66. data/app/pb_kits/playbook/pb_typeahead/components/MultiValue.tsx +4 -1
  67. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_truncated_text.html.erb +1 -1
  68. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_truncated_text.jsx +1 -1
  69. data/app/pb_kits/playbook/pb_typeahead/typeahead.rb +4 -0
  70. data/dist/chunks/_typeahead-C4YsbA48.js +1 -0
  71. data/dist/chunks/vendor.js +2 -2
  72. data/dist/playbook-rails-react-bindings.js +1 -1
  73. data/dist/playbook-rails.js +1 -1
  74. data/dist/playbook.css +1 -1
  75. data/lib/playbook/forms/builder/phone_number_field.rb +9 -0
  76. data/lib/playbook/truncate.rb +1 -1
  77. data/lib/playbook/version.rb +1 -1
  78. metadata +22 -3
  79. data/dist/chunks/_typeahead-B9a6ZsEP.js +0 -1
@@ -14,6 +14,7 @@ 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
16
  const CLEAR_ICON_SELECTOR = "#dropdown_clear_icon";
17
+ const LABEL_SELECTOR = '[data-dropdown="pb-dropdown-label"]';
17
18
 
18
19
  export default class PbDropdown extends PbEnhancedElement {
19
20
  static get selector() {
@@ -30,14 +31,15 @@ export default class PbDropdown extends PbEnhancedElement {
30
31
  connect() {
31
32
  // Store instance on element for DatePicker sync
32
33
  this.element._pbDropdownInstance = this;
33
-
34
+
34
35
  this.keyboardHandler = new PbDropdownKeyboard(this);
35
36
  this.isMultiSelect = this.element.dataset.pbDropdownMultiSelect === "true";
36
37
  this.formPillProps = this.element.dataset.formPillProps
37
38
  ? JSON.parse(this.element.dataset.formPillProps)
38
39
  : {};
39
40
  const baseInput = this.element.querySelector(DROPDOWN_INPUT);
40
- this.wasOriginallyRequired = baseInput && baseInput.hasAttribute("required");
41
+ this.wasOriginallyRequired =
42
+ baseInput && baseInput.hasAttribute("required");
41
43
  this.setDefaultValue();
42
44
  this.bindEventListeners();
43
45
  this.bindSearchInput();
@@ -48,6 +50,7 @@ export default class PbDropdown extends PbEnhancedElement {
48
50
  this.updatePills();
49
51
 
50
52
  this.clearBtn = this.element.querySelector(CLEAR_ICON_SELECTOR);
53
+ this.isClearable = this.element.dataset.pbDropdownClearable !== "false";
51
54
  if (this.clearBtn) {
52
55
  this.clearBtn.style.display = "none";
53
56
  this.clearBtn.addEventListener("click", (e) => {
@@ -60,6 +63,10 @@ export default class PbDropdown extends PbEnhancedElement {
60
63
 
61
64
  updateClearButton() {
62
65
  if (!this.clearBtn) return;
66
+ if (!this.isClearable) {
67
+ this.clearBtn.style.display = "none";
68
+ return;
69
+ }
63
70
  const hasSelection = this.isMultiSelect
64
71
  ? this.selectedOptions.size > 0
65
72
  : Boolean(this.element.querySelector(DROPDOWN_INPUT).value);
@@ -70,15 +77,24 @@ export default class PbDropdown extends PbEnhancedElement {
70
77
  bindEventListeners() {
71
78
  const customTrigger =
72
79
  this.element.querySelector(CUSTOM_DISPLAY_SELECTOR) || this.element;
73
- customTrigger.addEventListener("click", () =>
74
- this.toggleElement(this.target)
75
- );
80
+ customTrigger.addEventListener("click", (e) => {
81
+ const label = e.target.closest(LABEL_SELECTOR);
82
+ if (label && label.htmlFor) {
83
+ const trigger = this.element.querySelector(
84
+ `#${CSS.escape(label.htmlFor)}`,
85
+ );
86
+ if (trigger) {
87
+ trigger.focus();
88
+ }
89
+ }
90
+ this.toggleElement(this.target);
91
+ });
76
92
 
77
93
  this.target.addEventListener("click", this.handleOptionClick.bind(this));
78
94
  document.addEventListener(
79
95
  "click",
80
96
  this.handleDocumentClick.bind(this),
81
- true
97
+ true,
82
98
  );
83
99
  }
84
100
 
@@ -87,7 +103,7 @@ export default class PbDropdown extends PbEnhancedElement {
87
103
  if (!this.searchBar) return;
88
104
 
89
105
  this.searchBar.addEventListener("input", (e) =>
90
- this.handleSearch(e.target.value)
106
+ this.handleSearch(e.target.value),
91
107
  );
92
108
  }
93
109
 
@@ -102,46 +118,85 @@ export default class PbDropdown extends PbEnhancedElement {
102
118
 
103
119
  // Live filter
104
120
  this.searchInput.addEventListener("input", (e) =>
105
- this.handleSearch(e.target.value)
121
+ this.handleSearch(e.target.value),
106
122
  );
107
123
  }
108
124
 
109
125
  adjustDropdownHeight() {
110
126
  if (this.target.classList.contains("open")) {
111
127
  const el = this.target;
128
+ const shouldConstrain = el.classList.contains("constrain_height");
112
129
  el.style.height = "auto";
113
130
  requestAnimationFrame(() => {
114
- const newHeight = el.scrollHeight + "px";
115
- el.offsetHeight; // force reflow
116
- el.style.height = newHeight;
131
+ if (shouldConstrain) {
132
+ // Calculate 18em in pixels (matches SCSS max-height: 18em)
133
+ const fontSize = parseFloat(getComputedStyle(el).fontSize) || 16;
134
+ const maxHeight = fontSize * 18;
135
+ const scrollHeight = el.scrollHeight;
136
+ const newHeight = Math.min(scrollHeight, maxHeight);
137
+ el.offsetHeight; // force reflow
138
+ el.style.height = newHeight + "px";
139
+ } else {
140
+ el.offsetHeight; // force reflow
141
+ el.style.height = el.scrollHeight + "px";
142
+ }
117
143
  });
118
144
  }
119
145
  }
120
146
 
147
+ adjustDropdownPosition(container) {
148
+ if (!container) return;
149
+
150
+ const wrapper = this.element.querySelector(".dropdown_wrapper");
151
+ if (!wrapper) return;
152
+
153
+ const wrapperRect = wrapper.getBoundingClientRect();
154
+ const h = container.getBoundingClientRect().height || container.scrollHeight;
155
+ const spaceBelow = window.innerHeight - wrapperRect.bottom;
156
+ const spaceAbove = wrapperRect.top;
157
+
158
+ // If not enough space below but enough space above, position above
159
+ if (spaceBelow < h + 10 && spaceAbove >= h + 10) {
160
+ container.style.top = "auto";
161
+ container.style.bottom = "calc(100% + 5px)";
162
+ container.style.marginTop = "0";
163
+ container.style.marginBottom = "0";
164
+ } else {
165
+ // Default: position below
166
+ container.style.top = "";
167
+ container.style.bottom = "";
168
+ container.style.marginTop = "";
169
+ container.style.marginBottom = "";
170
+ }
171
+ }
172
+
121
173
  handleSearch(term = "") {
122
174
  const lcTerm = term.toLowerCase();
123
- let hasMatch = false
175
+ let hasMatch = false;
124
176
  this.element.querySelectorAll(OPTION_SELECTOR).forEach((opt) => {
125
177
  //make it so that if the option is selected, it will not show up in the search results
126
- if (this.isMultiSelect && this.selectedOptions.has(opt.dataset.dropdownOptionLabel)) {
127
- opt.style.display = "none";
128
- return;
129
- }
178
+ if (
179
+ this.isMultiSelect &&
180
+ this.selectedOptions.has(opt.dataset.dropdownOptionLabel)
181
+ ) {
182
+ opt.style.display = "none";
183
+ return;
184
+ }
130
185
  const label = JSON.parse(opt.dataset.dropdownOptionLabel)
131
186
  .label.toString()
132
187
  .toLowerCase();
133
188
 
134
- // hide or show option
189
+ // hide or show option
135
190
  const match = label.includes(lcTerm);
136
191
  opt.style.display = match ? "" : "none";
137
- if (match) hasMatch = true
192
+ if (match) hasMatch = true;
138
193
  });
139
194
 
140
195
  this.adjustDropdownHeight();
141
196
 
142
- this.removeNoOptionsMessage()
197
+ this.removeNoOptionsMessage();
143
198
  if (!hasMatch) {
144
- this.showNoOptionsMessage()
199
+ this.showNoOptionsMessage();
145
200
  }
146
201
  }
147
202
 
@@ -149,7 +204,8 @@ export default class PbDropdown extends PbEnhancedElement {
149
204
  if (this.element.querySelector(".dropdown_no_options")) return;
150
205
 
151
206
  const noOptionElement = document.createElement("div");
152
- noOptionElement.className = "pb_body_kit_light dropdown_no_options pb_item_kit p_xs display_flex justify_content_center";
207
+ noOptionElement.className =
208
+ "pb_body_kit_light dropdown_no_options pb_item_kit p_xs display_flex justify_content_center";
153
209
  noOptionElement.textContent = "no option";
154
210
 
155
211
  this.target.appendChild(noOptionElement);
@@ -200,6 +256,8 @@ export default class PbDropdown extends PbEnhancedElement {
200
256
  }
201
257
 
202
258
  isClickOutside(event) {
259
+ const label = event.target.closest(LABEL_SELECTOR);
260
+ if (label && this.element.contains(label)) return false;
203
261
  const customTrigger = this.element.querySelector(CUSTOM_DISPLAY_SELECTOR);
204
262
  if (customTrigger) {
205
263
  return !customTrigger.contains(event.target);
@@ -230,8 +288,8 @@ export default class PbDropdown extends PbEnhancedElement {
230
288
  ? JSON.parse(
231
289
  this.element.querySelector(
232
290
  OPTION_SELECTOR +
233
- `[data-dropdown-option-label*='"id":"${hiddenInput.value}"']`
234
- ).dataset.dropdownOptionLabel
291
+ `[data-dropdown-option-label*='"id":"${hiddenInput.value}"']`,
292
+ ).dataset.dropdownOptionLabel,
235
293
  )
236
294
  : null;
237
295
  }
@@ -240,14 +298,14 @@ export default class PbDropdown extends PbEnhancedElement {
240
298
  new CustomEvent("pb:dropdown:selected", {
241
299
  detail,
242
300
  bubbles: true,
243
- })
301
+ }),
244
302
  );
245
303
  }
246
304
 
247
305
  onOptionSelected(value, selectedOption) {
248
306
  const triggerElement = this.element.querySelector(DROPDOWN_TRIGGER_DISPLAY);
249
307
  const customDisplayElement = this.element.querySelector(
250
- "#dropdown_trigger_custom_display"
308
+ "#dropdown_trigger_custom_display",
251
309
  );
252
310
 
253
311
  if (triggerElement) {
@@ -255,36 +313,46 @@ export default class PbDropdown extends PbEnhancedElement {
255
313
  const selectedLabel = JSON.parse(value).label;
256
314
  triggerElement.textContent = selectedLabel;
257
315
  this.emitSelectionChange();
258
-
316
+
259
317
  // Handle quickpick variant: populate start/end date hidden inputs
260
318
  const optionData = JSON.parse(value);
261
319
  const startDateId = this.element.dataset.startDateId;
262
320
  const endDateId = this.element.dataset.endDateId;
263
321
  const controlsStartId = this.element.dataset.controlsStartId;
264
322
  const controlsEndId = this.element.dataset.controlsEndId;
265
-
323
+
266
324
  if (optionData.formatted_start_date && optionData.formatted_end_date) {
267
325
  // Populate date inputs when option has date fields
268
326
  if (startDateId) {
269
327
  const startDateInput = document.getElementById(startDateId);
270
- if (startDateInput) startDateInput.value = optionData.formatted_start_date;
328
+ if (startDateInput)
329
+ startDateInput.value = optionData.formatted_start_date;
271
330
  }
272
-
331
+
273
332
  if (endDateId) {
274
333
  const endDateInput = document.getElementById(endDateId);
275
- if (endDateInput) endDateInput.value = optionData.formatted_end_date;
334
+ if (endDateInput)
335
+ endDateInput.value = optionData.formatted_end_date;
276
336
  }
277
-
337
+
278
338
  // Sync with DatePickers if controlsStartId/controlsEndId are present
279
339
  if (controlsStartId) {
280
- const startPicker = document.querySelector(`#${controlsStartId}`)?._flatpickr;
340
+ const startPicker = document.querySelector(
341
+ `#${controlsStartId}`,
342
+ )?._flatpickr;
281
343
  if (startPicker) {
282
- startPicker.setDate(optionData.formatted_start_date, true, "m/d/Y");
344
+ startPicker.setDate(
345
+ optionData.formatted_start_date,
346
+ true,
347
+ "m/d/Y",
348
+ );
283
349
  }
284
350
  }
285
-
351
+
286
352
  if (controlsEndId) {
287
- const endPicker = document.querySelector(`#${controlsEndId}`)?._flatpickr;
353
+ const endPicker = document.querySelector(
354
+ `#${controlsEndId}`,
355
+ )?._flatpickr;
288
356
  if (endPicker) {
289
357
  endPicker.setDate(optionData.formatted_end_date, true, "m/d/Y");
290
358
  }
@@ -295,22 +363,26 @@ export default class PbDropdown extends PbEnhancedElement {
295
363
  const startDateInput = document.getElementById(startDateId);
296
364
  if (startDateInput) startDateInput.value = "";
297
365
  }
298
-
366
+
299
367
  if (endDateId) {
300
368
  const endDateInput = document.getElementById(endDateId);
301
369
  if (endDateInput) endDateInput.value = "";
302
370
  }
303
-
371
+
304
372
  // Clear DatePickers as well
305
373
  if (controlsStartId) {
306
- const startPicker = document.querySelector(`#${controlsStartId}`)?._flatpickr;
374
+ const startPicker = document.querySelector(
375
+ `#${controlsStartId}`,
376
+ )?._flatpickr;
307
377
  if (startPicker) {
308
378
  startPicker.clear();
309
379
  }
310
380
  }
311
-
381
+
312
382
  if (controlsEndId) {
313
- const endPicker = document.querySelector(`#${controlsEndId}`)?._flatpickr;
383
+ const endPicker = document.querySelector(
384
+ `#${controlsEndId}`,
385
+ )?._flatpickr;
314
386
  if (endPicker) {
315
387
  endPicker.clear();
316
388
  }
@@ -350,7 +422,9 @@ export default class PbDropdown extends PbEnhancedElement {
350
422
  this.adjustDropdownHeight();
351
423
  }
352
424
  });
353
- this.element.querySelector(DROPDOWN_INPUT).value = Array.from(this.selectedOptions)
425
+ this.element.querySelector(DROPDOWN_INPUT).value = Array.from(
426
+ this.selectedOptions,
427
+ )
354
428
  .map((opt) => JSON.parse(opt).id)
355
429
  .join(",");
356
430
  } else {
@@ -365,7 +439,21 @@ export default class PbDropdown extends PbEnhancedElement {
365
439
  showElement(elem) {
366
440
  elem.classList.remove("close");
367
441
  elem.classList.add("open");
368
- elem.style.height = elem.scrollHeight + "px";
442
+
443
+ const shouldConstrain = elem.classList.contains("constrain_height");
444
+ if (shouldConstrain) {
445
+ // Calculate height respecting max-height constraint (18em)
446
+ const fontSize = parseFloat(getComputedStyle(elem).fontSize) || 16;
447
+ const maxHeight = fontSize * 18; // matches SCSS max-height: 18em
448
+ const scrollHeight = elem.scrollHeight;
449
+ const height = Math.min(scrollHeight, maxHeight);
450
+ elem.style.height = height + "px";
451
+ } else {
452
+ elem.style.height = elem.scrollHeight + "px";
453
+ }
454
+
455
+ // Auto-position dropdown above if not enough space below
456
+ this.adjustDropdownPosition(elem);
369
457
  }
370
458
 
371
459
  hideElement(elem) {
@@ -382,7 +470,7 @@ export default class PbDropdown extends PbEnhancedElement {
382
470
  this.keyboardHandler.focusedOptionIndex = -1;
383
471
  const options = this.element.querySelectorAll(OPTION_SELECTOR);
384
472
  options.forEach((option) =>
385
- option.classList.remove("pb_dropdown_option_focused")
473
+ option.classList.remove("pb_dropdown_option_focused"),
386
474
  );
387
475
  }
388
476
  }
@@ -417,7 +505,7 @@ export default class PbDropdown extends PbEnhancedElement {
417
505
  hiddenInput.closest(".dropdown_wrapper").classList.add("error");
418
506
  }
419
507
  },
420
- true
508
+ true,
421
509
  );
422
510
  }
423
511
 
@@ -427,7 +515,7 @@ export default class PbDropdown extends PbEnhancedElement {
427
515
  const dropdownWrapperElement = input.closest(".dropdown_wrapper");
428
516
  dropdownWrapperElement.classList.remove("error");
429
517
  const errorLabelElement = dropdownWrapperElement.querySelector(
430
- ".pb_body_kit_negative"
518
+ ".pb_body_kit_negative",
431
519
  );
432
520
  if (errorLabelElement) {
433
521
  errorLabelElement.remove();
@@ -435,13 +523,13 @@ export default class PbDropdown extends PbEnhancedElement {
435
523
  return;
436
524
  }
437
525
  }
438
-
526
+
439
527
  if (input.checkValidity()) {
440
528
  const dropdownWrapperElement = input.closest(".dropdown_wrapper");
441
529
  dropdownWrapperElement.classList.remove("error");
442
530
 
443
531
  const errorLabelElement = dropdownWrapperElement.querySelector(
444
- ".pb_body_kit_negative"
532
+ ".pb_body_kit_negative",
445
533
  );
446
534
  if (errorLabelElement) {
447
535
  errorLabelElement.remove();
@@ -452,7 +540,7 @@ export default class PbDropdown extends PbEnhancedElement {
452
540
  setDefaultValue() {
453
541
  const hiddenInput = this.element.querySelector(DROPDOWN_INPUT);
454
542
  const optionEls = Array.from(
455
- this.element.querySelectorAll(OPTION_SELECTOR)
543
+ this.element.querySelectorAll(OPTION_SELECTOR),
456
544
  );
457
545
  const defaultValue = hiddenInput.dataset.defaultValue || "";
458
546
  if (!defaultValue) return;
@@ -498,44 +586,53 @@ export default class PbDropdown extends PbEnhancedElement {
498
586
  selectedOption.classList.add("pb_dropdown_option_selected");
499
587
  const optionData = JSON.parse(selectedOption.dataset.dropdownOptionLabel);
500
588
  this.setTriggerElementText(optionData.label);
501
-
589
+
502
590
  // Handle quickpick variant: populate start/end date hidden inputs and sync DatePickers
503
591
  if (optionData.formatted_start_date && optionData.formatted_end_date) {
504
592
  const startDateId = this.element.dataset.startDateId;
505
593
  const endDateId = this.element.dataset.endDateId;
506
594
  const controlsStartId = this.element.dataset.controlsStartId;
507
595
  const controlsEndId = this.element.dataset.controlsEndId;
508
-
596
+
509
597
  if (startDateId) {
510
598
  const startDateInput = document.getElementById(startDateId);
511
- if (startDateInput) startDateInput.value = optionData.formatted_start_date;
599
+ if (startDateInput)
600
+ startDateInput.value = optionData.formatted_start_date;
512
601
  }
513
-
602
+
514
603
  if (endDateId) {
515
604
  const endDateInput = document.getElementById(endDateId);
516
605
  if (endDateInput) endDateInput.value = optionData.formatted_end_date;
517
606
  }
518
-
607
+
519
608
  // Sync with DatePickers - retry with delays to ensure DatePickers are initialized
520
609
  const syncDatePickers = () => {
521
610
  if (controlsStartId) {
522
- const startPicker = document.querySelector(`#${controlsStartId}`)?._flatpickr;
611
+ const startPicker = document.querySelector(
612
+ `#${controlsStartId}`,
613
+ )?._flatpickr;
523
614
  if (startPicker) {
524
- startPicker.setDate(optionData.formatted_start_date, true, "m/d/Y");
615
+ startPicker.setDate(
616
+ optionData.formatted_start_date,
617
+ true,
618
+ "m/d/Y",
619
+ );
525
620
  }
526
621
  }
527
-
622
+
528
623
  if (controlsEndId) {
529
- const endPicker = document.querySelector(`#${controlsEndId}`)?._flatpickr;
624
+ const endPicker = document.querySelector(
625
+ `#${controlsEndId}`,
626
+ )?._flatpickr;
530
627
  if (endPicker) {
531
628
  endPicker.setDate(optionData.formatted_end_date, true, "m/d/Y");
532
629
  }
533
630
  }
534
631
  };
535
-
632
+
536
633
  // Try immediately
537
634
  syncDatePickers();
538
-
635
+
539
636
  // Retry after short delay in case DatePickers aren't ready yet
540
637
  setTimeout(syncDatePickers, 100);
541
638
  setTimeout(syncDatePickers, 300);
@@ -598,7 +695,7 @@ export default class PbDropdown extends PbEnhancedElement {
598
695
 
599
696
  const wrapper = this.element.querySelector("#dropdown_pills_wrapper");
600
697
  const placeholder = this.element.querySelector(
601
- "#dropdown_trigger_display_multi_select"
698
+ "#dropdown_trigger_display_multi_select",
602
699
  );
603
700
  if (!wrapper) return;
604
701
 
@@ -616,7 +713,12 @@ export default class PbDropdown extends PbEnhancedElement {
616
713
  // Create a form pill for each selected option
617
714
  const pill = document.createElement("div");
618
715
  const color = this.formPillProps.color || "primary";
619
- pill.classList.add("pb_form_pill_kit", `pb_form_pill_${color}`, "pb_form_pill_none", "mr_xs");
716
+ pill.classList.add(
717
+ "pb_form_pill_kit",
718
+ `pb_form_pill_${color}`,
719
+ "pb_form_pill_none",
720
+ "mr_xs",
721
+ );
620
722
  if (this.formPillProps.size === "small") {
621
723
  pill.classList.add("pb_form_pill_small");
622
724
  }
@@ -641,8 +743,8 @@ export default class PbDropdown extends PbEnhancedElement {
641
743
 
642
744
  const optEl = this.element.querySelector(
643
745
  `${OPTION_SELECTOR}[data-dropdown-option-label*='"id":${JSON.stringify(
644
- id
645
- )}']`
746
+ id,
747
+ )}']`,
646
748
  );
647
749
  if (optEl) {
648
750
  optEl.style.display = "";
@@ -671,18 +773,18 @@ export default class PbDropdown extends PbEnhancedElement {
671
773
  }
672
774
  }
673
775
  const customDisplay = this.element.querySelector(
674
- "#dropdown_trigger_custom_display"
776
+ "#dropdown_trigger_custom_display",
675
777
  );
676
778
  if (customDisplay) {
677
779
  customDisplay.style.display = "none";
678
780
  }
679
-
781
+
680
782
  // Clear quickpick hidden inputs
681
783
  const startDateId = this.element.dataset.startDateId;
682
784
  const endDateId = this.element.dataset.endDateId;
683
785
  const controlsStartId = this.element.dataset.controlsStartId;
684
786
  const controlsEndId = this.element.dataset.controlsEndId;
685
-
787
+
686
788
  if (startDateId) {
687
789
  const startDateInput = document.getElementById(startDateId);
688
790
  if (startDateInput) startDateInput.value = "";
@@ -691,22 +793,24 @@ export default class PbDropdown extends PbEnhancedElement {
691
793
  const endDateInput = document.getElementById(endDateId);
692
794
  if (endDateInput) endDateInput.value = "";
693
795
  }
694
-
796
+
695
797
  // Clear linked DatePickers if controlsStartId/controlsEndId are present
696
798
  if (controlsStartId) {
697
- const startPicker = document.querySelector(`#${controlsStartId}`)?._flatpickr;
799
+ const startPicker = document.querySelector(
800
+ `#${controlsStartId}`,
801
+ )?._flatpickr;
698
802
  if (startPicker) {
699
803
  startPicker.clear();
700
804
  }
701
805
  }
702
-
806
+
703
807
  if (controlsEndId) {
704
808
  const endPicker = document.querySelector(`#${controlsEndId}`)?._flatpickr;
705
809
  if (endPicker) {
706
810
  endPicker.clear();
707
811
  }
708
812
  }
709
-
813
+
710
814
  this.resetDropdownValue();
711
815
  this.updatePills();
712
816
  this.updateClearButton();
@@ -717,21 +821,24 @@ export default class PbDropdown extends PbEnhancedElement {
717
821
  // Method for DatePicker sync - only clears the dropdown, not the DatePickers
718
822
  clearSelected() {
719
823
  // Only clear if this is a single-select quickpick variant
720
- if (this.element.dataset.pbDropdownVariant !== "quickpick" || this.isMultiSelect) {
824
+ if (
825
+ this.element.dataset.pbDropdownVariant !== "quickpick" ||
826
+ this.isMultiSelect
827
+ ) {
721
828
  return;
722
829
  }
723
-
830
+
724
831
  const customDisplay = this.element.querySelector(
725
- "#dropdown_trigger_custom_display"
832
+ "#dropdown_trigger_custom_display",
726
833
  );
727
834
  if (customDisplay) {
728
835
  customDisplay.style.display = "none";
729
836
  }
730
-
837
+
731
838
  // Clear quickpick hidden inputs only (not the DatePickers)
732
839
  const startDateId = this.element.dataset.startDateId;
733
840
  const endDateId = this.element.dataset.endDateId;
734
-
841
+
735
842
  if (startDateId) {
736
843
  const startDateInput = document.getElementById(startDateId);
737
844
  if (startDateInput) startDateInput.value = "";
@@ -740,7 +847,7 @@ export default class PbDropdown extends PbEnhancedElement {
740
847
  const endDateInput = document.getElementById(endDateId);
741
848
  if (endDateInput) endDateInput.value = "";
742
849
  }
743
-
850
+
744
851
  this.resetDropdownValue();
745
852
  this.updateClearButton();
746
853
  this.emitSelectionChange();
@@ -767,7 +874,7 @@ export default class PbDropdown extends PbEnhancedElement {
767
874
  inp.dataset.generated = "true";
768
875
  baseInput.insertAdjacentElement("afterend", inp);
769
876
  });
770
-
877
+
771
878
  // For multi-select, remove required from base input when there are selections
772
879
  // The generated inputs handle the form submission with actual values
773
880
  // Restore required attribute when there are no selections (if it was originally required)
@@ -19,6 +19,7 @@ type DropdownContainerProps = {
19
19
  aria?: { [key: string]: string };
20
20
  children?: React.ReactChild[] | React.ReactChild;
21
21
  className?: string;
22
+ constrainHeight?: boolean;
22
23
  dark?: boolean;
23
24
  data?: { [key: string]: string };
24
25
  htmlOptions?: {[key: string]: string | number | boolean | (() => void)},
@@ -31,6 +32,7 @@ const DropdownContainer = (props: DropdownContainerProps) => {
31
32
  aria = {},
32
33
  children,
33
34
  className,
35
+ constrainHeight = false,
34
36
  dark = false,
35
37
  data = {},
36
38
  htmlOptions = {},
@@ -54,6 +56,7 @@ const DropdownContainer = (props: DropdownContainerProps) => {
54
56
  const classes = classnames(
55
57
  buildCss("pb_dropdown_container"),
56
58
  `${isDropDownClosed ? "close" : "open"}`,
59
+ constrainHeight && "constrain_height",
57
60
  globalProps(props),
58
61
  className
59
62
  );
@@ -44,6 +44,9 @@ const DropdownTrigger = (props: DropdownTriggerProps) => {
44
44
 
45
45
  const {
46
46
  autocomplete,
47
+ clearable,
48
+ error,
49
+ errorId,
47
50
  filterItem,
48
51
  handleBackspace,
49
52
  handleChange,
@@ -52,8 +55,10 @@ const DropdownTrigger = (props: DropdownTriggerProps) => {
52
55
  inputWrapperRef,
53
56
  isDropDownClosed,
54
57
  isInputFocused,
58
+ label: contextLabel,
55
59
  multiSelect,
56
60
  selected,
61
+ selectId,
57
62
  setIsInputFocused,
58
63
  toggleDropdown,
59
64
  } = useContext(DropdownContext);
@@ -103,6 +108,10 @@ const DropdownTrigger = (props: DropdownTriggerProps) => {
103
108
  ? placeholder
104
109
  : "Select...";
105
110
 
111
+ const triggerAriaLabel = contextLabel
112
+ ? (children ? contextLabel : `${contextLabel}, ${defaultDisplayPlaceholder}`)
113
+ : undefined;
114
+
106
115
  return (
107
116
  <div {...ariaProps}
108
117
  {...dataProps}
@@ -113,6 +122,10 @@ const DropdownTrigger = (props: DropdownTriggerProps) => {
113
122
  {
114
123
  children ? (
115
124
  <div
125
+ aria-describedby={errorId}
126
+ aria-invalid={!!error}
127
+ aria-label={triggerAriaLabel}
128
+ id={selectId}
116
129
  onClick={() => toggleDropdown()}
117
130
  onKeyDown= {handleKeyDown}
118
131
  ref={inputWrapperRef}
@@ -129,6 +142,10 @@ const DropdownTrigger = (props: DropdownTriggerProps) => {
129
142
  className={triggerWrapperClasses}
130
143
  cursor={`${autocomplete ? "text" : "pointer"}`}
131
144
  htmlOptions={{
145
+ "aria-describedby": errorId,
146
+ "aria-invalid": !!error,
147
+ "aria-label": triggerAriaLabel,
148
+ id: selectId,
132
149
  onClick: () => handleWrapperClick(),
133
150
  onKeyDown: handleKeyDown,
134
151
  tabIndex: "0",
@@ -225,7 +242,7 @@ const DropdownTrigger = (props: DropdownTriggerProps) => {
225
242
  key={`${isDropDownClosed ? "chevron-down" : "chevron-up"}`}
226
243
  >
227
244
  {
228
- selectedArray.length > 0 && (
245
+ clearable !== false && selectedArray.length > 0 && (
229
246
  <div onClick={(e)=>{e.stopPropagation();handleBackspace()}}>
230
247
  <Icon
231
248
  cursor="pointer"