playbook_ui 16.1.0.pre.rc.1 → 16.1.0.pre.rc.3

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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/pb_advanced_table/advanced_table.html.erb +2 -2
  3. data/app/pb_kits/playbook/pb_advanced_table/advanced_table.rb +4 -0
  4. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_inline_row_loading.md +2 -2
  5. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_inline_row_loading_rails.html.erb +64 -0
  6. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_inline_row_loading_rails.md +18 -0
  7. data/app/pb_kits/playbook/pb_advanced_table/docs/example.yml +1 -0
  8. data/app/pb_kits/playbook/pb_advanced_table/table_body.rb +51 -1
  9. data/app/pb_kits/playbook/pb_advanced_table/table_header.html.erb +1 -1
  10. data/app/pb_kits/playbook/pb_advanced_table/table_header.rb +34 -0
  11. data/app/pb_kits/playbook/pb_advanced_table/table_row.html.erb +1 -1
  12. data/app/pb_kits/playbook/pb_advanced_table/table_row.rb +19 -0
  13. data/app/pb_kits/playbook/pb_background/docs/_background_responsive.jsx +30 -0
  14. data/app/pb_kits/playbook/pb_background/docs/_background_responsive.md +1 -0
  15. data/app/pb_kits/playbook/pb_background/docs/example.yml +1 -0
  16. data/app/pb_kits/playbook/pb_background/docs/index.js +1 -0
  17. data/app/pb_kits/playbook/pb_form/docs/_form_with_required_indicator.html.erb +3 -1
  18. data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_input_display.html.erb +74 -0
  19. data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_input_display.jsx +87 -0
  20. data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_input_display.md +3 -0
  21. data/app/pb_kits/playbook/pb_multi_level_select/docs/example.yml +35 -33
  22. data/app/pb_kits/playbook/pb_multi_level_select/docs/index.js +1 -0
  23. data/app/pb_kits/playbook/pb_rich_text_editor/_rich_text_editor.tsx +33 -6
  24. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_required_indicator.jsx +35 -0
  25. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_required_indicator.md +3 -0
  26. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_required_indicator.html.erb +10 -0
  27. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_required_indicator.jsx +21 -0
  28. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_required_indicator.md +3 -0
  29. data/app/pb_kits/playbook/pb_rich_text_editor/docs/example.yml +3 -0
  30. data/app/pb_kits/playbook/pb_rich_text_editor/docs/index.js +2 -0
  31. data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor.rb +5 -0
  32. data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor.test.js +33 -18
  33. data/app/pb_kits/playbook/pb_table/docs/_sections.yml +68 -0
  34. data/app/pb_kits/playbook/pb_textarea/_textarea.tsx +29 -11
  35. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_input_options.html.erb +39 -0
  36. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_input_options.md +3 -0
  37. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_required_indicator.html.erb +5 -0
  38. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_required_indicator.jsx +25 -0
  39. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_required_indicator.md +3 -0
  40. data/app/pb_kits/playbook/pb_textarea/docs/example.yml +4 -1
  41. data/app/pb_kits/playbook/pb_textarea/docs/index.js +1 -0
  42. data/app/pb_kits/playbook/pb_textarea/index.ts +12 -5
  43. data/app/pb_kits/playbook/pb_textarea/textarea.html.erb +10 -10
  44. data/app/pb_kits/playbook/pb_textarea/textarea.rb +30 -0
  45. data/app/pb_kits/playbook/pb_textarea/textarea.test.js +18 -1
  46. data/app/pb_kits/playbook/utilities/test/globalProps/borderRadius.test.js +33 -0
  47. data/app/pb_kits/playbook/utilities/test/globalProps/bottom.test.js +60 -0
  48. data/app/pb_kits/playbook/utilities/test/globalProps/cursor.test.js +42 -0
  49. data/app/pb_kits/playbook/utilities/test/globalProps/dark.test.js +33 -0
  50. data/app/pb_kits/playbook/utilities/test/globalProps/gap.test.js +87 -0
  51. data/app/pb_kits/playbook/utilities/test/globalProps/globalProps.integration.test.js +936 -0
  52. data/app/pb_kits/playbook/utilities/test/globalProps/height.test.js +68 -0
  53. data/app/pb_kits/playbook/utilities/test/globalProps/htmlOptions.test.js +510 -0
  54. data/app/pb_kits/playbook/utilities/test/globalProps/left.test.js +60 -0
  55. data/app/pb_kits/playbook/utilities/test/globalProps/lineHeight.test.js +33 -0
  56. data/app/pb_kits/playbook/utilities/test/globalProps/margin.test.js +95 -0
  57. data/app/pb_kits/playbook/utilities/test/globalProps/numberSpacing.test.js +33 -0
  58. data/app/pb_kits/playbook/utilities/test/globalProps/overflow.test.js +68 -0
  59. data/app/pb_kits/playbook/utilities/test/globalProps/padding.test.js +95 -0
  60. data/app/pb_kits/playbook/utilities/test/globalProps/position.test.js +33 -0
  61. data/app/pb_kits/playbook/utilities/test/globalProps/right.test.js +60 -0
  62. data/app/pb_kits/playbook/utilities/test/globalProps/shadow.test.js +33 -0
  63. data/app/pb_kits/playbook/utilities/test/globalProps/textAlign.test.js +41 -0
  64. data/app/pb_kits/playbook/utilities/test/globalProps/top.test.js +60 -0
  65. data/app/pb_kits/playbook/utilities/test/globalProps/verticalAlign.test.js +40 -0
  66. data/app/pb_kits/playbook/utilities/test/globalProps/width.test.js +66 -0
  67. data/app/pb_kits/playbook/utilities/test/globalProps/zIndex.test.js +50 -0
  68. data/dist/chunks/{_pb_line_graph-hxi01lk7.js → _pb_line_graph-BgKF_zz1.js} +1 -1
  69. data/dist/chunks/{_typeahead-BgLnlhzP.js → _typeahead-B9a6ZsEP.js} +1 -1
  70. data/dist/chunks/{globalProps-DgYwLYNx.js → globalProps-BhVYCqRf.js} +1 -1
  71. data/dist/chunks/{lib-NLxTo8OB.js → lib-DD34ZrWL.js} +1 -1
  72. data/dist/chunks/vendor.js +2 -2
  73. data/dist/playbook-rails-react-bindings.js +1 -1
  74. data/dist/playbook-rails.js +1 -1
  75. data/lib/playbook/version.rb +1 -1
  76. metadata +46 -6
