playbook_ui 18.2.0.pre.rc.2 → 18.2.0

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 (116) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/pb_background/_background.tsx +3 -1
  3. data/app/pb_kits/playbook/pb_background/kit.schema.json +1 -0
  4. data/app/pb_kits/playbook/pb_caption/_caption.tsx +3 -1
  5. data/app/pb_kits/playbook/pb_caption/kit.schema.json +1 -0
  6. data/app/pb_kits/playbook/pb_container/_container.tsx +3 -1
  7. data/app/pb_kits/playbook/pb_container/kit.schema.json +1 -0
  8. data/app/pb_kits/playbook/pb_currency/_currency.tsx +3 -1
  9. data/app/pb_kits/playbook/pb_currency/kit.schema.json +1 -0
  10. data/app/pb_kits/playbook/pb_dashboard_value/_dashboard_value.tsx +3 -1
  11. data/app/pb_kits/playbook/pb_dashboard_value/kit.schema.json +1 -0
  12. data/app/pb_kits/playbook/pb_detail/_detail.tsx +3 -1
  13. data/app/pb_kits/playbook/pb_detail/kit.schema.json +1 -0
  14. data/app/pb_kits/playbook/pb_dropdown/_dropdown.tsx +6 -5
  15. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_custom.md +7 -2
  16. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_custom_rails.md +7 -2
  17. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_enable_time.html.erb +33 -0
  18. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_enable_time.jsx +42 -0
  19. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_enable_time.md +24 -0
  20. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_highlight.jsx +101 -0
  21. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_highlight.md +3 -0
  22. data/app/pb_kits/playbook/pb_dropdown/docs/example.yml +3 -0
  23. data/app/pb_kits/playbook/pb_dropdown/docs/index.js +2 -0
  24. data/app/pb_kits/playbook/pb_dropdown/index.js +19 -10
  25. data/app/pb_kits/playbook/pb_dropdown/quickpick/index.ts +60 -18
  26. data/app/pb_kits/playbook/pb_dropdown/quickpick_helper.rb +92 -3
  27. data/app/pb_kits/playbook/pb_dropdown/utilities/index.ts +8 -1
  28. data/app/pb_kits/playbook/pb_dropdown/utilities/quickpickDateSync.ts +137 -0
  29. data/app/pb_kits/playbook/pb_filter/Filter/InteractiveFilter.tsx +1 -1
  30. data/app/pb_kits/playbook/pb_highlight/_highlight.tsx +3 -1
  31. data/app/pb_kits/playbook/pb_highlight/kit.schema.json +1 -0
  32. data/app/pb_kits/playbook/pb_icon_stat_value/_icon_stat_value.tsx +3 -1
  33. data/app/pb_kits/playbook/pb_icon_stat_value/kit.schema.json +1 -0
  34. data/app/pb_kits/playbook/pb_icon_value/_icon_value.tsx +3 -1
  35. data/app/pb_kits/playbook/pb_icon_value/kit.schema.json +1 -0
  36. data/app/pb_kits/playbook/pb_label_value/_label_value.tsx +3 -1
  37. data/app/pb_kits/playbook/pb_label_value/kit.schema.json +1 -0
  38. data/app/pb_kits/playbook/pb_map/docs/_map_default.md +3 -3
  39. data/app/pb_kits/playbook/pb_message/_message.tsx +3 -1
  40. data/app/pb_kits/playbook/pb_message/kit.schema.json +1 -0
  41. data/app/pb_kits/playbook/pb_multiple_users/_multiple_users.tsx +3 -1
  42. data/app/pb_kits/playbook/pb_multiple_users/kit.schema.json +1 -0
  43. data/app/pb_kits/playbook/pb_multiple_users_stacked/_multiple_users_stacked.tsx +4 -2
  44. data/app/pb_kits/playbook/pb_multiple_users_stacked/kit.schema.json +1 -0
  45. data/app/pb_kits/playbook/pb_rich_text_editor/TipTap/MoreExtensionsDropdown.tsx +10 -3
  46. data/app/pb_kits/playbook/pb_rich_text_editor/TipTap/Toolbar.tsx +16 -1
  47. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_playground.json +2 -3
  48. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_playground.overrides.json +2 -3
  49. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_attributes.jsx +1 -5
  50. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_default.jsx +1 -5
  51. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_default.md +2 -3
  52. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_focus.jsx +1 -5
  53. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_height.jsx +3 -13
  54. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_inline.jsx +1 -5
  55. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_label.jsx +2 -3
  56. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_markdown_support.jsx +1 -2
  57. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_min_height.jsx +3 -13
  58. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_preview.jsx +1 -5
  59. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_required_indicator.jsx +1 -5
  60. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_simple.jsx +1 -5
  61. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_sticky.jsx +1 -5
  62. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_templates.jsx +1 -5
  63. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_more_extensions.jsx +3 -8
  64. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_more_extensions.md +1 -1
  65. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_rails_default.md +2 -2
  66. data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor.test.js +2 -1
  67. data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor_advanced.test.js +7 -5
  68. data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor_rails.js +15 -4
  69. data/app/pb_kits/playbook/pb_stat_value/_stat_value.tsx +3 -1
  70. data/app/pb_kits/playbook/pb_stat_value/kit.schema.json +1 -0
  71. data/app/pb_kits/playbook/pb_table/_table.tsx +70 -197
  72. data/app/pb_kits/playbook/pb_table/index.ts +53 -122
  73. data/app/pb_kits/playbook/pb_table/styles/_sticky_columns.scss +26 -3
  74. data/app/pb_kits/playbook/pb_table/utilities/stickyColumns.test.ts +266 -0
  75. data/app/pb_kits/playbook/pb_table/utilities/stickyColumns.ts +145 -0
  76. data/app/pb_kits/playbook/pb_title_count/_title_count.tsx +3 -1
  77. data/app/pb_kits/playbook/pb_title_count/kit.schema.json +1 -0
  78. data/app/pb_kits/playbook/pb_title_detail/_title_detail.tsx +3 -1
  79. data/app/pb_kits/playbook/pb_title_detail/kit.schema.json +1 -0
  80. data/app/pb_kits/playbook/utilities/test/globalProps/onClick.test.js +237 -0
  81. data/dist/ai/all-schemas.json +16 -0
  82. data/dist/ai/forms.json +1 -1
  83. data/dist/ai/index.json +2 -2
  84. data/dist/ai/kits/background.schema.json +1 -0
  85. data/dist/ai/kits/caption.schema.json +1 -0
  86. data/dist/ai/kits/container.schema.json +1 -0
  87. data/dist/ai/kits/currency.schema.json +1 -0
  88. data/dist/ai/kits/dashboard_value.schema.json +1 -0
  89. data/dist/ai/kits/detail.schema.json +1 -0
  90. data/dist/ai/kits/highlight.schema.json +1 -0
  91. data/dist/ai/kits/icon_stat_value.schema.json +1 -0
  92. data/dist/ai/kits/icon_value.schema.json +1 -0
  93. data/dist/ai/kits/label_value.schema.json +1 -0
  94. data/dist/ai/kits/message.schema.json +1 -0
  95. data/dist/ai/kits/multiple_users.schema.json +1 -0
  96. data/dist/ai/kits/multiple_users_stacked.schema.json +1 -0
  97. data/dist/ai/kits/stat_value.schema.json +1 -0
  98. data/dist/ai/kits/title_count.schema.json +1 -0
  99. data/dist/ai/kits/title_detail.schema.json +1 -0
  100. data/dist/ai/playgrounds/rich_text_editor.json +1 -1
  101. data/dist/chunks/{_detail-BqqzPTNr.js → _detail-Bsvl694i.js} +2 -2
  102. data/dist/chunks/{_pb_line_graph-BGyCJjgG.js → _pb_line_graph-Du7xE9vl.js} +1 -1
  103. data/dist/chunks/_table-Bf6LGyL9.js +1 -0
  104. data/dist/chunks/_typeahead-w6S5Vx4b.js +1 -0
  105. data/dist/chunks/{globalProps-K1DCn3il.js → globalProps-B120NbSJ.js} +1 -1
  106. data/dist/chunks/{lib-7vm9BMqp.js → lib-Buo6zHEw.js} +1 -1
  107. data/dist/chunks/quickpickDateSync-DLDx4zce.js +1 -0
  108. data/dist/chunks/{time_picker_helper-BZutIzB3.js → time_picker_helper-CYGg0UQ0.js} +1 -1
  109. data/dist/chunks/vendor.js +2 -2
  110. data/dist/playbook-rails-react-bindings.js +1 -1
  111. data/dist/playbook-rails.js +1 -1
  112. data/dist/playbook.css +1 -1
  113. data/lib/playbook/version.rb +1 -1
  114. metadata +19 -13
  115. data/dist/chunks/_table-CEZU0qUK.js +0 -1
  116. data/dist/chunks/_typeahead-C7iPR5ZF.js +0 -1
