playbook_ui 16.3.0 → 16.4.0.pre.alpha.PLAY2846reactadvancedtablecalcheaderpinnedrows15356

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 (148) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/pb_advanced_table/Components/CustomCell.tsx +17 -4
  3. data/app/pb_kits/playbook/pb_advanced_table/Components/TableHeaderCell.tsx +3 -1
  4. data/app/pb_kits/playbook/pb_advanced_table/Context/AdvancedTableContext.tsx +5 -2
  5. data/app/pb_kits/playbook/pb_advanced_table/Hooks/useTableActions.ts +21 -9
  6. data/app/pb_kits/playbook/pb_advanced_table/Hooks/useTableState.ts +5 -2
  7. data/app/pb_kits/playbook/pb_advanced_table/SubKits/TableHeader.tsx +9 -11
  8. data/app/pb_kits/playbook/pb_advanced_table/Utilities/ExpansionControlHelpers.tsx +25 -1
  9. data/app/pb_kits/playbook/pb_advanced_table/Utilities/RowModelUtils.ts +100 -0
  10. data/app/pb_kits/playbook/pb_advanced_table/_advanced_table.scss +65 -1
  11. data/app/pb_kits/playbook/pb_advanced_table/_advanced_table.tsx +12 -2
  12. data/app/pb_kits/playbook/pb_advanced_table/advanced_table.html.erb +2 -2
  13. data/app/pb_kits/playbook/pb_advanced_table/advanced_table.rb +9 -0
  14. data/app/pb_kits/playbook/pb_advanced_table/advanced_table.test.jsx +109 -2
  15. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_cascade_collapse.jsx +50 -0
  16. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_cascade_collapse.md +1 -0
  17. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_pinned_rows_rails.html.erb +57 -0
  18. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_pinned_rows_rails.md +7 -0
  19. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_sort_parent_only.jsx +175 -0
  20. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_sort_parent_only.md +5 -0
  21. data/app/pb_kits/playbook/pb_advanced_table/docs/example.yml +3 -0
  22. data/app/pb_kits/playbook/pb_advanced_table/docs/index.js +3 -1
  23. data/app/pb_kits/playbook/pb_advanced_table/index.js +130 -29
  24. data/app/pb_kits/playbook/pb_advanced_table/scss_partials/advanced_table_sticky_mixin.scss +6 -2
  25. data/app/pb_kits/playbook/pb_advanced_table/table_body.html.erb +21 -4
  26. data/app/pb_kits/playbook/pb_advanced_table/table_body.rb +115 -9
  27. data/app/pb_kits/playbook/pb_advanced_table/table_row.html.erb +3 -1
  28. data/app/pb_kits/playbook/pb_advanced_table/table_row.rb +12 -1
  29. data/app/pb_kits/playbook/pb_advanced_table/table_subrow_header.html.erb +4 -1
  30. data/app/pb_kits/playbook/pb_advanced_table/table_subrow_header.rb +9 -1
  31. data/app/pb_kits/playbook/pb_button/_button_mixins.scss +6 -1
  32. data/app/pb_kits/playbook/pb_button/docs/_button_full_width_rails.md +19 -0
  33. data/app/pb_kits/playbook/pb_button/docs/_button_full_width_react.md +23 -0
  34. data/app/pb_kits/playbook/pb_circle_icon_button/_circle_icon_button.scss +5 -0
  35. data/app/pb_kits/playbook/pb_collapsible/index.js +15 -26
  36. data/app/pb_kits/playbook/pb_date_picker/date_picker_helper.ts +3 -1
  37. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_compound_components.html.erb +1 -1
  38. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_compound_components.jsx +6 -3
  39. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_full_height.html.erb +3 -3
  40. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_full_height.jsx +6 -3
  41. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_full_height_placement.html.erb +3 -3
  42. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_full_height_placement.jsx +6 -3
  43. data/app/pb_kits/playbook/pb_dropdown/_dropdown.scss +3 -0
  44. data/app/pb_kits/playbook/pb_dropdown/_dropdown.tsx +1 -0
  45. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_closing_options_rails.html.erb +16 -0
  46. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_closing_options_rails.md +1 -0
  47. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_custom_event_type.html.erb +224 -0
  48. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_custom_event_type.md +7 -0
  49. data/app/pb_kits/playbook/pb_dropdown/docs/example.yml +2 -0
  50. data/app/pb_kits/playbook/pb_dropdown/dropdown.rb +8 -1
  51. data/app/pb_kits/playbook/pb_dropdown/index.js +255 -46
  52. data/app/pb_kits/playbook/pb_dropdown/subcomponents/DropdownTrigger.tsx +19 -14
  53. data/app/pb_kits/playbook/pb_fixed_confirmation_toast/_fixed_confirmation_toast.scss +4 -0
  54. data/app/pb_kits/playbook/pb_fixed_confirmation_toast/_fixed_confirmation_toast.tsx +3 -0
  55. data/app/pb_kits/playbook/pb_fixed_confirmation_toast/docs/_fixed_confirmation_toast_nav_margin.html.erb +46 -0
  56. data/app/pb_kits/playbook/pb_fixed_confirmation_toast/docs/_fixed_confirmation_toast_nav_margin.jsx +42 -0
  57. data/app/pb_kits/playbook/pb_fixed_confirmation_toast/docs/_fixed_confirmation_toast_nav_margin_rails.md +1 -0
  58. data/app/pb_kits/playbook/pb_fixed_confirmation_toast/docs/_fixed_confirmation_toast_nav_margin_react.md +1 -0
  59. data/app/pb_kits/playbook/pb_fixed_confirmation_toast/docs/example.yml +2 -0
  60. data/app/pb_kits/playbook/pb_fixed_confirmation_toast/docs/index.js +2 -1
  61. data/app/pb_kits/playbook/pb_fixed_confirmation_toast/fixed_confirmation_toast.rb +7 -1
  62. data/app/pb_kits/playbook/pb_icon/icon.rb +7 -1
  63. data/app/pb_kits/playbook/pb_list/_list_mixin.scss +4 -4
  64. data/app/pb_kits/playbook/pb_multi_level_select/_helper_functions.tsx +1 -1
  65. data/app/pb_kits/playbook/pb_multi_level_select/_multi_level_select.tsx +27 -16
  66. data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_placeholder.html.erb +109 -0
  67. data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_placeholder.jsx +127 -0
  68. data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_placeholder.md +1 -0
  69. data/app/pb_kits/playbook/pb_multi_level_select/docs/example.yml +2 -0
  70. data/app/pb_kits/playbook/pb_multi_level_select/docs/index.js +1 -0
  71. data/app/pb_kits/playbook/pb_multi_level_select/multi_level_select.rb +3 -0
  72. data/app/pb_kits/playbook/pb_multi_level_select/multi_level_select.test.jsx +27 -0
  73. data/app/pb_kits/playbook/pb_popover/docs/_popover_placement.jsx +81 -0
  74. data/app/pb_kits/playbook/pb_popover/docs/_popover_placement_react.md +1 -0
  75. data/app/pb_kits/playbook/pb_popover/docs/_popover_position.html.erb +128 -0
  76. data/app/pb_kits/playbook/pb_popover/docs/_popover_position_rails.md +1 -0
  77. data/app/pb_kits/playbook/pb_popover/docs/example.yml +2 -0
  78. data/app/pb_kits/playbook/pb_popover/docs/index.js +2 -1
  79. data/app/pb_kits/playbook/pb_rich_text_editor/_rich_text_editor.tsx +35 -134
  80. data/app/pb_kits/playbook/pb_rich_text_editor/_tiptap_editor.tsx +51 -0
  81. data/app/pb_kits/playbook/pb_rich_text_editor/_trix_editor.tsx +206 -0
  82. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_default.jsx +56 -0
  83. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_default.md +1 -0
  84. data/app/pb_kits/playbook/pb_rich_text_editor/docs/example.yml +13 -21
  85. data/app/pb_kits/playbook/pb_rich_text_editor/docs/index.js +0 -10
  86. data/app/pb_kits/playbook/pb_rich_text_editor/inlineFocus.ts +5 -4
  87. data/app/pb_kits/playbook/pb_selectable_list/_selectable_list.scss +19 -1
  88. data/app/pb_kits/playbook/pb_table/_table.tsx +24 -21
  89. data/app/pb_kits/playbook/pb_table/docs/_sections.yml +1 -0
  90. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_external_filter_rails.html.erb +45 -0
  91. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_external_filter_rails.md +39 -0
  92. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_rails.md +2 -1
  93. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_with_card_title_props.jsx +152 -0
  94. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_with_card_title_props.md +17 -0
  95. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_with_card_title_props_rails.html.erb +121 -0
  96. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_with_card_title_props_rails.md +17 -0
  97. data/app/pb_kits/playbook/pb_table/docs/example.yml +3 -0
  98. data/app/pb_kits/playbook/pb_table/docs/index.js +1 -0
  99. data/app/pb_kits/playbook/pb_table/table.html.erb +17 -13
  100. data/app/pb_kits/playbook/pb_table/table.rb +8 -0
  101. data/app/pb_kits/playbook/pb_table/table.test.js +33 -0
  102. data/app/pb_kits/playbook/pb_textarea/_textarea.scss +4 -1
  103. data/app/pb_kits/playbook/pb_typeahead/_typeahead.tsx +105 -3
  104. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_with_highlight.jsx +20 -8
  105. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_with_highlight.md +3 -0
  106. data/app/pb_kits/playbook/utilities/_hover.scss +6 -3
  107. data/app/pb_kits/playbook/utilities/domHelpers.ts +50 -0
  108. data/dist/chunks/{_pb_line_graph-CKBPxTmM.js → _pb_line_graph-D6s5rymw.js} +1 -1
  109. data/dist/chunks/_typeahead-BNp_YiTh.js +1 -0
  110. data/dist/chunks/componentRegistry-DRSp5D_e.js +1 -0
  111. data/dist/chunks/{globalProps-DLCfJwiU.js → globalProps-Ds_6HBhX.js} +1 -1
  112. data/dist/chunks/lib-BaO72ugL.js +29 -0
  113. data/dist/chunks/vendor.js +5 -5
  114. data/dist/menu.yml +3 -2
  115. data/dist/playbook-rails-react-bindings.js +1 -1
  116. data/dist/playbook-rails.js +1 -1
  117. data/dist/playbook.css +1 -1
  118. data/lib/playbook/pb_forms_helper.rb +3 -0
  119. data/lib/playbook/version.rb +2 -2
  120. metadata +42 -32
  121. data/app/pb_kits/playbook/pb_button/docs/_button_full_width.md +0 -1
  122. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_attributes.html.erb +0 -5
  123. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_attributes.jsx +0 -15
  124. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_default.html.erb +0 -1
  125. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_focus.html.erb +0 -3
  126. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_focus.jsx +0 -17
  127. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_inline.html.erb +0 -6
  128. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_inline.jsx +0 -16
  129. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_label.jsx +0 -28
  130. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_label.md +0 -1
  131. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_preview.html.erb +0 -35
  132. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_preview.jsx +0 -45
  133. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_required_indicator.html.erb +0 -10
  134. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_required_indicator.jsx +0 -22
  135. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_required_indicator.md +0 -3
  136. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_simple.html.erb +0 -1
  137. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_simple.jsx +0 -13
  138. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_sticky.html.erb +0 -1
  139. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_sticky.jsx +0 -15
  140. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_templates.html.erb +0 -115
  141. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_templates.jsx +0 -42
  142. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_toolbar_bottom.html.erb +0 -4
  143. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_toolbar_bottom.jsx +0 -14
  144. data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor.html.erb +0 -5
  145. data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor.rb +0 -63
  146. data/dist/chunks/_typeahead-B7bktFm6.js +0 -1
  147. data/dist/chunks/componentRegistry-DzmmLR2x.js +0 -1
  148. data/dist/chunks/lib-QT_7rPYf.js +0 -29
