playbook_ui 16.1.0.pre.rc.2 → 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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/pb_background/docs/_background_responsive.jsx +30 -0
  3. data/app/pb_kits/playbook/pb_background/docs/_background_responsive.md +1 -0
  4. data/app/pb_kits/playbook/pb_background/docs/example.yml +1 -0
  5. data/app/pb_kits/playbook/pb_background/docs/index.js +1 -0
  6. data/app/pb_kits/playbook/pb_form/docs/_form_with_required_indicator.html.erb +3 -1
  7. data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_input_display.html.erb +74 -0
  8. data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_input_display.jsx +87 -0
  9. data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_input_display.md +3 -0
  10. data/app/pb_kits/playbook/pb_multi_level_select/docs/example.yml +35 -33
  11. data/app/pb_kits/playbook/pb_multi_level_select/docs/index.js +1 -0
  12. data/app/pb_kits/playbook/pb_rich_text_editor/_rich_text_editor.tsx +33 -6
  13. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_required_indicator.jsx +35 -0
  14. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_required_indicator.md +3 -0
  15. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_required_indicator.html.erb +10 -0
  16. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_required_indicator.jsx +21 -0
  17. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_required_indicator.md +3 -0
  18. data/app/pb_kits/playbook/pb_rich_text_editor/docs/example.yml +3 -0
  19. data/app/pb_kits/playbook/pb_rich_text_editor/docs/index.js +2 -0
  20. data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor.rb +5 -0
  21. data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor.test.js +33 -18
  22. data/app/pb_kits/playbook/pb_textarea/_textarea.tsx +29 -11
  23. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_required_indicator.html.erb +5 -0
  24. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_required_indicator.jsx +25 -0
  25. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_required_indicator.md +3 -0
  26. data/app/pb_kits/playbook/pb_textarea/docs/example.yml +3 -1
  27. data/app/pb_kits/playbook/pb_textarea/docs/index.js +1 -0
  28. data/app/pb_kits/playbook/pb_textarea/index.ts +12 -5
  29. data/app/pb_kits/playbook/pb_textarea/textarea.html.erb +6 -0
  30. data/app/pb_kits/playbook/pb_textarea/textarea.rb +2 -0
  31. data/app/pb_kits/playbook/pb_textarea/textarea.test.js +18 -1
  32. data/app/pb_kits/playbook/utilities/test/globalProps/globalProps.integration.test.js +936 -0
  33. data/dist/chunks/{_pb_line_graph-hxi01lk7.js → _pb_line_graph-BgKF_zz1.js} +1 -1
  34. data/dist/chunks/{_typeahead-BgLnlhzP.js → _typeahead-B9a6ZsEP.js} +1 -1
  35. data/dist/chunks/{globalProps-DgYwLYNx.js → globalProps-BhVYCqRf.js} +1 -1
  36. data/dist/chunks/{lib-NLxTo8OB.js → lib-DD34ZrWL.js} +1 -1
  37. data/dist/chunks/vendor.js +2 -2
  38. data/dist/playbook-rails-react-bindings.js +1 -1
  39. data/dist/playbook-rails.js +1 -1
  40. data/lib/playbook/version.rb +1 -1
  41. metadata +20 -6
@@ -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
-
@@ -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,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,7 @@ 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
11
12
  - textarea_input_options: Input Options
12
13
 
13
14
  react:
@@ -18,8 +19,9 @@ examples:
18
19
  - textarea_character_counter: Character Counter
19
20
  - textarea_inline: Inline
20
21
  - textarea_emoji_mask: Emoji Mask
22
+ - textarea_required_indicator: Required Indicator
21
23
 
22
24
  swift:
23
25
  - textarea_default_swift: Default
24
26
  - textarea_error_swift: Textarea w/ Error
25
- - 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'
@@ -11,18 +11,21 @@ export default class PbTextarea extends PbEnhancedElement {
11
11
  }
12
12
 
13
13
  hasEmojiMask(): boolean {
14
+ if (!this.element) return false
14
15
  return (this.element as HTMLElement).dataset.pbEmojiMask === "true"
15
16
  }
16
17
 
