playbook_ui 16.1.0.pre.alpha.play2724typeaheadindicator13970 → 16.1.0.pre.alpha.play2771passphraseaccessibility14033

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/pb_checkbox/_checkbox.scss +1 -1
  3. data/app/pb_kits/playbook/pb_checkbox/_checkbox.tsx +0 -17
  4. data/app/pb_kits/playbook/pb_checkbox/checkbox.html.erb +1 -10
  5. data/app/pb_kits/playbook/pb_checkbox/checkbox.rb +0 -2
  6. data/app/pb_kits/playbook/pb_checkbox/docs/example.yml +0 -2
  7. data/app/pb_kits/playbook/pb_checkbox/docs/index.js +0 -1
  8. data/app/pb_kits/playbook/pb_date_picker/_date_picker.tsx +14 -5
  9. data/app/pb_kits/playbook/pb_date_picker/docs/_date_picker_default.md +1 -0
  10. data/app/pb_kits/playbook/pb_form/docs/_form_with_required_indicator.html.erb +4 -5
  11. data/app/pb_kits/playbook/pb_form/pb_form_validation.js +9 -2
  12. data/app/pb_kits/playbook/pb_passphrase/_passphrase.tsx +20 -2
  13. data/app/pb_kits/playbook/pb_text_input/text_input.html.erb +10 -10
  14. data/app/pb_kits/playbook/pb_textarea/_textarea.tsx +10 -0
  15. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_default.html.erb +3 -3
  16. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_default.jsx +3 -0
  17. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_default.md +1 -0
  18. data/app/pb_kits/playbook/pb_textarea/textarea.html.erb +25 -9
  19. data/app/pb_kits/playbook/pb_textarea/textarea.rb +7 -1
  20. data/app/pb_kits/playbook/pb_time_picker/_time_picker.tsx +97 -11
  21. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_on_handler.jsx +5 -2
  22. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_required_indicator.html.erb +6 -0
  23. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_required_indicator.jsx +16 -0
  24. data/app/pb_kits/playbook/{pb_typeahead/docs/_typeahead_required_indicator.md → pb_time_picker/docs/_time_picker_required_indicator.md} +2 -2
  25. data/app/pb_kits/playbook/pb_time_picker/docs/example.yml +2 -0
  26. data/app/pb_kits/playbook/pb_time_picker/docs/index.js +1 -0
  27. data/app/pb_kits/playbook/pb_time_picker/time_picker.rb +3 -0
  28. data/app/pb_kits/playbook/pb_time_picker/time_picker.test.jsx +47 -1
  29. data/app/pb_kits/playbook/pb_typeahead/_typeahead.tsx +323 -410
  30. data/app/pb_kits/playbook/pb_typeahead/components/Control.tsx +0 -2
  31. data/app/pb_kits/playbook/pb_typeahead/docs/example.yml +0 -2
  32. data/app/pb_kits/playbook/pb_typeahead/docs/index.js +21 -22
  33. data/app/pb_kits/playbook/pb_typeahead/typeahead.html.erb +2 -3
  34. data/app/pb_kits/playbook/pb_typeahead/typeahead.rb +1 -3
  35. data/dist/chunks/{_pb_line_graph-CFpRBk64.js → _pb_line_graph-BgKF_zz1.js} +1 -1
  36. data/dist/chunks/_typeahead-D-8xZ__X.js +1 -0
  37. data/dist/chunks/{globalProps-CXOg_9fx.js → globalProps-BhVYCqRf.js} +1 -1
  38. data/dist/chunks/lib-DD34ZrWL.js +29 -0
  39. data/dist/chunks/vendor.js +3 -3
  40. data/dist/menu.yml +1 -1
  41. data/dist/playbook-rails-react-bindings.js +1 -1
  42. data/dist/playbook-rails.js +1 -1
  43. data/lib/playbook/forms/builder/form_field_builder.rb +1 -1
  44. data/lib/playbook/forms/builder/typeahead_field.rb +1 -15
  45. data/lib/playbook/forms/builder.rb +2 -2
  46. data/lib/playbook/version.rb +1 -1
  47. metadata +11 -12
  48. data/app/pb_kits/playbook/pb_checkbox/docs/_checkbox_required_indicator.html.erb +0 -6
  49. data/app/pb_kits/playbook/pb_checkbox/docs/_checkbox_required_indicator.jsx +0 -17
  50. data/app/pb_kits/playbook/pb_checkbox/docs/_checkbox_required_indicator.md +0 -3
  51. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_required_indicator.html.erb +0 -16
  52. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_required_indicator.jsx +0 -23
  53. data/dist/chunks/_typeahead-D2EbYcmG.js +0 -1
  54. data/dist/chunks/lib-DTxpoePf.js +0 -29
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8541b6e0d600a07afc16b3a1f87e7851e84af95a8fdf2299b00075eabd2799e7
4
- data.tar.gz: 75ff644b0208f6b9da46789cdce6882ec381b3910cb1237694cca0061ab88fe1
3
+ metadata.gz: db94c53faae46d3ac450cae169b49010d0fea2eef2df809b1b498a803bf97275
4
+ data.tar.gz: 481cc281f583cd5db425d32dea0bbd0d87cd5fff7093745f3fa674b7900064da
5
5
  SHA512:
