playbook_ui_docs 16.4.0 → 16.5.0.pre.alpha.PLAY2893datepickerlabelclicktoggle15576

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_grouped_headers_composition.jsx +235 -0
  3. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_grouped_headers_composition.md +17 -0
  4. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_pinned_rows_rails.html.erb +57 -0
  5. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_pinned_rows_rails.md +7 -0
  6. data/app/pb_kits/playbook/pb_advanced_table/docs/advanced_table_grouped_headers_composition_mock_data.json +98 -0
  7. data/app/pb_kits/playbook/pb_advanced_table/docs/example.yml +2 -0
  8. data/app/pb_kits/playbook/pb_advanced_table/docs/index.js +1 -0
  9. data/app/pb_kits/playbook/pb_button/docs/_button_full_width_rails.md +19 -0
  10. data/app/pb_kits/playbook/pb_button/docs/_button_full_width_react.md +23 -0
  11. data/app/pb_kits/playbook/pb_button/docs/{_button_loading.html.erb → _button_loading_rails.html.erb} +5 -5
  12. data/app/pb_kits/playbook/pb_button/docs/_button_loading_rails.md +3 -0
  13. data/app/pb_kits/playbook/pb_button/docs/example.yml +1 -1
  14. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_default.html.erb +9 -18
  15. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_default.jsx +5 -24
  16. data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_placeholder.html.erb +109 -0
  17. data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_placeholder.jsx +127 -0
  18. data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_placeholder.md +1 -0
  19. data/app/pb_kits/playbook/pb_multi_level_select/docs/example.yml +2 -0
  20. data/app/pb_kits/playbook/pb_multi_level_select/docs/index.js +1 -0
  21. data/app/pb_kits/playbook/pb_select/docs/_select_attributes.html.erb +1 -0
  22. data/app/pb_kits/playbook/pb_select/docs/_select_blank.html.erb +1 -0
  23. data/app/pb_kits/playbook/pb_select/docs/_select_custom_select.html.erb +1 -1
  24. data/app/pb_kits/playbook/pb_select/docs/_select_custom_select_subheaders.html.erb +1 -1
  25. data/app/pb_kits/playbook/pb_select/docs/_select_default.html.erb +1 -0
  26. data/app/pb_kits/playbook/pb_select/docs/_select_disabled.html.erb +1 -0
  27. data/app/pb_kits/playbook/pb_select/docs/_select_disabled_options.html.erb +1 -0
  28. data/app/pb_kits/playbook/pb_select/docs/_select_error.html.erb +1 -0
  29. data/app/pb_kits/playbook/pb_select/docs/_select_inline.html.erb +1 -0
  30. data/app/pb_kits/playbook/pb_select/docs/_select_inline_compact.html.erb +1 -0
  31. data/app/pb_kits/playbook/pb_select/docs/_select_inline_show_arrow.html.erb +1 -0
  32. data/app/pb_kits/playbook/pb_select/docs/_select_multiple.html.erb +1 -0
  33. data/app/pb_kits/playbook/pb_select/docs/_select_required.html.erb +1 -0
  34. data/app/pb_kits/playbook/pb_select/docs/_select_required_indicator.html.erb +1 -0
  35. data/app/pb_kits/playbook/pb_select/docs/_select_value_text_same.html.erb +1 -0
  36. data/app/pb_kits/playbook/pb_table/docs/_sections.yml +1 -0
  37. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_external_filter_rails.html.erb +45 -0
  38. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_external_filter_rails.md +39 -0
  39. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_rails.md +2 -1
  40. data/app/pb_kits/playbook/pb_table/docs/example.yml +1 -0
  41. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_with_highlight.jsx +20 -8
  42. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_with_highlight.md +3 -0
  43. metadata +17 -4
  44. data/app/pb_kits/playbook/pb_button/docs/_button_full_width.md +0 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d75941f7f1cb8007566e28c430baaee9882fed47bb4ac81e68962ce8355edf6d
4
- data.tar.gz: df099e190d6c86ddcc19811aba14f0b347b5cee4f6d34828ade8d4a044bac51f
3
+ metadata.gz: 2d60c2ba4f82d282d7852f35732e089f16535c728299a163cbe6518a7a617f56
4
+ data.tar.gz: a99eab01b457c02313dfdb725d53c1caf606f1d8f98b2bd083db81b9ff96ce14
5
5
  SHA512:
