playbook_ui 14.12.0.pre.rc.11 → 14.12.0.pre.rc.12

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/_playbook.scss +2 -0
  3. data/app/pb_kits/playbook/pb_advanced_table/Components/CustomCell.tsx +18 -2
  4. data/app/pb_kits/playbook/pb_advanced_table/Components/TableHeaderCell.tsx +27 -5
  5. data/app/pb_kits/playbook/pb_advanced_table/SubKits/TableBody.tsx +17 -2
  6. data/app/pb_kits/playbook/pb_advanced_table/SubKits/TableHeader.tsx +23 -1
  7. data/app/pb_kits/playbook/pb_advanced_table/_advanced_table.scss +29 -0
  8. data/app/pb_kits/playbook/pb_advanced_table/_advanced_table.tsx +61 -4
  9. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_no_subrows.jsx +50 -0
  10. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_pagination.jsx +1 -0
  11. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_pagination_with_props.jsx +1 -0
  12. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_selectable_rows.jsx +60 -0
  13. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_selectable_rows.md +5 -0
  14. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_selectable_rows_actions.jsx +78 -0
  15. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_selectable_rows_actions.md +1 -0
  16. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_selectable_rows_header.jsx +53 -0
  17. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_selectable_rows_header.md +1 -0
  18. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_selectable_rows_no_subrows.jsx +52 -0
  19. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_selectable_rows_no_subrows.md +1 -0
  20. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_sort_control.md +2 -2
  21. data/app/pb_kits/playbook/pb_advanced_table/docs/advanced_table_mock_data_no_subrows.json +42 -0
  22. data/app/pb_kits/playbook/pb_advanced_table/docs/advanced_table_mock_data_with_id.json +299 -0
  23. data/app/pb_kits/playbook/pb_advanced_table/docs/example.yml +5 -0
  24. data/app/pb_kits/playbook/pb_advanced_table/docs/index.js +6 -1
  25. data/app/pb_kits/playbook/pb_collapsible/collapsible.html.erb +3 -1
  26. data/app/pb_kits/playbook/pb_collapsible/collapsible.rb +3 -0
  27. data/app/pb_kits/playbook/pb_date_picker/date_picker.html.erb +22 -10
  28. data/app/pb_kits/playbook/pb_date_picker/date_picker.rb +2 -0
  29. data/app/pb_kits/playbook/pb_date_picker/docs/_date_picker_turbo_frames.html.erb +13 -0
  30. data/app/pb_kits/playbook/pb_date_picker/docs/_date_picker_turbo_frames_rails.md +3 -0
  31. data/app/pb_kits/playbook/pb_date_picker/docs/example.yml +1 -0
  32. data/app/pb_kits/playbook/pb_drawer/_drawer.scss +145 -183
  33. data/app/pb_kits/playbook/pb_drawer/_drawer.tsx +158 -268
  34. data/app/pb_kits/playbook/pb_drawer/context.ts +11 -0
  35. data/app/pb_kits/playbook/pb_drawer/docs/_drawer_behavior.jsx +38 -0
  36. data/app/pb_kits/playbook/pb_drawer/docs/_drawer_borders.jsx +3 -45
  37. data/app/pb_kits/playbook/pb_drawer/docs/_drawer_breakpoints.jsx +0 -1
  38. data/app/pb_kits/playbook/pb_drawer/docs/_drawer_default.jsx +9 -16
  39. data/app/pb_kits/playbook/pb_drawer/docs/_drawer_menu.jsx +44 -19
  40. data/app/pb_kits/playbook/pb_drawer/docs/_drawer_menu.md +21 -3
  41. data/app/pb_kits/playbook/pb_drawer/docs/_drawer_overlay.jsx +16 -21
  42. data/app/pb_kits/playbook/pb_drawer/docs/_drawer_sizes.jsx +2 -19
  43. data/app/pb_kits/playbook/pb_drawer/docs/example.yml +2 -1
  44. data/app/pb_kits/playbook/pb_drawer/docs/index.js +1 -0
  45. data/app/pb_kits/playbook/pb_drawer/drawer.test.jsx +5 -5
  46. data/app/pb_kits/playbook/pb_drawer/hooks/useBreakpoint.tsx +60 -0
  47. data/app/pb_kits/playbook/pb_drawer/hooks/useDrawerAnimation.tsx +21 -0
  48. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_subtle_variant.md +1 -1
  49. data/app/pb_kits/playbook/pb_icon_button/_icon_button.scss +78 -0
  50. data/app/pb_kits/playbook/pb_icon_button/docs/_icon_button_default.html.erb +3 -0
  51. data/app/pb_kits/playbook/pb_icon_button/docs/example.yml +7 -0
  52. data/app/pb_kits/playbook/pb_icon_button/icon_button.html.erb +16 -0
  53. data/app/pb_kits/playbook/pb_icon_button/icon_button.rb +22 -0
  54. data/app/pb_kits/playbook/pb_loading_inline/_loading_inline.tsx +6 -1
  55. data/app/pb_kits/playbook/pb_multiple_users/_multiple_users.scss +4 -0
  56. data/app/pb_kits/playbook/pb_multiple_users/_multiple_users.tsx +1 -0
  57. data/app/pb_kits/playbook/pb_multiple_users/multiple_users.html.erb +1 -1
  58. data/app/pb_kits/playbook/pb_radio/_radio.scss +12 -8
  59. data/app/pb_kits/playbook/pb_radio/docs/_radio_custom_children.jsx +8 -3
  60. data/app/pb_kits/playbook/pb_select/_select.scss +3 -5
  61. data/app/pb_kits/playbook/pb_select/_select.tsx +5 -1
  62. data/app/pb_kits/playbook/pb_select/select.html.erb +2 -2
  63. data/app/pb_kits/playbook/pb_selectable_icon/_selectable_icon.tsx +9 -1
  64. data/app/pb_kits/playbook/pb_selectable_icon/docs/_selectable_icon_default.jsx +4 -1
  65. data/app/pb_kits/playbook/pb_selectable_icon/docs/_selectable_icon_single_select.jsx +4 -1
  66. data/app/pb_kits/playbook/pb_table/docs/_table_with_collapsible.html.erb +47 -0
  67. data/app/pb_kits/playbook/pb_table/docs/_table_with_collapsible_rails.md +2 -0
  68. data/app/pb_kits/playbook/pb_table/docs/example.yml +1 -0
  69. data/app/pb_kits/playbook/pb_table/index.ts +177 -137
  70. data/app/pb_kits/playbook/pb_table/styles/_collapsible.scss +12 -0
  71. data/app/pb_kits/playbook/pb_table/table_row.html.erb +20 -1
  72. data/app/pb_kits/playbook/pb_table/table_row.rb +5 -0
  73. data/app/pb_kits/playbook/pb_text_input/docs/_text_input_mask.html.erb +46 -0
  74. data/app/pb_kits/playbook/pb_text_input/docs/_text_input_mask_rails.md +3 -0
  75. data/app/pb_kits/playbook/pb_text_input/docs/example.yml +2 -1
  76. data/app/pb_kits/playbook/pb_text_input/index.js +103 -0
  77. data/app/pb_kits/playbook/pb_text_input/text_input.html.erb +4 -0
  78. data/app/pb_kits/playbook/pb_text_input/text_input.rb +33 -3
  79. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_truncated_text.html.erb +19 -0
  80. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_truncated_text.jsx +27 -0
  81. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_truncated_text.md +1 -0
  82. data/app/pb_kits/playbook/pb_typeahead/docs/example.yml +2 -0
  83. data/app/pb_kits/playbook/pb_typeahead/docs/index.js +1 -0
  84. data/dist/chunks/_typeahead-BWwaAo_0.js +36 -0
  85. data/dist/chunks/_weekday_stacked-zyBCd1s8.js +45 -0
  86. data/dist/chunks/{lib-OFT985dg.js → lib-kMuhBuU7.js} +1 -1
  87. data/dist/chunks/{pb_form_validation-CrsXd1-Y.js → pb_form_validation-DBJ0wZuS.js} +1 -1
  88. data/dist/chunks/vendor.js +1 -1
  89. data/dist/menu.yml +6 -0
  90. data/dist/playbook-doc.js +1 -1
  91. data/dist/playbook-rails-react-bindings.js +1 -1
  92. data/dist/playbook-rails.js +1 -1
  93. data/dist/playbook.css +1 -1
  94. data/lib/playbook/version.rb +1 -1
  95. metadata +37 -7
  96. data/dist/chunks/_typeahead-TN5aDUj9.js +0 -36
  97. data/dist/chunks/_weekday_stacked-en9fB1YM.js +0 -45
  98. /data/app/pb_kits/playbook/pb_table/docs/{_table_with_collapsible.md → _table_with_collapsible_react.md} +0 -0