6
- metadata.gz: 7f70875cad44dcc1e4e8429f6720472ee8f38dec0ecd23a540bc40467ded607d0479dcd3f400587749c40159baf4c145375872082e0260d2afbc6f5bf2484beb
7
- data.tar.gz: 2ccc12dd5562413ba97d26fdb29fb093c8e9213febc89a52f8c55e223e6c2278279732bcd46c307f50d32018ad3aeff6dd9e387dd6d37c5ae7c6c5cc084fe846
6
+ metadata.gz: a9dc190816e2a5b470ee310291cf8144b8ab3a2b26e5a251770dfb9b7b3b310c394b8a1d7dd355e21fbd320267ebb4e573a97cd6cfb1816eec07253ac36e9222
7
+ data.tar.gz: 325c99459bc7e398c41c7437fd373f559b32634dbca9f7119a684fb0166961c8303563f9f7f30cd51edc90843b36235ac7b67180c112d9688b430dfcf653ce38
@@ -168,4 +168,4 @@ $transition: $transition_cubic;
168
168
  border-color: $error;
169
169
  }
170
170
  }
171
- }
171
+ }
@@ -4,8 +4,6 @@ import Icon from '../pb_icon/_icon'
4
4
  import { buildAriaProps, buildCss, buildDataProps, buildHtmlProps } from '../utilities/props'
5
5
  import classnames from 'classnames'
6
6
  import { globalProps, GlobalProps } from '../utilities/globalProps'
7
- import colors from '../tokens/exports/_colors.module.scss'
8
- import spacing from '../tokens/exports/_spacing.module.scss'
9
7
 