@@ -0,0 +1,266 @@
1
+ import { applyStickyColumns } from "./stickyColumns"
2
+
3
+ const stubWidth = (element: HTMLElement, width: number) => {
4
+ Object.defineProperty(element, "offsetWidth", {
5
+ configurable: true,
6
+ get: () => Math.floor(width),
7
+ })
8
+ element.getBoundingClientRect = () =>
9
+ ({
10
+ width,
11
+ height: 40,
12
+ top: 0,
13
+ left: 0,
14
+ bottom: 40,
15
+ right: width,
16
+ x: 0,
17
+ y: 0,
18
+ toJSON: () => ({}),
19
+ } as DOMRect)
20
+ }
21
+
22
+ const buildStickyTable = () => {
23
+ document.body.innerHTML = `
24
+ <table class="pb_table">
25
+ <thead>
26
+ <tr>
27
+ <th data-sticky-id="1">Col 1</th>
28
+ <th data-sticky-id="2">Col 2</th>
29
+ <th data-sticky-id="3">Col 3</th>
30
+ <th>Col 4</th>
31
+ </tr>
32
+ </thead>
33
+ <tbody>
34
+ <tr>
35
+ <td data-sticky-id="1">A</td>
36
+ <td data-sticky-id="2">B</td>
37
+ <td data-sticky-id="3">C</td>
38
+ <td>D</td>
39
+ </tr>
40
+ </tbody>
41
+ </table>
42
+ `
43
+
44
+ return document.querySelector("table") as HTMLTableElement
45
+ }
46
+
47
+ describe("applyStickyColumns", () => {
48
+ afterEach(() => {
49
+ document.body.innerHTML = ""
50
+ })
51
+
52
+ test("scopes sticky left offsets to the given table", () => {
53
+ document.body.innerHTML = `
54
+ <table id="other">
55
+ <thead><tr><th data-sticky-id="1">Other</th></tr></thead>
56
+ </table>
57
+ <table id="target" class="pb_table">
58
+ <thead>
59
+ <tr>
60
+ <th data-sticky-id="1">Col 1</th>
61
+ <th data-sticky-id="2">Col 2</th>
62
+ </tr>
63
+ </thead>
64
+ <tbody>
65
+ <tr>
66
+ <td data-sticky-id="1">A</td>
67
+ <td data-sticky-id="2">B</td>
68
+ </tr>
69
+ </tbody>
70
+ </table>
71
+ `
72
+
73
+ const target = document.getElementById("target") as HTMLTableElement
74
+ const otherHeader = document.querySelector(
75
+ "#other th[data-sticky-id='1']"
76
+ ) as HTMLElement
77
+ const targetHeader1 = target.querySelector(
78
+ "th[data-sticky-id='1']"
79
+ ) as HTMLElement
80
+ const targetHeader2 = target.querySelector(
81
+ "th[data-sticky-id='2']"
82
+ ) as HTMLElement
83
+
84
+ stubWidth(targetHeader1, 100)
85
+ stubWidth(targetHeader2, 80)
86
+ stubWidth(otherHeader, 250)
87
+
88
+ applyStickyColumns(target, ["1", "2"], "left")
89
+
90
+ expect(otherHeader.classList.contains("sticky")).toBe(false)
91
+ expect(otherHeader.style.left).toBe("")
92
+ expect(targetHeader1.style.left).toBe("0px")
93
+ expect(targetHeader2.style.left).toBe("100px")
94
+ })
95
+
96
+ test("uses fractional widths so sticky left columns stay flush", () => {
97
+ const table = buildStickyTable()
98
+ const header1 = table.querySelector(
99
+ "th[data-sticky-id='1']"
100
+ ) as HTMLElement
101
+ const header2 = table.querySelector(
102
+ "th[data-sticky-id='2']"
103
+ ) as HTMLElement
104
+ const header3 = table.querySelector(
105
+ "th[data-sticky-id='3']"
106
+ ) as HTMLElement
107
+ const cell2 = table.querySelector(
108
+ "td[data-sticky-id='2']"
109
+ ) as HTMLElement
110
+
111
+ stubWidth(header1, 100.4)
112
+ stubWidth(header2, 80.2)
113
+ stubWidth(header3, 60)
114
+
115
+ applyStickyColumns(table, ["1", "2", "3"], "left")
116
+
117
+ expect(header1.style.left).toBe("0px")
118
+ expect(header1.classList.contains("with-border-right")).toBe(true)
119
+ // Must not use integer offsetWidth (100) or a gap appears at some zooms
120
+ expect(header2.style.left).toBe("100.4px")
121
+ expect(header2.classList.contains("with-border-right")).toBe(true)
122
+ expect(cell2.style.left).toBe(header2.style.left)
123
+ expect(header3.style.left).toBe("180.6px")
124
+ expect(header3.classList.contains("sticky-left-shadow")).toBe(true)
125
+ expect(header3.classList.contains("with-border-right")).toBe(false)
126
+ })
127
+
128
+ test("applies sticky right offsets from the trailing edge", () => {
129
+ const table = buildStickyTable()
130
+ const header1 = table.querySelector(
131
+ "th[data-sticky-id='1']"
132
+ ) as HTMLElement
133
+ const header2 = table.querySelector(
134
+ "th[data-sticky-id='2']"
135
+ ) as HTMLElement
136
+ const header3 = table.querySelector(
137
+ "th[data-sticky-id='3']"
138
+ ) as HTMLElement
139
+
140
+ stubWidth(header1, 60)
141
+ stubWidth(header2, 80)
142
+ stubWidth(header3, 100)
143
+
144
+ applyStickyColumns(table, ["1", "2", "3"], "right")
145
+
146
+ // Right sticky walks ids in reverse: 3 at right:0, then 2, then 1 with shadow
147
+ expect(header3.style.right).toBe("0px")
148
+ expect(header3.classList.contains("with-border-left")).toBe(true)
149
+ expect(header2.style.right).toBe("100px")
150
+ expect(header1.style.right).toBe("180px")
151
+ expect(header1.classList.contains("sticky-right-shadow")).toBe(true)
152
+ })
153
+
154
+ test("does not mutate the column id array when applying right sticky", () => {
155
+ const table = buildStickyTable()
156
+ const ids = ["1", "2", "3"]
157
+ const header1 = table.querySelector(
158
+ "th[data-sticky-id='1']"
159
+ ) as HTMLElement
160
+ const header2 = table.querySelector(
161
+ "th[data-sticky-id='2']"
162
+ ) as HTMLElement
163
+ const header3 = table.querySelector(
164
+ "th[data-sticky-id='3']"
165
+ ) as HTMLElement
166
+
167
+ stubWidth(header1, 60)
168
+ stubWidth(header2, 80)
169
+ stubWidth(header3, 100)
170
+
171
+ applyStickyColumns(table, ids, "right")
172
+
173
+ expect(ids).toEqual(["1", "2", "3"])
174
+ })
175
+
176
+ test("pins every header row cell that shares a sticky id", () => {
177
+ document.body.innerHTML = `
178
+ <table class="pb_table">
179
+ <thead>
180
+ <tr>
181
+ <th data-sticky-id="1"></th>
182
+ <th colspan="2">Group</th>
183
+ </tr>
184
+ <tr>
185
+ <th data-sticky-id="1">Territory</th>
186
+ <th>Shadow</th>
187
+ <th>Delta</th>
188
+ </tr>
189
+ </thead>
190
+ <tbody>
191
+ <tr>
192
+ <td data-sticky-id="1">East</td>
193
+ <td>1</td>
194
+ <td>2</td>
195
+ </tr>
196
+ </tbody>
197
+ </table>
198
+ `
199
+
200
+ const table = document.querySelector("table") as HTMLTableElement
201
+ const headers = table.querySelectorAll(
202
+ "th[data-sticky-id='1']"
203
+ ) as NodeListOf<HTMLElement>
204
+ const cell = table.querySelector(
205
+ "td[data-sticky-id='1']"
206
+ ) as HTMLElement
207
+
208
+ // First header row cell is empty/narrow; second row is wider — use max width
209
+ stubWidth(headers[0], 40)
210
+ stubWidth(headers[1], 120)
211
+ stubWidth(cell, 120)
212
+
213
+ applyStickyColumns(table, ["1"], "left")
214
+
215
+ expect(headers[0].classList.contains("sticky")).toBe(true)
216
+ expect(headers[0].style.left).toBe("0px")
217
+ expect(headers[0].classList.contains("sticky-left-shadow")).toBe(true)
218
+ expect(headers[1].classList.contains("sticky")).toBe(true)
219
+ expect(headers[1].style.left).toBe("0px")
220
+ expect(headers[1].classList.contains("sticky-left-shadow")).toBe(true)
221
+ expect(cell.style.left).toBe("0px")
222
+ })
223
+
224
+ test("sets top on sticky header cells when sticky-header is enabled", () => {
225
+ document.body.innerHTML = `
226
+ <table class="pb_table sticky-header sticky-left-column">
227
+ <thead>
228
+ <tr>
229
+ <th data-sticky-id="1">Group</th>
230
+ <th>Other</th>
231
+ </tr>
232
+ <tr>
233
+ <th data-sticky-id="1">Sub</th>
234
+ <th>Col</th>
235
+ </tr>
236
+ </thead>
237
+ <tbody>
238
+ <tr>
239
+ <td data-sticky-id="1">A</td>
240
+ <td>B</td>
241
+ </tr>
242
+ </tbody>
243
+ </table>
244
+ `
245
+
246
+ const table = document.querySelector("table") as HTMLTableElement
247
+ const headers = table.querySelectorAll(
248
+ "th[data-sticky-id='1']"
249
+ ) as NodeListOf<HTMLElement>
250
+ const firstRow = table.querySelector("thead tr") as HTMLElement
251
+
252
+ Object.defineProperty(firstRow, "offsetHeight", {
253
+ configurable: true,
254
+ get: () => 48,
255
+ })
256
+ stubWidth(headers[0], 100)
257
+ stubWidth(headers[1], 100)
258
+
259
+ applyStickyColumns(table, ["1"], "left")
260
+
261
+ expect(headers[0].style.top).toBe("0px")
262
+ expect(headers[1].style.top).toBe("48px")
263
+ expect(headers[0].style.left).toBe("0px")
264
+ expect(headers[1].style.left).toBe("0px")
265
+ })
266
+ })
@@ -0,0 +1,145 @@
1
+ type StickySide = "left" | "right"
2
+
3
+ /**
4
+ * Prefer the more precise layout width. Fractional values avoid integer
5
+ * offsetWidth rounding that leaves gaps between sticky columns at some
6
+ * zoom/viewport sizes.
7
+ */
8
+ const measureStickyWidth = (element: HTMLElement): number => {
9
+ const rectWidth = element.getBoundingClientRect().width
10
+ return rectWidth > 0 ? rectWidth : element.offsetWidth
11
+ }
12
+
13
+ const formatStickyOffset = (offset: number): string =>
14
+ `${parseFloat(offset.toFixed(3))}px`
15
+
16
+ const applyStickySideClasses = (
17
+ element: HTMLElement,
18
+ isLastColumn: boolean,
19
+ borderClass: string,
20
+ shadowClass: string
21
+ ): void => {
22
+ if (!isLastColumn) {
23
+ element.classList.add(borderClass)
24
+ element.classList.remove(shadowClass)
25
+ } else {
26
+ element.classList.remove(borderClass)
27
+ element.classList.add(shadowClass)
28
+ }
29
+ }
30
+
31
+ const stickyHeaderSelector = (colId: string): string =>
32
+ `th[data-sticky-id="${colId}"], .pb_table_th[data-sticky-id="${colId}"]`
33
+
34
+ const stickyCellSelector = (colId: string): string =>
35
+ `td[data-sticky-id="${colId}"], .pb_table_td[data-sticky-id="${colId}"]`
36
+
37
+ /**
38
+ * Vertical offset within thead so sticky column header cells keep sticking
39
+ * with sticky-header on multi-row theads (row 2+ need top: row1Height, etc.).
40
+ */
41
+ const stickyHeaderRowTop = (header: HTMLElement): number => {
42
+ const row = header.closest("tr, .pb_table_tr") as HTMLElement | null
43
+ if (!row) return 0
44
+
45
+ let top = 0
46
+ let previous = row.previousElementSibling as HTMLElement | null
47
+ while (previous) {
48
+ if (previous instanceof HTMLElement) {
49
+ top += previous.offsetHeight
50
+ }
51
+ previous = previous.previousElementSibling as HTMLElement | null
52
+ }
53
+ return top
54
+ }
55
+
56
+ const applyStickyHeaderTop = (
57
+ table: Element,
58
+ headerElement: HTMLElement
59
+ ): void => {
60
+ if (!table.classList.contains("sticky-header")) return
61
+ headerElement.style.top = formatStickyOffset(stickyHeaderRowTop(headerElement))
62
+ }
63
+
64
+ export const applyStickyColumns = (
65
+ table: Element,
66
+ columnIds: string[],
67
+ side: StickySide
68
+ ): void => {
69
+ if (!columnIds.length) return
70
+
71
+ const orderedIds = side === "right" ? [...columnIds].reverse() : columnIds
72
+ const borderClass = side === "left" ? "with-border-right" : "with-border-left"
73
+ const shadowClass = side === "left" ? "sticky-left-shadow" : "sticky-right-shadow"
74
+
75
+ let accumulatedWidth = 0
76
+
77
+ orderedIds.forEach((colId, index) => {
78
+ const isLastColumn = index === orderedIds.length - 1
79
+ // Multi-row theads can repeat the same data-sticky-id on each header row —
80
+ // pin every matching th, not only the first.
81
+ const headers = table.querySelectorAll(stickyHeaderSelector(colId))
82
+ const cells = table.querySelectorAll(stickyCellSelector(colId))
83
+
84
+ if (!headers.length) return
85
+
86
+ const offset = accumulatedWidth
87
+ const offsetValue = formatStickyOffset(offset)
88
+ let columnWidth = 0
89
+
90
+ headers.forEach((header) => {
91
+ const headerElement = header as HTMLElement
92
+ headerElement.classList.add("sticky")
93
+ headerElement.style[side] = offsetValue
94
+ applyStickyHeaderTop(table, headerElement)
95
+ applyStickySideClasses(headerElement, isLastColumn, borderClass, shadowClass)
96
+ columnWidth = Math.max(columnWidth, measureStickyWidth(headerElement))
97
+ })
98
+
99
+ accumulatedWidth = offset + columnWidth
100
+
101
+ cells.forEach((cell) => {
102
+ const cellElement = cell as HTMLElement
103
+ cellElement.classList.add("sticky")
104
+ cellElement.style[side] = offsetValue
105
+ applyStickySideClasses(cellElement, isLastColumn, borderClass, shadowClass)
106
+ })
107
+ })
108
+
109
+ // Corner cells may use .sticky without data-sticky-id (e.g. colspan spanning
110
+ // multiple sticky columns). Still need top when sticky-header is on.
111
+ if (table.classList.contains("sticky-header")) {
112
+ table
113
+ .querySelectorAll("thead .sticky, .pb_table_thead .sticky")
114
+ .forEach((header) => {
115
+ applyStickyHeaderTop(table, header as HTMLElement)
116
+ })
117
+ }
118
+ }
119
+
120
+ /** Double rAF + fonts.ready so offsets are measured after layout settles. */
121
+ export const scheduleStickyColumns = (apply: () => void): (() => void) => {
122
+ let cancelled = false
123
+ let frameId = 0
124
+
125
+ const run = () => {
126
+ if (!cancelled) apply()
127
+ }
128
+
129
+ frameId = requestAnimationFrame(() => {
130
+ run()
131
+ frameId = requestAnimationFrame(run)
132
+ })
133
+
134
+ const fonts = (
135
+ document as Document & { fonts?: { ready?: Promise<unknown> } }
136
+ ).fonts
137
+ if (fonts?.ready) {
138
+ fonts.ready.then(run).catch((): undefined => undefined)
139
+ }
140
+
141
+ return () => {
142
+ cancelled = true
143
+ cancelAnimationFrame(frameId)
144
+ }
145
+ }
@@ -4,6 +4,7 @@ import classnames from 'classnames'
4
4
 