6
- metadata.gz: ee1f847879b0fea73374b12a9b6504a406db13deab901edeb3f1c34fb60b25b80bf630b31dcc74d82c3a59fa5ef9e18ae3d6f9907cd80f6aec8895592cdaf0fc
7
- data.tar.gz: 6a4d4074147c4150f0536117b80bc0b9578dc48c3e0b5e3c9c63325d929be8f982d84d71d181114ec5f728afc156ef365377a3e2eb04172d00d8bb7962beb792
6
+ metadata.gz: 67020badde5cfd53a0cd98149ecdb17c36916996930f619141fff9d50c4e5aca171bb1ead458f62a96d7e1d47c2ea6ae85f919098a44fe5c4e55cc92657ed4a9
7
+ data.tar.gz: 264b9593b9e1c11ce394361f1b62eb12ac05c73a0e06bdb9b7e175958a642bc979b8d488c2cf8e094e5ba3cf4321996a3567d5267846bd9a0a9376faf8099e0d
@@ -0,0 +1,235 @@
1
+ /* eslint-disable react/no-multi-comp, react/prop-types */
2
+ import React, { useCallback, useState } from "react"
3
+
4
+ import AdvancedTable from "../_advanced_table"
5
+ import Flex from "../../pb_flex/_flex"
6
+ import Icon from "../../pb_icon/_icon"
7
+ import List from "../../pb_list/_list"
8
+ import ListItem from "../../pb_list/_list_item"
9
+ import PbReactPopover from "../../pb_popover/_popover"
10
+ import SectionSeparator from "../../pb_section_separator/_section_separator"
11
+ import StarRating from "../../pb_star_rating/_star_rating"
12
+ import COMPOSITION_MOCK_DATA from "./advanced_table_grouped_headers_composition_mock_data.json"
13
+
14
+ const LEAF_COUNT = "newEnrollments"
15
+ const LEAF_SCHEDULED = "scheduledMeetings"
16
+
17
+ const ICON_UNSORTED = "arrow-up-arrow-down"
18
+ const iconSorted = (desc) => (desc ? "arrow-up-wide-short" : "arrow-down-short-wide")
19
+
20
+ const STAR_MENU = [
21
+ { id: LEAF_COUNT, label: "Count" },
22
+ { id: LEAF_SCHEDULED, label: "Scheduled" },
23
+ ]
24
+
25
+ const MENU_BTN = {
26
+ alignItems: "center",
27
+ background: "transparent",
28
+ border: "none",
29
+ cursor: "pointer",
30
+ display: "flex",
31
+ font: "inherit",
32
+ gap: 12,
33
+ justifyContent: "space-between",
34
+ padding: "8px 14px",
35
+ textAlign: "left",
36
+ width: "100%",
37
+ }
38
+
39
+ const labelStyle = (active) => ({
40
+ color: active ? "#0056cf" : "#242930",
41
+ flex: 1,
42
+ fontSize: 14,
43
+ fontWeight: active ? 600 : 400,
44
+ })
45
+
46
+ /** Hooks + popover; `header` callback cannot use hooks directly. */
47
+ const StarMetricGroupHeader = ({ table }) => {
48
+ const [open, setOpen] = useState(false)
49
+ const menuId = "playbook-star-metric-sort-menu"
50
+
51
+ const sort0 = table.getState().sorting[0]
52
+ const groupActive =
53
+ sort0?.id === LEAF_COUNT || sort0?.id === LEAF_SCHEDULED
54
+
55
+ const close = useCallback((shouldClose) => setOpen(!shouldClose), [])
56
+ const toggle = useCallback((e) => {
57
+ e.stopPropagation()
58
+ setOpen((v) => !v)
59
+ }, [])
60
+
61
+ const applySort = useCallback(
62
+ (columnId, e) => {
63
+ e.stopPropagation()
64
+ const cur = table.getState().sorting[0]
65
+ const nextDesc = cur?.id === columnId ? !cur.desc : true
66
+ table.setSorting([{ desc: nextDesc, id: columnId }])
67
+ setOpen(false)
68
+ },
69
+ [table]
70
+ )
71
+
72
+ return (
73
+ <PbReactPopover
74
+ closeOnClick="outside"
75
+ offset
76
+ padding="none"
77
+ placement="bottom-start"
78
+ reference={
79
+ <button
80
+ aria-controls={menuId}
81
+ aria-expanded={open}
82
+ aria-haspopup="menu"
83
+ aria-label="Sort by Count or Scheduled"
84
+ onClick={toggle}
85
+ style={{
86
+ background: open ? "rgba(115, 134, 169, 0.14)" : "transparent",
87
+ border: "none",
88
+ borderRadius: 6,
89
+ cursor: "pointer",
90
+ font: "inherit",
91
+ margin: 0,
92
+ padding: "2px 4px",
93
+ }}
94
+ title="Open menu to sort by Count or Scheduled"
95
+ type="button"
96
+ >
97
+ <Flex alignItems="center"
98
+ gap="xs"
99
+ justifyContent="center"
100
+ >
101
+ <StarRating
102
+ backgroundType="outline"
103
+ colorOption="primary"
104
+ justifyContent="center"
105
+ maxWidth="102px"
106
+ rating={5}
107
+ />
108
+ <Icon
109
+ color={groupActive ? "primary" : "default"}
110
+ fixedWidth
111
+ icon={
112
+ groupActive
113
+ ? iconSorted(Boolean(sort0?.desc))
114
+ : ICON_UNSORTED
115
+ }
116
+ size="md"
117
+ />
118
+ </Flex>
119
+ </button>
120
+ }
121
+ shouldClosePopover={close}
122
+ show={open}
123
+ zIndex={1200}
124
+ >
125
+ <Flex id={menuId}
126
+ minWidth="220px"
127
+ orientation="column"
128
+ >
129
+ <List borderless
130
+ padding="none"
131
+ >
132
+ {STAR_MENU.map(({ id, label }, i) => {
133
+ const active = sort0?.id === id
134
+ return (
135
+ <React.Fragment key={id}>
136
+ {i > 0 ? <SectionSeparator margin="none" /> : null}
137
+ <ListItem padding="none">
138
+ <button
139
+ onClick={(e) => applySort(id, e)}
140
+ style={MENU_BTN}
141
+ type="button"
142
+ >
143
+ <span style={labelStyle(active)}>{label}</span>
144
+ <Icon
145
+ color={active ? "primary" : "default"}
146
+ fixedWidth
147
+ icon={
148
+ active
149
+ ? iconSorted(Boolean(sort0?.desc))
150
+ : ICON_UNSORTED
151
+ }
152
+ size="md"
153
+ />
154
+ </button>
155
+ </ListItem>
156
+ </React.Fragment>
157
+ )
158
+ })}
159
+ </List>
160
+ </Flex>
161
+ </PbReactPopover>
162
+ )
163
+ }
164
+
165
+ const AdvancedTableGroupedHeadersComposition = (props) => {
166
+ const [pinnedRows, setPinnedRows] = useState({ top: ["12"] })
167
+
168
+ const columnDefinitions = [
169
+ {
170
+ accessor: "year",
171
+ cellAccessors: ["quarter", "month", "day"],
172
+ label: "Year",
173
+ },
174
+ {
175
+ columns: [
176
+ {
177
+ columns: [
178
+ {
179
+ accessor: "newEnrollments",
180
+ enableSort: true,
181
+ id: LEAF_COUNT,
182
+ label: "Count",
183
+ },
184
+ {
185
+ accessor: "scheduledMeetings",
186
+ enableSort: true,
187
+ id: LEAF_SCHEDULED,
188
+ label: "Scheduled",
189
+ },
190
+ ],
191
+ id: "starLeafPair",
192
+ label: "Metrics",
193
+ },
194
+ ],
195
+ header: ({ table }) => (
196
+ <Flex justify="center">
197
+ <StarMetricGroupHeader table={table} />
198
+ </Flex>
199
+ ),
200
+ id: "starMetricGroup",
201
+ label: "Rating group (custom header)",
202
+ },
203
+ {
204
+ columns: [
205
+ { accessor: "attendanceRate", label: "Attendance" },
206
+ {
207
+ accessor: "classCompletionRate",
208
+ enableSort: true,
209
+ id: "classCompletionRate",
210
+ label: "Completion %",
211
+ },
212
+ ],
213
+ label: "Performance",
214
+ },
215
+ ]
216
+
217
+ return (
218
+ <div>
219
+ <AdvancedTable
220
+ columnDefinitions={columnDefinitions}
221
+ enableSortingRemoval
222
+ maxHeight="md"
223
+ pinnedRows={{ onChange: setPinnedRows, value: pinnedRows }}
224
+ tableData={COMPOSITION_MOCK_DATA}
225
+ tableProps={{ sticky: true }}
226
+ {...props}
227
+ >
228
+ <AdvancedTable.Header enableSorting />
229
+ <AdvancedTable.Body />
230
+ </AdvancedTable>
231
+ </div>
232
+ )
233
+ }
234
+
235
+ export default AdvancedTableGroupedHeadersComposition
@@ -0,0 +1,17 @@
1
+ ### Grouped headers, custom UI, sort, and pinned rows
2
+
3
+ This example combines patterns that often show up together in product tables:
4
+
5
+ 1. **Multi-level headers** — nested `columns` in `columnDefinitions`.
6
+ 2. **Custom group header** — a `header` function returning React (here: `StarRating`, sort icon, `PbReactPopover` menu). Parent groups are not sort targets; **only leaf columns** use `enableSort: true` (see **Enable Sort By Column (Multi-Column)**).
7
+ 3. **Programmatic sort** — the group `header` receives TanStack’s **`table`**. Call **`table.setSorting([{ id: "<leafColumnId>", desc: boolean }])`** using the same **`id`** values as your leaf columns. Match **direction icons** to the built-in kit (`arrow-up-arrow-down` unsorted; `arrow-up-wide-short` / `arrow-down-short-wide` when sorted).
8
+ 4. **Pinned row + sticky header** — `pinnedRows` with row **`id`**s and `tableProps={{ sticky: true }}` (see **Pinned Rows**).
9
+
10
+ **Implementation notes**
11
+
12
+ - **`header` must be a child component** if you need hooks (e.g. popover open state). Render it from `header: ({ table }) => <YourHeader table={table} />`.
13
+ - **Avoid wrapping the primary control in `Tooltip`** — it can steal the first tap/click. Use a **`title`** on the button or copy in the doc instead.
14
+ - **Popover rows:** use a plain **`<button>`** as `reference`, `closeOnClick="outside"`. Picking the **same** metric again should **toggle** `desc`; switching to another leaf column often defaults to **`desc: true`** to align with **`sortDescFirst`**.
15
+ - **Data:** **`advanced_table_grouped_headers_composition_mock_data.json`** — twelve flat rows (2015–2026) with **varied** Count / Scheduled / % values so sorting is obvious; **`id` `"12"`** is **2026** and is pinned by default.
16
+
17
+ Other building blocks on this kit: **Custom Header with Multiple Headers**, **Sticky Header**.
@@ -0,0 +1,57 @@
1
+ <%# Example sort method for demonstration purposes %>
2
+ <% if params["sort"] %>
3
+ <% sort_param = params["sort"].gsub(/_(asc|desc)\z/, "") %>
4
+ <% sort_direction = params["sort"].end_with?("_asc") ? 1 : -1 %>
5
+ <% @table_data_with_id.sort! do |a, b|
6
+ value_a = a[sort_param] || a[sort_param.to_sym]
7
+ value_b = b[sort_param] || b[sort_param.to_sym]
8
+
9
+ value_a = value_a.to_i if value_a.is_a?(String) && value_a.match?(/^\d+$/)
10
+ value_b = value_b.to_i if value_b.is_a?(String) && value_b.match?(/^\d+$/)
11
+
12
+ sort_direction * (value_a <=> value_b)
13
+ end %>
14
+ <% end %>
15
+
16
+ <% column_definitions = [
17
+ {
18
+ accessor: "year",
19
+ label: "Year",
20
+ cellAccessors: ["quarter", "month", "day"],
21
+ sort_menu: [
22
+ { item: "Year", link: "?sort=year_asc#pinned_rows_table", active: params["sort"] == "year_asc", direction: "asc" },
23
+ { item: "Year", link: "?sort=year_desc#pinned_rows_table", active: params["sort"] == "year_desc", direction: "desc" }
24
+ ],
25
+ },
26
+ {
27
+ accessor: "newEnrollments",
28
+ label: "New Enrollments",
29
+ },
30
+ {
31
+ accessor: "scheduledMeetings",
32
+ label: "Scheduled Meetings",
33
+ },
34
+ {
35
+ accessor: "attendanceRate",
36
+ label: "Attendance Rate",
37
+ },
38
+ {
39
+ accessor: "completedClasses",
40
+ label: "Completed Classes",
41
+ },
42
+ {
43
+ accessor: "classCompletionRate",
44
+ label: "Class Completion Rate",
45
+ },
46
+ {
47
+ accessor: "graduatedStudents",
48
+ label: "Graduated Students",
49
+ }
50
+ ] %>
51
+
52
+ <% pinned_rows = { top: ["8"] } %>
53
+
54
+ <%= pb_rails("advanced_table", props: { id: "pinned_rows_table", table_data: @table_data_with_id, column_definitions: column_definitions, max_height: "xs", pinned_rows: pinned_rows, responsive: "none", table_props: { sticky: true }}) do %>
55
+ <%= pb_rails("advanced_table/table_header", props: { table_id: "pinned_rows_table", column_definitions: column_definitions }) %>
56
+ <%= pb_rails("advanced_table/table_body", props: { table_id: "pinned_rows_table", table_data: @table_data_with_id, column_definitions: column_definitions, pinned_rows: pinned_rows }) %>
57
+ <% end %>
@@ -0,0 +1,7 @@
1
+ Use the `pinned_rows` prop to pin specific rows to the top of an Advanced Table. Pinned rows will remain at the top when scrolling through table data and will not change position if sorting is used.
2
+
3
+ **NOTE:**
4
+ - Sticky header required: Pinned rows must be used with `sticky: true` via `table_props` (works with both responsive and non-responsive tables)
5
+ - Row ids required: Each object within the `table_data` array must contain a unique `id` in order to attach an id to all Rows for this to function.
6
+ - `pinned_rows` takes a hash with a `top` key containing an array of row ids, as shown in the code snippet below.
7
+ - For expandable rows, use the parent id in `pinned_rows[:top]`; all its children will automatically be pinned with it. If a child id is passed without the parent being pinned, nothing will be pinned.
@@ -0,0 +1,98 @@
1
+ [
2
+ {
3
+ "id": "1",
4
+ "year": "2015",
5
+ "newEnrollments": "12",
6
+ "scheduledMeetings": "40",
7
+ "attendanceRate": "62%",
8
+ "classCompletionRate": "28%"
9
+ },
10
+ {
11
+ "id": "2",
12
+ "year": "2016",
13
+ "newEnrollments": "88",
14
+ "scheduledMeetings": "12",
15
+ "attendanceRate": "71%",
16
+ "classCompletionRate": "55%"
17
+ },
18
+ {
19
+ "id": "3",
20
+ "year": "2017",
21
+ "newEnrollments": "34",
22
+ "scheduledMeetings": "67",
23
+ "attendanceRate": "58%",
24
+ "classCompletionRate": "41%"
25
+ },
26
+ {
27
+ "id": "4",
28
+ "year": "2018",
29
+ "newEnrollments": "05",
30
+ "scheduledMeetings": "91",
31
+ "attendanceRate": "44%",
32
+ "classCompletionRate": "73%"
33
+ },
34
+ {
35
+ "id": "5",
36
+ "year": "2019",
37
+ "newEnrollments": "61",
38
+ "scheduledMeetings": "19",
39
+ "attendanceRate": "83%",
40
+ "classCompletionRate": "36%"
41
+ },
42
+ {
43
+ "id": "6",
44
+ "year": "2020",
45
+ "newEnrollments": "19",
46
+ "scheduledMeetings": "54",
47
+ "attendanceRate": "67%",
48
+ "classCompletionRate": "62%"
49
+ },
50
+ {
51
+ "id": "7",
52
+ "year": "2021",
53
+ "newEnrollments": "73",
54
+ "scheduledMeetings": "08",
55
+ "attendanceRate": "52%",
56
+ "classCompletionRate": "49%"
57
+ },
58
+ {
59
+ "id": "8",
60
+ "year": "2022",
61
+ "newEnrollments": "50",
62
+ "scheduledMeetings": "50",
63
+ "attendanceRate": "75%",
64
+ "classCompletionRate": "45%"
65
+ },
66
+ {
67
+ "id": "9",
68
+ "year": "2023",
69
+ "newEnrollments": "95",
70
+ "scheduledMeetings": "03",
71
+ "attendanceRate": "69%",
72
+ "classCompletionRate": "81%"
73
+ },
74
+ {
75
+ "id": "10",
76
+ "year": "2024",
77
+ "newEnrollments": "27",
78
+ "scheduledMeetings": "76",
79
+ "attendanceRate": "91%",
80
+ "classCompletionRate": "22%"
81
+ },
82
+ {
83
+ "id": "11",
84
+ "year": "2025",
85
+ "newEnrollments": "41",
86
+ "scheduledMeetings": "33",
87
+ "attendanceRate": "48%",
88
+ "classCompletionRate": "94%"
89
+ },
90
+ {
91
+ "id": "12",
92
+ "year": "2026",
93
+ "newEnrollments": "66",
94
+ "scheduledMeetings": "66",
95
+ "attendanceRate": "55%",
96
+ "classCompletionRate": "58%"
97
+ }
98
+ ]
@@ -7,6 +7,7 @@ examples:
7
7
  - advanced_table_table_props: Table Props
