playbook_ui 16.1.0.pre.alpha.play264213817 → 16.1.0.pre.alpha.play273614090

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