playbook_ui 16.2.0.pre.rc.0 → 16.2.0.pre.rc.2

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 (89) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/pb_advanced_table/Components/RegularTableView.tsx +12 -2
  3. data/app/pb_kits/playbook/pb_advanced_table/advanced_table.test.jsx +33 -0
  4. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_column_styling_background_custom.jsx +71 -0
  5. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_column_styling_background_custom.md +4 -0
  6. data/app/pb_kits/playbook/pb_advanced_table/docs/example.yml +1 -0
  7. data/app/pb_kits/playbook/pb_advanced_table/docs/index.js +2 -1
  8. data/app/pb_kits/playbook/pb_checkbox/_checkbox.scss +1 -1
  9. data/app/pb_kits/playbook/pb_checkbox/_checkbox.tsx +17 -0
  10. data/app/pb_kits/playbook/pb_checkbox/checkbox.html.erb +10 -1
  11. data/app/pb_kits/playbook/pb_checkbox/checkbox.rb +2 -0
  12. data/app/pb_kits/playbook/pb_checkbox/docs/_checkbox_required_indicator.html.erb +6 -0
  13. data/app/pb_kits/playbook/pb_checkbox/docs/_checkbox_required_indicator.jsx +17 -0
  14. data/app/pb_kits/playbook/pb_checkbox/docs/_checkbox_required_indicator.md +3 -0
  15. data/app/pb_kits/playbook/pb_checkbox/docs/example.yml +2 -0
  16. data/app/pb_kits/playbook/pb_checkbox/docs/index.js +1 -0
  17. data/app/pb_kits/playbook/pb_date_picker/_date_picker.tsx +14 -5
  18. data/app/pb_kits/playbook/pb_date_picker/docs/_date_picker_default.md +1 -0
  19. data/app/pb_kits/playbook/pb_dropdown/_dropdown.tsx +46 -11
  20. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_label.html.erb +6 -3
  21. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_label.jsx +1 -0
  22. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_label.md +3 -1
  23. data/app/pb_kits/playbook/pb_dropdown/dropdown.html.erb +10 -4
  24. data/app/pb_kits/playbook/pb_dropdown/dropdown.rb +9 -0
  25. data/app/pb_kits/playbook/pb_dropdown/dropdown_trigger.html.erb +7 -2
  26. data/app/pb_kits/playbook/pb_dropdown/dropdown_trigger.rb +4 -0
  27. data/app/pb_kits/playbook/pb_dropdown/index.js +125 -73
  28. data/app/pb_kits/playbook/pb_dropdown/subcomponents/DropdownTrigger.tsx +16 -0
  29. data/app/pb_kits/playbook/pb_dropdown/utilities/clickOutsideHelper.tsx +6 -0
  30. data/app/pb_kits/playbook/pb_form/docs/_form_with_required_indicator.html.erb +6 -3
  31. data/app/pb_kits/playbook/pb_form/pb_form_validation.js +9 -2
  32. data/app/pb_kits/playbook/pb_multi_level_select/_multi_level_select.scss +7 -0
  33. data/app/pb_kits/playbook/pb_multi_level_select/_multi_level_select.tsx +638 -549
  34. data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_label.html.erb +3 -3
  35. data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_label.jsx +4 -7
  36. data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_label.md +3 -0
  37. data/app/pb_kits/playbook/pb_multi_level_select/multi_level_select.test.jsx +4 -4
  38. data/app/pb_kits/playbook/pb_passphrase/_passphrase.tsx +20 -2
  39. data/app/pb_kits/playbook/pb_rich_text_editor/_rich_text_editor.tsx +51 -16
  40. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_label.jsx +44 -0
  41. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_label.md +1 -0
  42. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_required_indicator.jsx +1 -0
  43. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_label.jsx +28 -0
  44. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_label.md +1 -0
  45. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_required_indicator.jsx +1 -0
  46. data/app/pb_kits/playbook/pb_rich_text_editor/docs/example.yml +2 -0
  47. data/app/pb_kits/playbook/pb_rich_text_editor/docs/index.js +2 -0
  48. data/app/pb_kits/playbook/pb_table/index.ts +29 -27
  49. data/app/pb_kits/playbook/pb_text_input/text_input.html.erb +10 -10
  50. data/app/pb_kits/playbook/pb_textarea/_textarea.tsx +10 -0
  51. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_default.html.erb +3 -3
  52. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_default.jsx +3 -0
  53. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_default.md +1 -0
  54. data/app/pb_kits/playbook/pb_textarea/textarea.html.erb +25 -9
  55. data/app/pb_kits/playbook/pb_textarea/textarea.rb +7 -1
  56. data/app/pb_kits/playbook/pb_time_picker/_time_picker.tsx +97 -11
  57. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_on_handler.jsx +5 -2
  58. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_required_indicator.html.erb +6 -0
  59. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_required_indicator.jsx +16 -0
  60. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_required_indicator.md +3 -0
  61. data/app/pb_kits/playbook/pb_time_picker/docs/example.yml +2 -0
  62. data/app/pb_kits/playbook/pb_time_picker/docs/index.js +1 -0
  63. data/app/pb_kits/playbook/pb_time_picker/time_picker.rb +3 -0
  64. data/app/pb_kits/playbook/pb_time_picker/time_picker.test.jsx +47 -1
  65. data/app/pb_kits/playbook/pb_typeahead/_typeahead.tsx +410 -323
  66. data/app/pb_kits/playbook/pb_typeahead/components/Control.tsx +2 -0
  67. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_required_indicator.html.erb +16 -0
  68. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_required_indicator.jsx +23 -0
  69. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_required_indicator.md +3 -0
  70. data/app/pb_kits/playbook/pb_typeahead/docs/example.yml +2 -0
  71. data/app/pb_kits/playbook/pb_typeahead/docs/index.js +22 -21
  72. data/app/pb_kits/playbook/pb_typeahead/typeahead.html.erb +3 -2
  73. data/app/pb_kits/playbook/pb_typeahead/typeahead.rb +3 -1
  74. data/dist/chunks/{_pb_line_graph-BgKF_zz1.js → _pb_line_graph-DuJNCf7N.js} +1 -1
  75. data/dist/chunks/_typeahead-BKSzddAX.js +1 -0
  76. data/dist/chunks/{globalProps-BhVYCqRf.js → globalProps-Bc-FVsRt.js} +1 -1
  77. data/dist/chunks/lib-BwX82vim.js +29 -0
  78. data/dist/chunks/vendor.js +3 -3
  79. data/dist/menu.yml +2 -2
  80. data/dist/playbook-rails-react-bindings.js +1 -1
  81. data/dist/playbook-rails.js +1 -1
  82. data/dist/playbook.css +1 -1
  83. data/lib/playbook/forms/builder/form_field_builder.rb +1 -1
  84. data/lib/playbook/forms/builder/typeahead_field.rb +15 -1
  85. data/lib/playbook/forms/builder.rb +2 -2
  86. data/lib/playbook/version.rb +1 -1
  87. metadata +24 -6
  88. data/dist/chunks/_typeahead-Bfy-4mll.js +0 -1
  89. data/dist/chunks/lib-DD34ZrWL.js +0 -29