8
8
  - advanced_table_sticky_header_rails: Sticky Header
9
9
  - advanced_table_table_props_sticky_header: Sticky Header for Responsive Table
10
+ - advanced_table_pinned_rows_rails: Pinned Rows
10
11
  - advanced_table_beta_sort: Enable Sorting
11
12
  - advanced_table_responsive: Responsive Tables
12
13
  - advanced_table_custom_cell_rails: Custom Components for Cells
@@ -74,6 +75,7 @@ examples:
74
75
  - advanced_table_column_visibility_with_state: Column Visibility Control With State
75
76
  - advanced_table_column_visibility_custom: Column Visibility Control with Custom Dropdown
76
77
  - advanced_table_column_visibility_multi: Column Visibility Control with Multi-Header Columns
78
+ - advanced_table_grouped_headers_composition: Grouped headers, custom headers, sort, and pinned rows
77
79
  - advanced_table_scrollbar_none: Advanced Table Scrollbar None
78
80
  - advanced_table_row_styling: Row Styling
79
81
  - advanced_table_padding_control_per_row: Padding Control using Row Styling
@@ -42,6 +42,7 @@ export { default as AdvancedTableInfiniteScroll} from './_advanced_table_infinit
42
42
  export {default as AdvancedTableWithCustomHeader} from './_advanced_table_with_custom_header.jsx'