@@ -1,5 +1,6 @@
1
1
  import PbEnhancedElement from "../pb_enhanced_element";
2
2
  import { PbDropdownKeyboard } from "./keyboard_accessibility";
3
+ import { setArrowVisibility, toggleVisibility } from "../utilities/domHelpers";
3
4
 
4
5
  const DROPDOWN_SELECTOR = "[data-pb-dropdown]";
5
6
  const TRIGGER_SELECTOR = "[data-dropdown-trigger]";
@@ -23,7 +24,38 @@ export default class PbDropdown extends PbEnhancedElement {
23
24
  }
24
25
 
25
26
  get target() {
26
- return this.element.querySelector(CONTAINER_SELECTOR);
27
+ return this.cachedElements?.target || this.element.querySelector(CONTAINER_SELECTOR);
28
+ }
29
+
30
+ get baseInput() {
31
+ return this.cachedElements?.baseInput || this.element.querySelector(DROPDOWN_INPUT);
32
+ }
33
+
34
+ get trigger() {
35
+ return this.cachedElements?.trigger || this.element.querySelector(TRIGGER_SELECTOR);
36
+ }
37
+
38
+ get customTrigger() {
39
+ return this.cachedElements?.customTrigger || this.element.querySelector(CUSTOM_DISPLAY_SELECTOR);
40
+ }
41
+
42
+ get dropdownWrapper() {
43
+ return this.cachedElements?.dropdownWrapper || this.element.querySelector(".dropdown_wrapper");
44
+ }
45
+
46
+ get placeholder() {
47
+ return this.cachedElements?.placeholder || this.element.querySelector(DROPDOWN_PLACEHOLDER);
48
+ }
49
+
50
+ cacheElements() {
51
+ this.cachedElements = {
52
+ target: this.element.querySelector(CONTAINER_SELECTOR),
53
+ baseInput: this.element.querySelector(DROPDOWN_INPUT),
54
+ trigger: this.element.querySelector(TRIGGER_SELECTOR),
55
+ customTrigger: this.element.querySelector(CUSTOM_DISPLAY_SELECTOR),
56
+ dropdownWrapper: this.element.querySelector(".dropdown_wrapper"),
57
+ placeholder: this.element.querySelector(DROPDOWN_PLACEHOLDER),
58
+ };
27
59
  }
