playbook_ui 16.1.0 → 16.2.0.pre.rc.1
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_dialog/_dialog.scss +8 -6
- data/app/pb_kits/playbook/pb_dropdown/_dropdown.scss +6 -0
- data/app/pb_kits/playbook/pb_dropdown/_dropdown.tsx +37 -2
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_blank_selection_rails.md +3 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_blank_selection_react.md +3 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_clearable.html.erb +52 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_clearable.jsx +72 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_clearable.md +5 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_constrain_height.jsx +33 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_constrain_height_rails.html.erb +20 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_constrain_height_rails.md +8 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_constrain_height_react.md +8 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_placeholder.html.erb +9 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_placeholder.jsx +33 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_placeholder.md +3 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/example.yml +6 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/index.js +4 -1
- data/app/pb_kits/playbook/pb_dropdown/dropdown.html.erb +2 -2
- data/app/pb_kits/playbook/pb_dropdown/dropdown.rb +6 -0
- data/app/pb_kits/playbook/pb_dropdown/dropdown.test.jsx +94 -0
- data/app/pb_kits/playbook/pb_dropdown/dropdown_container.rb +5 -1
- data/app/pb_kits/playbook/pb_dropdown/index.js +59 -4
- data/app/pb_kits/playbook/pb_dropdown/subcomponents/DropdownContainer.tsx +3 -0
- data/app/pb_kits/playbook/pb_dropdown/subcomponents/DropdownTrigger.tsx +2 -1
- data/app/pb_kits/playbook/pb_filter/Filter/SortMenu.tsx +1 -1
- data/app/pb_kits/playbook/pb_filter/docs/_filter_default.html.erb +2 -2
- data/app/pb_kits/playbook/pb_filter/docs/_filter_default.jsx +16 -9
- data/app/pb_kits/playbook/pb_filter/filter.rb +2 -2
- data/app/pb_kits/playbook/pb_form/docs/_form_with_required_indicator.html.erb +2 -0
- data/app/pb_kits/playbook/pb_form/pb_form_validation.js +9 -2
- data/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_truncated_text.html.erb +5 -5
- data/app/pb_kits/playbook/pb_form_pill/docs/_form_pill_truncated_text.jsx +4 -4
- data/app/pb_kits/playbook/pb_form_pill/form_pill.rb +4 -0
- data/app/pb_kits/playbook/pb_passphrase/_passphrase.tsx +20 -5
- data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_meter_settings.jsx +1 -0
- data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_required_indicator.html.erb +7 -0
- data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_required_indicator.jsx +24 -0
- data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_required_indicator.md +3 -0
- data/app/pb_kits/playbook/pb_passphrase/docs/example.yml +2 -0
- data/app/pb_kits/playbook/pb_passphrase/docs/index.js +1 -0
- data/app/pb_kits/playbook/pb_passphrase/passphrase.rb +2 -0
- data/app/pb_kits/playbook/pb_passphrase/passphrase.test.jsx +30 -1
- data/app/pb_kits/playbook/pb_phone_number_input/_phone_number_input.tsx +3 -0
- data/app/pb_kits/playbook/pb_phone_number_input/docs/_phone_number_input_required_indicator.html.erb +5 -0
- data/app/pb_kits/playbook/pb_phone_number_input/docs/_phone_number_input_required_indicator.jsx +14 -0
- data/app/pb_kits/playbook/pb_phone_number_input/docs/_phone_number_input_required_indicator.md +3 -0
- data/app/pb_kits/playbook/pb_phone_number_input/docs/example.yml +2 -0
- data/app/pb_kits/playbook/pb_phone_number_input/docs/index.js +1 -0
- data/app/pb_kits/playbook/pb_phone_number_input/phone_number_input.rb +3 -0
- data/app/pb_kits/playbook/pb_phone_number_input/phone_number_input.test.js +34 -3
- 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/app/pb_kits/playbook/pb_typeahead/_typeahead.test.jsx +24 -1
- data/app/pb_kits/playbook/pb_typeahead/_typeahead.tsx +2 -1
- data/app/pb_kits/playbook/pb_typeahead/components/MultiValue.tsx +4 -1
- data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_truncated_text.html.erb +1 -1
- data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_truncated_text.jsx +1 -1
- data/app/pb_kits/playbook/pb_typeahead/typeahead.rb +4 -0
- data/dist/chunks/{_pb_line_graph-ERhTGrxH.js → _pb_line_graph-BgKF_zz1.js} +1 -1
- data/dist/chunks/_typeahead-CWA5wlah.js +1 -0
- data/dist/chunks/{globalProps-C5qTX7aJ.js → globalProps-BhVYCqRf.js} +1 -1
- data/dist/chunks/{lib-B7ivt23s.js → lib-DD34ZrWL.js} +2 -2
- data/dist/chunks/vendor.js +4 -4
- data/dist/menu.yml +1 -1
- 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/align_content.rb +13 -3
- data/lib/playbook/align_items.rb +13 -3
- data/lib/playbook/align_self.rb +13 -3
- data/lib/playbook/display.rb +5 -0
- data/lib/playbook/flex.rb +13 -3
- data/lib/playbook/flex_direction.rb +13 -3
- data/lib/playbook/flex_grow.rb +13 -3
- data/lib/playbook/flex_shrink.rb +13 -3
- data/lib/playbook/flex_wrap.rb +13 -3
- data/lib/playbook/forms/builder/phone_number_field.rb +9 -0
- data/lib/playbook/justify_content.rb +13 -3
- data/lib/playbook/justify_self.rb +13 -3
- data/lib/playbook/order.rb +13 -3
- data/lib/playbook/spacing.rb +39 -9
- data/lib/playbook/text_align.rb +13 -3
- data/lib/playbook/truncate.rb +1 -1
- data/lib/playbook/version.rb +2 -2
- data/lib/playbook/vertical_align.rb +13 -3
- data/lib/playbook/z_index.rb +5 -0
- metadata +29 -6
- data/dist/chunks/_typeahead-DmWq2Utd.js +0 -1
|
@@ -36,6 +36,7 @@ type PhoneNumberInputProps = {
|
|
|
36
36
|
excludeCountries: string[],
|
|
37
37
|
preferredCountries?: string[],
|
|
38
38
|
required?: boolean,
|
|
39
|
+
requiredIndicator?: boolean,
|
|
39
40
|
value?: string,
|
|
40
41
|
formatAsYouType?: boolean,
|
|
41
42
|
strictMode?: boolean,
|
|
@@ -91,6 +92,7 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
|
|
|
91
92
|
onlyCountries = [],
|
|
92
93
|
excludeCountries = [],
|
|
93
94
|
required = false,
|
|
95
|
+
requiredIndicator = false,
|
|
94
96
|
preferredCountries = [],
|
|
95
97
|
value = "",
|
|
96
98
|
formatAsYouType = false,
|
|
@@ -533,6 +535,7 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
|
|
|
533
535
|
validateErrors()
|
|
534
536
|
},
|
|
535
537
|
onChange: formatAsYouType ? undefined : handleOnChange,
|
|
538
|
+
requiredIndicator,
|
|
536
539
|
value: inputValue
|
|
537
540
|
}
|
|
538
541
|
|
data/app/pb_kits/playbook/pb_phone_number_input/docs/_phone_number_input_required_indicator.jsx
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import PhoneNumberInput from '../../pb_phone_number_input/_phone_number_input'
|
|
3
|
+
|
|
4
|
+
const PhoneNumberInputRequiredIndicator = (props) => (
|
|
5
|
+
<>
|
|
6
|
+
<PhoneNumberInput
|
|
7
|
+
id='phone_number_input_required_indicator'
|
|
8
|
+
label='Phone Number'
|
|
9
|
+
requiredIndicator
|
|
10
|
+
{...props} />
|
|
11
|
+
</>
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
export default PhoneNumberInputRequiredIndicator
|
data/app/pb_kits/playbook/pb_phone_number_input/docs/_phone_number_input_required_indicator.md
ADDED
|
@@ -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.
|
|
@@ -12,6 +12,7 @@ examples:
|
|
|
12
12
|
- phone_number_input_format: Format as You Type
|
|
13
13
|
- phone_number_input_strict_mode: Strict Mode
|
|
14
14
|
- phone_number_input_country_search: Country Search
|
|
15
|
+
- phone_number_input_required_indicator: Required Indicator
|
|
15
16
|
|
|
16
17
|
rails:
|
|
17
18
|
- phone_number_input_default: Default
|
|
@@ -24,3 +25,4 @@ examples:
|
|
|
24
25
|
- phone_number_input_strict_mode: Strict Mode
|
|
25
26
|
- phone_number_input_hidden_inputs: Hidden Inputs
|
|
26
27
|
- phone_number_input_country_search: Country Search
|
|
28
|
+
- phone_number_input_required_indicator: Required Indicator
|
|
@@ -9,3 +9,4 @@ export { default as PhoneNumberInputAccessInputElement } from './_phone_number_i
|
|
|
9
9
|
export { default as PhoneNumberInputFormat } from './_phone_number_input_format'
|
|
10
10
|
export { default as PhoneNumberInputStrictMode } from './_phone_number_input_strict_mode'
|
|
11
11
|
export { default as PhoneNumberInputCountrySearch } from './_phone_number_input_country_search'
|
|
12
|
+
export { default as PhoneNumberInputRequiredIndicator } from './_phone_number_input_required_indicator.jsx'
|
|
@@ -7,6 +7,8 @@ module Playbook
|
|
|
7
7
|
default: false
|
|
8
8
|
prop :required, type: Playbook::Props::Boolean,
|
|
9
9
|
default: false
|
|
10
|
+
prop :required_indicator, type: Playbook::Props::Boolean,
|
|
11
|
+
default: false
|
|
10
12
|
prop :initial_country, type: Playbook::Props::String,
|
|
11
13
|
default: ""
|
|
12
14
|
prop :label, type: Playbook::Props::String,
|
|
@@ -52,6 +54,7 @@ module Playbook
|
|
|
52
54
|
excludeCountries: exclude_countries,
|
|
53
55
|
preferredCountries: preferred_countries,
|
|
54
56
|
required: required,
|
|
57
|
+
requiredIndicator: required_indicator,
|
|
55
58
|
value: value,
|
|
56
59
|
countrySearch: country_search,
|
|
57
60
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { render, screen, act } from "../utilities/test-utils";
|
|
2
|
+
import { render, screen, act, within } from "../utilities/test-utils";
|
|
3
3
|
import PhoneNumberInput from "./_phone_number_input";
|
|
4
4
|
|
|
5
5
|
const testId = "phoneNumberInput";
|
|
@@ -129,7 +129,7 @@ test("should format phone number as '555-555-5555' with formatAsYouType and 'us'
|
|
|
129
129
|
};
|
|
130
130
|
|
|
131
131
|
render(<PhoneNumberInput {...props} />);
|
|
132
|
-
|
|
132
|
+
|
|
133
133
|
const input = screen.getByRole("textbox");
|
|
134
134
|
|
|
135
135
|
act(() => {
|
|
@@ -154,7 +154,38 @@ test("should pass countrySearch prop to component", () => {
|
|
|
154
154
|
};
|
|
155
155
|
|
|
156
156
|
render(<PhoneNumberInput {...props} />);
|
|
157
|
-
|
|
157
|
+
|
|
158
158
|
const wrapper = screen.getByTestId('phone-input-with-search');
|
|
159
159
|
expect(wrapper).toBeInTheDocument();
|
|
160
160
|
});
|
|
161
|
+
|
|
162
|
+
test("renders required indicator asterisk when requiredIndicator is true", () => {
|
|
163
|
+
const props = {
|
|
164
|
+
data: { testid: testId },
|
|
165
|
+
id: testId,
|
|
166
|
+
label: "Required Phone Number",
|
|
167
|
+
requiredIndicator: true,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
render(<PhoneNumberInput {...props} />);
|
|
171
|
+
|
|
172
|
+
const kit = screen.getByTestId(testId);
|
|
173
|
+
const label = within(kit).getByText(/Required Phone Number/);
|
|
174
|
+
expect(label).toBeInTheDocument();
|
|
175
|
+
expect(kit).toHaveTextContent("*");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("does not render required indicator asterisk when requiredIndicator is false", () => {
|
|
179
|
+
const props = {
|
|
180
|
+
data: { testid: testId },
|
|
181
|
+
id: testId,
|
|
182
|
+
label: "Phone Number",
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
render(<PhoneNumberInput {...props} />);
|
|
186
|
+
|
|
187
|
+
const kit = screen.getByTestId(testId);
|
|
188
|
+
const label = within(kit).getByText(/Phone Number/);
|
|
189
|
+
expect(label).toBeInTheDocument();
|
|
190
|
+
expect(kit).not.toHaveTextContent("*");
|
|
191
|
+
});
|
|
@@ -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
|
})
|