playbook_ui 16.1.0.pre.alpha.play276813969 → 16.1.0.pre.alpha.play277814027
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_advanced_table/Components/RegularTableView.tsx +12 -2
- data/app/pb_kits/playbook/pb_advanced_table/advanced_table.test.jsx +33 -0
- data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_column_styling_background_custom.jsx +71 -0
- data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_column_styling_background_custom.md +4 -0
- data/app/pb_kits/playbook/pb_advanced_table/docs/example.yml +1 -0
- data/app/pb_kits/playbook/pb_advanced_table/docs/index.js +2 -1
- 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_dropdown/_dropdown.tsx +11 -46
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_label.html.erb +3 -6
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_label.jsx +0 -1
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_label.md +1 -3
- data/app/pb_kits/playbook/pb_dropdown/dropdown.html.erb +4 -10
- data/app/pb_kits/playbook/pb_dropdown/dropdown.rb +0 -9
- data/app/pb_kits/playbook/pb_dropdown/dropdown_trigger.html.erb +2 -7
- data/app/pb_kits/playbook/pb_dropdown/dropdown_trigger.rb +0 -4
- data/app/pb_kits/playbook/pb_dropdown/index.js +73 -125
- data/app/pb_kits/playbook/pb_dropdown/subcomponents/DropdownTrigger.tsx +0 -16
- data/app/pb_kits/playbook/pb_dropdown/utilities/clickOutsideHelper.tsx +0 -6
- 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_multi_level_select/_multi_level_select.scss +0 -7
- data/app/pb_kits/playbook/pb_multi_level_select/_multi_level_select.tsx +549 -638
- data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_label.html.erb +3 -3
- data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_label.jsx +7 -4
- data/app/pb_kits/playbook/pb_multi_level_select/multi_level_select.test.jsx +4 -4
- 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-CWA5wlah.js +1 -0
- data/dist/chunks/vendor.js +3 -3
- data/dist/menu.yml +2 -2
- data/dist/playbook-rails-react-bindings.js +1 -1
- data/dist/playbook-rails.js +1 -1
- data/dist/playbook.css +1 -1
- data/lib/playbook/version.rb +1 -1
- metadata +10 -4
- data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_label.md +0 -3
- data/dist/chunks/_typeahead-C4YsbA48.js +0 -1
|
@@ -73,11 +73,14 @@ const MultiLevelSelectDefault = (props) => {
|
|
|
73
73
|
return (
|
|
74
74
|
<div>
|
|
75
75
|
<MultiLevelSelect
|
|
76
|
-
id=
|
|
76
|
+
id='multiselect-label'
|
|
77
77
|
label="Select a Department"
|
|
78
78
|
onSelect={(selectedNodes) =>
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
console.log(
|
|
80
|
+
"Selected Items",
|
|
81
|
+
selectedNodes
|
|
82
|
+
)
|
|
83
|
+
}
|
|
81
84
|
treeData={treeData}
|
|
82
85
|
{...props}
|
|
83
86
|
/>
|
|
@@ -85,4 +88,4 @@ const MultiLevelSelectDefault = (props) => {
|
|
|
85
88
|
)
|
|
86
89
|
};
|
|
87
90
|
|
|
88
|
-
export default MultiLevelSelectDefault;
|
|
91
|
+
export default MultiLevelSelectDefault;
|
|
@@ -192,7 +192,7 @@ describe('MultiLevelSelect multi variant', () => {
|
|
|
192
192
|
/>
|
|
193
193
|
)
|
|
194
194
|
const kit = screen.getByTestId(testId)
|
|
195
|
-
const input = kit.querySelector('#
|
|
195
|
+
const input = kit.querySelector('#multiselect_input')
|
|
196
196
|
fireEvent.click(input)
|
|
197
197
|
|
|
198
198
|
const disabledCheckbox = kit.querySelector('input[type="checkbox"][disabled]')
|
|
@@ -227,7 +227,7 @@ describe('MultiLevelSelect single variant', () => {
|
|
|
227
227
|
/>
|
|
228
228
|
)
|
|
229
229
|
const kit = screen.getByTestId(testId)
|
|
230
|
-
const input = kit.querySelector('#
|
|
230
|
+
const input = kit.querySelector('#multiselect_input')
|
|
231
231
|
fireEvent.click(input)
|
|
232
232
|
|
|
233
233
|
const disabledRadio = kit.querySelector('input[type="radio"][disabled]')
|
|
@@ -246,7 +246,7 @@ describe('MultiLevelSelect single variant', () => {
|
|
|
246
246
|
/>
|
|
247
247
|
)
|
|
248
248
|
const kit = screen.getByTestId(testId)
|
|
249
|
-
const input = kit.querySelector('#
|
|
249
|
+
const input = kit.querySelector('#multiselect_input')
|
|
250
250
|
fireEvent.click(input)
|
|
251
251
|
|
|
252
252
|
const disabledRadio = kit.querySelector('input[type="radio"][disabled]')
|
|
@@ -267,7 +267,7 @@ describe('MultiLevelSelect single variant', () => {
|
|
|
267
267
|
/>
|
|
268
268
|
)
|
|
269
269
|
const kit = screen.getByTestId(testId)
|
|
270
|
-
const input = kit.querySelector('#
|
|
270
|
+
const input = kit.querySelector('#multiselect_input')
|
|
271
271
|
fireEvent.click(input)
|
|
272
272
|
|
|
273
273
|
const enabledRadio = kit.querySelector('input[type="radio"]:not([disabled])')
|
|
@@ -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}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useState } from 'react'
|
|
2
2
|
import TimePicker from '../../pb_time_picker/_time_picker'
|
|
3
3
|
import Body from '../../pb_body/_body'
|
|
4
|
+
import Flex from '../../pb_flex/_flex'
|
|
4
5
|
|
|
5
6
|
const TimePickerOnHandler = (props) => {
|
|
6
7
|
const [selectedTime, setSelectedTime] = useState('')
|
|
@@ -17,7 +18,9 @@ const TimePickerOnHandler = (props) => {
|
|
|
17
18
|
return (
|
|
18
19
|
<div>
|
|
19
20
|
{(selectedTime || closedTime) && (
|
|
20
|
-
<
|
|
21
|
+
<Flex marginBottom="sm"
|
|
22
|
+
orientation="column"
|
|
23
|
+
>
|
|
21
24
|
{selectedTime && (
|
|
22
25
|
<Body
|
|
23
26
|
text={`onChange: ${selectedTime}`}
|
|
@@ -29,7 +32,7 @@ const TimePickerOnHandler = (props) => {
|
|
|
29
32
|
text={`onClose: ${closedTime}`}
|
|
30
33
|
/>
|
|
31
34
|
)}
|
|
32
|
-
</
|
|
35
|
+
</Flex>
|
|
33
36
|
)}
|
|
34
37
|
<TimePicker
|
|
35
38
|
id="time-picker-on-handler"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import TimePicker from '../_time_picker'
|
|
3
|
+
|
|
4
|
+
const TimePickerRequiredIndicator = (props) => (
|
|
5
|
+
<div>
|
|
6
|
+
<TimePicker
|
|
7
|
+
id="time-picker-required-indicator"
|
|
8
|
+
label="Select Time"
|
|
9
|
+
requiredIndicator
|
|
10
|
+
{...props}
|
|
11
|
+
/>
|
|
12
|
+
</div>
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
export default TimePickerRequiredIndicator
|
|
16
|
+
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
The `requiredIndicator`/`required_indicator` prop displays a red asterisk (*) next to the label, visually indicating that the field is required. This is purely visual and does not enforce validation.
|
|
2
|
+
|
|
3
|
+
You can use `requiredIndicator`/`required_indicator` with any validation approach: HTML5 validation via the `required` prop, client-side validation, or backend validation. For this reason, it works independently and doesn't need to be paired with the `required` prop.
|
|
@@ -9,6 +9,7 @@ examples:
|
|
|
9
9
|
- time_picker_min_max_time: Min & Max Time
|
|
10
10
|
- time_picker_error: Error
|
|
11
11
|
- time_picker_disabled: Disabled
|
|
12
|
+
- time_picker_required_indicator: Required Indicator
|
|
12
13
|
- time_picker_input_options: Input Options
|
|
13
14
|
|
|
14
15
|
|
|
@@ -21,4 +22,5 @@ examples:
|
|
|
21
22
|
- time_picker_min_max_time: Min & Max Time
|
|
22
23
|
- time_picker_error: Error
|
|
23
24
|
- time_picker_disabled: Disabled
|
|
25
|
+
- time_picker_required_indicator: Required Indicator
|
|
24
26
|
- time_picker_on_handler: onChange & onClose Handlers
|
|
@@ -7,3 +7,4 @@ export { default as TimePickerOnHandler } from './_time_picker_on_handler.jsx'
|
|
|
7
7
|
export { default as TimePickerMinMaxTime } from './_time_picker_min_max_time.jsx'
|
|
8
8
|
export { default as TimePickerError } from './_time_picker_error.jsx'
|
|
9
9
|
export { default as TimePickerDisabled } from './_time_picker_disabled.jsx'
|
|
10
|
+
export { default as TimePickerRequiredIndicator } from './_time_picker_required_indicator.jsx'
|
|
@@ -16,6 +16,8 @@ module Playbook
|
|
|
16
16
|
prop :name, type: Playbook::Props::String
|
|
17
17
|
prop :required, type: Playbook::Props::Boolean,
|
|
18
18
|
default: false
|
|
19
|
+
prop :required_indicator, type: Playbook::Props::Boolean,
|
|
20
|
+
default: false
|
|
19
21
|
prop :show_timezone, type: Playbook::Props::Boolean,
|
|
20
22
|
default: false
|
|
21
23
|
prop :time_format, type: Playbook::Props::Enum,
|
|
@@ -68,6 +70,7 @@ module Playbook
|
|
|
68
70
|
minTime: min_time,
|
|
69
71
|
name: name,
|
|
70
72
|
required: required,
|
|
73
|
+
requiredIndicator: required_indicator,
|
|
71
74
|
showTimezone: show_timezone,
|
|
72
75
|
timeFormat: time_format,
|
|
73
76
|
validationMessage: validation_message,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React 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 TimePicker from './_time_picker'
|
|
5
5
|
|
|
@@ -111,4 +111,50 @@ describe('TimePicker', () => {
|
|
|
111
111
|
expect(screen.getByText('AM')).toBeInTheDocument()
|
|
112
112
|
expect(screen.getByText('PM')).toBeInTheDocument()
|
|
113
113
|
})
|
|
114
|
+
|
|
115
|
+
test('renders required indicator asterisk when requiredIndicator is true', () => {
|
|
116
|
+
render(
|
|
117
|
+
<TimePicker
|
|
118
|
+
data={{ testid: 'required-indicator-picker' }}
|
|
119
|
+
label="Select Time"
|
|
120
|
+
requiredIndicator
|
|
121
|
+
/>
|
|
122
|
+
)
|
|
123
|
+
const kit = screen.getByTestId('required-indicator-picker')
|
|
124
|
+
const label = within(kit).getByText(/Select Time/)
|
|
125
|
+
|
|
126
|
+
expect(label).toBeInTheDocument()
|
|
127
|
+
expect(kit).toHaveTextContent('*')
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test('requiredIndicator works independently of required prop', () => {
|
|
131
|
+
render(
|
|
132
|
+
<TimePicker
|
|
133
|
+
data={{ testid: 'indicator-without-required' }}
|
|
134
|
+
label="Select Time"
|
|
135
|
+
requiredIndicator
|
|
136
|
+
/>
|
|
137
|
+
)
|
|
138
|
+
const kit = screen.getByTestId('indicator-without-required')
|
|
139
|
+
|
|
140
|
+
expect(kit).toHaveTextContent('*')
|
|
141
|
+
const input = screen.getByPlaceholderText('Select Time')
|
|
142
|
+
expect(input).not.toHaveAttribute('required')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('requiredIndicator and required can be used together', () => {
|
|
146
|
+
render(
|
|
147
|
+
<TimePicker
|
|
148
|
+
data={{ testid: 'both-props-picker' }}
|
|
149
|
+
label="Select Time"
|
|
150
|
+
required
|
|
151
|
+
requiredIndicator
|
|
152
|
+
/>
|
|
153
|
+
)
|
|
154
|
+
const kit = screen.getByTestId('both-props-picker')
|
|
155
|
+
const input = screen.getByPlaceholderText('Select Time')
|
|
156
|
+
|
|
157
|
+
expect(kit).toHaveTextContent('*')
|
|
158
|
+
expect(input).toHaveAttribute('required')
|
|
159
|
+
})
|
|
114
160
|
})
|