@@ -4,6 +4,8 @@ import { TrixEditor } from 'react-trix'
4
4
 
5
5
  import inlineFocus from './inlineFocus'
6
6
  import useFocus from './useFocus'
7
+ import Caption from '../pb_caption/_caption'
8
+ import colors from '../tokens/exports/_colors.module.scss'
7
9
  import { globalProps, GlobalProps } from '../utilities/globalProps'
8
10
  import { buildAriaProps, buildDataProps, noop, buildHtmlProps } from '../utilities/props'
9
11
 
@@ -40,12 +42,14 @@ type RichTextEditorProps = {
40
42
  inputOptions?: { [key: string]: string | number | boolean | (() => void) },
41
43
  id?: string,
42
44
  inline?: boolean,
45
+ label?: string,
43
46
  extensions?: { [key: string]: string }[],
44
47
  name?: string,
45
48
  onChange: (html: string, text: string) => void,
46
49
  placeholder?: string,
47
50
  inputHeight?: "sm" | "md" | "lg",
48
51
  inputMinHeight?: "sm" | "md" | "lg",
52
+ requiredIndicator?: boolean,
49
53
  simple?: boolean,
50
54
  sticky?: boolean,
51
55
  template: string,
@@ -64,6 +68,7 @@ const RichTextEditor = (props: RichTextEditorProps): React.ReactElement => {
64
68
  data = {},
65
69
  focus = false,
66
70
  htmlOptions = {},
71
+ id,
67
72
  inputOptions = {},
68
73
  inline = false,
69
74
  extensions,
@@ -76,7 +81,9 @@ const RichTextEditor = (props: RichTextEditorProps): React.ReactElement => {
76
81
  sticky = false,
77
82
  template = '',
78
83
  value = '',
79
- maxWidth = "md"
84
+ maxWidth = "md",
85
+ requiredIndicator = false,
86
+ label,
80
87
  } = props
81
88
 
82
89
  const ariaProps = buildAriaProps(aria),
@@ -86,7 +93,7 @@ const RichTextEditor = (props: RichTextEditorProps): React.ReactElement => {
86
93
  containerRef = useRef<HTMLDivElement>(null)
87
94
 
88
95
  const htmlProps = buildHtmlProps(htmlOptions)
89
-
96
+
90
97
  const handleOnEditorReady = (editorInstance: Editor) => {
91
98
  setEditor(editorInstance)
92
99
  setTimeout(() => {
@@ -94,7 +101,7 @@ const RichTextEditor = (props: RichTextEditorProps): React.ReactElement => {
94
101
  if (oldId) {
95
102
  const hiddenInput = document.getElementById(oldId)
96
103
  if (hiddenInput) {
97
- const newId = (inputOptions.id as string) || oldId
104
+ const newId = (inputOptions.id as string) || oldId
98
105
  hiddenInput.id = newId
99
106
  editorInstance.element.setAttribute('input', newId)
100
107
 
@@ -119,7 +126,7 @@ const RichTextEditor = (props: RichTextEditorProps): React.ReactElement => {
119
126
  // set button attributes
120
127
  inlineCodeButton.dataset.trixAttribute = 'inlineCode'
121
128
  blockCodeButton.insertAdjacentElement('afterend', inlineCodeButton)
122
- }
129
+ }
123
130
 
124
131
  if (toolbarBottom) editor.element.after(toolbarElement)
125
132
 
@@ -147,7 +154,7 @@ const RichTextEditor = (props: RichTextEditorProps): React.ReactElement => {
147
154
  if (!advancedEditor || !focus) return
148
155
 
149
156
  const handleFocus = () => setShowToolbarOnFocus(true)
150
-
157
+
151
158
  const handleClickOutside = (event: Event) => {
152
159
  if (isClickInContainer(event) || isClickInPopover(event)) return
153
160
  setShowToolbarOnFocus(false)
@@ -215,9 +222,29 @@ const RichTextEditor = (props: RichTextEditorProps): React.ReactElement => {
215
222
  className={css}
216
223
  ref={focus ? containerRef : undefined}
217
224
  >
225
+ {label && (
226
+ <label htmlFor={id}>
227
+ {
228
+ requiredIndicator ? (
229
+ <Caption className="pb_text_input_kit_label"
230
+ marginBottom="xs"
231
+ >
232
+ {label} <span style={{ color: `${colors.error}` }}>*</span>
233
+ </Caption>
234
+ ) : (
235
+ <Caption
236
+ className="pb_text_input_kit_label"
237
+ marginBottom="xs"
238
+ text={label}
239
+ />
240
+ )
241
+ }
242
+
243
+ </label>
244
+ )}
218
245
  {
219
246
  advancedEditor ? (
220
- <div
247
+ <div
221
248
  className={classnames(
222
249
  "pb_rich_text_editor_advanced_container",
223
250
  { [`input_height_${inputHeight}`]: !!inputHeight,[`input_min_height_${inputMinHeight}`]: !!inputMinHeight ,["toolbar-active"]: shouldShowToolbar }
@@ -0,0 +1,35 @@
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 RichTextEditorAdvancedRequiredIndicator = (props) => {
9
+
10
+ const editor = useEditor({
11
+ extensions: [
12
+ StarterKit,
13
+ Link
14
+ ],
15
+ content:"Add your text here. You can format your text, add links, quotes, and bullets."
16
+ })
17
+ if (!editor) {
18
+ return null
19
+ }
20
+
21
+ return (
22
+ <div>
23
+ <RichTextEditor
24
+ advancedEditor={editor}
25
+ label="Label"
26
+ requiredIndicator
27
+ {...props}
28
+ >
29
+ <EditorContent editor={editor}/>
30
+ </RichTextEditor>
31
+ </div>
32
+ )
33
+ }
34
+
35
+ export default RichTextEditorAdvancedRequiredIndicator
@@ -0,0 +1,3 @@
1
+ The `requiredIndicator`/`required_indicator` prop displays a red asterisk (*) next to the label, visually indicating that the field is required. This is purely visual and does not enforce validation.
2
+
3
+ You can use `requiredIndicator`/`required_indicator` with any validation approach: HTML5 validation via the `required` prop, client-side validation, or backend validation. For this reason, it works independently and doesn't need to be paired with the `required` prop.
@@ -0,0 +1,10 @@
1
+ <%= pb_rails("rich_text_editor", props: {
2
+ id: "required-indicator",
3
+ input_options: {
4
+ id: 'hidden_input_id',
5
+ name: "hidden_input_name"
6
+ },
7
+ label: "Label",
8
+ required_indicator: true,
9
+ value: "Add your text here. You can format your text, add links, quotes, and bullets."
10
+ }) %>
@@ -0,0 +1,21 @@
1
+ import React, { useState } from 'react'
2
+ import RichTextEditor from '../../pb_rich_text_editor/_rich_text_editor'
3
+
4
+ const RichTextEditorRequiredIndicator = (props) => {
5
+ const [value, setValue] = useState('Add your text here. You can format your text, add links, quotes, and bullets.'),
6
+ handleOnChange = (html) => setValue(html)
7
+
8
+ return (
9
+ <div>
10
+ <RichTextEditor
11
+ label="Label"
12
+ onChange={handleOnChange}
13
+ requiredIndicator
14
+ value={value}
15
+ {...props}
16
+ />
17
+ </div>
18
+ )
19
+ }
20
+
21
+ export default RichTextEditorRequiredIndicator
@@ -0,0 +1,3 @@
1
+ The `requiredIndicator`/`required_indicator` prop displays a red asterisk (*) next to the label, visually indicating that the field is required. This is purely visual and does not enforce validation.
2
+
3
+ You can use `requiredIndicator`/`required_indicator` with any validation approach: HTML5 validation via the `required` prop, client-side validation, or backend validation. For this reason, it works independently and doesn't need to be paired with the `required` prop.
@@ -9,6 +9,7 @@ examples:
9
9
  - rich_text_editor_templates: Templates
10
10
  # - rich_text_editor_toolbar_bottom: Toolbar Bottom
11
11
  - rich_text_editor_inline: Inline
12
+ - rich_text_editor_required_indicator: Required Indicator
12
13
  - rich_text_editor_preview: Preview
13
14
 
14
15
  react:
@@ -31,5 +32,7 @@ examples:
31
32
  - rich_text_editor_advanced_inline: Advanced (Inline)
32
33
  - rich_text_editor_advanced_height: Advanced Height
33
34
  - rich_text_editor_advanced_min_height: Advanced Min Height
35
+ - rich_text_editor_required_indicator: Required Indicator
36
+ - rich_text_editor_advanced_required_indicator: Advanced Required Indicator
34
37
  - rich_text_editor_preview: Preview
35
38
  - rich_text_editor_advanced_preview: Advanced Preview
@@ -19,3 +19,5 @@ export { default as RichTextEditorAdvancedSticky } from './_rich_text_editor_adv
19
19
  export { default as RichTextEditorAdvancedInline } from './_rich_text_editor_advanced_inline.jsx'
20
20
  export { default as RichTextEditorAdvancedHeight } from './_rich_text_editor_advanced_height.jsx'
21
21
  export { default as RichTextEditorAdvancedMinHeight } from './_rich_text_editor_advanced_min_height.jsx'
22
+ export { default as RichTextEditorRequiredIndicator } from './_rich_text_editor_required_indicator.jsx'
23
+ export { default as RichTextEditorAdvancedRequiredIndicator } from './_rich_text_editor_advanced_required_indicator.jsx'
@@ -21,6 +21,9 @@ module Playbook
21
21
  prop :template
22
22
  prop :placeholder
23
23
  prop :input_options
24
+ prop :label
25
+ prop :required_indicator, type: Playbook::Props::Boolean,
26
+ default: false
24
27
 
25
28
  def classname
26
29
  generate_classname("pb_rich_text_editor_kit", simple_class, focus_class, sticky_class, separator: " ")
@@ -51,6 +54,8 @@ module Playbook
51
54
  template: template,
52
55
  placeholder: placeholder,
53
56
  inputOptions: input_options,
57
+ label: label,
58
+ requiredIndicator: required_indicator,
54
59
  }
55
60
  end
56
61
  end
@@ -1,5 +1,5 @@
1
1
  import React from 'react'
2
- import { render, screen, fireEvent, waitFor } from '../utilities/test-utils'
2
+ import { render, screen, fireEvent, waitFor, within } from '../utilities/test-utils'
3
3
  import { useEditor, EditorContent } from "@tiptap/react"
4
4
  import StarterKit from "@tiptap/starter-kit"
5
5
 
@@ -89,14 +89,14 @@ const TestAdvancedEditor = ({ toolbarOnFocus = false, ...props }) => {
89
89
  describe('Advanced TipTap Editor works as expected', () => {
90
90
  test('renders advanced editor with toolbar', () => {
91
91
  render(<TestAdvancedEditor />)
92
-
92
+
93
93
  const kit = screen.getByTestId(testId)
94
94
  expect(kit).toHaveClass(kitClass)
95
-
95
+
96
96
  // Check for advanced container
97
97
  const advancedContainer = kit.querySelector('.pb_rich_text_editor_advanced_container')
98
98
  expect(advancedContainer).toBeInTheDocument()
99
-
99
+
100
100
  // Check for toolbar
101
101
  const toolbar = kit.querySelector('.toolbar')
102
102
  expect(toolbar).toBeInTheDocument()
@@ -104,7 +104,7 @@ describe('Advanced TipTap Editor works as expected', () => {
104
104
 
105
105
  test('renders advanced editor without toolbar when advancedEditorToolbar is false', () => {
106
106
  render(<TestAdvancedEditor advancedEditorToolbar={false} />)
107
-
107
+
108
108
  const kit = screen.getByTestId(testId)
109
109
  const toolbar = kit.querySelector('.toolbar')
110
110
  expect(toolbar).not.toBeInTheDocument()
@@ -112,17 +112,17 @@ describe('Advanced TipTap Editor works as expected', () => {
112
112
 
113
113
  test('shows/hides toolbar on focus when focus is enabled', async () => {
114
114
  render(<TestAdvancedEditor focus />)
115
-
115
+
116
116
  const kit = screen.getByTestId(testId)
117
-
117
+
118
118
  // Initially toolbar should be hidden
119
119
  let toolbar = kit.querySelector('.toolbar')
120
120
  expect(toolbar).not.toBeInTheDocument()
121
-
121
+
122
122
  const editorElement = kit.querySelector('.ProseMirror')
123
123
  // Focus the editor
124
124
  fireEvent.focus(editorElement)
125
-
125
+
126
126
  // Toolbar should now be visible
127
127
  await waitFor(() => {
128
128
  toolbar = kit.querySelector('.toolbar')
@@ -133,7 +133,7 @@ describe('Advanced TipTap Editor works as expected', () => {
133
133
 
134
134
  test('supports simple prop with advanced editor', () => {
135
135
  render(<TestAdvancedEditor simple />)
136
-
136
+
137
137
  const kit = screen.getByTestId(testId)
138
138
  const toolbar = kit.querySelector('.toolbar')
139
139
  expect(toolbar).toBeInTheDocument()
@@ -144,7 +144,7 @@ describe('Advanced TipTap Editor works as expected', () => {
144
144
 
145
145
  test('supports sticky prop with advanced editor', () => {
146
146
  render(<TestAdvancedEditor sticky />)
147
-
147
+
148
148
  const kit = screen.getByTestId(testId)
149
149
  const stickyToolbar = kit.querySelector('.pb_rich_text_editor_tiptap_toolbar_sticky')
150
150
  expect(stickyToolbar).toBeInTheDocument()
@@ -154,37 +154,52 @@ describe('Advanced TipTap Editor works as expected', () => {
154
154
  test('applies aria-label when provided', () => {
155
155
  const ariaLabel = 'Rich Text Editor'
156
156
  render(<TestAdvancedEditor aria={{ label: ariaLabel }} />)
157
-
157
+
158
158
  const kit = screen.getByTestId(testId)
159
159
  expect(kit).toHaveAttribute('aria-label', ariaLabel)
160
160
  })
161
161
 
162
162
  test('supports inline prop with advanced editor', () => {
163
163
  render(<TestAdvancedEditor inline />)
164
-
164
+
165
165
  const kit = screen.getByTestId(testId)
166
166
  const toolbar = kit.querySelector('.toolbar')
167
167
  expect(toolbar).toBeInTheDocument()
168
168
  expect(kit).toHaveClass(`${kitClass} inline`)
169
169
  })
170
170
 
171
+ test('renders required indicator asterisk when requiredIndicator is true', () => {
172
+ render(
173
+ <RichTextEditor
174
+ data={{ testid: testId }}
175
+ label="Label"
176
+ requiredIndicator
177
+ />
178
+ )
179
+
180
+ const kit = screen.getByTestId(testId)
181
+ const label = within(kit).getByText(/Label/)
182
+
183
+ expect(label).toBeInTheDocument()
184
+ expect(kit).toHaveTextContent('*')
185
+ })
186
+
171
187
  describe('TipTap Editor Functionality', () => {
172
188
  test('can type and update content', async () => {
173
189
  render(<TestAdvancedEditor />)
174
-
190
+
175
191
  const kit = screen.getByTestId(testId)
176
192
  const editorContent = kit.querySelector('.ProseMirror')
177
-
193
+
178
194
  // Focus and type in the editor
179
195
  fireEvent.focus(editorContent)
180
- fireEvent.input(editorContent, {
196
+ fireEvent.input(editorContent, {
181
197
  target: { textContent: 'New content' }
182
198
  })
183
-
199
+
184
200
  await waitFor(() => {
185
201
  expect(editorContent).toHaveTextContent('New content')
186
202
  })
187
203
  })
188
204
  })
189
205
  })
190
-
@@ -0,0 +1,68 @@
1
+ sections:
2
+ - title: "Size & Density"
3
+ examples:
4
+ - table_sm
5
+ - table_md
6
+ - table_lg
7
+ - table_multiline
8
+ - table_single_line
9
+ - table_outer_padding
10
+
11
+ - title: "Layout & Structure"
12
+ examples:
13
+ - table_responsive_table
14
+ - table_with_subcomponents
15
+ - table_with_subcomponents_as_divs
16
+ - table_with_background_kit
17
+
18
+
19
+ - title: "Sticky & Positional Behaviors"
20
+ examples:
21
+ - table_sticky
22
+ - table_sticky_left_columns
23
+ - table_sticky_right_columns
24
+ - table_sticky_columns
25
+ - table_alignment_row
26
+ - table_alignment_column
27
+ - table_alignment_shift_row
28
+ - table_alignment_shift_data
29
+ - table_side_highlight
30
+ - table_container
31
+
32
+ - title: "Collapsible & Nested Behaviors"
33
+ examples:
34
+ - table_with_collapsible
35
+ - table_with_dynamic_collapsible
36
+ - table_with_collapsible_with_custom_click
37
+ - table_with_collapsible_with_custom_content
38
+ - table_with_collapsible_with_nested_rows
39
+ - table_with_collapsible_with_nested_table
40
+
41
+ - title: "Data Presentation"
42
+ examples:
43
+ - table_data_table
44
+ - table_vertical_border
45
+ - table_striped
46
+
47
+ - title: "Header Variants"
48
+ examples:
49
+ - table_header
50
+ - table_with_header_style_borderless
51
+ - table_with_header_style_floating
52
+
53
+ - title: "Interactive Tables"
54
+ examples:
55
+ - table_with_clickable_rows
56
+ - table_with_selectable_rows
57
+ - table_with_filter_variant
58
+ - table_with_filter_variant_with_pagination
59
+ - table_disable_hover
60
+
61
+ - title: "Table Actions"
62
+ examples:
63
+ - table_one_action
64
+ - table_two_actions
65
+ - table_two_plus_actions
66
+ - table_action_middle
67
+ - table_icon_buttons
68
+
@@ -13,6 +13,7 @@ import Body from '../pb_body/_body'
13
13
  import Caption from '../pb_caption/_caption'
14
14
  import Flex from '../pb_flex/_flex'
15
15
  import FlexItem from '../pb_flex/_flex_item'
16
+ import colors from '../tokens/exports/_colors.module.scss'
16
17
 
17
18
  import { stripEmojisForPaste, applyEmojiMask } from '../utilities/emojiMask'
18
19
 
@@ -36,6 +37,7 @@ type TextareaProps = {
36
37
  value?: string,
37
38
  name?: string,
38
39
  required?: boolean,
40
+ requiredIndicator?: boolean,
39
41
  rows?: number,
40
42
  resize: "none" | "both" | "horizontal" | "vertical" | "auto",
41
43
  onChange?: InputCallback<HTMLTextAreaElement>,
@@ -50,6 +52,7 @@ const Textarea = ({
50
52
  disabled,
51
53
  emojiMask = false,
52
54
  htmlOptions = {},
55
+ id,
53
56
  inline = false,
54
57
  resize = 'none',
55
58
  error,
@@ -60,6 +63,7 @@ const Textarea = ({
60
63
  onChange = () => {},
61
64
  placeholder,
62
65
  required,
66
+ requiredIndicator = false,
63
67
  rows = 4,
64
68
  value,
65
69
  ...props
@@ -84,7 +88,7 @@ const Textarea = ({
84
88
  if (emojiMask) {
85
89
  const pastedText = e.clipboardData.getData('text')
86
90
  const filteredText = stripEmojisForPaste(pastedText)
87
-
91
+
88
92
  if (pastedText !== filteredText) {
89
93
  e.preventDefault()
90
94
  const textarea = e.currentTarget
@@ -93,10 +97,10 @@ const Textarea = ({
93
97
  const currentValue = textarea.value
94
98
  const newValue = currentValue.slice(0, start) + filteredText + currentValue.slice(end)
95
99
  const newCursorPosition = start + filteredText.length
96
-
100
+
97
101
  textarea.value = newValue
98
102
  textarea.selectionStart = textarea.selectionEnd = newCursorPosition
99
-
103
+
100
104
  onChange({ ...e, target: textarea, currentTarget: textarea } as unknown as ChangeEvent<HTMLTextAreaElement>)
101
105
  }
102
106
  }
@@ -124,7 +128,21 @@ const Textarea = ({
124
128
  {...htmlProps}
125
129
  className={classes}
126
130
  >
127
- <Caption text={label} />
131
+ {label && (
132
+ <label htmlFor={id}>
133
+ {
134
+ requiredIndicator ? (
135
+ <Caption className="pb_text_input_kit_label">
136
+ {label} <span style={{ color: `${colors.error}` }}>*</span>
137
+ </Caption>
138
+ ) : (
139
+ <Caption className="pb_text_input_kit_label"
140
+ text={label}
141
+ />
142
+ )
143
+ }
144
+ </label>
145
+ )}
128
146
  {children || (
129
147
  <textarea
130
148
  disabled={disabled}
@@ -143,19 +161,19 @@ const Textarea = ({
143
161
  {error ? (
144
162
  <>
145
163
  {characterCount ? (
146
- <Flex
147
- spacing="between"
164
+ <Flex
165
+ spacing="between"
148
166
  vertical="center"
149
167
  >
150
168
  <FlexItem>
151
- <Body
169
+ <Body
152
170
  margin="none"
153
171
  status="negative"
154
- text={error}
172
+ text={error}
155
173
  />
156
174
  </FlexItem>
157
175
  <FlexItem>
158
- <Caption
176
+ <Caption
159
177
  margin="none"
160
178
  size="xs"
161
179
  text={characterCounter()}
@@ -163,7 +181,7 @@ const Textarea = ({
163
181
  </FlexItem>
164
182
  </Flex>
165
183
  ) : (
166
- <Body
184
+ <Body
167
185
  status="negative"
168
186
  text={error}
169
187
  />
@@ -171,7 +189,7 @@ const Textarea = ({
171
189
  </>
172
190
  ) : (
173
191
  noCount && (
174
- <Caption
192
+ <Caption
175
193
  margin="none"
176
194
  size="xs"
177
195
  text={characterCounter()}
@@ -0,0 +1,39 @@
1
+ <%= pb_rails("textarea", props: {
2
+ label: "ID on Container",
3
+ id: "container-id",
4
+ name: "comment",
5
+ rows: 4
6
+ }) %>
7
+
8
+ <br/>
9
+
10
+ <%= pb_rails("textarea", props: {
11
+ label: "ID on Textarea via input_options",
12
+ input_options: { id: "textarea-id" },
13
+ name: "comment",
14
+ rows: 4
15
+ }) %>
16
+
17
+ <br/>
18
+
19
+ <%= pb_rails("textarea", props: {
20
+ label: "Both Container and Textarea IDs",
21
+ id: "container-id-2",
22
+ input_options: { id: "textarea-id-2" },
23
+ name: "comment",
24
+ rows: 4
25
+ }) %>
26
+
27
+ <br/>
28
+
29
+ <%= pb_rails("textarea", props: {
30
+ label: "Data and ARIA Attributes",
31
+ name: "description",
32
+ rows: 4,
33
+ input_options: {
34
+ 'aria-label': "Enter description",
35
+ 'aria-describedby': "help-text",
36
+ data: { controller: "textarea", action: "focus->handleFocus" },
37
+ id: "description-textarea"
38
+ }
39
+ }) %>
@@ -0,0 +1,3 @@
1
+ Use the `input_options` / `inputOptions` prop to pass additional attributes directly to the underlying `<textarea>` element instead of the outer wrapper. This is useful for applying data attributes, custom IDs, ARIA attributes, or other HTML attributes that need to be on the textarea element itself.
2
+
3
+ Additional HTML attributes (e.g. data or ARIA attributes) can also be passed directly to the `<textarea>` via `input_options`.
@@ -0,0 +1,5 @@
1
+ <%= pb_rails("textarea", props: {
2
+ label: "Label",
3
+ placeholder: "Placeholder text",
4
+ required_indicator: true
5
+ }) %>
@@ -0,0 +1,25 @@
1
+ import React, {useState} from 'react'
2
+
3
+ import Textarea from '../_textarea'
4
+
5
+ const TextareaRequiredIndicator = (props) => {
6
+ const [value, setValue] = useState('Default value text')
7
+ const handleChange = (event) => {
8
+ setValue(event.target.value)
9
+ }
10
+ return (
11
+ <div>
12
+ <Textarea
13
+ label="Label"
14
+ name="comment"
15
+ onChange={(e) => handleChange(e)}
16
+ placeholder="Placeholder text"
17
+ requiredIndicator
18
+ value={value}
19
+ {...props}
20
+ />
21
+ </div>
22
+ )
23
+ }
24
+
25
+ export default TextareaRequiredIndicator
@@ -0,0 +1,3 @@
1
+ The `requiredIndicator`/`required_indicator` prop displays a red asterisk (*) next to the label, visually indicating that the field is required. This is purely visual and does not enforce validation.
2
+
3
+ You can use `requiredIndicator`/`required_indicator` with any validation approach: HTML5 validation via the `required` prop, client-side validation, or backend validation. For this reason, it works independently and doesn't need to be paired with the `required` prop.
@@ -8,6 +8,8 @@ examples:
8
8
  - textarea_character_counter: Character Counter
9
9
  - textarea_inline: Inline
10
10
  - textarea_emoji_mask: Emoji Mask
11
+ - textarea_required_indicator: Required Indicator
12
+ - textarea_input_options: Input Options
11
13
 
12
14
  react:
13
15
  - textarea_default: Default
@@ -17,8 +19,9 @@ examples:
17
19
  - textarea_character_counter: Character Counter
18
20
  - textarea_inline: Inline
19
21
  - textarea_emoji_mask: Emoji Mask
22
+ - textarea_required_indicator: Required Indicator
20
23
 
21
24
  swift:
22
25
  - textarea_default_swift: Default
23
26
  - textarea_error_swift: Textarea w/ Error
24
- - textarea_props_swift: ""
27
+ - textarea_props_swift: ""
@@ -5,3 +5,4 @@ export { default as TextareaError } from './_textarea_error.jsx'
5
5
  export { default as TextareaCharacterCounter } from './_textarea_character_counter.jsx'
6
6
  export { default as TextareaInline } from './_textarea_inline.jsx'
7
7
  export { default as TextareaEmojiMask } from './_textarea_emoji_mask.jsx'
8
+ export { default as TextareaRequiredIndicator } from './_textarea_required_indicator.jsx'