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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/pb_date_picker/_date_picker.tsx +14 -5
  3. data/app/pb_kits/playbook/pb_date_picker/docs/_date_picker_default.md +1 -0
  4. data/app/pb_kits/playbook/pb_form/docs/_form_with_required_indicator.html.erb +1 -0
  5. data/app/pb_kits/playbook/pb_form/pb_form_validation.js +9 -2
  6. data/app/pb_kits/playbook/pb_rich_text_editor/_rich_text_editor.tsx +28 -14
  7. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_label.jsx +44 -0
  8. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_label.md +1 -0
  9. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_label.jsx +28 -0
  10. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_label.md +1 -0
  11. data/app/pb_kits/playbook/pb_rich_text_editor/docs/example.yml +2 -0
  12. data/app/pb_kits/playbook/pb_rich_text_editor/docs/index.js +2 -0
  13. data/app/pb_kits/playbook/pb_textarea/_textarea.tsx +10 -0
  14. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_default.html.erb +3 -3
  15. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_default.jsx +3 -0
  16. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_default.md +1 -0
  17. data/app/pb_kits/playbook/pb_textarea/textarea.html.erb +25 -9
  18. data/app/pb_kits/playbook/pb_textarea/textarea.rb +7 -1
  19. data/app/pb_kits/playbook/pb_time_picker/_time_picker.tsx +97 -11
  20. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_on_handler.jsx +5 -2
  21. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_required_indicator.html.erb +6 -0
  22. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_required_indicator.jsx +16 -0
  23. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_required_indicator.md +3 -0
  24. data/app/pb_kits/playbook/pb_time_picker/docs/example.yml +2 -0
  25. data/app/pb_kits/playbook/pb_time_picker/docs/index.js +1 -0
  26. data/app/pb_kits/playbook/pb_time_picker/time_picker.rb +3 -0
  27. data/app/pb_kits/playbook/pb_time_picker/time_picker.test.jsx +47 -1
  28. data/dist/chunks/{_typeahead-CL1iI9Gs.js → _typeahead-Do4yr_4Z.js} +1 -1
  29. data/dist/chunks/vendor.js +3 -3
  30. data/dist/menu.yml +1 -1
  31. data/dist/playbook-rails-react-bindings.js +1 -1
  32. data/dist/playbook-rails.js +1 -1
  33. data/lib/playbook/version.rb +1 -1
  34. metadata +12 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 20d0e323a27105cf7e8461882ce7f5583274e5b1e98e583c2bc4619df555c0b5
4
- data.tar.gz: b2504aaf9ae1131b25172450253ffb6c19e3bedcfc27b639f90c0c7452cc86a7
3
+ metadata.gz: c80372f38c690f8a892d1558660133f123bd5fdab59ee755fb09517e3ea094c6
4
+ data.tar.gz: 03c79eade34ad7bf5f7df371fe01754f406ff0e6542f4091917a3de83970d3f9
5
5
  SHA512:
6
- metadata.gz: 3c8a45b0dce0af74401d7c7c0b38337785a5764dc25b146b0ac5599bba9dcbedb0bf4d01f4a23636a7ac28b33c91f3500cfc500a636adc72c9868eb4ff125ac1
7
- data.tar.gz: fc2d676c70f12a2e4350516925b918e1b81cda11b7073370fc3d98f9bf7de550993131bd504daf90f48c709a460563c1805fbe78da150ed80b29e1539d717ca9
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?: string,
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
- <Caption
215
- className="pb_date_picker_kit_label"
216
- text={label}
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 = (inputOptions.id as string) || (id as string) || null
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
- if (inputOptions.name) hiddenInput.setAttribute("name", inputOptions.name as string)
110
+ const hiddenInputId = (inputOptions.id as string) || oldId
111
111
 
112
- editorInstance.element.id = fieldId
113
- editorInstance.element.setAttribute("aria-labelledby", labelElementId)
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={fieldId}
241
- id={labelElementId}
255
+ {...(labelFor ? { htmlFor: labelFor, id: labelElementId } : {})}
242
256
  onMouseDown={(e) => {
243
- if (!advancedEditor) return
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
- "pb_rich_text_editor_advanced_container",
271
- { [`input_height_${inputHeight}`]: !!inputHeight,[`input_min_height_${inputMinHeight}`]: !!inputMinHeight ,["toolbar-active"]: shouldShowToolbar }
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
- <% 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
@@ -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
- <Caption
629
- className="pb_time_picker_kit_label"
630
- marginBottom="xs"
631
- size="md"
632
- text={label}
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}