@@ -1,167 +1,207 @@
1
1
  import PbEnhancedElement from '../pb_enhanced_element'
2
2
 
3
3
  const TABLE_WRAPPER_SELECTOR = "[data-pb-table-wrapper]";
4
+ const TABLE_COLLAPSIBLE_WRAPPER_SELECTOR = "[data-pb-table-collapsible-wrapper]";
4
5
 
5
6
  export default class PbTable extends PbEnhancedElement {
6
- stickyLeftColumns: string[] = [];
7
- stickyRightColumns: string[] = [];
8
- stickyRightColumnsReversed: string[] = [];
9
-
10
- static get selector(): string {
11
- return TABLE_WRAPPER_SELECTOR;
12
- }
13
-
14
- connect() {
15
- if (this.element.classList.contains('table-responsive-collapse')) {
16
- const headers: string[] = [];
7
+ stickyLeftColumns: string[] = [];
8
+ stickyRightColumns: string[] = [];
9
+ stickyRightColumnsReversed: string[] = [];
10
+
11
+ static get selector(): string {
12
+ return TABLE_WRAPPER_SELECTOR;
13
+ }
14
+
15
+ connect() {
16
+ if (this.element.classList.contains('table-responsive-collapse')) {
17
+ const headers: string[] = [];
18
+
19
+ [].forEach.call(this.element.querySelectorAll('th'), (header: HTMLTableCellElement) => {
20
+ const colSpan = header.colSpan
21
+ for (let i = 0; i < colSpan; i++) {
22
+ headers.push(header.textContent.replace(/\r?\n|\r/, ''));
23
+ }
24
+ });
17
25
 
18
- [].forEach.call(this.element.querySelectorAll('th'), (header: HTMLTableCellElement) => {
19
- const colSpan = header.colSpan
20
- for (let i = 0; i < colSpan; i++) {
21
- headers.push(header.textContent.replace(/\r?\n|\r/, ''));
22
- }
23
- });
24
-
25
- [].forEach.call(this.element.querySelectorAll('tbody tr'), (row: HTMLTableRowElement) => {
26
- [].forEach.call(row.cells, (cell: HTMLTableCellElement, headerIndex: number) => {
27
- cell.setAttribute('data-title', headers[headerIndex])
28
- })
26
+ [].forEach.call(this.element.querySelectorAll('tbody tr'), (row: HTMLTableRowElement) => {
27
+ [].forEach.call(row.cells, (cell: HTMLTableCellElement, headerIndex: number) => {
28
+ cell.setAttribute('data-title', headers[headerIndex])
29
29
  })
30
- }
31
-
32
- this.initStickyLeftColumns();
33
- this.initStickyRightColumns();
30
+ })
34
31
  }
35
32
 
36
- initStickyLeftColumns() {
37
- const table = this.element.querySelector('.sticky-left-column');
38
-
39
- if (table) {
40
- const classList = Array.from(table.classList);
41
- const stickyColumnClass = classList.find(cls => cls.startsWith('sticky-left-columns-ids-'));
42
-
43
- if (stickyColumnClass) {
44
- this.stickyLeftColumns = stickyColumnClass
45
- .replace('sticky-left-columns-ids-', '')
46
- .split('-');
47
-
48
- if (this.stickyLeftColumns.length > 0) {
49
- setTimeout(() => {
50
- this.handleStickyLeftColumns();
51
- window.addEventListener('resize', () => this.handleStickyLeftColumns());
52
- }, 10);
53
- }
33
+ this.initStickyLeftColumns();
34
+ this.initStickyRightColumns();
35
+ this.handleCollapsibleClick();
36
+ this.handleCollapsibleRow();
37
+ }
38
+
39
+ initStickyLeftColumns() {
40
+ const table = this.element.querySelector('.sticky-left-column');
41
+
42
+ if (table) {
43
+ const classList = Array.from(table.classList);
44
+ const stickyColumnClass = classList.find(cls => cls.startsWith('sticky-left-columns-ids-'));
45
+
46
+ if (stickyColumnClass) {
47
+ this.stickyLeftColumns = stickyColumnClass
48
+ .replace('sticky-left-columns-ids-', '')
49
+ .split('-');
50
+
51
+ if (this.stickyLeftColumns.length > 0) {
52
+ setTimeout(() => {
53
+ this.handleStickyLeftColumns();
54
+ window.addEventListener('resize', () => this.handleStickyLeftColumns());
55
+ }, 10);
54
56
  }
55
57
  }
56
58
  }
59
+ }
60
+
61
+ handleStickyLeftColumns() {
62
+ let accumulatedWidth = 0;
63
+
64
+ this.stickyLeftColumns.forEach((colId, index) => {
65
+ const isLastColumn = index === this.stickyLeftColumns.length - 1;
66
+ const header = this.element.querySelector(`th[id="${colId}"]`);
67
+ const cells = this.element.querySelectorAll(`td[id="${colId}"]`);
68
+
69
+ if (header) {
70
+ header.classList.add('sticky');
71
+ (header as HTMLElement).style.left = `${accumulatedWidth}px`;
72
+
73
+ if (!isLastColumn) {
74
+ header.classList.add('with-border-right');
75
+ header.classList.remove('sticky-left-shadow');
76
+ } else {
77
+ header.classList.remove('with-border-right');
78
+ header.classList.add('sticky-left-shadow');
79
+ }
57
80
 
58
- handleStickyLeftColumns() {
59
- let accumulatedWidth = 0;
60
-
61
- this.stickyLeftColumns.forEach((colId, index) => {
62
- const isLastColumn = index === this.stickyLeftColumns.length - 1;
63
- const header = this.element.querySelector(`th[id="${colId}"]`);
64
- const cells = this.element.querySelectorAll(`td[id="${colId}"]`);
65
-
66
- if (header) {
67
- header.classList.add('sticky');
68
- (header as HTMLElement).style.left = `${accumulatedWidth}px`;
81
+ accumulatedWidth += (header as HTMLElement).offsetWidth;
82
+ }
69
83
 
70
- if (!isLastColumn) {
71
- header.classList.add('with-border-right');
72
- header.classList.remove('sticky-left-shadow');
73
- } else {
74
- header.classList.remove('with-border-right');
75
- header.classList.add('sticky-left-shadow');
76
- }
84
+ cells.forEach((cell) => {
85
+ cell.classList.add('sticky');
86
+ (cell as HTMLElement).style.left = `${accumulatedWidth - (header as HTMLElement).offsetWidth}px`;
77
87
 
78
- accumulatedWidth += (header as HTMLElement).offsetWidth;
88
+ if (!isLastColumn) {
89
+ cell.classList.add('with-border-right');
90
+ cell.classList.remove('sticky-left-shadow');
91
+ } else {
92
+ cell.classList.remove('with-border-right');
93
+ cell.classList.add('sticky-left-shadow');
79
94
  }
80
-
81
- cells.forEach((cell) => {
82
- cell.classList.add('sticky');
83
- (cell as HTMLElement).style.left = `${accumulatedWidth - (header as HTMLElement).offsetWidth}px`;
84
-
85
- if (!isLastColumn) {
86
- cell.classList.add('with-border-right');
87
- cell.classList.remove('sticky-left-shadow');
88
- } else {
89
- cell.classList.remove('with-border-right');
90
- cell.classList.add('sticky-left-shadow');
91
- }
92
- });
93
95
  });
94
- }
95
-
96
- initStickyRightColumns() {
97
- const table = this.element.querySelector('.sticky-right-column');
98
-
99
- if (table) {
100
- const classList = Array.from(table.classList);
101
- const stickyColumnClass = classList.find(cls => cls.startsWith('sticky-right-columns-ids-'));
102
-
103
- if (stickyColumnClass) {
104
- this.stickyRightColumns = stickyColumnClass
105
- .replace('sticky-right-columns-ids-', '')
106
- .split('-');
107
- this.stickyRightColumnsReversed = this.stickyRightColumns.reverse();
108
-
109
- if (this.stickyRightColumns.length > 0) {
110
- setTimeout(() => {
111
- this.handleStickyRightColumns();
112
- window.addEventListener('resize', () => this.handleStickyRightColumns());
113
- }, 10);
114
- }
96
+ });
97
+ }
98
+
99
+ initStickyRightColumns() {
100
+ const table = this.element.querySelector('.sticky-right-column');
101
+
102
+ if (table) {
103
+ const classList = Array.from(table.classList);
104
+ const stickyColumnClass = classList.find(cls => cls.startsWith('sticky-right-columns-ids-'));
105
+
106
+ if (stickyColumnClass) {
107
+ this.stickyRightColumns = stickyColumnClass
108
+ .replace('sticky-right-columns-ids-', '')
109
+ .split('-');
110
+ this.stickyRightColumnsReversed = this.stickyRightColumns.reverse();
111
+
112
+ if (this.stickyRightColumns.length > 0) {
113
+ setTimeout(() => {
114
+ this.handleStickyRightColumns();
115
+ window.addEventListener('resize', () => this.handleStickyRightColumns());
116
+ }, 10);
115
117
  }
116
118
  }
117
119
  }
120
+ }
121
+
122
+ handleStickyRightColumns() {
123
+ let accumulatedWidth = 0;
124
+
125
+ this.stickyRightColumnsReversed.forEach((colId, index) => {
126
+ const isLastColumn = index === this.stickyRightColumns.length - 1;
127
+ const header = this.element.querySelector(`th[id="${colId}"]`);
128
+ const cells = this.element.querySelectorAll(`td[id="${colId}"]`);
129
+
130
+ if (header) {
131
+ header.classList.add('sticky');
132
+ (header as HTMLElement).style.right = `${accumulatedWidth}px`;
133
+
134
+ if (!isLastColumn) {
135
+ header.classList.add('with-border-left');
136
+ header.classList.remove('sticky-right-shadow');
137
+ } else {
138
+ header.classList.remove('with-border-right');
139
+ header.classList.add('sticky-right-shadow');
140
+ }
118
141
 
119
- handleStickyRightColumns() {
120
- let accumulatedWidth = 0;
121
-
122
- this.stickyRightColumnsReversed.forEach((colId, index) => {
123
- const isLastColumn = index === this.stickyRightColumns.length - 1;
124
- const header = this.element.querySelector(`th[id="${colId}"]`);
125
- const cells = this.element.querySelectorAll(`td[id="${colId}"]`);
126
-
127
- if (header) {
128
- header.classList.add('sticky');
129
- (header as HTMLElement).style.right = `${accumulatedWidth}px`;
142
+ accumulatedWidth += (header as HTMLElement).offsetWidth;
143
+ }
130
144
 
131
- if (!isLastColumn) {
132
- header.classList.add('with-border-left');
133
- header.classList.remove('sticky-right-shadow');
134
- } else {
135
- header.classList.remove('with-border-right');
136
- header.classList.add('sticky-right-shadow');
137
- }
145
+ cells.forEach((cell) => {
146
+ cell.classList.add('sticky');
147
+ (cell as HTMLElement).style.right = `${accumulatedWidth - (header as HTMLElement).offsetWidth}px`;
138
148
 
139
- accumulatedWidth += (header as HTMLElement).offsetWidth;
149
+ if (!isLastColumn) {
150
+ cell.classList.add('with-border-left');
151
+ cell.classList.remove('sticky-right-shadow');
152
+ } else {
153
+ cell.classList.remove('with-border-left');
154
+ cell.classList.add('sticky-right-shadow');
140
155
  }
141
-
142
- cells.forEach((cell) => {
143
- cell.classList.add('sticky');
144
- (cell as HTMLElement).style.right = `${accumulatedWidth - (header as HTMLElement).offsetWidth}px`;
145
-
146
- if (!isLastColumn) {
147
- cell.classList.add('with-border-left');
148
- cell.classList.remove('sticky-right-shadow');
149
- } else {
150
- cell.classList.remove('with-border-left');
151
- cell.classList.add('sticky-right-shadow');
152
- }
156
+ });
157
+ });
158
+ }
159
+
160
+ handleCollapsibleClick() {
161
+ const collapsibleElements = this.element.querySelectorAll(TABLE_COLLAPSIBLE_WRAPPER_SELECTOR);
162
+ collapsibleElements.forEach((collapsibleElement) => {
163
+ collapsibleElement.addEventListener('click', (event) => {
164
+ document.dispatchEvent(new CustomEvent(`collapsed-toggle${(event.currentTarget as HTMLElement).id}`))
165
+
166
+ const toggleElements = this.element.querySelectorAll(`.collapsible_border_toggle${(event.currentTarget as HTMLElement).id}`);
167
+ toggleElements.forEach(element => {
168
+ element.classList.toggle('no-border');
169
+ element.classList.toggle('border-active');
153
170
  });
171
+ })
172
+ })
173
+ }
174
+
175
+ handleCollapsibleRow() {
176
+ const collapsibleRows = this.element.querySelectorAll('.pb_table_collapsible_row');
177
+ if (collapsibleRows.length > 0) {
178
+ collapsibleRows.forEach((row) => {
179
+ const previousRow = row.previousElementSibling;
180
+
181
+ if (
182
+ previousRow &&
183
+ previousRow.tagName === 'TR'
184
+ ) {
185
+ const tdCount = previousRow.querySelectorAll('td').length;
186
+ const collapsibleTd = row.querySelector('td');
187
+ if (collapsibleTd) {
188
+ collapsibleTd.colSpan = tdCount;
189
+ }
190
+ } else {
191
+ return
192
+ }
154
193
  });
155
194
  }
195
+ }
156
196
 
157
- // Cleanup method to remove event listener
158
- disconnect() {
159
- if (this.stickyLeftColumns.length > 0) {
160
- window.removeEventListener('resize', () => this.handleStickyLeftColumns());
161
- }
197
+ // Cleanup method to remove event listener
198
+ disconnect() {
199
+ if (this.stickyLeftColumns.length > 0) {
200
+ window.removeEventListener('resize', () => this.handleStickyLeftColumns());
201
+ }
162
202
 
163
- if (this.stickyRightColumns.length > 0) {
164
- window.removeEventListener('resize', () => this.handleStickyRightColumns());
165
- }
203
+ if (this.stickyRightColumns.length > 0) {
204
+ window.removeEventListener('resize', () => this.handleStickyRightColumns());
166
205
  }
167
- }
206
+ }
207
+ }
@@ -32,4 +32,16 @@
32
32
  }
33
33
  }
34
34
  }
35
+
36
+ .collapsible-tr {
37
+ cursor: pointer;
38
+ }
39
+
40
+ .no-border {
41
+ border-bottom: none !important;
42
+ }
43
+
44
+ .border-active {
45
+ border-bottom: 1px;
46
+ }
35
47
  }
@@ -1,4 +1,23 @@
1
- <% if object.tag == "table" %>
1
+ <% if object.collapsible && object.tag == "table" %>
2
+ <%= content_tag(:tr,
3
+ aria: object.aria,
4
+ class: object.classname + " collapsible-tr",
5
+ data: object.data.merge(id: object.id),
6
+ id: object.id,
7
+ 'data-pb-table-collapsible-wrapper' => true,
8
+ **combined_html_options) do %>
9
+ <%= content.presence %>
10
+ <% end %>
11
+
12
+ <tr class="pb_table_collapsible_row">
13
+ <%= pb_rails("collapsible", props: { classname: "collapsible_border_toggle#{object.id}" + " no-border", name: "default-example", tag: "td", padding: "none" }) do %>
14
+ <%= pb_rails("flex", props: { data: { "collapsible-main": "true"} }) %>
15
+ <%= pb_rails("collapsible/collapsible_content", props: { classname: object.collapsible_side_highlight ? "table_collapsible_side_highlight" : "", padding: "none", margin: "none", id: "collapsed-toggle#{object.id}" }) do %>
16
+ <%= object.collapsible_content %>
17
+ <% end %>
18
+ <% end %>
19
+ </tr>
20
+ <% elsif object.tag == "table" %>
2
21
  <%= content_tag(:tr,
3
22
  aria: object.aria,
4
23
  class: object.classname,
@@ -8,6 +8,11 @@ module Playbook
8
8
  prop :tag, type: Playbook::Props::Enum,
9
9
  values: %w[table div],
10
10
  default: "table"
11
+ prop :collapsible, type: Playbook::Props::Boolean,
12
+ default: false
13
+ prop :collapsible_content
14
+ prop :collapsible_side_highlight, type: Playbook::Props::Boolean,
15
+ default: false
11
16
 
12
17
  def classname
13
18
  generate_classname("pb_table_row_kit", side_highlight_class) + tag_class
@@ -0,0 +1,46 @@
1
+ <%= pb_rails("text_input", props: {
2
+ label: "Currency",
3
+ mask: "currency",
4
+ margin_bottom: "md",
5
+ name: "currency_name",
6
+ placeholder:"$0.00"
7
+ }) %>
8
+
9
+ <%= pb_rails("text_input", props: {
10
+ label: "ZIP Code",
11
+ mask: "zip_code",
12
+ margin_bottom: "md",
13
+ placeholder: "12345"
14
+ }) %>
15
+
16
+ <%= pb_rails("text_input", props: {
17
+ label: "Postal Code",
18
+ mask: "postal_code",
19
+ placeholder: "12345-6789",
20
+ margin_bottom: "md",
21
+ }) %>
22
+
23
+ <%= pb_rails("text_input", props: {
24
+ label: "SSN",
25
+ mask: "ssn",
26
+ margin_bottom: "md",
27
+ placeholder: "123-45-6789"
28
+ }) %>
29
+
30
+ <%= pb_rails("title" , props: {
31
+ text: "Hidden Input Under The Hood",
32
+ padding_bottom: "sm"
33
+ })%>
34
+
35
+ <%= pb_rails("text_input", props: {
36
+ label: "Currency",
37
+ mask: "currency",
38
+ margin_bottom: "md",
39
+ name: "currency_name",
40
+ id: "example-currency",
41
+ placeholder: "$0.00",
42
+ }) %>
43
+
44
+ <style>
45
+ #example-currency-sanitized {display: flex !important;}
46
+ </style>
@@ -0,0 +1,3 @@
1
+ The mask prop lets you style your inputs while maintaining the value that the user typed in.
2
+
3
+ It uses a hidden input field to submit the unformatted value as it will have the proper `name` attribute. It will also copy the id field with a `"#{your-id-sanitized}"`
@@ -8,6 +8,7 @@ examples:
8
8
  - text_input_inline: Inline
9
9
  - text_input_no_label: No Label
10
10
  - text_input_options: Input Options
11
+ - text_input_mask: Mask
11
12
  react:
12
13
  - text_input_default: Default
13
14
  - text_input_error: With Error
@@ -23,4 +24,4 @@ examples:
23
24
  - text_input_error_swift: With Error
24
25
  - text_input_disabled_swift: Disabled
25
26
  - text_input_add_on_swift: Add On
26
- - text_input_props_swift: ""
27
+ - text_input_props_swift: ""
@@ -0,0 +1,103 @@
1
+ export default class PbTextInput {
2
+ static start() {
3
+ const inputElements = document.querySelectorAll('[data-pb-input-mask="true"]');
4
+
5
+ inputElements.forEach((inputElement) => {
6
+ inputElement.addEventListener("input", (event) => {
7
+ const maskType = inputElement.getAttribute("mask");
8
+ const cursorPosition = inputElement.selectionStart;
9
+
10
+ let rawValue = event.target.value;
11
+ let formattedValue = rawValue;
12
+
13
+ // Apply formatting based on the mask type
14
+ switch (maskType) {
15
+ case "currency":
16
+ formattedValue = formatCurrency(rawValue);
17
+ break;
18
+ case "ssn":
19
+ formattedValue = formatSSN(rawValue);
20
+ break;
21
+ case "postal_code":
22
+ formattedValue = formatPostalCode(rawValue);
23
+ break;
24
+ case "zip_code":
25
+ formattedValue = formatZipCode(rawValue);
26
+ break;
27
+ }
28
+
29
+ // Update the sanitized input field in the same wrapper
30
+ const sanitizedInput = inputElement
31
+ .closest(".text_input_wrapper")
32
+ ?.querySelector('[data="sanitized-pb-input"]');
33
+
34
+ if (sanitizedInput) {
35
+ switch (maskType) {
36
+ case "ssn":
37
+ sanitizedInput.value = sanitizeSSN(formattedValue);
38
+ break;
39
+ case "currency":
40
+ sanitizedInput.value = sanitizeCurrency(formattedValue);
41
+ break;
42
+ default:
43
+ sanitizedInput.value = formattedValue;
44
+ }
45
+ }
46
+
47
+ inputElement.value = formattedValue;
48
+ setCursorPosition(inputElement, cursorPosition, rawValue, formattedValue);
49
+ });
50
+ });
51
+
52
+ }
53
+ }
54
+
55
+ function formatCurrency(value) {
56
+ const numericValue = value.replace(/[^0-9]/g, "").slice(0, 15);
57
+
58
+ if (!numericValue) return "";
59
+
60
+ const dollars = parseFloat((parseInt(numericValue) / 100).toFixed(2));
61
+ if (dollars === 0) return "";
62
+
63
+ return new Intl.NumberFormat("en-US", {
64
+ style: "currency",
65
+ currency: "USD",
66
+ maximumFractionDigits: 2,
67
+ }).format(dollars);
68
+ }
69
+
70
+ function formatSSN(value) {
71
+ const cleaned = value.replace(/\D/g, "").slice(0, 9);
72
+ return cleaned
73
+ .replace(/(\d{5})(?=\d)/, "$1-")
74
+ .replace(/(\d{3})(?=\d)/, "$1-");
75
+ }
76
+
77
+ function formatZipCode(value) {
78
+ return value.replace(/\D/g, "").slice(0, 5);
79
+ }
80
+
81
+ function formatPostalCode(value) {
82
+ const cleaned = value.replace(/\D/g, "").slice(0, 9);
83
+ return cleaned.replace(/(\d{5})(?=\d)/, "$1-");
84
+ }
85
+
86
+ function sanitizeSSN(input) {
87
+ return input.replace(/\D/g, "");
88
+ }
89
+
90
+ function sanitizeCurrency(input) {
91
+ return input.replace(/[$,]/g, "");
92
+ }
93
+
94
+ // function to set cursor position
95
+ function setCursorPosition(inputElement, cursorPosition, rawValue, formattedValue) {
96
+ const difference = formattedValue.length - rawValue.length;
97
+
98
+ const newPosition = Math.max(0, cursorPosition + difference);
99
+
100
+ requestAnimationFrame(() => {
101
+ inputElement.setSelectionRange(newPosition, newPosition);
102
+ });
103
+ }
@@ -13,9 +13,13 @@
13
13
  <%= pb_rails("text_input/add_on", props: object.add_on_props) do %>
14
14
  <%= input_tag %>
15
15
  <% end %>
16
+ <% elsif mask.present? %>
17
+ <%= input_tag %>
18
+ <%= tag(:input, data: "sanitized-pb-input", id: sanitized_id, name: object.name, style: "display: none;") %>
16
19
  <% else %>
17
20
  <%= input_tag %>
18
21
  <% end %>
19
22
  <%= pb_rails("body", props: {dark: object.dark, status: "negative", text: object.error}) if object.error %>
20
23
  <% end %>
21
24
  <% end %>
25
+
@@ -4,6 +4,15 @@
4
4
  module Playbook
5
5
  module PbTextInput
6
6
  class TextInput < Playbook::KitBase
7
+ VALID_MASKS = %w[currency zipCode postalCode ssn].freeze
8
+
9
+ MASK_PATTERNS = {
10
+ "currency" => '^\$\d{1,3}(?:,\d{3})*(?:\.\d{2})?$',
11
+ "zip_code" => '\d{5}',
12
+ "postal_code" => '\d{5}-\d{4}',
13
+ "ssn" => '\d{3}-\d{2}-\d{4}',
14
+ }.freeze
15
+
7
16
  prop :autocomplete, type: Playbook::Props::Boolean,
8
17
  default: true
9
18
  prop :disabled, type: Playbook::Props::Boolean,
@@ -25,6 +34,9 @@ module Playbook
25
34
  prop :add_on, type: Playbook::Props::NestedProps,
26
35
  nested_kit: Playbook::PbTextInput::AddOn
27
36
 
37
+ prop :mask, type: Playbook::Props::String,
38
+ default: nil
39
+
28
40
  def classname
29
41
  default_margin_bottom = margin_bottom.present? ? "" : " mb_sm"
30
42
  generate_classname("pb_text_input_kit") + default_margin_bottom + error_class + inline_class
@@ -46,6 +58,10 @@ module Playbook
46
58
  { dark: dark }.merge(add_on || {})
47
59
  end
48
60
 
61
+ def sanitized_id
62
+ "#{object.id}-sanitized" if id.present?
63
+ end
64
+
49
65
  private
50
66
 
51
67
  def all_input_options
@@ -55,12 +71,13 @@ module Playbook
55
71
  data: validation_data,
56
72
  disabled: disabled,
57
73
  id: input_options.dig(:id) || id,
58
- name: name,
59
- pattern: validation_pattern,
74
+ name: mask.present? ? "" : name,
75
+ pattern: validation_pattern || mask_pattern,
60
76
  placeholder: placeholder,
61
77
  required: required,
62
78
  type: type,
63
79
  value: value,
80
+ mask: mask,
64
81
  }.merge(input_options)
65
82
  end
66
83
 
@@ -75,7 +92,7 @@ module Playbook
75
92
  def validation_data
76
93
  fields = input_options.dig(:data) || {}
77
94
  fields[:message] = validation_message unless validation_message.blank?
78
- fields
95
+ mask ? fields.merge(pb_input_mask: true) : fields
79
96
  end
80
97
 
81
98
  def error_class
@@ -85,6 +102,19 @@ module Playbook
85
102
  def inline_class
86
103
  inline ? " inline" : ""
87
104
  end
105
+
106
+ def mask_data
107
+ return {} unless mask
108
+ raise ArgumentError, "mask must be one of: #{VALID_MASKS.join(', ')}" unless VALID_MASKS.include?(mask)
109
+
110
+ { mask: mask }
111
+ end
112
+
113
+ def mask_pattern
114
+ return nil unless mask
115
+
116
+ MASK_PATTERNS[mask]
117
+ end
88
118
  end
89
119
  end
90
120
  end