28
60
 
29
61
  selectedOptions = new Set();
@@ -32,13 +64,15 @@ export default class PbDropdown extends PbEnhancedElement {
32
64
  connect() {
33
65
  // Store instance on element for DatePicker sync
34
66
  this.element._pbDropdownInstance = this;
67
+ this.cacheElements();
35
68
 
36
69
  this.keyboardHandler = new PbDropdownKeyboard(this);
37
70
  this.isMultiSelect = this.element.dataset.pbDropdownMultiSelect === "true";
71
+ this.closeOnClick = this.element.dataset.pbDropdownCloseOnClick || "any";
38
72
  this.formPillProps = this.element.dataset.formPillProps
39
73
  ? JSON.parse(this.element.dataset.formPillProps)
40
74
  : {};
41
- const baseInput = this.element.querySelector(DROPDOWN_INPUT);
75
+ const baseInput = this.baseInput;
42
76
  this.wasOriginallyRequired =
43
77
  baseInput && baseInput.hasAttribute("required");
44
78
  this.setDefaultValue();
@@ -61,6 +95,25 @@ export default class PbDropdown extends PbEnhancedElement {
61
95
  this.clearBtn.addEventListener("click", this.clearBtnHandler);
62
96
  }
63
97
  this.updateClearButton();
98
+
99
+ // Listen for clear and select events from external source
100
+ this.handleClearEventBound = this.handleClearEvent.bind(this);
101
+ document.addEventListener("pb:dropdown:clear", this.handleClearEventBound);
102
+ this.handleSelectEventBound = this.handleSelectEvent.bind(this);
103
+ document.addEventListener("pb:dropdown:select", this.handleSelectEventBound);
104
+
105
+ // Listen for custom_event_type to clear on custom events
106
+ const customEventTypeString = this.element.dataset.customEventType;
107
+ if (customEventTypeString) {
108
+ this.customClearEventTypes = customEventTypeString
109
+ .split(",")
110
+ .map((e) => e.trim())
111
+ .filter(Boolean);
112
+ this.handleCustomClearBound = this.handleCustomClearEvent.bind(this);
113
+ this.customClearEventTypes.forEach((eventType) => {
114
+ document.addEventListener(eventType, this.handleCustomClearBound);
115
+ });
116
+ }
64
117
  }
65
118
 
66
119
  disconnect() {
@@ -76,7 +129,7 @@ export default class PbDropdown extends PbEnhancedElement {
76
129
 
77
130
  // Clean up custom trigger click listener
78
131
  if (this.customTriggerClickHandler) {
79
- const customTrigger = this.element.querySelector(CUSTOM_DISPLAY_SELECTOR) || this.element
132
+ const customTrigger = this.customTrigger || this.element
80
133
  customTrigger.removeEventListener('click', this.customTriggerClickHandler)
81
134
  }
82
135
 
@@ -98,7 +151,7 @@ export default class PbDropdown extends PbEnhancedElement {
98
151
  // Clean up search input listeners
99
152
  if (this.searchInput) {
100
153
  if (this.searchInputFocusHandler) {
101
- const trigger = this.element.querySelector(TRIGGER_SELECTOR)
154
+ const trigger = this.trigger
102
155
  if (trigger) {
103
156
  trigger.removeEventListener('click', this.searchInputFocusHandler)
104
157
  }
@@ -112,6 +165,19 @@ export default class PbDropdown extends PbEnhancedElement {
112
165
  if (this.clearBtn && this.clearBtnHandler) {
113
166
  this.clearBtn.removeEventListener('click', this.clearBtnHandler)
114
167
  }
168
+
169
+ // Clean up external clear/select listeners
170
+ if (this.handleClearEventBound) {
171
+ document.removeEventListener("pb:dropdown:clear", this.handleClearEventBound)
172
+ }
173
+ if (this.handleSelectEventBound) {
174
+ document.removeEventListener("pb:dropdown:select", this.handleSelectEventBound)
175
+ }
176
+ if (this.customClearEventTypes && this.handleCustomClearBound) {
177
+ this.customClearEventTypes.forEach((eventType) => {
178
+ document.removeEventListener(eventType, this.handleCustomClearBound)
179
+ })
180
+ }
115
181
  }
116
182
 
117
183
  updateClearButton() {
@@ -122,14 +188,13 @@ export default class PbDropdown extends PbEnhancedElement {
122
188
  }
123
189
  const hasSelection = this.isMultiSelect
124
190
  ? this.selectedOptions.size > 0
125
- : Boolean(this.element.querySelector(DROPDOWN_INPUT).value);
191
+ : Boolean(this.baseInput?.value);
126
192
 
127
193
  this.clearBtn.style.display = hasSelection ? "" : "none";
128
194
  }
129
195
 
130
196
  bindEventListeners() {
131
- const customTrigger =
132
- this.element.querySelector(CUSTOM_DISPLAY_SELECTOR) || this.element;
197
+ const customTrigger = this.customTrigger || this.element;
133
198
  this.customTriggerClickHandler = (e) => {
134
199
  const label = e.target.closest(LABEL_SELECTOR);
135
200
  if (label && label.htmlFor) {
@@ -140,6 +205,12 @@ export default class PbDropdown extends PbEnhancedElement {
140
205
  trigger.focus();
141
206
  }
142
207
  }
208
+ if (
209
+ this.closeOnClick === "outside" &&
210
+ this.target?.contains(e.target)
211
+ ) {
212
+ return;
213
+ }
143
214
  this.toggleElement(this.target);
144
215
  }
145
216
  customTrigger.addEventListener("click", this.customTriggerClickHandler);
@@ -169,9 +240,7 @@ export default class PbDropdown extends PbEnhancedElement {
169
240
 
170
241
  // Focus the input when anyone clicks the wrapper
171
242
  this.searchInputFocusHandler = () => this.searchInput.focus()
172
- this.element
173
- .querySelector(TRIGGER_SELECTOR)
174
- ?.addEventListener('click', this.searchInputFocusHandler);
243
+ this.trigger?.addEventListener('click', this.searchInputFocusHandler);
175
244
 
176
245
  // Live filter
177
246
  this.searchInputHandler = (e) => this.handleSearch(e.target.value)
@@ -203,7 +272,7 @@ export default class PbDropdown extends PbEnhancedElement {
203
272
  adjustDropdownPosition(container) {
204
273
  if (!container) return;
205
274
 
206
- const wrapper = this.element.querySelector(".dropdown_wrapper");
275
+ const wrapper = this.dropdownWrapper;
207
276
  if (!wrapper) return;
208
277
 
209
278
  const wrapperRect = wrapper.getBoundingClientRect();
@@ -276,7 +345,7 @@ export default class PbDropdown extends PbEnhancedElement {
276
345
 
277
346
  handleOptionClick(event) {
278
347
  const option = event.target.closest(OPTION_SELECTOR);
279
- const hiddenInput = this.element.querySelector(DROPDOWN_INPUT);
348
+ const hiddenInput = this.baseInput;
280
349
 
281
350
  if (option) {
282
351
  const value = option.dataset.dropdownOptionLabel;
@@ -305,22 +374,158 @@ export default class PbDropdown extends PbEnhancedElement {
305
374
 
306
375
  handleDocumentClick(event) {
307
376
  if (event.target.closest(SEARCH_BAR_SELECTOR)) return;
308
- if (this.isClickOutside(event) && this.target.classList.contains("open")) {
377
+ const shouldCloseOnOutsideClick =
378
+ this.closeOnClick === "outside" || this.closeOnClick === "any";
379
+ if (
380
+ shouldCloseOnOutsideClick &&
381
+ this.isClickOutside(event) &&
382
+ this.target.classList.contains("open")
383
+ ) {
309
384
  this.hideElement(this.target);
310
385
  this.updateArrowDisplay(false);
311
386
  }
312
387
  }
313
388
 
389
+ // ----- External events handling section -----
390
+ // Handles pb:dropdown:clear - clear this dropdown when event.detail.dropdownId matches.
391
+ handleClearEvent(event) {
392
+ const targetId = event.detail?.dropdownId;
393
+ if (targetId && this.element.id === targetId) {
394
+ this.clearSelection();
395
+ }
396
+ }
397
+
398
+ // Handles custom_event_type events (e.g. turbo:submit-end) - clear when detail.dropdownId matches or is omitted.
399
+ handleCustomClearEvent(event) {
400
+ const targetId = event.detail?.dropdownId;
401
+ if (targetId == null || this.element.id === targetId) {
402
+ this.clearSelection();
403
+ }
404
+ }
405
+
406
+ // Handles pb:dropdown:select - set dropdown selection by option id(s).
407
+ // Single: detail: { dropdownId, optionId }. Multi: detail: { dropdownId, optionIds: ['id1', 'id2'] }.
408
+ handleSelectEvent(event) {
409
+ const targetId = event.detail?.dropdownId;
410
+ if (!targetId || this.element.id !== targetId) return;
411
+
412
+ const optionId = event.detail?.optionId;
413
+ const optionIds = event.detail?.optionIds;
414
+ if (optionId != null) {
415
+ this.setSelectionByOptionId(optionId);
416
+ } else if (Array.isArray(optionIds)) {
417
+ this.setSelectionByOptionIds(optionIds);
418
+ }
419
+ }
420
+
421
+ // Set single-select dropdown to the option with the given id. No-op if id not found.
422
+ setSelectionByOptionId(optionId) {
423
+ if (this.isMultiSelect) return;
424
+ const hiddenInput = this.baseInput;
425
+ const optionEls = Array.from(this.element.querySelectorAll(OPTION_SELECTOR));
426
+ const selectedOption = optionEls.find((opt) => {
427
+ try {
428
+ return JSON.parse(opt.dataset.dropdownOptionLabel).id === optionId;
429
+ } catch {
430
+ return false;
431
+ }
432
+ });
433
+ if (!selectedOption) return;
434
+
435
+ optionEls.forEach((opt) => opt.classList.remove("pb_dropdown_option_selected"));
436
+ selectedOption.classList.add("pb_dropdown_option_selected");
437
+ if (hiddenInput) hiddenInput.value = optionId;
438
+ const optionData = JSON.parse(selectedOption.dataset.dropdownOptionLabel);
439
+ const customDisplayElement = this.element.querySelector(
440
+ '[data-dropdown-trigger-custom-display]',
441
+ );
442
+ if (customDisplayElement) {
443
+ this.setTriggerElementText("");
444
+ customDisplayElement.style.display = "block";
445
+ customDisplayElement.style.paddingRight = "8px";
446
+ } else {
447
+ this.setTriggerElementText(optionData.label);
448
+ }
449
+ if (this.searchInput) {
450
+ this.searchInput.value = optionData.label;
451
+ }
452
+
453
+ if (optionData.formatted_start_date && optionData.formatted_end_date) {
454
+ const startDateId = this.element.dataset.startDateId;
455
+ const endDateId = this.element.dataset.endDateId;
456
+ const controlsStartId = this.element.dataset.controlsStartId;
457
+ const controlsEndId = this.element.dataset.controlsEndId;
458
+ if (startDateId) {
459
+ const startDateInput = document.getElementById(startDateId);
460
+ if (startDateInput) startDateInput.value = optionData.formatted_start_date;
461
+ }
462
+ if (endDateId) {
463
+ const endDateInput = document.getElementById(endDateId);
464
+ if (endDateInput) endDateInput.value = optionData.formatted_end_date;
465
+ }
466
+ const syncDatePickers = () => {
467
+ if (controlsStartId) {
468
+ const startPicker = document.querySelector(`#${CSS.escape(controlsStartId)}`)?._flatpickr;
469
+ if (startPicker) startPicker.setDate(optionData.formatted_start_date, true, "m/d/Y");
470
+ }
471
+ if (controlsEndId) {
472
+ const endPicker = document.querySelector(`#${CSS.escape(controlsEndId)}`)?._flatpickr;
473
+ if (endPicker) endPicker.setDate(optionData.formatted_end_date, true, "m/d/Y");
474
+ }
475
+ };
476
+ syncDatePickers();
477
+ setTimeout(syncDatePickers, 100);
478
+ setTimeout(syncDatePickers, 300);
479
+ }
480
+
481
+ this.updateClearButton();
482
+ this.emitSelectionChange();
483
+ }
484
+
485
+ // Set multi-select dropdown to the options with the given ids. Invalid ids are skipped.
486
+ setSelectionByOptionIds(optionIds) {
487
+ if (!this.isMultiSelect || !optionIds.length) return;
488
+ const optionEls = Array.from(this.element.querySelectorAll(OPTION_SELECTOR));
489
+ this.selectedOptions.clear();
490
+ optionEls.forEach((opt) => {
491
+ opt.classList.remove("pb_dropdown_option_selected");
492
+ opt.style.display = "";
493
+ });
494
+
495
+ optionIds.forEach((id) => {
496
+ const opt = optionEls.find((o) => {
497
+ try {
498
+ return JSON.parse(o.dataset.dropdownOptionLabel).id === id;
499
+ } catch {
500
+ return false;
501
+ }
502
+ });
503
+ if (opt) {
504
+ const raw = opt.dataset.dropdownOptionLabel;
505
+ this.selectedOptions.add(raw);
506
+ opt.style.display = "none";
507
+ }
508
+ });
509
+
510
+ this.updatePills();
511
+ this.updateClearButton();
512
+ this.adjustDropdownHeight();
513
+ this.syncHiddenInputs();
514
+ this.emitSelectionChange();
515
+ }
516
+ // ----- End External events handling section -----
517
+
314
518
  isClickOutside(event) {
315
519
  const label = event.target.closest(LABEL_SELECTOR);
316
520
  if (label && this.element.contains(label)) return false;
317
- const customTrigger = this.element.querySelector(CUSTOM_DISPLAY_SELECTOR);
521
+ const customTrigger = this.customTrigger;
318
522
  if (customTrigger) {
319
- return !customTrigger.contains(event.target);
523
+ const clickInTrigger = customTrigger.contains(event.target);
524
+ const clickInContainer = this.target?.contains(event.target);
525
+ return !clickInTrigger && !clickInContainer;
320
526
  } else {
321
- const triggerElement = this.element.querySelector(TRIGGER_SELECTOR);
322
- const containerElement =
323
- this.element.parentNode.querySelector(CONTAINER_SELECTOR);
527
+ const triggerElement = this.trigger;
528
+ const containerElement = this.element.querySelector(CONTAINER_SELECTOR);
324
529
 
325
530
  const isOutsideTrigger = triggerElement
326
531
  ? !triggerElement.contains(event.target)
@@ -339,7 +544,7 @@ export default class PbDropdown extends PbEnhancedElement {
339
544
  if (this.isMultiSelect) {
340
545
  detail = Array.from(this.selectedOptions).map(JSON.parse);
341
546
  } else {
342
- const hiddenInput = this.element.querySelector(DROPDOWN_INPUT);
547
+ const hiddenInput = this.baseInput;
343
548
  detail = hiddenInput.value
344
549
  ? JSON.parse(
345
550
  this.element.querySelector(
@@ -458,12 +663,16 @@ export default class PbDropdown extends PbEnhancedElement {
458
663
  this.emitSelectionChange();
459
664
  }
460
665
 
461
- const customTrigger = this.element.querySelector(CUSTOM_DISPLAY_SELECTOR);
462
- if (customTrigger) {
463
- if (this.target.classList.contains("open")) {
464
- this.hideElement(this.target);
465
- this.updateArrowDisplay(false);
466
- }
666
+ const customTrigger = this.customTrigger;
667
+ const shouldCloseOnOptionSelect =
668
+ this.closeOnClick === "any" || this.closeOnClick === "inside";
669
+ if (
670
+ customTrigger &&
671
+ shouldCloseOnOptionSelect &&
672
+ this.target.classList.contains("open")
673
+ ) {
674
+ this.hideElement(this.target);
675
+ this.updateArrowDisplay(false);
467
676
  }
468
677
 
469
678
  const options = this.element.querySelectorAll(OPTION_SELECTOR);
@@ -478,7 +687,7 @@ export default class PbDropdown extends PbEnhancedElement {
478
687
  this.adjustDropdownHeight();
479
688
  }
480
689
  });
481
- this.element.querySelector(DROPDOWN_INPUT).value = Array.from(
690
+ this.baseInput.value = Array.from(
482
691
  this.selectedOptions,
483
692
  )
484
693
  .map((opt) => JSON.parse(opt).id)
@@ -532,26 +741,26 @@ export default class PbDropdown extends PbEnhancedElement {
532
741
  }
533
742
 
534
743
  toggleElement(elem) {
535
- if (elem.classList.contains("open")) {
536
- this.hideElement(elem);
537
- this.updateArrowDisplay(false);
538
- return;
539
- }
540
- this.showElement(elem);
541
- this.updateArrowDisplay(true);
744
+ const isOpen = toggleVisibility({
745
+ isVisible: elem.classList.contains("open"),
746
+ onHide: () => this.hideElement(elem),
747
+ onShow: () => this.showElement(elem),
748
+ });
749
+
750
+ this.updateArrowDisplay(isOpen);
542
751
  }
543
752
 
544
753
  updateArrowDisplay(isOpen) {
545
- const downArrow = this.element.querySelector(DOWN_ARROW_SELECTOR);
546
- const upArrow = this.element.querySelector(UP_ARROW_SELECTOR);
547
- if (downArrow && upArrow) {
548
- downArrow.style.display = isOpen ? "none" : "inline-block";
549
- upArrow.style.display = isOpen ? "inline-block" : "none";
550
- }
754
+ setArrowVisibility({
755
+ rootElement: this.element,
756
+ downSelector: DOWN_ARROW_SELECTOR,
757
+ upSelector: UP_ARROW_SELECTOR,
758
+ showDownArrow: !isOpen,
759
+ });
551
760
  }
552
761
 
553
762
  handleFormValidation() {
554
- const hiddenInput = this.element.querySelector(DROPDOWN_INPUT);
763
+ const hiddenInput = this.baseInput;
555
764
 
556
765
  hiddenInput.addEventListener(
557
766
  "invalid",
@@ -594,7 +803,7 @@ export default class PbDropdown extends PbEnhancedElement {
594
803
  }
595
804
 
596
805
  setDefaultValue() {
597
- const hiddenInput = this.element.querySelector(DROPDOWN_INPUT);
806
+ const hiddenInput = this.baseInput;
598
807
  const optionEls = Array.from(
599
808
  this.element.querySelectorAll(OPTION_SELECTOR),
600
809
  );
@@ -707,7 +916,7 @@ export default class PbDropdown extends PbEnhancedElement {
707
916
  }
708
917
 
709
918
  resetDropdownValue() {
710
- const hiddenInput = this.element.querySelector(DROPDOWN_INPUT);
919
+ const hiddenInput = this.baseInput;
711
920
  const options = this.element.querySelectorAll(OPTION_SELECTOR);
712
921
  options.forEach((option) => {
713
922
  option.classList.remove("pb_dropdown_option_selected");
@@ -716,7 +925,7 @@ export default class PbDropdown extends PbEnhancedElement {
716
925
 
717
926
  hiddenInput.value = "";
718
927
 
719
- const defaultPlaceholder = this.element.querySelector(DROPDOWN_PLACEHOLDER);
928
+ const defaultPlaceholder = this.placeholder;
720
929
  this.setTriggerElementText(defaultPlaceholder.dataset.dropdownPlaceholder);
721
930
 
722
931
  if (this.searchInput) {
@@ -915,7 +1124,7 @@ export default class PbDropdown extends PbEnhancedElement {
915
1124
  .querySelectorAll('input[data-generated="true"]')
916
1125
  .forEach((n) => n.remove());
917
1126
 
918
- const baseInput = this.element.querySelector(DROPDOWN_INPUT);
1127
+ const baseInput = this.baseInput;
919
1128
  if (!baseInput) return;
920
1129
  // for multi_select, for each selectedOption, create a hidden input
921
1130
  const name = baseInput.getAttribute("name");
@@ -953,10 +1162,10 @@ export default class PbDropdown extends PbEnhancedElement {
953
1162
  this.adjustDropdownHeight();
954
1163
  });
955
1164
 
956
- const hiddenInput = this.element.querySelector(DROPDOWN_INPUT);
1165
+ const hiddenInput = this.baseInput;
957
1166
  if (hiddenInput) hiddenInput.value = "";
958
1167
 
959
- const placeholder = this.element.querySelector(DROPDOWN_PLACEHOLDER);
1168
+ const placeholder = this.placeholder;
960
1169
  if (placeholder)
961
1170
  this.setTriggerElementText(placeholder.dataset.dropdownPlaceholder);
962
1171
  }
@@ -44,6 +44,7 @@ const DropdownTrigger = (props: DropdownTriggerProps) => {
44
44
 
45
45
  const {
46
46
  autocomplete,
47
+ blankSelection,
47
48
  clearable,
48
49
  error,
49
50
  errorId,
@@ -57,6 +58,7 @@ const DropdownTrigger = (props: DropdownTriggerProps) => {
57
58
  isInputFocused,
58
59
  label: contextLabel,
59
60
  multiSelect,
61
+ optionsWithBlankSelection,
60
62
  selected,
61
63
  selectId,
62
64
  setIsInputFocused,
@@ -240,20 +242,22 @@ const DropdownTrigger = (props: DropdownTriggerProps) => {
240
242
  onClick: (e: Event) => {e.stopPropagation();handleWrapperClick()}
241
243
  }}
242
244
  key={`${isDropDownClosed ? "chevron-down" : "chevron-up"}`}
243
- >
244
- {
245
- clearable !== false && selectedArray.length > 0 && (
246
- <div onClick={(e)=>{e.stopPropagation();handleBackspace()}}>
247
- <Icon
248
- cursor="pointer"
249
- dark={dark}
250
- icon="times"
251
- paddingRight="xs"
252
- size="sm"
253
- />
254
- </div>
255
- )
256
- }
245
+ >
246
+ {(!blankSelection || selected?.value !== optionsWithBlankSelection?.[0]?.value) && (
247
+ <>
248
+ {clearable !== false && selectedArray.length > 0 && (
249
+ <div onClick={(e)=>{e.stopPropagation();handleBackspace()}}>
250
+ <Icon
251
+ cursor="pointer"
252
+ dark={dark}
253
+ icon="times"
254
+ paddingRight="xs"
255
+ size="sm"
256
+ />
257
+ </div>
258
+ )}
259
+ </>
260
+ )}
257
261
  <Icon
258
262
  cursor="pointer"
259
263
  dark={dark}
@@ -261,6 +265,7 @@ const DropdownTrigger = (props: DropdownTriggerProps) => {
261
265
  size="sm"
262
266
  />
263
267
  </Body>
268
+
264
269
  </FlexItem>
265
270
  </Flex>
266
271
  </>
@@ -71,6 +71,10 @@ $confirmation_toast_colors: (
71
71
  }
72
72
  }
73
73
 
74
+ .nav_margin_top {
75
+ margin-top: 49px;
76
+ }
77
+
74
78
  .pb_fixed_confirmation_toast_kit_neutral,
75
79
  .pb_fixed_confirmation_toast_kit_neutral_multi_line {
76
80
  background: $text_lt_light;
@@ -26,6 +26,7 @@ type FixedConfirmationToastProps = {
26
26
  icon?: string,
27
27
  id?: string;
28
28
  multiLine?: boolean;
29
+ navMarginTop?: boolean;
29
30
  onClose?: VoidCallback;
30
31
  open?: boolean;
31
32
  status?: "success" | "error" | "neutral" | "tip";
@@ -47,6 +48,7 @@ const FixedConfirmationToast = (props: FixedConfirmationToastProps): React.React
47
48
  htmlOptions = {},
48
49
  icon,
49
50
  multiLine = false,
51
+ navMarginTop = false,
50
52
  onClose = () => undefined,
51
53
  open = true,
52
54
  status = "neutral",
@@ -61,6 +63,7 @@ const FixedConfirmationToast = (props: FixedConfirmationToastProps): React.React
61
63
  const css = classnames(
62
64
  `pb_fixed_confirmation_toast_kit_${status}${multiLine ? '_multi_line' : ''}`,
63
65
  { [`positioned_toast ${vertical} ${horizontal}`]: vertical && horizontal },
66
+ { [`nav_margin_top`]: navMarginTop },
64
67
  `${iconClass}`,
65
68
  globalProps(props, { zIndex }),
66
69
  className
@@ -0,0 +1,46 @@
1
+ <%= pb_rails("button", props: { text: "Top Nav Toast", variant: "secondary", data: { toast: "#top-nav" } }) %>
2
+
3
+ <%= pb_rails("fixed_confirmation_toast", props: {
4
+ classname: "top-nav-margin",
5
+ closeable: true,
6
+ id: "top-nav",
7
+ multi_line: true,
8
+ text: "Top nav Margin.",
9
+ status: "tip",
10
+ vertical: "top",
11
+ horizontal: "center",
12
+ nav_margin_top: true
13
+ }) %>
14
+
15
+
16
+
17
+ <script type="text/javascript">
18
+ const navtoast = document.querySelectorAll(".top-nav-margin")
19
+ const navbutton = document.querySelectorAll("button[data-toast]")
20
+
21
+ const hideNavToast = () => {
22
+ navtoast.forEach((toast) => {
23
+ toast.style.display = "none"
24
+ })
25
+ }
26
+
27
+ // Hide toasts immediately
28
+ hideNavToast()
29
+
30
+ // Handle various page load/restore events
31
+ window.addEventListener('pageshow', hideNavToast)
32
+ document.addEventListener('DOMContentLoaded', hideNavToast)
33
+ document.addEventListener('turbolinks:load', hideNavToast)
34
+ document.addEventListener('turbo:load', hideNavToast)
35
+
36
+ navbutton.forEach((button) => {
37
+ button.onclick = () => {
38
+ hideNavToast()
39
+ let toast = document.querySelector(button.getAttribute("data-toast"))
40
+
41
+ if (toast) {
42
+ toast.style.display = "flex"
43
+ }
44
+ }
45
+ })
46
+ </script>
@@ -0,0 +1,42 @@
1
+ import React, { useState } from 'react'
2
+
3
+ import Button from '../../pb_button/_button'
4
+ import FixedConfirmationToast from '../_fixed_confirmation_toast'
5
+
6
+ const FixedConfirmationToastNavMargin = (props) => {
7
+ const [openToast, setOpenToast] = useState(false)
8
+
9
+
10
+ const handleClickShort = () => {
11
+ setOpenToast(true)
12
+ }
13
+
14
+ const handleCloseShort = () => {
15
+ setOpenToast(false)
16
+ }
17
+
18
+ return (
19
+ <div>
20
+ <Button
21
+ onClick={handleClickShort}
22
+ text="Top Nav Margin"
23
+ variant="secondary"
24
+ {...props}
25
+ />
26
+
27
+ <FixedConfirmationToast
28
+ closeable
29
+ horizontal='center'
30
+ navMarginTop
31
+ onClose={handleCloseShort}
32
+ open={openToast}
33
+ status="tip"
34
+ text="Top nav Margin."
35
+ vertical='top'
36
+ {...props}
37
+ />
38
+ </div>
39
+ )
40
+ }
41
+
42
+ export default FixedConfirmationToastNavMargin
@@ -0,0 +1 @@
1
+ Use the `nav_margin_top` prop to position the toast lower on the page. This is useful for cases where we want to position the toast below a header or nav.
@@ -0,0 +1 @@
1
+ Use the `navMarginTop` prop to position the toast lower on the page. This is useful for cases where we want to position the toast below a header or nav.
@@ -2,6 +2,7 @@ examples:
2
2
 
3
3
  rails:
4
4
  - fixed_confirmation_toast_default: Default
5
+ - fixed_confirmation_toast_nav_margin: Nav Margin Top
5
6
  - fixed_confirmation_toast_multi_line: Multi Line
6
7
  - fixed_confirmation_toast_close: Click to Close
7
8
  - fixed_confirmation_toast_positions: Click to Show Positions
@@ -12,6 +13,7 @@ examples:
12
13
 
13
14
  react:
14
15
  - fixed_confirmation_toast_default: Default
16
+ - fixed_confirmation_toast_nav_margin: Nav Margin Top
15
17
  - fixed_confirmation_toast_multi_line: Multi Line
16
18
  - fixed_confirmation_toast_close: Click to Close
17
19
  - fixed_confirmation_toast_positions: Click to Show Positions