43
43
  export { default as AdvancedTableCustomSort } from './_advanced_table_custom_sort.jsx'
44
44
  export { default as AdvancedTableWithCustomHeaderMultiHeader } from './_advanced_table_with_custom_header_multi_header.jsx'
45
+ export { default as AdvancedTableGroupedHeadersComposition } from './_advanced_table_grouped_headers_composition.jsx'
45
46
  export { default as AdvancedTableSortPerColumn } from './_advanced_table_sort_per_column.jsx'
46
47
  export { default as AdvancedTableSortPerColumnForMultiColumn } from './_advanced_table_sort_per_column_for_multi_column.jsx'
47
48
  export { default as AdvancedTablePaddingControl } from './_advanced_table_padding_control.jsx'
@@ -0,0 +1,19 @@
1
+ This button is used many times for mobile or other things like cards and sidebars.
2
+
3
+ ### Responsive `display` and `full_width`
4
+
5
+ `full_width` applies block styling that includes `display: flex` on the **same element** as the button. The **`display` global prop** also sets `display` (via utility classes, often with `!important`).
6
+
7
+ Putting **both** on one button means **two systems control `display` on one node**, which can cause wrong visibility (e.g. both a header and a full-width mobile button showing) or confusing cascade behavior.
8
+
9
+ **Recommended:** Put responsive `display` on a **parent** (e.g. `Flex`, `Card`, or a plain wrapper) and keep `full_width` only on the `Button` inside. The wrapper handles show/hide by breakpoint; the button only handles full-width layout.
10
+
11
+ ```erb
12
+ <%= pb_rails("flex", props: {
13
+ display: { xs: "flex", default: "none" },
14
+ orientation: "column",
15
+ width: "100%",
16
+ }) do %>
17
+ <%= pb_rails("button", props: { full_width: true, text: "Add" }) %>
18
+ <% end %>
19
+ ```
@@ -0,0 +1,23 @@
1
+ This button is used many times for mobile or other things like cards and sidebars.
2
+
3
+ ### Responsive `display` and `full_width`
4
+
5
+ `full_width` applies block styling that includes `display: flex` on the **same element** as the button. The **`display` global prop** also sets `display` (via utility classes, often with `!important`).
6
+
7
+ Putting **both** on one button means **two systems control `display` on one node**, which can cause wrong visibility (e.g. both a header and a full-width mobile button showing) or confusing cascade behavior.
8
+
9
+ **Recommended:** Put responsive `display` on a **parent** (e.g. `Flex`, `Card`, or a plain wrapper) and keep `fullWidth` only on the `Button` inside. The wrapper handles show/hide by breakpoint; the button only handles full-width layout.
10
+
11
+ ```jsx
12
+ import { Flex, Button } from "playbook-ui"
13
+
14
+ const Example = () => (
15
+ <Flex
16
+ display={{ xs: "flex", default: "none" }}
17
+ orientation="column"
18
+ width="100%"
19
+ >
20
+ <Button fullWidth text="Add" />
21
+ </Flex>
22
+ )
23
+ ```
@@ -1,7 +1,7 @@
1
1
  <%= pb_rails("caption", props: { margin_y: "md", text: "Button variants with loading" }) %>
2
- <%= pb_rails("button", props: { aria: { label: "Loading" }, loading: true, margin_right: "lg" }) %>
3
- <%= pb_rails("button", props: { aria: { label: "Loading" }, variant: "secondary", loading: true, margin_right: "lg" }) %>
4
- <%= pb_rails("button", props: { aria: { label: "Loading" }, variant: "link", loading: true, margin_right: "lg" }) %>
2
+ <%= pb_rails("button", props: { aria: { label: "Loading" }, loading: true, text: "Loading", margin_right: "lg" }) %>
3
+ <%= pb_rails("button", props: { aria: { label: "Loading" }, variant: "secondary", loading: true, text: "Loading", margin_right: "lg" }) %>
4
+ <%= pb_rails("button", props: { aria: { label: "Loading" }, variant: "link", loading: true, text: "Loading", margin_right: "lg" }) %>
5
5
  <%= pb_rails("caption", props: { margin_y: "md", text: "Button sizes with loading" }) %>
6
- <%= pb_rails("button", props: { aria: { label: "Loading" }, loading: true, size: "sm", margin_right: "lg" }) %>
7
- <%= pb_rails("button", props: { aria: { label: "Loading" }, loading: true, size: "lg", margin_right: "lg" }) %>
6
+ <%= pb_rails("button", props: { aria: { label: "Loading" }, loading: true, size: "sm", text: "Loading", margin_right: "lg" }) %>
7
+ <%= pb_rails("button", props: { aria: { label: "Loading" }, loading: true, size: "lg", text: "Loading", margin_right: "lg" }) %>
@@ -0,0 +1,3 @@
1
+ Used when a button will take a little while to load. The spinner lets the user know that the button has worked and it is in the process of loading.
2
+
3
+ **NOTE**: In Rails, both the button text and loading spinner are rendered at the same time, and visibility is toggled between them. Even though the text prop is not required, providing it ensures the button maintains its correct shape during loading.
@@ -4,7 +4,7 @@ examples:
4
4
  - button_reaction: Reaction Button
5
5
  - button_full_width: Button Full Width
6
6
  - button_link: Button Links
7
- - button_loading: Button Loading
7
+ - button_loading_rails: Button Loading
8
8
  - button_block_content: Button Block Content
9
9
  - button_icon_options: Button Icon Options
10
10
  - button_accessibility: Button Accessibility Options
@@ -1,20 +1,11 @@
1
1
  <%= pb_rails("button", props: { text: "Open Dialog", data: {"open-dialog": "dialog-1"} }) %>
2
2
 