17
- onInput(): void {
18
+ onInput = (): void => {
19
+ if (!this.element) return
20
+
18
21
  if ((this.element as HTMLElement).closest('.resize_auto')) {
19
- this.style.height = 'auto'
20
- this.style.height = (this.scrollHeight) + 'px'
22
+ (this.element as HTMLTextAreaElement).style.height = 'auto';
23
+ (this.element as HTMLTextAreaElement).style.height = (this.element as HTMLTextAreaElement).scrollHeight + 'px'
21
24
  }
22
25
  }
23
26
 
24
27
  handleEmojiInput = (): void => {
25
- if (!this.hasEmojiMask()) return
28
+ if (!this.element || !this.hasEmojiMask()) return
26
29
 
27
30
  if (this.skipNextEmojiFilter) {
28
31
  this.skipNextEmojiFilter = false
@@ -33,7 +36,7 @@ export default class PbTextarea extends PbEnhancedElement {
33
36
  }
34
37
 
35
38
  handleEmojiPaste = (event: ClipboardEvent): void => {
36
- if (!this.hasEmojiMask()) return
39
+ if (!this.element || !this.hasEmojiMask()) return
37
40
 
38
41
  const pastedText = event.clipboardData?.getData('text') || ''
39
42
  const filteredText = stripEmojisForPaste(pastedText)
@@ -57,6 +60,8 @@ export default class PbTextarea extends PbEnhancedElement {
57
60
  }
58
61
 
59
62
  connect(): void {
63
+ if (!this.element) return
64
+
60
65
  if ((this.element as HTMLElement).closest('.resize_auto')) {
61
66
  this.element.setAttribute('style', 'height:' + (this.element as HTMLTextAreaElement).scrollHeight + 'px;overflow-y:hidden;')
62
67
  this.element.addEventListener('input', this.onInput, false)
@@ -69,6 +74,8 @@ export default class PbTextarea extends PbEnhancedElement {
69
74
  }
70
75
 
71
76
  disconnect(): void {
77
+ if (!this.element) return
78
+
72
79
  this.element.removeEventListener('input', this.onInput, false)
73
80
  this.element.removeEventListener('input', this.handleEmojiInput, false)
74
81
  this.element.removeEventListener('paste', this.handleEmojiPaste as EventListener, false)
@@ -1,6 +1,12 @@
1
1
  <%= pb_content_tag 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>
6
+ <% end %>
7
+ <% else %>
3
8
  <%= pb_rails("caption", props: {text: object.label, dark: object.dark}) %>
9
+ <% end %>
4
10
  <% end %>
5
11
  <% if content.present? %>
6
12
  <%= content %>
@@ -23,6 +23,8 @@ module Playbook
23
23
  prop :character_count
24
24
  prop :onkeyup
25
25
  prop :max_characters
26
+ prop :required_indicator, type: Playbook::Props::Boolean,
27
+ default: false
26
28
 
27
29
  def classname
28
30
  generate_classname("pb_textarea_kit") + error_class + resize_class + inline_class
@@ -1,5 +1,5 @@
1
1
  import React, { useState } from "react"
2
- import { render, screen, fireEvent } from "../utilities/test-utils"
2
+ import { render, screen, fireEvent, within } from "../utilities/test-utils"
3
3
 
4
4
  import Textarea from "./_textarea"
5
5
 
@@ -265,4 +265,21 @@ describe("Textarea Emoji Mask", () => {
265
265
  fireEvent.change(textarea, { target: { value: 'àëǒüñ' } })
266
266
  expect(textarea.value).toBe('àëǒüñ')
267
267
  })
268
+
269
+ test('renders required indicator asterisk when requiredIndicator is true', () => {
270
+ render(
271
+ <Textarea
272
+ data={{ testid: testId }}
273
+ label="Name"
274
+ required
275
+ requiredIndicator
276
+ />
277
+ )
278
+
279
+ const kit = screen.getByTestId(testId)
280
+ const label = within(kit).getByText(/Name/)
281
+
282
+ expect(label).toBeInTheDocument()
283
+ expect(kit).toHaveTextContent('*')
284
+ })
268
285
  })