@@ -65,7 +65,7 @@
65
65
  }] %>
66
66
 
67
67
  <%= pb_rails("multi_level_select", props: {
68
- id: "multi-level-select-label-rails",
69
- label: "Select a department",
70
- tree_data: treeData
68
+ id: "select_a_department",
69
+ label: "Select a Department",
70
+ tree_data: treeData
71
71
  }) %>
@@ -73,14 +73,11 @@ const MultiLevelSelectDefault = (props) => {
73
73
  return (
74
74
  <div>
75
75
  <MultiLevelSelect
76
- id='multiselect-label'
76
+ id="select_a_department"
77
77
  label="Select a Department"
78
78
  onSelect={(selectedNodes) =>
79
- console.log(
80
- "Selected Items",
81
- selectedNodes
82
- )
83
- }
79
+ console.log("Selected Items", selectedNodes)
80
+ }
84
81
  treeData={treeData}
85
82
  {...props}
86
83
  />
@@ -88,4 +85,4 @@ const MultiLevelSelectDefault = (props) => {
88
85
  )
89
86
  };
90
87
 
91
- export default MultiLevelSelectDefault;
88
+ export default MultiLevelSelectDefault;
@@ -0,0 +1,3 @@
1
+ The MultiLevelSelect component optionally accepts a `label` prop to produce a label above the input.
2
+
3
+ Add an `id` to wire the label to the input so that clicking the label will move focus directly to the input, and open the drop-down.
@@ -192,7 +192,7 @@ describe('MultiLevelSelect multi variant', () => {
192
192
  />
193
193
  )
194
194
  const kit = screen.getByTestId(testId)
195
- const input = kit.querySelector('#multiselect_input')
195
+ const input = kit.querySelector('#multi-disabled-test_input')
196
196
  fireEvent.click(input)
197
197
 
198
198
  const disabledCheckbox = kit.querySelector('input[type="checkbox"][disabled]')
@@ -227,7 +227,7 @@ describe('MultiLevelSelect single variant', () => {
227
227
  />
228
228
  )
229
229
  const kit = screen.getByTestId(testId)
230
- const input = kit.querySelector('#multiselect_input')
230
+ const input = kit.querySelector('#single-disabled-test_input')
231
231
  fireEvent.click(input)
232
232
 
233
233
  const disabledRadio = kit.querySelector('input[type="radio"][disabled]')
@@ -246,7 +246,7 @@ describe('MultiLevelSelect single variant', () => {
246
246
  />
247
247
  )
248
248
  const kit = screen.getByTestId(testId)
249
- const input = kit.querySelector('#multiselect_input')
249
+ const input = kit.querySelector('#single-disabled-click-test_input')
250
250
  fireEvent.click(input)
251
251
 
252
252
  const disabledRadio = kit.querySelector('input[type="radio"][disabled]')
@@ -267,7 +267,7 @@ describe('MultiLevelSelect single variant', () => {
267
267
  />
268
268
  )
269
269
  const kit = screen.getByTestId(testId)
270
- const input = kit.querySelector('#multiselect_input')
270
+ const input = kit.querySelector('#single-enabled-click-test_input')
271
271
  fireEvent.click(input)
272
272
 
273
273
  const enabledRadio = kit.querySelector('input[type="radio"]:not([disabled])')
@@ -178,22 +178,40 @@ const Passphrase = (props: PassphraseProps): React.ReactElement => {
178
178
  {...inputProps}
179
179
  />
180
180
  <span
181
+ aria-label={
182
+ showPassphrase
183
+ ? "Passphrase currently visible. Click icon to hide password"
184
+ : "Passphrase currently hidden. Click icon to reveal password"
185
+ }
186
+ aria-pressed={showPassphrase}
181
187
  className="show-passphrase-icon"
182
188
  onClick={toggleShowPassphrase}
189
+ onKeyDown={(e) => {
190
+ if (e.key === "Enter" || e.key === " ") {
191
+ e.preventDefault()
192
+ toggleShowPassphrase(e as any)
193
+ }
194
+ }}
195
+ role="button"
196
+ tabIndex={0}
183
197
  >
184
198
  <Body
185
199
  className={showPassphrase ? "hide-icon" : ""}
186
200
  color="light"
187
201
  dark={dark}
188
202
  >
189
- <Icon icon="eye-slash" />
203
+ <Icon
204
+ aria={{ label: "eye icon" }}
205
+ icon="eye-slash"
206
+ />
190
207
  </Body>
191
208
  <Body
192
209
  className={showPassphrase ? "" : "hide-icon"}
193
210
  color="light"
194
211
  dark={dark}
195
212
  >
196
- <Icon
213
+ <Icon
214
+ aria={{ label: "eye icon" }}
197
215
  className="svg-inline--fa"
198
216
  customIcon={eyeIcon.icon as unknown as { [key: string]: SVGElement }}
199
217
  />
@@ -94,25 +94,51 @@ const RichTextEditor = (props: RichTextEditorProps): React.ReactElement => {
94
94
 
95
95
  const htmlProps = buildHtmlProps(htmlOptions)
96
96
 
97
+ const fieldId = id ? (id as string) : null
98
+ const labelElementId = fieldId ? `${fieldId}-label` : null
99
+
97
100
  const handleOnEditorReady = (editorInstance: Editor) => {
98
101
  setEditor(editorInstance)
102
+
99
103
  setTimeout(() => {
100
- const oldId = editorInstance.element.getAttribute('input')
101
- if (oldId) {
102
- const hiddenInput = document.getElementById(oldId)
103
- if (hiddenInput) {
104
- const newId = (inputOptions.id as string) || oldId
105
- hiddenInput.id = newId
106
- editorInstance.element.setAttribute('input', newId)
107
-
108
- if (inputOptions.name) {
109
- hiddenInput.setAttribute('name', inputOptions.name as string)
110
- }
111
- }
104
+ const oldId = editorInstance.element?.getAttribute("input")
105
+ if (!oldId) return
106
+
107
+ const hiddenInput = document.getElementById(oldId) as HTMLElement | null
108
+ if (!hiddenInput) return
109
+
110
+ const hiddenInputId = (inputOptions.id as string) || oldId
111
+
112
+ if (hiddenInputId !== oldId) {
113
+ hiddenInput.id = hiddenInputId
114
+ editorInstance.element?.setAttribute("input", hiddenInputId)
115
+ }
116
+
117
+ if (inputOptions.name) {
118
+ hiddenInput.setAttribute("name", inputOptions.name as string)
112
119
  }
120
+
121
+ const editorDomId = (id as string) || `${hiddenInputId}_trix`
122
+ const trixLabelId = ((id as string) || hiddenInputId) + "-label"
123
+
124
+ if (label) {
125
+ editorInstance.element?.setAttribute("aria-labelledby", trixLabelId)
126
+ }
127
+ editorInstance.element!.id = editorDomId
113
128
  })
114
129
  }
115
130
 
131
+ useEffect(() => {
132
+ if (!advancedEditor || !fieldId || !labelElementId) return
133
+
134
+ const dom = advancedEditor.view?.dom as HTMLElement | undefined
135
+ if (!dom) return
136
+
137
+ dom.setAttribute("aria-labelledby", labelElementId)
138
+ dom.setAttribute("role", "textbox")
139
+ dom.setAttribute("aria-multiline", "true")
140
+ }, [advancedEditor, fieldId, labelElementId])
141
+
116
142
  // DOM manipulation must wait for editor to be ready
117
143
  if (editor && editor.element) {
118
144
  const toolbarElement = editor.element.parentElement.querySelector('trix-toolbar') as HTMLElement,
@@ -214,6 +240,8 @@ const RichTextEditor = (props: RichTextEditorProps): React.ReactElement => {
214
240
  // Determine if toolbar should be shown
215
241
  const shouldShowToolbar = focus && advancedEditor ? showToolbarOnFocus : advancedEditorToolbar
216
242
 
243
+ const labelFor = advancedEditor ? fieldId : (id ? id : (inputOptions.id ? `${inputOptions.id}_trix` : undefined))
244
+
217
245
  return (
218
246
  <div
219
247
  {...ariaProps}
@@ -223,7 +251,14 @@ const RichTextEditor = (props: RichTextEditorProps): React.ReactElement => {
223
251
  ref={focus ? containerRef : undefined}
224
252
  >
225
253
  {label && (
226
- <label htmlFor={id}>
254
+ <label
255
+ {...(labelFor ? { htmlFor: labelFor, id: labelElementId } : {})}
256
+ onMouseDown={(e) => {
257
+ if (!advancedEditor || !fieldId) return
258
+ e.preventDefault()
259
+ advancedEditor.commands.focus()
260
+ }}
261
+ >
227
262
  {
228
263
  requiredIndicator ? (
229
264
  <Caption className="pb_text_input_kit_label"
@@ -246,9 +281,9 @@ const RichTextEditor = (props: RichTextEditorProps): React.ReactElement => {
246
281
  advancedEditor ? (
247
282
  <div
248
283
  className={classnames(
249
- "pb_rich_text_editor_advanced_container",
250
- { [`input_height_${inputHeight}`]: !!inputHeight,[`input_min_height_${inputMinHeight}`]: !!inputMinHeight ,["toolbar-active"]: shouldShowToolbar }
251
- )}
284
+ "pb_rich_text_editor_advanced_container",
285
+ { [`input_height_${inputHeight}`]: !!inputHeight,[`input_min_height_${inputMinHeight}`]: !!inputMinHeight ,["toolbar-active"]: shouldShowToolbar }
286
+ )}
252
287
  >
253
288
  {shouldShowToolbar && (
254
289
  <EditorToolbar editor={advancedEditor}
@@ -0,0 +1,44 @@
1
+ import React from 'react'
2
+ import RichTextEditor from '../../pb_rich_text_editor/_rich_text_editor'
3
+ import { useEditor, EditorContent } from "@tiptap/react"
4
+ import StarterKit from "@tiptap/starter-kit"
5
+ import Link from '@tiptap/extension-link'
6
+
7
+
8
+ const RichTextEditorAdvancedLabel = (props) => {
9
+
10
+ const editor = useEditor({
11
+ extensions: [StarterKit, Link],
12
+ content: "Add your text here. You can format your text, add links, quotes, and bullets.",
13
+ })
14
+
15
+ const editorNoLabel = useEditor({
16
+ extensions: [StarterKit, Link],
17
+ content: "Add your text here. You can format your text, add links, quotes, and bullets.",
18
+ })
19
+
20
+ if (!editor || !editorNoLabel) return null
21
+
22
+ return (
23
+ <div>
24
+ <RichTextEditor
25
+ advancedEditor={editor}
26
+ id={"advanced-example"}
27
+ label="Advanced Example Label"
28
+ {...props}
29
+ >
30
+ <EditorContent editor={editor}/>
31
+ </RichTextEditor>
32
+ <br/>
33
+ <RichTextEditor
34
+ advancedEditor={editorNoLabel}
35
+ label="Advanced Example Label No ID"
36
+ {...props}
37
+ >
38
+ <EditorContent editor={editorNoLabel}/>
39
+ </RichTextEditor>
40
+ </div>
41
+ )
42
+ }
43
+
44
+ export default RichTextEditorAdvancedLabel
@@ -0,0 +1 @@
1
+ The optional `label` prop adds a visible label to the advanced editor. Passing in the `id` prop associates the `label` with the editor for accessibility, enabling screen reader support and label-based focus behavior.
@@ -22,6 +22,7 @@ const RichTextEditorAdvancedRequiredIndicator = (props) => {
22
22
  <div>
23
23
  <RichTextEditor
24
24
  advancedEditor={editor}
25
+ id={"required-advanced"}
25
26
  label="Label"
26
27
  requiredIndicator
27
28
  {...props}
@@ -0,0 +1,28 @@
1
+ import React, { useState } from 'react'
2
+ import RichTextEditor from '../../pb_rich_text_editor/_rich_text_editor'
3
+
4
+ const RichTextEditorLabel = (props) => {
5
+ const [value, setValue] = useState(''),
6
+ handleOnChange = (html) => setValue(html)
7
+
8
+ return (
9
+ <div>
10
+ <RichTextEditor
11
+ id="example"
12
+ label="Example Label"
13
+ onChange={handleOnChange}
14
+ value={value}
15
+ {...props}
16
+ />
17
+ <br/>
18
+ <RichTextEditor
19
+ label="Example Label No ID"
20
+ onChange={handleOnChange}
21
+ value={value}
22
+ {...props}
23
+ />
24
+ </div>
25
+ )
26
+ }
27
+
28
+ export default RichTextEditorLabel
@@ -0,0 +1 @@
1
+ The optional `label` prop adds a visible label to the editor. Passing in the `id` prop associates the `label` with the editor for accessibility, enabling screen reader support and label-based focus behavior.
@@ -8,6 +8,7 @@ const RichTextEditorRequiredIndicator = (props) => {
8
8
  return (
9
9
  <div>
10
10
  <RichTextEditor
11
+ inputOptions = {{ id: "required" }}
11
12
  label="Label"
12
13
  onChange={handleOnChange}
13
14
  requiredIndicator
@@ -32,6 +32,8 @@ examples:
32
32
  - rich_text_editor_advanced_inline: Advanced (Inline)
33
33
  - rich_text_editor_advanced_height: Advanced Height
34
34
  - rich_text_editor_advanced_min_height: Advanced Min Height
35
+ - rich_text_editor_label: Label
36
+ - rich_text_editor_advanced_label: Advanced (Label)
35
37
  - rich_text_editor_required_indicator: Required Indicator
36
38
  - rich_text_editor_advanced_required_indicator: Advanced Required Indicator
37
39
  - rich_text_editor_preview: Preview
@@ -21,3 +21,5 @@ export { default as RichTextEditorAdvancedHeight } from './_rich_text_editor_adv
21
21
  export { default as RichTextEditorAdvancedMinHeight } from './_rich_text_editor_advanced_min_height.jsx'
22
22
  export { default as RichTextEditorRequiredIndicator } from './_rich_text_editor_required_indicator.jsx'
23
23
  export { default as RichTextEditorAdvancedRequiredIndicator } from './_rich_text_editor_advanced_required_indicator.jsx'
24
+ export { default as RichTextEditorLabel } from './_rich_text_editor_label.jsx'
25
+ export { default as RichTextEditorAdvancedLabel } from './_rich_text_editor_advanced_label.jsx'
@@ -79,21 +79,22 @@ export default class PbTable extends PbEnhancedElement {
79
79
  header.classList.add('sticky-left-shadow');
80
80
  }
81
81
 
82
- accumulatedWidth += (header as HTMLElement).offsetWidth;
82
+ const headerWidth = (header as HTMLElement).offsetWidth;
83
+ accumulatedWidth += headerWidth;
84
+
85
+ cells.forEach((cell) => {
86
+ cell.classList.add('sticky');
87
+ (cell as HTMLElement).style.left = `${accumulatedWidth - headerWidth}px`;
88
+
89
+ if (!isLastColumn) {
90
+ cell.classList.add('with-border-right');
91
+ cell.classList.remove('sticky-left-shadow');
92
+ } else {
93
+ cell.classList.remove('with-border-right');
94
+ cell.classList.add('sticky-left-shadow');
95
+ }
96
+ });
83
97
  }
84
-
85
- cells.forEach((cell) => {
86
- cell.classList.add('sticky');
87
- (cell as HTMLElement).style.left = `${accumulatedWidth - (header as HTMLElement).offsetWidth}px`;
88
-
89
- if (!isLastColumn) {
90
- cell.classList.add('with-border-right');
91
- cell.classList.remove('sticky-left-shadow');
92
- } else {
93
- cell.classList.remove('with-border-right');
94
- cell.classList.add('sticky-left-shadow');
95
- }
96
- });
97
98
  });
98
99
  }
99
100
 
@@ -140,21 +141,22 @@ export default class PbTable extends PbEnhancedElement {
140
141
  header.classList.add('sticky-right-shadow');
141
142
  }
142
143
 
143
- accumulatedWidth += (header as HTMLElement).offsetWidth;
144
- }
144
+ const headerWidth = (header as HTMLElement).offsetWidth;
145
+ accumulatedWidth += headerWidth;
145
146
 
146
- cells.forEach((cell) => {
147
- cell.classList.add('sticky');
148
- (cell as HTMLElement).style.right = `${accumulatedWidth - (header as HTMLElement).offsetWidth}px`;
147
+ cells.forEach((cell) => {
148
+ cell.classList.add('sticky');
149
+ (cell as HTMLElement).style.right = `${accumulatedWidth - headerWidth}px`;
149
150
 
150
- if (!isLastColumn) {
151
- cell.classList.add('with-border-left');
152
- cell.classList.remove('sticky-right-shadow');
153
- } else {
154
- cell.classList.remove('with-border-left');
155
- cell.classList.add('sticky-right-shadow');
156
- }
157
- });
151
+ if (!isLastColumn) {
152
+ cell.classList.add('with-border-left');
153
+ cell.classList.remove('sticky-right-shadow');
154
+ } else {
155
+ cell.classList.remove('with-border-left');
156
+ cell.classList.add('sticky-right-shadow');
157
+ }
158
+ });
159
+ }
158
160
  });
159
161
  }
160
162
 
@@ -1,15 +1,16 @@
1
- <%= pb_content_tag(:div, id: nil ) do %>
1
+ <%= pb_content_tag(:div, id: nil) do %>
2
2
  <% if object.label.present? %>
3
- <label for="<%= object.input_options[:id] || object.id %>" >
4
- <% if object.required_indicator %>
5
- <%= pb_rails("caption", props: { dark: object.dark, classname: "pb_text_input_kit_label" }) do %>
6
- <%= object.label %><span style="color: #DA0014;"> *</span>
3
+ <label for="<%= object.input_options[:id] || object.id %>">
4
+ <% if object.required_indicator %>
5
+ <%= pb_rails("caption", props: { dark: object.dark, classname: "pb_text_input_kit_label" }) do %>
6
+ <%= object.label %><span style="color: #DA0014;"> *</span>
7
+ <% end %>
8
+ <% else %>
9
+ <%= pb_rails("caption", props: { text: object.label, dark: object.dark, classname: "pb_text_input_kit_label" }) %>
7
10
  <% end %>
8
- <% else %>
9
- <%= pb_rails("caption", props: { text: object.label, dark: object.dark, classname: "pb_text_input_kit_label" }) %>
10
- <% end %>
11
- </label>
11
+ </label>
12
12
  <% end %>
13
+
13
14
  <%= content_tag(:div, class: "#{add_on_class} text_input_wrapper") do %>
14
15
  <% if content.present? %>
15
16
  <%= content %>
@@ -26,4 +27,3 @@
26
27
  <%= pb_rails("body", props: {dark: object.dark, status: "negative", text: object.error, id: object.error_id, aria: { atomic: "true", live: "polite" }, html_options: { role: "alert" }}) if object.error %>
27
28
  <% end %>
28
29
  <% end %>
29
-
@@ -120,6 +120,7 @@ const Textarea = ({
120
120
  const characterCounter = () => {
121
121
  return maxCharacters && characterCount ? `${checkIfZero(characterCount)} / ${maxCharacters}` : `${checkIfZero(characterCount)}`
122
122
  }
123
+ const errorId = error ? `${id}-error` : undefined
123
124
 
124
125
  return (
125
126
  <div
@@ -145,7 +146,10 @@ const Textarea = ({
145
146
  )}
146
147
  {children || (
147
148
  <textarea
149
+ aria-describedby={errorId}
150
+ aria-invalid={!!error}
148
151
  disabled={disabled}
152
+ id={id}
149
153
  name={name}
150
154
  onChange={emojiMask ? handleChange : onChange}
151
155
  onPaste={emojiMask ? handlePaste : undefined}
@@ -167,6 +171,9 @@ const Textarea = ({
167
171
  >
168
172
  <FlexItem>
169
173
  <Body
174
+ aria={{ atomic: "true", live: "polite" }}
175
+ htmlOptions={{ role: "alert" }}
176
+ id={errorId}
170
177
  margin="none"
171
178
  status="negative"
172
179
  text={error}
@@ -182,6 +189,9 @@ const Textarea = ({
182
189
  </Flex>
183
190
  ) : (
184
191
  <Body
192
+ aria={{ atomic: "true", live: "polite" }}
193
+ htmlOptions={{ role: "alert" }}
194
+ id={errorId}
185
195
  status="negative"
186
196
  text={error}
187
197
  />
@@ -1,9 +1,9 @@
1
- <%= pb_rails("textarea", props: { label: "Label", rows: 4}) %>
1
+ <%= pb_rails("textarea", props: { label: "Label", rows: 4, id: "default-example-1" }) %>
2
2
 
3
3
  <br/>
4
4
 
5
- <%= pb_rails("textarea", props: { label: "Label", placeholder: "Placeholder text" }) %>
5
+ <%= pb_rails("textarea", props: { label: "Label", placeholder: "Placeholder text", id: "default-example-2" }) %>
6
6
 
7
7
  <br/>
8
8
 
9
- <%= pb_rails("textarea", props: { label: "Label", name: "comment", value: "Default value text" }) %>
9
+ <%= pb_rails("textarea", props: { label: "Label", name: "comment", value: "Default value text", id: "default-example-3" }) %>
@@ -13,6 +13,7 @@ const TextareaDefault = (props) => {
13
13
  label="Label"
14
14
  rows={4}
15
15
  {...props}
16
+ id="default-example-1"
16
17
  />
17
18
 
18
19
  <br />
@@ -21,6 +22,7 @@ const TextareaDefault = (props) => {
21
22
  label="Label"
22
23
  placeholder="Placeholder text"
23
24
  {...props}
25
+ id="default-example-2"
24
26
  />
25
27
 
26
28
  <br />
@@ -32,6 +34,7 @@ const TextareaDefault = (props) => {
32
34
  placeholder="Placeholder text"
33
35
  value={value}
34
36
  {...props}
37
+ id="default-example-3"
35
38
  />
36
39
 
37
40
  </div>
@@ -0,0 +1 @@
1
+ Add an `id` to your Textarea so that clicking the label will move focus directly to the input.
@@ -1,12 +1,14 @@
1
- <%= pb_content_tag do %>
1
+ <%= pb_content_tag(:div, id: nil) do %>
2
2
  <% if object.label.present? %>
3
- <% if object.required_indicator %>
4
- <%= pb_rails("caption", props: { text: object.label, dark: object.dark }) do %>
5
- <%= object.label %><span style="color: #DA0014;"> *</span>
3
+ <label for="<%= object.input_options[:id] || object.id %>" >
4
+ <% if object.required_indicator %>
5
+ <%= pb_rails("caption", props: { text: object.label, dark: object.dark }) do %>
6
+ <%= object.label %><span style="color: #DA0014;"> *</span>
7
+ <% end %>
8
+ <% else %>
9
+ <%= pb_rails("caption", props: {text: object.label, dark: object.dark}) %>
6
10
  <% end %>
7
- <% else %>
8
- <%= pb_rails("caption", props: {text: object.label, dark: object.dark}) %>
9
- <% end %>
11
+ </label>
10
12
  <% end %>
11
13
  <% if content.present? %>
12
14
  <%= content %>
@@ -22,14 +24,28 @@
22
24
  <% if object.character_count %>
23
25
  <%= pb_rails("flex", props: { spacing: "between", vertical: "center" }) do %>
24
26
  <%= pb_rails("flex/flex_item") do %>
25
- <%= pb_rails("body", props: { dark: object.dark, status: "negative", text: object.error }) %>
27
+ <%= pb_rails("body", props: {
28
+ dark: object.dark,
29
+ status: "negative",
30
+ text: object.error,
31
+ id: object.error_id,
32
+ aria: { atomic: "true", live: "polite" },
33
+ html_options: { role: "alert" },
34
+ }) %>
26
35
  <% end %>
27
36
  <%= pb_rails("flex/flex_item") do %>
28
37
  <%= pb_rails("caption", props: { margin: "none", size: "xs", text: object.character_counter }) %>
29
38
  <% end %>
30
39
  <% end %>
31
40
  <% else %>
32
- <%= pb_rails("body", props: { dark: object.dark, status: "negative", text: object.error }) %>
41
+ <%= pb_rails("body", props: {
42
+ dark: object.dark,
43
+ status: "negative",
44
+ text: object.error,
45
+ id: object.error_id,
46
+ aria: { atomic: "true", live: "polite" },
47
+ html_options: { role: "alert" },
48
+ }) %>
33
49
  <% end %>
34
50
  <% else %>
35
51
  <%= pb_rails("caption", props: { margin: "none", size: "xs", text: object.character_counter }) %>
@@ -47,7 +47,9 @@ module Playbook
47
47
  merged_data = data_attrs.merge(input_data)
48
48
 
49
49
  base_attributes = {
50
- id: input_options[:id] || "object_method",
50
+ 'aria-describedby': error.present? ? error_id : nil,
51
+ 'aria-invalid': error.present?,
52
+ id: input_options[:id] || id || "object_method",
51
53
  max_characters: max_characters,
52
54
  name: name,
53
55
  onkeyup: onkeyup,
@@ -66,6 +68,10 @@ module Playbook
66
68
  result
67
69
  end
68
70
 
71
+ def error_id
72
+ "#{id}-error" if error.present?
73
+ end
74
+
69
75
  private
70
76
 
71
77
  def error_class