3
- <%= pb_rails("dialog", props: {
4
- id:"dialog-1",
5
- size: "md",
6
- title: "Header Title is the Title Prop"
7
- }) do %>
8
- <%= pb_rails("dialog/dialog_body") do %>
9
- <%= pb_rails("button", props: { aria: { label: "Loading" }, loading: true, margin_right: "lg", text: "Button Primary" }) %>
10
- <div style="height: 800px; background-color: lightgray;"></div>
11
- <%= pb_rails("button", props: { loading: true, text: "Loading..." }) %>
12
- <% end %>
13
-
14
- <%= pb_rails("dialog/dialog_footer") do %>
15
- <%= pb_rails("flex", props: { spacing: "between", padding_x: "md", padding_bottom: "md", padding: "sm" }) do %>
16
- <%= pb_rails("button", props: { loading: true, text: "Send My Issue" }) %>
17
- <%= pb_rails("button", props: { text: "Back", variant: "link", data: {"close-dialog": "dialog-1"} }) %>
18
- <% end %>
19
- <% end %>
20
- <% end %>
3
+ <%= pb_rails("dialog", props: {
4
+ id:"dialog-1",
5
+ size: "sm",
6
+ title: "Header Title is the Title Prop",
7
+ text: "Hello Body Text, Nice to meet ya.",
8
+ cancel_button: "Cancel Button",
9
+ confirm_button: "Okay",
10
+ confirm_button_id: "confirm-button-1"
11
+ }) %>
@@ -12,35 +12,16 @@ const DialogDefault = () => {
12
12
  <>
13
13
  <Button onClick={open}>{'Open Dialog'}</Button>
14
14
  <Dialog
15
+ cancelButton="Cancel Button"
16
+ confirmButton="Okay"
15
17
  onCancel={close}
16
18
  onClose={close}
17
19
  onConfirm={close}
18
20
  opened={isOpen}
19
- size="md"
21
+ size="sm"
22
+ text="Hello Body Text, Nice to meet ya."
20
23
  title="Header Title is the Title Prop"
21
- >
22
- <Dialog.Body>
23
- <Button
24
- aria={{ label: 'Loading' }}
25
- loading
26
- text="Button Primary"
27
- />
28
- <div style={{height: '800px', backgroundColor: 'lightgray'}} />
29
- <Button
30
- loading
31
- text="Loading..."
32
- />
33
- </Dialog.Body>
34
- <Dialog.Footer>
35
- <Button
36
- loading
37
- text="Send My Issue"
38
- />
39
- <Button variant="link">
40
- {"Back"}
41
- </Button>
42
- </Dialog.Footer>
43
- </Dialog>
24
+ />
44
25
  </>
45
26
  )
46
27
  }
@@ -0,0 +1,109 @@
1
+ <%
2
+ tree_base = [{
3
+ label: "Power Home Remodeling",
4
+ value: "powerHomeRemodeling",
5
+ id: "100",
6
+ expanded: true,
7
+ children: [
8
+ {
9
+ label: "People",
10
+ value: "people",
11
+ id: "101",
12
+ expanded: true,
13
+ children: [
14
+ {
15
+ label: "Talent Acquisition",
16
+ value: "talentAcquisition",
17
+ id: "102",
18
+ },
19
+ {
20
+ label: "Business Affairs",
21
+ value: "business Affairs",
22
+ id: "103",
23
+ children: [
24
+ {
25
+ label: "Initiatives",
26
+ value: "initiatives",
27
+ id: "104",
28
+ },
29
+ {
30
+ label: "Learning & Development",
31
+ value: "learningAndDevelopment",
32
+ id: "105",
33
+ },
34
+ ],
35
+ },
36
+ {
37
+ label: "People Experience",
38
+ value: "peopleExperience",
39
+ id: "106",
40
+ },
41
+ ],
42
+ },
43
+ {
44
+ label: "Contact Center",
45
+ value: "contactCenter",
46
+ id: "107",
47
+ children: [
48
+ {
49
+ label: "Appointment Management",
50
+ value: "appointmentManagement",
51
+ id: "108",
52
+ },
53
+ {
54
+ label: "Customer Service",
55
+ value: "customerService",
56
+ id: "109",
57
+ },
58
+ {
59
+ label: "Energy",
60
+ value: "energy",
61
+ id: "110",
62
+ },
63
+ ],
64
+ },
65
+ ],
66
+ }]
67
+
68
+ prefix_mls_ids = nil
69
+ prefix_mls_ids = ->(nodes, pfx) {
70
+ nodes.map do |n|
71
+ h = n.dup
72
+ h[:id] = "#{pfx}#{n[:id]}"
73
+ h[:children] = prefix_mls_ids.call(n[:children], pfx) if n[:children].present?
74
+ h
75
+ end
76
+ }
77
+
78
+ tree_multi = prefix_mls_ids.call(tree_base, "phm_")
79
+ tree_return_all = prefix_mls_ids.call(tree_base, "phr_")
80
+ tree_single = prefix_mls_ids.call(tree_base, "phs_")
81
+ %>
82
+
83
+ <%= pb_rails("multi_level_select", props: {
84
+ id: "multi-level-select-placeholder-multi-rails",
85
+ label: "Multi (default)",
86
+ margin_bottom: "sm",
87
+ name: "placeholder_multi",
88
+ tree_data: tree_multi,
89
+ placeholder: "Search or choose options…",
90
+ }) %>
91
+
92
+ <%= pb_rails("multi_level_select", props: {
93
+ id: "multi-level-select-placeholder-return-all-rails",
94
+ label: "Multi (return all selected)",
95
+ margin_bottom: "sm",
96
+ name: "placeholder_return_all",
97
+ placeholder: "Departments...",
98
+ return_all_selected: true,
99
+ tree_data: tree_return_all,
100
+ }) %>
101
+
102
+ <%= pb_rails("multi_level_select", props: {
103
+ id: "multi-level-select-placeholder-single-rails",
104
+ label: "Single",
105
+ name: "placeholder_single",
106
+ placeholder: "Select one option…",
107
+ tree_data: tree_single,
108
+ variant: "single",
109
+ }) %>
@@ -0,0 +1,127 @@
1
+ import React from "react";
2
+
3
+ import MultiLevelSelect from "../_multi_level_select";
4
+
5
+ const treeTemplate = [
6
+ {
7
+ label: "Power Home Remodeling",
8
+ value: "powerHomeRemodeling",
9
+ id: "powerhome1",
10
+ expanded: true,
11
+ children: [
12
+ {
13
+ label: "People",
14
+ value: "people",
15
+ id: "people1",
16
+ expanded: true,
17
+ children: [
18
+ {
19
+ label: "Talent Acquisition",
20
+ value: "talentAcquisition",
21
+ id: "talent1",
22
+ },
23
+ {
24
+ label: "Business Affairs",
25
+ value: "businessAffairs",
26
+ id: "business1",
27
+ children: [
28
+ {
29
+ label: "Initiatives",
30
+ value: "initiatives",
31
+ id: "initiative1",
32
+ },
33
+ {
34
+ label: "Learning & Development",
35
+ value: "learningAndDevelopment",
36
+ id: "development1",
37
+ },
38
+ ],
39
+ },
40
+ {
41
+ label: "People Experience",
42
+ value: "peopleExperience",
43
+ id: "experience1",
44
+ },
45
+ ],
46
+ },
47
+ {
48
+ label: "Contact Center",
49
+ value: "contactCenter",
50
+ id: "contact1",
51
+ children: [
52
+ {
53
+ label: "Appointment Management",
54
+ value: "appointmentManagement",
55
+ id: "appointment1",
56
+ },
57
+ {
58
+ label: "Customer Service",
59
+ value: "customerService",
60
+ id: "customer1",
61
+ },
62
+ {
63
+ label: "Energy",
64
+ value: "energy",
65
+ id: "energy1",
66
+ },
67
+ ],
68
+ },
69
+ ],
70
+ },
71
+ ];
72
+
73
+ function prefixTreeIds(nodes, prefix) {
74
+ return nodes.map((node) => ({
75
+ ...node,
76
+ id: `${prefix}${node.id}`,
77
+ children:
78
+ node.children && node.children.length > 0
79
+ ? prefixTreeIds(node.children, prefix)
80
+ : node.children,
81
+ }));
82
+ }
83
+
84
+ const treeDataMulti = prefixTreeIds(treeTemplate, "phm_");
85
+ const treeDataReturnAll = prefixTreeIds(treeTemplate, "phr_");
86
+ const treeDataSingle = prefixTreeIds(treeTemplate, "phs_");
87
+
88
+ const MultiLevelSelectPlaceholder = () => (
89
+ <>
90
+ <MultiLevelSelect
91
+ id="multi-level-select-placeholder-multi"
92
+ label="Multi (default)"
93
+ marginBottom="sm"
94
+ name="placeholder_multi"
95
+ onSelect={(selectedNodes) =>
96
+ console.log("Multi — default", selectedNodes)
97
+ }
98
+ placeholder="Search or choose options…"
99
+ treeData={treeDataMulti}
100
+ />
101
+ <MultiLevelSelect
102
+ id="multi-level-select-placeholder-return-all"
103
+ label="Multi (return all selected)"
104
+ marginBottom="sm"
105
+ name="placeholder_return_all"
106
+ onSelect={(selectedNodes) =>
107
+ console.log("Multi — return all selected", selectedNodes)
108
+ }
109
+ placeholder="Departments..."
110
+ returnAllSelected
111
+ treeData={treeDataReturnAll}
112
+ />
113
+ <MultiLevelSelect
114
+ id="multi-level-select-placeholder-single"
115
+ label="Single"
116
+ name="placeholder_single"
117
+ onSelect={(selectedNodes) =>
118
+ console.log("Single", selectedNodes)
119
+ }
120
+ placeholder="Select one option…"
121
+ treeData={treeDataSingle}
122
+ variant="single"
123
+ />
124
+ </>
125
+ );
126
+
127
+ export default MultiLevelSelectPlaceholder;
@@ -0,0 +1 @@
1
+ Use the `placeholder` prop to customize the initial text shown in the input when nothing is selected. The default is `Start typing...`.
@@ -18,6 +18,7 @@ examples:
18
18
  - multi_level_select_disabled_options_parent: Disabled Parent Option (Return All Selected)
