playbook_ui 16.1.0.pre.alpha.PLAY277013976 → 16.1.0.pre.alpha.PLAY277014019
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.
- checksums.yaml +4 -4
- data/app/pb_kits/playbook/pb_date_picker/_date_picker.tsx +14 -5
- data/app/pb_kits/playbook/pb_date_picker/docs/_date_picker_default.md +1 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_with_required_indicator.html.erb +1 -0
- data/app/pb_kits/playbook/pb_form/pb_form_validation.js +9 -2
- data/app/pb_kits/playbook/pb_rich_text_editor/_rich_text_editor.tsx +28 -14
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_label.jsx +44 -0
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_label.md +1 -0
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_label.jsx +28 -0
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_label.md +1 -0
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/example.yml +2 -0
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/index.js +2 -0
- data/app/pb_kits/playbook/pb_textarea/_textarea.tsx +10 -0
- data/app/pb_kits/playbook/pb_textarea/docs/_textarea_default.html.erb +3 -3
- data/app/pb_kits/playbook/pb_textarea/docs/_textarea_default.jsx +3 -0
- data/app/pb_kits/playbook/pb_textarea/docs/_textarea_default.md +1 -0
- data/app/pb_kits/playbook/pb_textarea/textarea.html.erb +25 -9
- data/app/pb_kits/playbook/pb_textarea/textarea.rb +7 -1
- data/app/pb_kits/playbook/pb_time_picker/_time_picker.tsx +97 -11
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_on_handler.jsx +5 -2
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_required_indicator.html.erb +6 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_required_indicator.jsx +16 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_required_indicator.md +3 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/example.yml +2 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/index.js +1 -0
- data/app/pb_kits/playbook/pb_time_picker/time_picker.rb +3 -0
- data/app/pb_kits/playbook/pb_time_picker/time_picker.test.jsx +47 -1
- data/dist/chunks/{_typeahead-CL1iI9Gs.js → _typeahead-Do4yr_4Z.js} +1 -1
- data/dist/chunks/vendor.js +3 -3
- data/dist/menu.yml +1 -1
- data/dist/playbook-rails-react-bindings.js +1 -1
- data/dist/playbook-rails.js +1 -1
- data/lib/playbook/version.rb +1 -1
- metadata +12 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c80372f38c690f8a892d1558660133f123bd5fdab59ee755fb09517e3ea094c6
|
|
4
|
+
data.tar.gz: 03c79eade34ad7bf5f7df371fe01754f406ff0e6542f4091917a3de83970d3f9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d943d2f57817921271bccba33530e373199c84cbecfc3d895a8189ecba601ae9ec319d04dd140f897d72a9a61d6e1e9c56460a4298abe077a17ae1247a748e5f
|
|
7
|
+
data.tar.gz: cec79e75ecb7caca2ff6a9286596236d72dc4dab14b5b3f51169fb05ca11960e8e7482c53a1a0fbdd8698c337bed0001a0c1827b2772a4ab4752606219b82490
|
|
@@ -40,7 +40,7 @@ type DatePickerProps = {
|
|
|
40
40
|
maxDate: string,
|
|
41
41
|
minDate: string,
|
|
42
42
|
name: string,
|
|
43
|
-
pickerId
|
|
43
|
+
pickerId: string,
|
|
44
44
|
placeholder?: string,
|
|
45
45
|
positionElement?: HTMLElement | null,
|
|
46
46
|
scrollContainer?: string,
|
|
@@ -196,6 +196,8 @@ const DatePicker = (props: DatePickerProps): React.ReactElement => {
|
|
|
196
196
|
|
|
197
197
|
const angleDown = getAllIcons()["angleDown"].icon as unknown as { [key: string]: SVGElement }
|
|
198
198
|
|
|
199
|
+
const errorId = error ? `${pickerId}-error` : undefined
|
|
200
|
+
|
|
199
201
|
return (
|
|
200
202
|
<div
|
|
201
203
|
{...ariaProps}
|
|
@@ -211,14 +213,18 @@ const DatePicker = (props: DatePickerProps): React.ReactElement => {
|
|
|
211
213
|
>
|
|
212
214
|
|
|
213
215
|
{!hideLabel && (
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
216
|
+
<label htmlFor={pickerId}>
|
|
217
|
+
<Caption
|
|
218
|
+
className="pb_date_picker_kit_label"
|
|
219
|
+
text={label}
|
|
220
|
+
/>
|
|
221
|
+
</label>
|
|
218
222
|
)}
|
|
219
223
|
<>
|
|
220
224
|
<div className="date_picker_input_wrapper">
|
|
221
225
|
<input
|
|
226
|
+
aria-describedby={errorId}
|
|
227
|
+
aria-invalid={!!error}
|
|
222
228
|
autoComplete="off"
|
|
223
229
|
className="date_picker_input"
|
|
224
230
|
disabled={disableInput}
|
|
@@ -232,6 +238,9 @@ const DatePicker = (props: DatePickerProps): React.ReactElement => {
|
|
|
232
238
|
|
|
233
239
|
{error &&
|
|
234
240
|
<Body
|
|
241
|
+
aria={{ atomic: "true", live: "polite" }}
|
|
242
|
+
htmlOptions={{ role: "alert" }}
|
|
243
|
+
id={errorId}
|
|
235
244
|
status="negative"
|
|
236
245
|
text={error}
|
|
237
246
|
variant={null}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
`pickerId`/`picker_id` is a **required prop** to instantiate the Date Picker. The presence of `pickerId`/`picker_id` in your Date Picker also associates the label with the input, providing the ability to focus the Date Picker by clicking the label.
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
<%= form.password_field :example_password_field, props: { label: true, required: true, required_indicator: true } %>
|
|
10
10
|
<%= form.url_field :example_url_field, props: { label: true, required: true, required_indicator: true } %>
|
|
11
11
|
<%= form.phone_number_field :example_phone_number_field, props: { label: true, required: true, required_indicator: true } %>
|
|
12
|
+
<%= form.time_picker :example_time_picker_required_indicator, props: { label: true, required: true, required_indicator: true } %>
|
|
12
13
|
|
|
13
14
|
<%= form.actions do |action| %>
|
|
14
15
|
<%= action.submit %>
|
|
@@ -26,6 +26,10 @@ class PbFormValidation extends PbEnhancedElement {
|
|
|
26
26
|
const isPhoneNumberInput = field.closest('.pb_phone_number_input')
|
|
27
27
|
if (isPhoneNumberInput) return
|
|
28
28
|
|
|
29
|
+
// Skip TimePicker inputs - they handle their own validation
|
|
30
|
+
const isTimePickerInput = field.closest('.pb_time_picker')
|
|
31
|
+
if (isTimePickerInput) return
|
|
32
|
+
|
|
29
33
|
FIELD_EVENTS.forEach((e) => {
|
|
30
34
|
field.addEventListener(e, debounce((event) => {
|
|
31
35
|
this.validateFormField(event)
|
|
@@ -67,13 +71,16 @@ class PbFormValidation extends PbEnhancedElement {
|
|
|
67
71
|
|
|
68
72
|
// Check if this is a phone number input
|
|
69
73
|
const isPhoneNumberInput = kitElement.classList.contains('pb_phone_number_input')
|
|
74
|
+
|
|
75
|
+
// Check if this is a TimePicker input
|
|
76
|
+
const isTimePickerInput = kitElement.classList.contains('pb_time_picker')
|
|
70
77
|
|
|
71
78
|
// ensure clean error message state
|
|
72
79
|
this.clearError(target)
|
|
73
80
|
kitElement.classList.add('error')
|
|
74
81
|
|
|
75
|
-
// Only add error message if it's NOT a phone number input
|
|
76
|
-
if (!isPhoneNumberInput) {
|
|
82
|
+
// Only add error message if it's NOT a phone number input or TimePicker input
|
|
83
|
+
if (!isPhoneNumberInput && !isTimePickerInput) {
|
|
77
84
|
// set the error message element
|
|
78
85
|
const errorMessageContainer = this.errorMessageContainer
|
|
79
86
|
|
|
@@ -94,8 +94,8 @@ const RichTextEditor = (props: RichTextEditorProps): React.ReactElement => {
|
|
|
94
94
|
|
|
95
95
|
const htmlProps = buildHtmlProps(htmlOptions)
|
|
96
96
|
|
|
97
|
-
const fieldId =
|
|
98
|
-
const labelElementId = `${fieldId}-label`
|
|
97
|
+
const fieldId = id ? (id as string) : null
|
|
98
|
+
const labelElementId = fieldId ? `${fieldId}-label` : null
|
|
99
99
|
|
|
100
100
|
const handleOnEditorReady = (editorInstance: Editor) => {
|
|
101
101
|
setEditor(editorInstance)
|
|
@@ -104,18 +104,32 @@ const RichTextEditor = (props: RichTextEditorProps): React.ReactElement => {
|
|
|
104
104
|
const oldId = editorInstance.element?.getAttribute("input")
|
|
105
105
|
if (!oldId) return
|
|
106
106
|
|
|
107
|
-
const hiddenInput = document.getElementById(oldId)
|
|
107
|
+
const hiddenInput = document.getElementById(oldId) as HTMLElement | null
|
|
108
108
|
if (!hiddenInput) return
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
const hiddenInputId = (inputOptions.id as string) || oldId
|
|
111
111
|
|
|
112
|
-
|
|
113
|
-
|
|
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)
|
|
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
|
|
114
128
|
})
|
|
115
129
|
}
|
|
116
130
|
|
|
117
131
|
useEffect(() => {
|
|
118
|
-
if (!advancedEditor) return
|
|
132
|
+
if (!advancedEditor || !fieldId || !labelElementId) return
|
|
119
133
|
|
|
120
134
|
const dom = advancedEditor.view?.dom as HTMLElement | undefined
|
|
121
135
|
if (!dom) return
|
|
@@ -123,7 +137,6 @@ const RichTextEditor = (props: RichTextEditorProps): React.ReactElement => {
|
|
|
123
137
|
dom.setAttribute("aria-labelledby", labelElementId)
|
|
124
138
|
dom.setAttribute("role", "textbox")
|
|
125
139
|
dom.setAttribute("aria-multiline", "true")
|
|
126
|
-
|
|
127
140
|
}, [advancedEditor, fieldId, labelElementId])
|
|
128
141
|
|
|
129
142
|
// DOM manipulation must wait for editor to be ready
|
|
@@ -227,6 +240,8 @@ const RichTextEditor = (props: RichTextEditorProps): React.ReactElement => {
|
|
|
227
240
|
// Determine if toolbar should be shown
|
|
228
241
|
const shouldShowToolbar = focus && advancedEditor ? showToolbarOnFocus : advancedEditorToolbar
|
|
229
242
|
|
|
243
|
+
const labelFor = advancedEditor ? fieldId : (id ? id : (inputOptions.id ? `${inputOptions.id}_trix` : undefined))
|
|
244
|
+
|
|
230
245
|
return (
|
|
231
246
|
<div
|
|
232
247
|
{...ariaProps}
|
|
@@ -237,10 +252,9 @@ const RichTextEditor = (props: RichTextEditorProps): React.ReactElement => {
|
|
|
237
252
|
>
|
|
238
253
|
{label && (
|
|
239
254
|
<label
|
|
240
|
-
htmlFor
|
|
241
|
-
id={labelElementId}
|
|
255
|
+
{...(labelFor ? { htmlFor: labelFor, id: labelElementId } : {})}
|
|
242
256
|
onMouseDown={(e) => {
|
|
243
|
-
|
|
257
|
+
if (!advancedEditor || !fieldId) return
|
|
244
258
|
e.preventDefault()
|
|
245
259
|
advancedEditor.commands.focus()
|
|
246
260
|
}}
|
|
@@ -267,9 +281,9 @@ const RichTextEditor = (props: RichTextEditorProps): React.ReactElement => {
|
|
|
267
281
|
advancedEditor ? (
|
|
268
282
|
<div
|
|
269
283
|
className={classnames(
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
284
|
+
"pb_rich_text_editor_advanced_container",
|
|
285
|
+
{ [`input_height_${inputHeight}`]: !!inputHeight,[`input_min_height_${inputMinHeight}`]: !!inputMinHeight ,["toolbar-active"]: shouldShowToolbar }
|
|
286
|
+
)}
|
|
273
287
|
>
|
|
274
288
|
{shouldShowToolbar && (
|
|
275
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.
|
|
@@ -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.
|
|
@@ -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'
|
|
@@ -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
|
-
|
|
4
|
-
|
|
5
|
-
<%= object.label
|
|
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
|
-
|
|
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
|
-
|
|
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: {
|
|
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
|
-
|
|
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
|
|
@@ -5,12 +5,12 @@ import { globalProps, GlobalProps } from '../utilities/globalProps'
|
|
|
5
5
|
import Caption from '../pb_caption/_caption'
|
|
6
6
|
import SelectableCard from '../pb_selectable_card/_selectable_card'
|
|
7
7
|
import TextInput from '../pb_text_input/_text_input'
|
|
8
|
+
import colors from '../tokens/exports/_colors.module.scss'
|
|
8
9
|
|
|
9
10
|
import {
|
|
10
11
|
parseTime,
|
|
11
12
|
parseTimeToMinutes,
|
|
12
13
|
isTimeInRange as isTimeInRangeHelper,
|
|
13
|
-
isHourDisabled as isHourDisabledHelper,
|
|
14
14
|
isAnyAMTimeValid as isAnyAMTimeValidHelper,
|
|
15
15
|
isAnyPMTimeValid as isAnyPMTimeValidHelper,
|
|
16
16
|
getDisplayTime,
|
|
@@ -48,6 +48,7 @@ type TimePickerProps = {
|
|
|
48
48
|
onChange?: (time: string) => void,
|
|
49
49
|
onClose?: (time: string) => void,
|
|
50
50
|
required?: boolean,
|
|
51
|
+
requiredIndicator?: boolean,
|
|
51
52
|
showTimezone?: boolean,
|
|
52
53
|
timeFormat?: TimeFormat,
|
|
53
54
|
validationMessage?: string,
|
|
@@ -72,6 +73,7 @@ const TimePicker = (props: TimePickerProps): JSX.Element => {
|
|
|
72
73
|
onChange,
|
|
73
74
|
onClose,
|
|
74
75
|
required = false,
|
|
76
|
+
requiredIndicator = false,
|
|
75
77
|
showTimezone = false,
|
|
76
78
|
timeFormat = 'AMPM',
|
|
77
79
|
validationMessage,
|
|
@@ -114,10 +116,6 @@ const TimePicker = (props: TimePickerProps): JSX.Element => {
|
|
|
114
116
|
return isTimeInRangeHelper(h, m, mer, timeFormat, minTimeMinutes, maxTimeMinutes)
|
|
115
117
|
}
|
|
116
118
|
|
|
117
|
-
const isHourDisabled = (h: number, mer?: 'AM' | 'PM'): boolean => {
|
|
118
|
-
return isHourDisabledHelper(h, mer, timeFormat, minTimeMinutes, maxTimeMinutes)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
119
|
const isCurrentTimeValid = (h: number, m: number, mer: 'AM' | 'PM'): boolean => {
|
|
122
120
|
return isTimeInRange(h, m, mer)
|
|
123
121
|
}
|
|
@@ -187,6 +185,28 @@ const TimePicker = (props: TimePickerProps): JSX.Element => {
|
|
|
187
185
|
const [showHourDropdown, setShowHourDropdown] = useState(false)
|
|
188
186
|
const [showMinuteDropdown, setShowMinuteDropdown] = useState(false)
|
|
189
187
|
|
|
188
|
+
// Clicking the clock add-on opens the dropdown
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
if (disabled) return
|
|
191
|
+
|
|
192
|
+
const addOnCard = document.querySelector(`#${uniqueId}-input`)?.closest('.text_input_wrapper_add_on')?.querySelector('.add-on-card') as HTMLElement
|
|
193
|
+
|
|
194
|
+
if (addOnCard) {
|
|
195
|
+
const handleAddOnClick = (e: Event) => {
|
|
196
|
+
e.preventDefault()
|
|
197
|
+
e.stopPropagation()
|
|
198
|
+
setShowDropdown(true)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
addOnCard.addEventListener('click', handleAddOnClick)
|
|
202
|
+
addOnCard.style.cursor = 'pointer'
|
|
203
|
+
|
|
204
|
+
return () => {
|
|
205
|
+
addOnCard.removeEventListener('click', handleAddOnClick)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}, [uniqueId, disabled, setShowDropdown])
|
|
209
|
+
|
|
190
210
|
// Input dropdown scrolling
|
|
191
211
|
const scrollDropdownToSelected = (dropdownRef: React.RefObject<HTMLDivElement>) => {
|
|
192
212
|
if (dropdownRef.current) {
|
|
@@ -369,6 +389,10 @@ const TimePicker = (props: TimePickerProps): JSX.Element => {
|
|
|
369
389
|
}
|
|
370
390
|
}
|
|
371
391
|
|
|
392
|
+
const handleHourFocus = (e: React.FocusEvent<HTMLInputElement>) => {
|
|
393
|
+
e.target.select()
|
|
394
|
+
}
|
|
395
|
+
|
|
372
396
|
const handleHourBlur = () => {
|
|
373
397
|
const result = normalizeHourOnBlur(hourInputValue, hour, timeFormat)
|
|
374
398
|
setHour(result.hour)
|
|
@@ -393,6 +417,10 @@ const TimePicker = (props: TimePickerProps): JSX.Element => {
|
|
|
393
417
|
}
|
|
394
418
|
}
|
|
395
419
|
|
|
420
|
+
const handleMinuteFocus = (e: React.FocusEvent<HTMLInputElement>) => {
|
|
421
|
+
e.target.select()
|
|
422
|
+
}
|
|
423
|
+
|
|
396
424
|
const handleMinuteBlur = () => {
|
|
397
425
|
const result = normalizeMinuteOnBlur(minuteInputValue, minute)
|
|
398
426
|
setMinute(result.minute)
|
|
@@ -482,6 +510,30 @@ const TimePicker = (props: TimePickerProps): JSX.Element => {
|
|
|
482
510
|
e.preventDefault()
|
|
483
511
|
setShowHourDropdown(false)
|
|
484
512
|
closeDropdown()
|
|
513
|
+
} else if (e.key === 'ArrowDown') {
|
|
514
|
+
// ArrowDown increases value (like scrolling down a list)
|
|
515
|
+
e.preventDefault()
|
|
516
|
+
const { maxHour, minHour } = getHourConstraints(timeFormat)
|
|
517
|
+
const newHour = hour >= maxHour ? minHour : hour + 1
|
|
518
|
+
setHour(newHour)
|
|
519
|
+
setHourInputValue(timeFormat === '24hour' ? newHour.toString().padStart(2, '0') : newHour.toString())
|
|
520
|
+
setHasSelectedTime(true)
|
|
521
|
+
const timeString = get24HourTime(newHour, minute, meridiem, timeFormat)
|
|
522
|
+
if (onChange) {
|
|
523
|
+
onChange(timeString)
|
|
524
|
+
}
|
|
525
|
+
} else if (e.key === 'ArrowUp') {
|
|
526
|
+
// ArrowUp decreases value (like scrolling up a list)
|
|
527
|
+
e.preventDefault()
|
|
528
|
+
const { maxHour, minHour } = getHourConstraints(timeFormat)
|
|
529
|
+
const newHour = hour <= minHour ? maxHour : hour - 1
|
|
530
|
+
setHour(newHour)
|
|
531
|
+
setHourInputValue(timeFormat === '24hour' ? newHour.toString().padStart(2, '0') : newHour.toString())
|
|
532
|
+
setHasSelectedTime(true)
|
|
533
|
+
const timeString = get24HourTime(newHour, minute, meridiem, timeFormat)
|
|
534
|
+
if (onChange) {
|
|
535
|
+
onChange(timeString)
|
|
536
|
+
}
|
|
485
537
|
}
|
|
486
538
|
}
|
|
487
539
|
|
|
@@ -513,6 +565,28 @@ const TimePicker = (props: TimePickerProps): JSX.Element => {
|
|
|
513
565
|
e.preventDefault()
|
|
514
566
|
setShowMinuteDropdown(false)
|
|
515
567
|
closeDropdown()
|
|
568
|
+
} else if (e.key === 'ArrowDown') {
|
|
569
|
+
// ArrowDown increases value (like scrolling down a list)
|
|
570
|
+
e.preventDefault()
|
|
571
|
+
const newMinute = minute >= 59 ? 0 : minute + 1
|
|
572
|
+
setMinute(newMinute)
|
|
573
|
+
setMinuteInputValue(newMinute.toString().padStart(2, '0'))
|
|
574
|
+
setHasSelectedTime(true)
|
|
575
|
+
const timeString = get24HourTime(hour, newMinute, meridiem, timeFormat)
|
|
576
|
+
if (onChange) {
|
|
577
|
+
onChange(timeString)
|
|
578
|
+
}
|
|
579
|
+
} else if (e.key === 'ArrowUp') {
|
|
580
|
+
// ArrowUp decreases value (like scrolling up a list)
|
|
581
|
+
e.preventDefault()
|
|
582
|
+
const newMinute = minute <= 0 ? 59 : minute - 1
|
|
583
|
+
setMinute(newMinute)
|
|
584
|
+
setMinuteInputValue(newMinute.toString().padStart(2, '0'))
|
|
585
|
+
setHasSelectedTime(true)
|
|
586
|
+
const timeString = get24HourTime(hour, newMinute, meridiem, timeFormat)
|
|
587
|
+
if (onChange) {
|
|
588
|
+
onChange(timeString)
|
|
589
|
+
}
|
|
516
590
|
}
|
|
517
591
|
}
|
|
518
592
|
|
|
@@ -625,12 +699,22 @@ const TimePicker = (props: TimePickerProps): JSX.Element => {
|
|
|
625
699
|
>
|
|
626
700
|
{label && (
|
|
627
701
|
<label htmlFor={`${uniqueId}-input`}>
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
702
|
+
{requiredIndicator ? (
|
|
703
|
+
<Caption
|
|
704
|
+
className="pb_time_picker_kit_label"
|
|
705
|
+
marginBottom="xs"
|
|
706
|
+
size="md"
|
|
707
|
+
>
|
|
708
|
+
{label} <span style={{ color: `${colors.error}` }}>{'*'}</span>
|
|
709
|
+
</Caption>
|
|
710
|
+
) : (
|
|
711
|
+
<Caption
|
|
712
|
+
className="pb_time_picker_kit_label"
|
|
713
|
+
marginBottom="xs"
|
|
714
|
+
size="md"
|
|
715
|
+
text={label}
|
|
716
|
+
/>
|
|
717
|
+
)}
|
|
634
718
|
</label>
|
|
635
719
|
)}
|
|
636
720
|
<div className="time_picker_wrapper">
|
|
@@ -689,6 +773,7 @@ const TimePicker = (props: TimePickerProps): JSX.Element => {
|
|
|
689
773
|
onBlur={handleHourBlur}
|
|
690
774
|
onChange={handleHourChange}
|
|
691
775
|
onClick={() => { setShowHourDropdown(!showHourDropdown); setShowMinuteDropdown(false) }}
|
|
776
|
+
onFocus={handleHourFocus}
|
|
692
777
|
onKeyDown={handleHourKeyDown}
|
|
693
778
|
pattern="[0-9]*"
|
|
694
779
|
ref={hourInputRef}
|
|
@@ -734,6 +819,7 @@ const TimePicker = (props: TimePickerProps): JSX.Element => {
|
|
|
734
819
|
onBlur={handleMinuteBlur}
|
|
735
820
|
onChange={handleMinuteChange}
|
|
736
821
|
onClick={() => { setShowMinuteDropdown(!showMinuteDropdown); setShowHourDropdown(false) }}
|
|
822
|
+
onFocus={handleMinuteFocus}
|
|
737
823
|
onKeyDown={handleMinuteKeyDown}
|
|
738
824
|
pattern="[0-9]*"
|
|
739
825
|
ref={minuteInputRef}
|