10
8
  type CheckboxProps = {
11
9
  aria?: {[key: string]: string},
@@ -21,7 +19,6 @@ type CheckboxProps = {
21
19
  indeterminate?: boolean,
22
20
  name?: string,
23
21
  onChange?: (event: React.FormEvent<HTMLInputElement>) => void,
24
- requiredIndicator?: boolean,
25
22
  tabIndex?: number,
26
23
  text?: string,
27
24
  value?: string,
@@ -42,7 +39,6 @@ const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>((props, ref) => {
42
39
  indeterminate = false,
43
40
  name = '',
44
41
  onChange = () => { void 0 },
45
- requiredIndicator = false,
46
42
  tabIndex,
47
43
  text = '',
48
44
  value = '',
@@ -128,20 +124,7 @@ const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>((props, ref) => {
128
124
  variant={null}
129
125
  >
130
126
  {text}
131
- {requiredIndicator && (
132
- <span
133
- aria-hidden="true"
134
- className="pb_required_indicator"
135
- style={{
136
- color: colors.error,
137
- marginLeft: spacing.space_xs,
138
- }}
139
- >
140
- {'*'}
141
- </span>
142
- )}
143
127
  </Body>
144
-
145
128
  </label>
146
129
  )
147
130
  })
@@ -5,15 +5,6 @@
5
5
  <%= pb_rails("icon", props: { icon: "minus", classname: "indeterminate_icon hidden", fixed_width: true}) %>
6
6
  </span>
7
7
  <span class="pb_checkbox_label">
8
- <%= pb_rails("body", props: { status: object.checkbox_label_status, dark: object.dark, margin_right: object.form_spacing ? "xs" : "" }) do %>
9
- <%= object.text%>
10
- <% if object.required_indicator %>
11
- <span
12
- class="pb_checkbox_required_indicator"
13
- aria-hidden="true"
14
- style="color: #DA0014;"
15
- >*</span>
16
- <% end %>
17
- <% end %>
8
+ <%= pb_rails("body", props: { status: object.checkbox_label_status, text: object.text, dark: object.dark, margin_right: object.form_spacing ? "xs" : "" }) %>
18
9
  </span>
19
10
  <% end %>
@@ -23,8 +23,6 @@ module Playbook
23
23
  prop :hidden_input, type: Playbook::Props::Boolean,
24
24
  default: false
25
25
  prop :hidden_value
26
- prop :required_indicator, type: Playbook::Props::Boolean,
27
- default: false
28
26
 
29
27
  def classname
30
28
  generate_classname("pb_checkbox_kit", checked_class) + error_class
@@ -8,7 +8,6 @@ examples:
8
8
  - checkbox_indeterminate: Indeterminate Checkbox
9
9
  - checkbox_disabled: Disabled Checkbox
10
10
  - checkbox_form: Form and Hidden Input
11
- # - checkbox_required_indicator: Required Indicator
12
11
 
13
12
  react:
14
13
  - checkbox_default: Default
@@ -18,7 +17,6 @@ examples:
18
17
  - checkbox_error: Default w/ Error
19
18
  - checkbox_indeterminate: Indeterminate Checkbox
20
19
  - checkbox_disabled: Disabled Checkbox
21
- # - checkbox_required_indicator: Required Indicator
22
20
 
23
21
  swift:
24
22
  - checkbox_default_swift: Default
@@ -5,4 +5,3 @@ export { default as CheckboxError } from './_checkbox_error.jsx'
5
5
  export { default as CheckboxChecked } from './_checkbox_checked.jsx'
6
6
  export { default as CheckboxIndeterminate } from './_checkbox_indeterminate.jsx'
7
7
  export { default as CheckboxDisabled } from './_checkbox_disabled.jsx'
8
- export { default as CheckboxRequiredIndicator } from './_checkbox_required_indicator.jsx'
@@ -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.
@@ -1,16 +1,15 @@
1
1
  <%= pb_form_with(scope: :example, url: "", method: :get, validate: true) do |form| %>
2
- <%= form.typeahead :example_typeahead_field, props: { label: true, required: true, required_indicator: true } %>
3
2
  <%= form.text_field :example_text_field, props: { label: true, required: true, required_indicator: true } %>
4
3
  <%= form.text_field :example_text_field_2, props: { label: "Text Field Custom Label", required: true, required_indicator: true } %>
5
- <%= form.phone_number_field :example_phone_number_field, props: { label: true, required: true, required_indicator: true } %>
4
+ <%= form.text_area :example_text_area, props: { label: true, required: true, required_indicator: true } %>
5
+ <%= form.text_area :example_text_area_2, props: { label: "Textarea Custom Label", required: true, required_indicator: true } %>
6
6
  <%= form.email_field :example_email_field, props: { label: true, required: true, required_indicator: true } %>
7
7
  <%= form.number_field :example_number_field, props: { label: true, required: true, required_indicator: true } %>
8
8
  <%= form.search_field :example_search_field, props: { label: true, required: true, required_indicator: true } %>
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
- <%= form.text_area :example_text_area, props: { label: true, required: true, required_indicator: true } %>
12
- <%= form.text_area :example_text_area_2, props: { label: "Textarea Custom Label", required: true, required_indicator: true } %>
13
- <%# <%= form.check_box :example_checkbox, props: { label: true, text: "Checkbox Label", required: true, required_indicator: true } %>
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 } %>
14
13
 
15
14
  <%= form.actions do |action| %>
16
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
 
@@ -178,22 +178,40 @@ const Passphrase = (props: PassphraseProps): React.ReactElement => {
178
178
  {...inputProps}
179
179
  />
180
180
  <span
181
+ aria-label={
182
+ showPassphrase
183
+ ? "Passphrase currently visible. Click icon to hide password"
184
+ : "Passphrase currently hidden. Click icon to reveal password"
185
+ }
186
+ aria-pressed={showPassphrase}
181
187
  className="show-passphrase-icon"
182
188
  onClick={toggleShowPassphrase}
189
+ onKeyDown={(e) => {
190
+ if (e.key === "Enter" || e.key === " ") {
191
+ e.preventDefault()
192
+ toggleShowPassphrase(e as any)
193
+ }
194
+ }}
195
+ role="button"
196
+ tabIndex={0}
183
197
  >
184
198
  <Body
185
199
  className={showPassphrase ? "hide-icon" : ""}
186
200
  color="light"
187
201
  dark={dark}
188
202
  >
189
- <Icon icon="eye-slash" />
203
+ <Icon
204
+ aria={{ label: "eye icon" }}
205
+ icon="eye-slash"
206
+ />
190
207
  </Body>
191
208
  <Body
192
209
  className={showPassphrase ? "" : "hide-icon"}
193
210
  color="light"
194
211
  dark={dark}
195
212
  >
196
- <Icon
213
+ <Icon
214
+ aria={{ label: "eye icon" }}
197
215
  className="svg-inline--fa"
198
216
  customIcon={eyeIcon.icon as unknown as { [key: string]: SVGElement }}
199
217
  />
@@ -1,16 +1,15 @@
1
- <%= pb_content_tag(:div, id: nil) do %>
1
+ <%= pb_content_tag(:div, id: nil ) do %>
2
2
  <% if object.label.present? %>
3
- <label for="<%= object.input_options[:id] || object.id %>">
4
- <% if object.required_indicator %>
5
- <%= pb_rails("caption", props: { dark: object.dark, classname: "pb_text_input_kit_label" }) do %>
6
- <%= object.label %><span style="color: #DA0014;"> *</span>
7
- <% end %>
8
- <% else %>
9
- <%= pb_rails("caption", props: { text: object.label, dark: object.dark, classname: "pb_text_input_kit_label" }) %>
3
+ <label for="<%= object.input_options[:id] || object.id %>" >
4
+ <% if object.required_indicator %>
5
+ <%= pb_rails("caption", props: { dark: object.dark, classname: "pb_text_input_kit_label" }) do %>
6
+ <%= object.label %><span style="color: #DA0014;"> *</span>
10
7
  <% end %>
11
- </label>
8
+ <% else %>
9
+ <%= pb_rails("caption", props: { text: object.label, dark: object.dark, classname: "pb_text_input_kit_label" }) %>
10
+ <% end %>
11
+ </label>
12
12
  <% end %>
13
-
14
13
  <%= content_tag(:div, class: "#{add_on_class} text_input_wrapper") do %>
15
14
  <% if content.present? %>
16
15
  <%= content %>
@@ -27,3 +26,4 @@
27
26
  <%= pb_rails("body", props: {dark: object.dark, status: "negative", text: object.error, id: object.error_id, aria: { atomic: "true", live: "polite" }, html_options: { role: "alert" }}) if object.error %>
28
27
  <% end %>
29
28
  <% end %>
29
+
@@ -120,6 +120,7 @@ const Textarea = ({
120
120
  const characterCounter = () => {
121
121
  return maxCharacters && characterCount ? `${checkIfZero(characterCount)} / ${maxCharacters}` : `${checkIfZero(characterCount)}`
122
122
  }
123
+ const errorId = error ? `${id}-error` : undefined
123
124
 
124
125
  return (
125
126
  <div
@@ -145,7 +146,10 @@ const Textarea = ({
145
146
  )}
146
147
  {children || (
147
148
  <textarea
149
+ aria-describedby={errorId}
150
+ aria-invalid={!!error}
148
151
  disabled={disabled}
152
+ id={id}
149
153
  name={name}
150
154
  onChange={emojiMask ? handleChange : onChange}
151
155
  onPaste={emojiMask ? handlePaste : undefined}
@@ -167,6 +171,9 @@ const Textarea = ({
167
171
  >
168
172
  <FlexItem>
169
173
  <Body
174
+ aria={{ atomic: "true", live: "polite" }}
175
+ htmlOptions={{ role: "alert" }}
176
+ id={errorId}
170
177
  margin="none"
171
178
  status="negative"
172
179
  text={error}
@@ -182,6 +189,9 @@ const Textarea = ({
182
189
  </Flex>
183
190
  ) : (
184
191
  <Body
192
+ aria={{ atomic: "true", live: "polite" }}
193
+ htmlOptions={{ role: "alert" }}
194
+ id={errorId}
185
195
  status="negative"
186
196
  text={error}
187
197
  />
@@ -1,9 +1,9 @@
1
- <%= pb_rails("textarea", props: { label: "Label", rows: 4}) %>
1
+ <%= pb_rails("textarea", props: { label: "Label", rows: 4, id: "default-example-1" }) %>
2
2
 
3
3
  <br/>
4
4
 
5
- <%= pb_rails("textarea", props: { label: "Label", placeholder: "Placeholder text" }) %>
5
+ <%= pb_rails("textarea", props: { label: "Label", placeholder: "Placeholder text", id: "default-example-2" }) %>
6
6
 
7
7
  <br/>
8
8
 
9
- <%= pb_rails("textarea", props: { label: "Label", name: "comment", value: "Default value text" }) %>
9
+ <%= pb_rails("textarea", props: { label: "Label", name: "comment", value: "Default value text", id: "default-example-3" }) %>
@@ -13,6 +13,7 @@ const TextareaDefault = (props) => {
13
13
  label="Label"
14
14
  rows={4}
15
15
  {...props}
16
+ id="default-example-1"
16
17
  />
17
18
 
18
19
  <br />
@@ -21,6 +22,7 @@ const TextareaDefault = (props) => {
21
22
  label="Label"
22
23
  placeholder="Placeholder text"
23
24
  {...props}
25
+ id="default-example-2"
24
26
  />
25
27
 
26
28
  <br />
@@ -32,6 +34,7 @@ const TextareaDefault = (props) => {
32
34
  placeholder="Placeholder text"
33
35
  value={value}
34
36
  {...props}
37
+ id="default-example-3"
35
38
  />
36
39
 
37
40
  </div>
@@ -0,0 +1 @@
1
+ Add an `id` to your Textarea so that clicking the label will move focus directly to the input.
@@ -1,12 +1,14 @@
1
- <%= pb_content_tag do %>
1
+ <%= pb_content_tag(:div, id: nil) do %>
2
2
  <% if object.label.present? %>
3
- <% if object.required_indicator %>
4
- <%= pb_rails("caption", props: { text: object.label, dark: object.dark }) do %>
5
- <%= object.label %><span style="color: #DA0014;"> *</span>
3
+ <label for="<%= object.input_options[:id] || object.id %>" >
4
+ <% if object.required_indicator %>
5
+ <%= pb_rails("caption", props: { text: object.label, dark: object.dark }) do %>
6
+ <%= object.label %><span style="color: #DA0014;"> *</span>
7
+ <% end %>
8
+ <% else %>
9
+ <%= pb_rails("caption", props: {text: object.label, dark: object.dark}) %>
6
10
  <% end %>
7
- <% else %>
8
- <%= pb_rails("caption", props: {text: object.label, dark: object.dark}) %>
9
- <% end %>
11
+ </label>
10
12
  <% end %>
11
13
  <% if content.present? %>
12
14
  <%= content %>
@@ -22,14 +24,28 @@
22
24
  <% if object.character_count %>
23
25
  <%= pb_rails("flex", props: { spacing: "between", vertical: "center" }) do %>
24
26
  <%= pb_rails("flex/flex_item") do %>
25
- <%= pb_rails("body", props: { dark: object.dark, status: "negative", text: object.error }) %>
27
+ <%= pb_rails("body", props: {
28
+ dark: object.dark,
29
+ status: "negative",
30
+ text: object.error,
31
+ id: object.error_id,
32
+ aria: { atomic: "true", live: "polite" },
33
+ html_options: { role: "alert" },
34
+ }) %>
26
35
  <% end %>
27
36
  <%= pb_rails("flex/flex_item") do %>
28
37
  <%= pb_rails("caption", props: { margin: "none", size: "xs", text: object.character_counter }) %>
29
38
  <% end %>
30
39
  <% end %>
31
40
  <% else %>
32
- <%= pb_rails("body", props: { dark: object.dark, status: "negative", text: object.error }) %>
41
+ <%= pb_rails("body", props: {
42
+ dark: object.dark,
43
+ status: "negative",
44
+ text: object.error,
45
+ id: object.error_id,
46
+ aria: { atomic: "true", live: "polite" },
47
+ html_options: { role: "alert" },
48
+ }) %>
33
49
  <% end %>
34
50
  <% else %>
35
51
  <%= pb_rails("caption", props: { margin: "none", size: "xs", text: object.character_counter }) %>
@@ -47,7 +47,9 @@ module Playbook
47
47
  merged_data = data_attrs.merge(input_data)
48
48
 
49
49
  base_attributes = {
50
- id: input_options[:id] || "object_method",
50
+ 'aria-describedby': error.present? ? error_id : nil,
51
+ 'aria-invalid': error.present?,
52
+ id: input_options[:id] || id || "object_method",
51
53
  max_characters: max_characters,
52
54
  name: name,
53
55
  onkeyup: onkeyup,
@@ -66,6 +68,10 @@ module Playbook
66
68
  result
67
69
  end
68
70
 
71
+ def error_id
72
+ "#{id}-error" if error.present?
73
+ end
74
+
69
75
  private
70
76
 
71
77
  def error_class
@@ -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}