19
19
  - multi_level_select_single_disabled: Disabled Options (Single Select)
20
20
  - multi_level_select_required_indicator: Required Indicator
21
+ - multi_level_select_placeholder: Placeholder
21
22
 
22
23
  react:
23
24
  - multi_level_select_default: Default
@@ -40,3 +41,4 @@ examples:
40
41
  - multi_level_select_single_disabled: Disabled Options (Single Select)
41
42
  - multi_level_select_required_indicator: Required Indicator
42
43
  - multi_level_select_react_reset_key: Reset with Key (React)
44
+ - multi_level_select_placeholder: Placeholder
@@ -18,3 +18,4 @@ export { default as MultiLevelSelectLabel } from "./_multi_level_select_label.js
18
18
  export { default as MultiLevelSelectSingleDisabled } from "./_multi_level_select_single_disabled.jsx"
19
19
  export { default as MultiLevelSelectRequiredIndicator } from "./_multi_level_select_required_indicator.jsx"
20
20
  export { default as MultiLevelSelectReactResetKey } from "./_multi_level_select_react_reset_key.jsx"
21
+ export { default as MultiLevelSelectPlaceholder } from "./_multi_level_select_placeholder.jsx"
@@ -1,4 +1,5 @@
1
1
  <%= pb_rails("select", props: {
2
+ id: "select-food-attributes",
2
3
  attributes: {
3
4
  data: { options: "data_attribute" },
4
5
  },
@@ -1,4 +1,5 @@
1
1
  <%= pb_rails("select", props: {
2
+ id: "select-location",
2
3
  label: "Where do you live",
3
4
  name: "location",
4
5
  blank_selection: "Select One...",
@@ -1,4 +1,4 @@
1
- <%= pb_rails("select", props: { label: "Favorite Holiday" }) do %>
1
+ <%= pb_rails("select", props: { id: "holiday", label: "Favorite Holiday" }) do %>
2
2
  <select name="holiday" id="holiday">
3
3
  <option value="1">Christmas</option>
4
4
  <option value="2">Thanksgiving</option>
@@ -1,4 +1,4 @@
1
- <%= pb_rails("select", props: { label: "Favorite Animal" }) do %>
1
+ <%= pb_rails("select", props: { id: "animal", label: "Favorite Animal" }) do %>
2
2
  <select name="animal" id="animal">
3
3
  <optgroup label="Mammal">
4
4
  <option value="1">Cat</option>
@@ -1,4 +1,5 @@
1
1
  <%= pb_rails("select", props: {
2
+ id: "select-favorite-food",
2
3
  label: "Favorite Food",
3
4
  name: "food",
4
5
  options: [
@@ -1,4 +1,5 @@
1
1
  <%= pb_rails("select", props: {
2
+ id: "select-dessert-disabled",
2
3
  label: "Favorite Dessert",
3
4
  disabled: true,
4
5
  name: "dessert",
@@ -1,4 +1,5 @@
1
1
  <%= pb_rails("select", props: {
2
+ id: "select-coffee",
2
3
  label: "Favorite Coffee",
3
4
  name: "coffee",
4
5
  options: [
@@ -1,5 +1,6 @@
1
1
  <%= pb_rails("select", props: {
2
2
  error: raw(pb_rails("icon", props: { icon: "warning" }) + " Please make a valid selection"),
3
+ id: "select-food-error",
3
4
  label: "Favorite Food",
4
5
  name: "food",
5
6
  options: [
@@ -1,4 +1,5 @@
1
1
  <%= pb_rails("select", props: {
2
+ id: "select-food-inline",
2
3
  label: "Favorite Food",
3
4
  name: "food",
4
5
  inline: true,
@@ -1,4 +1,5 @@
1
1
  <%= pb_rails("select", props: {
2
+ id: "select-food-inline-compact",
2
3
  label: "Favorite Food",
3
4
  name: "food",
4
5
  inline: true,
@@ -1,4 +1,5 @@
1
1
  <%= pb_rails("select", props: {
2
+ id: "select-food-inline-arrow",
2
3
  label: "Favorite Food",
3
4
  name: "food",
4
5
  inline: true,
@@ -1,4 +1,5 @@
1
1
  <%= pb_rails("select", props: {
2
+ id: "select-food-multiple",
2
3
  label: "Favorite Food",
3
4
  name: "food",
4
5
  multiple: true,
@@ -1,4 +1,5 @@
1
1
  <%= pb_rails("select", props: {
2
+ id: "select-shoe-required",
2
3
  label: "Which shoe do you tie first?",
3
4
  required: true,
4
5
  name: "shoe",
@@ -1,4 +1,5 @@
1
1
  <%= pb_rails("select", props: {
2
+ id: "select-snacks-required-indicator",
2
3
  label: "Favorite Snacks",
3
4
  name: "food",
4
5
  required_indicator: true,
@@ -1,4 +1,5 @@
1
1
  <%= pb_rails("select", props: {
2
+ id: "select-sport",
2
3
  label: "Favorite Sport",
3
4
  name: "sports",
4
5
  options: [
@@ -55,6 +55,7 @@ sections:
55
55
  - table_with_clickable_rows
56
56
  - table_with_selectable_rows
57
57
  - table_with_filter_variant
58
+ - table_with_filter_variant_external_filter_rails
58
59
  - table_with_filter_variant_with_pagination
59
60
  - table_disable_hover
60
61
 
@@ -0,0 +1,45 @@
1
+ <%# External filter: capture any filter markup and pass it via the filter prop.
2
+ Use your own helper (e.g. a search/filter form) or pb_rails("filter") as shown here. %>
3
+ <% users = [
4
+ { name: "Alex", role: "Engineer" },
5
+ { name: "Sam", role: "Designer" },
6
+ { name: "Jordan", role: "Manager" },
7
+ ] %>
8
+
9
+ <% filter_output = capture do %>
10
+ <%= pb_rails("filter", props: {
11
+ id: "external-filter-demo",
12
+ template: "single",
13
+ results: 3,
14
+ background: false,
15
+ sort_menu: [
16
+ { item: "Name", link: "#", active: true, direction: "asc" },
17
+ { item: "Role", link: "#", active: false },
18
+ ],
19
+ }) do %>
20
+ <%= pb_rails("text_input", props: { label: "Name", placeholder: "Search by name" }) %>
21
+ <%= pb_rails("text_input", props: { label: "Role", placeholder: "e.g. Engineer, Designer" }) %>
22
+ <%= pb_rails("button", props: { text: "Apply" }) %>
23
+ <% end %>
24
+ <% end %>
25
+
26
+ <%= pb_rails("table", props: {
27
+ variant: "with_filter",
28
+ title: "Table with External Filter",
29
+ filter: filter_output,
30
+ }) do %>
31
+ <%= pb_rails("table/table_head") do %>
32
+ <%= pb_rails("table/table_row") do %>
33
+ <%= pb_rails("table/table_header", props: { text: "Name" }) %>
34
+ <%= pb_rails("table/table_header", props: { text: "Role" }) %>
35
+ <% end %>
36
+ <% end %>
37
+ <%= pb_rails("table/table_body") do %>
38
+ <% users.each do |user| %>
39
+ <%= pb_rails("table/table_row") do %>
40
+ <%= pb_rails("table/table_cell") { user[:name] } %>
41
+ <%= pb_rails("table/table_cell") { user[:role] } %>
42
+ <% end %>
43
+ <% end %>
44
+ <% end %>
45
+ <% end %>
@@ -0,0 +1,39 @@
1
+ Use the **"with_filter"** variant with an **external filter** (Option B): pass pre-rendered filter markup via the `filter` prop. Same layout as Variant with Filter (card, title, separator, flex); only the filter slot is supplied by you. Use this when you need:
2
+
3
+ - **Manual filter submission** – Apply / Filter button instead of automatic application
4
+ - **Full control** – Over filter props, template, sort menu, and submission
5
+ - **Custom or app-specific filter helpers** – Any helper that returns filter markup (e.g. search/filter forms)
6
+
7
+ #### Required props
8
+
9
+ - `variant: "with_filter"`
10
+ - `filter` – Pre-rendered filter HTML (e.g. from `capture { ... }`)
11
+
12
+ When `filter` is present, `filter_content` and `filter_props` are ignored.
13
+
14
+ #### How to do it
15
+
16
+ 1. **Render your filter** (e.g. `pb_rails("filter", ...)` or any helper that returns filter markup).
17
+ 2. **Capture the output** with `capture do ... end`.
18
+ 3. **Pass it to the Table** as the `filter` prop.
19
+
20
+ **Example (generic pattern):**
21
+
22
+ ```erb
23
+ <% filter_output = capture do %>
24
+ <%= pb_rails("filter", props: { template: "single", results: 10, background: false }) do %>
25
+ <%= pb_rails("text_input", props: { label: "Name", placeholder: "Search by name" }) %>
26
+ <%= pb_rails("button", props: { text: "Apply" }) %>
27
+ <% end %>
28
+ <% end %>
29
+
30
+ <%= pb_rails("table", props: {
31
+ variant: "with_filter",
32
+ title: "My Table",
33
+ filter: filter_output,
34
+ }) do %>
35
+ <%# table_head / table_body ... %>
36
+ <% end %>
37
+ ```
38
+
39
+ For Nitro apps that use a shared search/filter pattern, reference the example on Alpha for implementation details.
@@ -26,8 +26,9 @@ The Table kit automatically sets these Filter defaults (which you can override v
26
26
  - `min_width: "xs"`
27
27
  - `popover_props: { width: "350px" }`
28
28
 
29
+ Alternatively, you can pass pre-rendered filter markup via the `filter` prop (e.g. for manual submission or custom filter helpers)—scroll down for that approach.
29
30
 
30
31
  **IMPORTANT NOTE**:
31
32
  The purpose of this variant is to provide an easy way to set up a Table with a Filter with Design standards applied by default.
32
33
 
33
- If you are looking for more customization than this embedded variant provides, you may be better served by using the individual kits as demonstrating in our Table Filter Card Building Block as seen [here](https://playbook.powerapp.cloud/building_blocks/table_filter_card/rails).
34
+ If you are looking for more customization than this embedded variant provides, you may be better served by using the individual kits as demonstrated in our Table Filter Card Building Block as seen [here](https://playbook.powerapp.cloud/building_blocks/table_filter_card/rails).
@@ -41,6 +41,7 @@ examples:
41
41
  - table_with_header_style_borderless: Header Style Borderless
42
42
  - table_with_header_style_floating: Header Style Floating
43
43
  - table_with_filter_variant_rails: Variant with Filter
44
+ - table_with_filter_variant_external_filter_rails: Variant with Filter (External Filter)
44
45
  - table_with_filter_variant_with_pagination_rails: Variant with Filter and Pagination
45
46
  - table_with_filter_with_card_title_props_rails: Variant with Filter (with Card and Title Props)
46
47
 
@@ -1,4 +1,3 @@
1
- /* eslint-disable react/no-danger */
2
1
  /* eslint-disable react/no-multi-comp */
3
2
 
4
3
  import React, { useState } from 'react'
@@ -38,14 +37,26 @@ const TypeaheadWithHighlight = (props) => {
38
37
  const [selectedUser, setSelectedUser] = useState()
39
38
 
40
39
  const formatOptionLabel = ({name, territory, title}, {inputValue}) => {
40
+ const escapeRegExp = (value = "") => (
41
+ value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
42
+ )
41
43
 
42
- const highlighted = (text) => {
44
+ const highlighted = (text = "") => {
43
45
  if (!inputValue.length) return text
44
- return text.replace(
45
- new RegExp(inputValue, 'gi'),
46
- (highlighted) => `<mark>${highlighted}</mark>`
47
- )
46
+
47
+ const escapedInputValue = escapeRegExp(inputValue)
48
+ const regex = new RegExp(`(${escapedInputValue})`, 'gi')
49
+ const parts = text.split(regex)
50
+
51
+ return parts.map((part, index) => {
52
+ if (part.toLowerCase() === inputValue.toLowerCase()) {
53
+ return <mark key={`${part}-${index}`}>{part}</mark>
54
+ }
55
+
56
+ return <React.Fragment key={`${part}-${index}`}>{part}</React.Fragment>
57
+ })
48
58
  }
59
+
49
60
  return (
50
61
  <Flex>
51
62
  <FlexItem>
@@ -61,11 +72,12 @@ const TypeaheadWithHighlight = (props) => {
61
72
  size={4}
62
73
  {...props}
63
74
  >
64
- <span dangerouslySetInnerHTML={{ __html: highlighted(name) }} /></Title>
75
+ {highlighted(name)}
76
+ </Title>
65
77
  <Body color="light"
66
78
  {...props}
67
79
  >
68
- <span dangerouslySetInnerHTML={{ __html: highlighted(title) }} />{" • "}
80
+ {highlighted(title)}{" • "}
69
81
  {territory}
70
82
  </Body>
71
83
  </FlexItem>
@@ -0,0 +1,3 @@
1
+ Use `formatOptionLabel` to customize each option row and highlight text that matches the current search input. Split each field (for example, `name` and `title`) by the typed value, then render matching parts inside `<mark>` so users can quickly see why a result matched.
2
+
3
+ See the code snippet below for more details.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: playbook_ui_docs
3
3
  version: !ruby/object:Gem::Version
4
- version: 16.4.0
4
+ version: 16.5.0.pre.alpha.PLAY2893datepickerlabelclicktoggle15576
5
5
  platform: ruby
6
6
  authors:
7
7
  - Power UX
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2026-03-17 00:00:00.000000000 Z
12
+ date: 2026-04-07 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: playbook_ui
@@ -98,6 +98,8 @@ files:
98
98
  - app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_expanded_control.md
99
99
  - app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_fullscreen.jsx
100
100
  - app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_fullscreen.md
101
+ - app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_grouped_headers_composition.jsx
102
+ - app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_grouped_headers_composition.md
101
103
  - app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_infinite_scroll.jsx
102
104
  - app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_infinite_scroll.md
103
105
  - app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_inline_editing.jsx
@@ -125,6 +127,8 @@ files:
125
127
  - app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_pagination_with_props.jsx
126
128
  - app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_pagination_with_props.md
127
129
  - app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_pinned_rows.jsx
130
+ - app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_pinned_rows_rails.html.erb
131
+ - app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_pinned_rows_rails.md
128
132
  - app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_pinned_rows_react.md
129
133
  - app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_responsive.html.erb
130
134
  - app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_responsive.jsx
@@ -191,6 +195,7 @@ files:
191
195
  - app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_with_custom_header_rails.md
192
196
  - app/pb_kits/playbook/pb_advanced_table/docs/_mock_data_inline_loading.js
193
197
  - app/pb_kits/playbook/pb_advanced_table/docs/_mock_data_inline_loading_empty_children.js
198
+ - app/pb_kits/playbook/pb_advanced_table/docs/advanced_table_grouped_headers_composition_mock_data.json
194
199
  - app/pb_kits/playbook/pb_advanced_table/docs/advanced_table_mock_data.json
195
200
  - app/pb_kits/playbook/pb_advanced_table/docs/advanced_table_mock_data_infinite_scroll.json
196
201
  - app/pb_kits/playbook/pb_advanced_table/docs/advanced_table_mock_data_no_subrows.json
@@ -307,7 +312,8 @@ files:
307
312
  - app/pb_kits/playbook/pb_button/docs/_button_form.jsx
308
313
  - app/pb_kits/playbook/pb_button/docs/_button_full_width.html.erb
309
314
  - app/pb_kits/playbook/pb_button/docs/_button_full_width.jsx
310
- - app/pb_kits/playbook/pb_button/docs/_button_full_width.md
315
+ - app/pb_kits/playbook/pb_button/docs/_button_full_width_rails.md
316
+ - app/pb_kits/playbook/pb_button/docs/_button_full_width_react.md
311
317
  - app/pb_kits/playbook/pb_button/docs/_button_full_width_swift.md
312
318
  - app/pb_kits/playbook/pb_button/docs/_button_hover.html.erb
313
319
  - app/pb_kits/playbook/pb_button/docs/_button_hover.jsx
@@ -322,9 +328,10 @@ files:
322
328
  - app/pb_kits/playbook/pb_button/docs/_button_link.html.erb
323
329
  - app/pb_kits/playbook/pb_button/docs/_button_link.jsx
324
330
  - app/pb_kits/playbook/pb_button/docs/_button_link.md
325
- - app/pb_kits/playbook/pb_button/docs/_button_loading.html.erb
326
331
  - app/pb_kits/playbook/pb_button/docs/_button_loading.jsx
327
332
  - app/pb_kits/playbook/pb_button/docs/_button_loading.md
333
+ - app/pb_kits/playbook/pb_button/docs/_button_loading_rails.html.erb
334
+ - app/pb_kits/playbook/pb_button/docs/_button_loading_rails.md
328
335
  - app/pb_kits/playbook/pb_button/docs/_button_managed_disabled.html.erb
329
336
  - app/pb_kits/playbook/pb_button/docs/_button_managed_disabled.md
330
337
  - app/pb_kits/playbook/pb_button/docs/_button_managed_disabled_helper.html.erb
@@ -1459,6 +1466,9 @@ files:
1459
1466
  - app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_label.html.erb
1460
1467
  - app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_label.jsx
1461
1468
  - app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_label.md
1469
+ - app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_placeholder.html.erb
1470
+ - app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_placeholder.jsx
1471
+ - app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_placeholder.md
1462
1472
  - app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_react_hook.jsx
1463
1473
  - app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_react_hook.md
1464
1474
  - app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_react_reset_key.jsx
@@ -2298,6 +2308,8 @@ files:
2298
2308
  - app/pb_kits/playbook/pb_table/docs/_table_with_dynamic_collapsible_react.md
2299
2309
  - app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant.jsx
2300
2310
  - app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant.md
2311
+ - app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_external_filter_rails.html.erb
2312
+ - app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_external_filter_rails.md
2301
2313
  - app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_rails.html.erb
2302
2314
  - app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_rails.md
2303
2315
  - app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_with_pagination.jsx
@@ -2654,6 +2666,7 @@ files:
2654
2666
  - app/pb_kits/playbook/pb_typeahead/docs/_typeahead_truncated_text.md
2655
2667
  - app/pb_kits/playbook/pb_typeahead/docs/_typeahead_with_context.html.erb
2656
2668
  - app/pb_kits/playbook/pb_typeahead/docs/_typeahead_with_highlight.jsx
2669
+ - app/pb_kits/playbook/pb_typeahead/docs/_typeahead_with_highlight.md
2657
2670
  - app/pb_kits/playbook/pb_typeahead/docs/_typeahead_with_pills.html.erb
2658
2671
  - app/pb_kits/playbook/pb_typeahead/docs/_typeahead_with_pills.jsx
2659
2672
  - app/pb_kits/playbook/pb_typeahead/docs/_typeahead_with_pills.md
@@ -1 +0,0 @@
1
- This button is used many times for mobile or other things like cards and sidebars.