5
5
  import { buildAriaProps, buildCss, buildDataProps, buildHtmlProps } from '../utilities/props'
6
6
  import { globalProps } from '../utilities/globalProps'
7
+ import { GlobalEventProps, globalEventProps } from '../utilities/globalEventProps'
7
8
 
8
9
  import Body from '../pb_body/_body'
9
10
  import Title from '../pb_title/_title'
@@ -19,7 +20,7 @@ type TitleCountProps = {
19
20
  id?: string,
20
21
  title?: string,
21
22
  size?: "lg" | "sm",
22
- };
23
+ } & GlobalEventProps;
23
24
 
24
25
  const TitleCount = (props: TitleCountProps): React.ReactElement => {
25
26
  const {
@@ -50,6 +51,7 @@ const TitleCount = (props: TitleCountProps): React.ReactElement => {
50
51
  {...ariaProps}
51
52
  {...dataProps}
52
53
  {...htmlProps}
54
+ {...globalEventProps(props)}
53
55
  className={css}
54
56
  id={id}
55
57
  >
@@ -54,6 +54,7 @@
54
54
  }
55
55
  },
56
56
  "globalProps": true,
57
+ "globalEventProps": true,
57
58
  "usage": {
58
59
  "react": {
59
60
  "import": "import { TitleCount } from 'playbook-ui'",
@@ -2,6 +2,7 @@ import React from 'react'
2
2
  import classnames from 'classnames'
3
3
 
4
4
  import { GlobalProps, globalProps } from '../utilities/globalProps'
5
+ import { GlobalEventProps, globalEventProps } from '../utilities/globalEventProps'
5
6
  import { buildAriaProps, buildCss, buildDataProps, buildHtmlProps } from '../utilities/props'
6
7
 
7
8
  import Body from '../pb_body/_body'
@@ -16,7 +17,7 @@ type TitleDetailProps = {
16
17
  htmlOptions?: {[key: string]: string | number | boolean | (() => void)},
17
18
  id?: string,
18
19
  title: string,
19
- } & GlobalProps
20
+ } & GlobalProps & GlobalEventProps
20
21
 
21
22
  const TitleDetail = (props: TitleDetailProps): React.ReactElement => {
22
23
  const {
@@ -40,6 +41,7 @@ const TitleDetail = (props: TitleDetailProps): React.ReactElement => {
40
41
  {...ariaProps}
41
42
  {...dataProps}
42
43
  {...htmlProps}
44
+ {...globalEventProps(props)}
43
45
  className={classnames(pbCss, globalProps(props), className)}
44
46
  id={id}
45
47
  >
@@ -36,6 +36,7 @@
36
36
  }
37
37
  },
38
38
  "globalProps": true,
39
+ "globalEventProps": true,
39
40
  "usage": {
40
41
  "react": {
41
42
  "import": "import { TitleDetail } from 'playbook-ui'",