playbook_ui 15.1.0.pre.alpha.PLAY2425textinputaccessibility10907 → 15.1.0.pre.alpha.PLAY2468phonenuminputvalidation10958
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_button/docs/_button_managed_disabled.html.erb +31 -0
- data/app/pb_kits/playbook/pb_button/docs/_button_managed_disabled.md +7 -0
- data/app/pb_kits/playbook/pb_button/docs/_button_managed_disabled_helper.html.erb +21 -0
- data/app/pb_kits/playbook/pb_button/docs/_button_managed_disabled_helper.md +7 -0
- data/app/pb_kits/playbook/pb_button/docs/example.yml +2 -0
- data/app/pb_kits/playbook/pb_button/index.js +99 -0
- data/app/pb_kits/playbook/pb_date_picker/_date_picker.scss +0 -4
- data/app/pb_kits/playbook/pb_form/docs/_form_form_with_validate.html.erb +1 -1
- data/app/pb_kits/playbook/pb_form/pb_form_validation.js +37 -13
- data/app/pb_kits/playbook/pb_phone_number_input/_phone_number_input.tsx +109 -24
- data/app/pb_kits/playbook/pb_text_input/_text_input.tsx +6 -14
- data/app/pb_kits/playbook/pb_text_input/docs/_text_input_default.html.erb +4 -8
- data/app/pb_kits/playbook/pb_text_input/docs/_text_input_default.jsx +0 -5
- data/app/pb_kits/playbook/pb_text_input/text_input.html.erb +1 -3
- data/app/pb_kits/playbook/pb_text_input/text_input.rb +0 -6
- data/dist/chunks/{_line_graph-BnK1i7QI.js → _line_graph-BLndveyW.js} +1 -1
- data/dist/chunks/{_typeahead-pbS3fEzb.js → _typeahead-C8u0HdSd.js} +1 -1
- data/dist/chunks/{_weekday_stacked-x-syST1P.js → _weekday_stacked-Ckwpc_D2.js} +1 -1
- data/dist/chunks/pb_form_validation-D1VURgVg.js +1 -0
- data/dist/chunks/vendor.js +1 -1
- data/dist/menu.yml +1 -1
- data/dist/playbook-doc.js +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 +11 -7
- data/app/pb_kits/playbook/pb_text_input/docs/_text_input_default.md +0 -1
- data/dist/chunks/pb_form_validation-CleM960_.js +0 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 72cfd26d4607165f040c9d27f0ce24d777167b2f2f584228fdfeaf336b423687
|
|
4
|
+
data.tar.gz: 699ecdaa5c3077dc49bb8c09a2ce16c21498ddce86d838a11392536b2bec4399
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e2f168293a3cbe4bbe2cfeb6acd637eb573d9523c17c3387f10e49b217e691e54e4d71dea4343bf64d8caeee97423dc7c0f97a9828200a5a1ead0e0f42539ac8
|
|
7
|
+
data.tar.gz: 4f58b9a3e38cc655712a2dd9d95a775f900d5d3a4689cdb12fa9a6998f778bb5fe73986ead2c3697d44f2a0f88790e75ff5aed0d82ea4cb66d216ec1c041edf9
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<%= pb_rails("body", props: { text: "Click to disable the Buttons below", id: "toggle-disabled-demo", cursor: "pointer", color:"link", margin_bottom:"sm" }) %>
|
|
2
|
+
<%= pb_rails("body", props: { text: "Click to enable the Buttons below", id: "toggle-enabled-demo", cursor: "pointer", color:"link", margin_bottom:"sm" }) %>
|
|
3
|
+
|
|
4
|
+
<%= pb_rails("card", props:{display:"flex", flex_direction:"row", justify_content:"center"}) do %>
|
|
5
|
+
<%= pb_rails("button", props: { text: "I am a Button", id: "normal_managed_button", data:{pb_button_managed: true}, margin_right: "lg" }) %>
|
|
6
|
+
<%= pb_rails("button", props: { text: "I am an <a> Button", id: "a_tag_managed_button", tag:"a", data:{pb_button_managed: true}, link: "http://google.com"}) %>
|
|
7
|
+
<% end %>
|
|
8
|
+
<script>
|
|
9
|
+
document.addEventListener('DOMContentLoaded', function () {
|
|
10
|
+
const disableTrigger = document.querySelector('#toggle-disabled-demo')
|
|
11
|
+
const enableTrigger = document.querySelector('#toggle-enabled-demo')
|
|
12
|
+
|
|
13
|
+
// Find the Buttons you want to 'manage'
|
|
14
|
+
const btn = document.querySelector('#normal_managed_button');
|
|
15
|
+
const link = document.querySelector('#a_tag_managed_button');
|
|
16
|
+
|
|
17
|
+
disableTrigger.addEventListener('click', (e) => {
|
|
18
|
+
// Disable default button
|
|
19
|
+
btn.setAttribute('disabled', true)
|
|
20
|
+
// Disable a tag button
|
|
21
|
+
link.setAttribute('aria-disabled', 'true')
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
enableTrigger.addEventListener('click', (e) => {
|
|
25
|
+
// Enable default button
|
|
26
|
+
btn.removeAttribute('disabled')
|
|
27
|
+
// Enable a tag button
|
|
28
|
+
link.removeAttribute('aria-disabled')
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
</script>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
If needing to toggle the disabled state of the Button dynamically (for example, within a Turbo or Stimulus context), you can now do so in rails using the `pb-button-managed` data attribute.
|
|
2
|
+
|
|
3
|
+
1) Add the following data attribute to your button kit: `data:{ pb-button-managed: true }`
|
|
4
|
+
|
|
5
|
+
2) To toggle enabled/disabled state via attributes: for buttons set/remove disabled, for links set/remove aria-disabled="true". This will handle disabling the button, preventing clicks as well as all style changes so you don't have to.
|
|
6
|
+
|
|
7
|
+
Click to enable or disable the buttons above and view the code snippet below for details!
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<%= pb_rails("body", props: { text: "Click to disable the Button below", id: "toggle-disabled-demo-with-helper", cursor: "pointer", color:"link", margin_bottom:"sm" }) %>
|
|
2
|
+
<%= pb_rails("body", props: { text: "Click to enable the Button below", id: "toggle-enabled-demo-with-helper", cursor: "pointer", color:"link", margin_bottom:"sm" }) %>
|
|
3
|
+
<br/>
|
|
4
|
+
<%= pb_rails("card", props:{display:"flex", flex_direction:"row", justify_content:"center"}) do %>
|
|
5
|
+
<%= pb_rails("button", props: { text: "Watch me Change!", id: "managed_button_with_helper", data:{pb_button_managed: true} }) %>
|
|
6
|
+
<% end %>
|
|
7
|
+
|
|
8
|
+
<script>
|
|
9
|
+
document.addEventListener('DOMContentLoaded', function () {
|
|
10
|
+
const disable = document.querySelector('#toggle-disabled-demo-with-helper')
|
|
11
|
+
const enable = document.querySelector('#toggle-enabled-demo-with-helper')
|
|
12
|
+
|
|
13
|
+
// Find the Button you want to 'manage'
|
|
14
|
+
const demoBtn = document.querySelector('#managed_button_with_helper')
|
|
15
|
+
|
|
16
|
+
// Use the pbButton object created by the kit to call the enable/disable methods
|
|
17
|
+
disable.addEventListener('click', (e) => {demoBtn._pbButton.disable()});
|
|
18
|
+
enable.addEventListener('click', (e) => {demoBtn._pbButton.enable()});
|
|
19
|
+
|
|
20
|
+
});
|
|
21
|
+
</script>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
The disabled state for the button can also be toggled via small helpers available through the `pb-button-managed` data attribute.
|
|
2
|
+
|
|
3
|
+
1) Add the following data attribute to your button kit: `data:{ pb-button-managed: true }`
|
|
4
|
+
|
|
5
|
+
2) Toggle state via the provided `_pbButton.disable()` and `_pbButton.enable()` helpers as shown in the code snippet below.
|
|
6
|
+
|
|
7
|
+
Click to enable or disable the buttons above to see this in action!
|
|
@@ -11,6 +11,8 @@ examples:
|
|
|
11
11
|
- button_options: Button Additional Options
|
|
12
12
|
- button_size: Button Size
|
|
13
13
|
- button_form: Button Form Attribute
|
|
14
|
+
- button_managed_disabled: Button Toggle Disabled State
|
|
15
|
+
- button_managed_disabled_helper: Button Toggle Disabled State Helper
|
|
14
16
|
|
|
15
17
|
react:
|
|
16
18
|
- button_default: Button Variants
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import PbEnhancedElement from "../pb_enhanced_element"
|
|
2
|
+
|
|
3
|
+
const BUTTON_SELECTOR = "[data-pb-button-managed]"
|
|
4
|
+
|
|
5
|
+
export default class PbButton extends PbEnhancedElement {
|
|
6
|
+
static get selector() {
|
|
7
|
+
return BUTTON_SELECTOR
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
connect() {
|
|
11
|
+
this._attrManaged = this._attributesPresent()
|
|
12
|
+
this.element._pbButton = this
|
|
13
|
+
|
|
14
|
+
this._onClick = (e) => {
|
|
15
|
+
if (this.isDisabled()) {
|
|
16
|
+
e.preventDefault()
|
|
17
|
+
e.stopImmediatePropagation()
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
this.element.addEventListener("click", this._onClick, true)
|
|
21
|
+
|
|
22
|
+
if (this._attrManaged) this._syncClassesFromAttributes()
|
|
23
|
+
|
|
24
|
+
this._observer = new MutationObserver(() => {
|
|
25
|
+
this._attrManaged = true
|
|
26
|
+
this._syncClassesFromAttributes()
|
|
27
|
+
})
|
|
28
|
+
this._observer.observe(this.element, {
|
|
29
|
+
attributes: true,
|
|
30
|
+
attributeFilter: ["disabled", "aria-disabled"],
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
disconnect() {
|
|
35
|
+
this.element.removeEventListener("click", this._onClick, true)
|
|
36
|
+
this._observer?.disconnect()
|
|
37
|
+
delete this.element._pbButton
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
disable() { this.setDisabled(true) }
|
|
41
|
+
enable() { this.setDisabled(false) }
|
|
42
|
+
|
|
43
|
+
setDisabled(state) {
|
|
44
|
+
if (this._isButton()) {
|
|
45
|
+
state
|
|
46
|
+
? this.element.setAttribute("disabled", "disabled")
|
|
47
|
+
: this.element.removeAttribute("disabled")
|
|
48
|
+
} else {
|
|
49
|
+
state
|
|
50
|
+
? this.element.setAttribute("aria-disabled", "true")
|
|
51
|
+
: this.element.removeAttribute("aria-disabled")
|
|
52
|
+
}
|
|
53
|
+
this._attrManaged = true
|
|
54
|
+
this._applyClassState(state)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
isDisabled() {
|
|
58
|
+
if (this._isButton()) {
|
|
59
|
+
if (this.element.hasAttribute("disabled")) return true
|
|
60
|
+
if (this._attrManaged && !this.element.hasAttribute("disabled")) return false
|
|
61
|
+
} else {
|
|
62
|
+
const aria = this.element.getAttribute("aria-disabled")
|
|
63
|
+
if (aria === "true") return true
|
|
64
|
+
if (this._attrManaged && aria !== "true") return false
|
|
65
|
+
}
|
|
66
|
+
return this.element.classList.contains("pb_button_disabled")
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
_isButton() {
|
|
70
|
+
return this.element.tagName === "BUTTON"
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
_attributesPresent() {
|
|
74
|
+
return this.element.hasAttribute("disabled") || this.element.hasAttribute("aria-disabled")
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
_syncClassesFromAttributes() {
|
|
78
|
+
const state = this._attrDisabledState()
|
|
79
|
+
const disabled = (state === null) ? false : state
|
|
80
|
+
this._applyClassState(disabled)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
_attrDisabledState() {
|
|
84
|
+
if (this._isButton()) {
|
|
85
|
+
return this.element.hasAttribute("disabled") ? true : null
|
|
86
|
+
} else {
|
|
87
|
+
const aria = this.element.getAttribute("aria-disabled")
|
|
88
|
+
if (aria === "true") return true
|
|
89
|
+
if (aria === "false") return false
|
|
90
|
+
return this.element.hasAttribute("aria-disabled") ? false : null
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
_applyClassState(disabled) {
|
|
95
|
+
this.element.classList.toggle("pb_button_disabled", !!disabled)
|
|
96
|
+
this.element.classList.toggle("pb_button_enabled", !disabled)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
<%= pb_form_with(scope: :example, method: :get, url: "", validate: true) do |form| %>
|
|
91
91
|
<%= form.typeahead :example_typeahead_validation, props: { data: { typeahead_example2: true, user: {} }, label: true, placeholder: "Search for a user", required: true, validation: { message: "Please select a user." } } %>
|
|
92
92
|
<%= form.text_field :example_text_field_validation, props: { label: true, required: true } %>
|
|
93
|
-
<%= form.phone_number_field :example_phone_number_field_validation, props: { label: "Example phone field", hidden_inputs: true } %>
|
|
93
|
+
<%= form.phone_number_field :example_phone_number_field_validation, props: { label: "Example phone field", hidden_inputs: true, required: true } %>
|
|
94
94
|
<%= form.email_field :example_email_field_validation, props: { label: true, required: true } %>
|
|
95
95
|
<%= form.number_field :example_number_field_validation, props: { label: true, required: true } %>
|
|
96
96
|
<%= form.search_field :example_project_number_validation, props: { label: true, required: true, validation: { pattern: "[0-9]{2}-[0-9]{5}", message: "Please enter a valid project number (example: 33-12345)." } } %>
|
|
@@ -2,12 +2,13 @@ import PbEnhancedElement from '../pb_enhanced_element'
|
|
|
2
2
|
import { debounce } from '../utilities/object'
|
|
3
3
|
|
|
4
4
|
// Kit selectors
|
|
5
|
-
const KIT_SELECTOR
|
|
6
|
-
const ERROR_MESSAGE_SELECTOR
|
|
5
|
+
const KIT_SELECTOR = '[class^="pb_"][class*="_kit"]'
|
|
6
|
+
const ERROR_MESSAGE_SELECTOR = '.pb_body_kit_negative'
|
|
7
7
|
|
|
8
8
|
// Validation selectors
|
|
9
|
-
const FORM_SELECTOR
|
|
10
|
-
const REQUIRED_FIELDS_SELECTOR
|
|
9
|
+
const FORM_SELECTOR = 'form[data-pb-form-validation="true"]'
|
|
10
|
+
const REQUIRED_FIELDS_SELECTOR = 'input[required],textarea[required],select[required]'
|
|
11
|
+
const PHONE_NUMBER_VALIDATION_ERROR_SELECTOR = '[data-pb-phone-validation-error="true"]'
|
|
11
12
|
|
|
12
13
|
const FIELD_EVENTS = [
|
|
13
14
|
'change',
|
|
@@ -22,12 +23,24 @@ class PbFormValidation extends PbEnhancedElement {
|
|
|
22
23
|
|
|
23
24
|
connect() {
|
|
24
25
|
this.formValidationFields.forEach((field) => {
|
|
26
|
+
// Skip phone number inputs - they handle their own validation
|
|
27
|
+
const isPhoneNumberInput = field.closest('.pb_phone_number_input')
|
|
28
|
+
if (isPhoneNumberInput) return
|
|
29
|
+
|
|
25
30
|
FIELD_EVENTS.forEach((e) => {
|
|
26
31
|
field.addEventListener(e, debounce((event) => {
|
|
27
32
|
this.validateFormField(event)
|
|
28
33
|
}, 250), false)
|
|
29
34
|
})
|
|
30
35
|
})
|
|
36
|
+
|
|
37
|
+
// Add event listener to check for phone number validation errors
|
|
38
|
+
this.element.addEventListener('submit', (event) => {
|
|
39
|
+
if (this.hasPhoneNumberValidationErrors()) {
|
|
40
|
+
event.preventDefault()
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
})
|
|
31
44
|
}
|
|
32
45
|
|
|
33
46
|
validateFormField(event) {
|
|
@@ -45,20 +58,25 @@ class PbFormValidation extends PbEnhancedElement {
|
|
|
45
58
|
|
|
46
59
|
showValidationMessage(target) {
|
|
47
60
|
const { parentElement } = target
|
|
61
|
+
const kitElement = parentElement.closest(KIT_SELECTOR)
|
|
62
|
+
|
|
63
|
+
// Check if this is a phone number input
|
|
64
|
+
const isPhoneNumberInput = kitElement && kitElement.classList.contains('pb_phone_number_input')
|
|
48
65
|
|
|
49
66
|
// ensure clean error message state
|
|
50
67
|
this.clearError(target)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
// set the error message element
|
|
54
|
-
const errorMessageContainer = this.errorMessageContainer
|
|
68
|
+
kitElement.classList.add('error')
|
|
55
69
|
|
|
56
|
-
|
|
70
|
+
// Only add error message if it's NOT a phone number input
|
|
71
|
+
if (!isPhoneNumberInput) {
|
|
72
|
+
// set the error message element
|
|
73
|
+
const errorMessageContainer = this.errorMessageContainer
|
|
74
|
+
if (target.dataset.message) target.setCustomValidity(target.dataset.message)
|
|
75
|
+
errorMessageContainer.innerHTML = target.validationMessage
|
|
57
76
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
parentElement.appendChild(errorMessageContainer)
|
|
77
|
+
// add the error message element to the dom tree
|
|
78
|
+
parentElement.appendChild(errorMessageContainer)
|
|
79
|
+
}
|
|
62
80
|
}
|
|
63
81
|
|
|
64
82
|
clearError(target) {
|
|
@@ -68,6 +86,12 @@ class PbFormValidation extends PbEnhancedElement {
|
|
|
68
86
|
if (errorMessageContainer) errorMessageContainer.remove()
|
|
69
87
|
}
|
|
70
88
|
|
|
89
|
+
// Check if there are phone number input errors
|
|
90
|
+
hasPhoneNumberValidationErrors() {
|
|
91
|
+
const phoneNumberErrors = this.element.querySelectorAll(PHONE_NUMBER_VALIDATION_ERROR_SELECTOR)
|
|
92
|
+
return phoneNumberErrors.length > 0
|
|
93
|
+
}
|
|
94
|
+
|
|
71
95
|
get errorMessageContainer() {
|
|
72
96
|
const errorContainer = document.createElement('div')
|
|
73
97
|
const kitClassName = ERROR_MESSAGE_SELECTOR.replace(/\./, '')
|
|
@@ -110,37 +110,43 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
|
|
|
110
110
|
|
|
111
111
|
const inputRef = useRef<HTMLInputElement | null>(null)
|
|
112
112
|
const itiRef = useRef<any>(null);
|
|
113
|
+
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
|
114
|
+
const textInputKitRef = useRef<HTMLDivElement | null>(null);
|
|
113
115
|
const [inputValue, setInputValue] = useState(value)
|
|
114
|
-
const [error, setError] = useState(
|
|
116
|
+
const [error, setError] = useState("")
|
|
115
117
|
const [dropDownIsOpen, setDropDownIsOpen] = useState(false)
|
|
116
118
|
const [selectedData, setSelectedData] = useState()
|
|
117
119
|
const [hasTyped, setHasTyped] = useState(false)
|
|
120
|
+
const [formSubmitted, setFormSubmitted] = useState(false)
|
|
121
|
+
|
|
122
|
+
// Function to update validation state on the wrapper element
|
|
123
|
+
// Only applies when input is required
|
|
124
|
+
const updateValidationState = (hasError: boolean) => {
|
|
125
|
+
if (wrapperRef.current && required) {
|
|
126
|
+
if (hasError) {
|
|
127
|
+
wrapperRef.current.setAttribute('data-pb-phone-validation-error', 'true')
|
|
128
|
+
} else {
|
|
129
|
+
wrapperRef.current.removeAttribute('data-pb-phone-validation-error')
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Determine which error to display
|
|
135
|
+
// Show internal errors on blur (hasTyped) or on form submission (formSubmitted)
|
|
136
|
+
const shouldShowInternalError = (hasTyped || formSubmitted) && required && error
|
|
137
|
+
const displayError = props.error || (shouldShowInternalError ? error : "")
|
|
118
138
|
|
|
119
139
|
useEffect(() => {
|
|
120
|
-
|
|
140
|
+
const hasError = error.length > 0
|
|
141
|
+
if (hasError) {
|
|
121
142
|
onValidate(false)
|
|
122
143
|
} else {
|
|
123
144
|
onValidate(true)
|
|
124
145
|
}
|
|
125
|
-
}, [error, onValidate])
|
|
126
146
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
Read: https://react.dev/reference/react/useImperativeHandle
|
|
131
|
-
*/
|
|
132
|
-
useImperativeHandle(ref, () => {
|
|
133
|
-
return {
|
|
134
|
-
clearField() {
|
|
135
|
-
setInputValue("")
|
|
136
|
-
setError("")
|
|
137
|
-
setHasTyped(false)
|
|
138
|
-
},
|
|
139
|
-
inputNode() {
|
|
140
|
-
return inputRef.current
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
})
|
|
147
|
+
// Update validation state whenever error changes
|
|
148
|
+
updateValidationState(hasError)
|
|
149
|
+
}, [error, onValidate])
|
|
144
150
|
|
|
145
151
|
const unformatNumber = (formattedNumber: any) => {
|
|
146
152
|
return formattedNumber.replace(/\D/g, "")
|
|
@@ -164,6 +170,13 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
|
|
|
164
170
|
|
|
165
171
|
const validateTooShortNumber = (itiInit: any) => {
|
|
166
172
|
if (!itiInit) return
|
|
173
|
+
|
|
174
|
+
// If field is empty, don't show "too short" error
|
|
175
|
+
if (!inputValue || inputValue.trim() === '') {
|
|
176
|
+
setError('')
|
|
177
|
+
return false
|
|
178
|
+
}
|
|
179
|
+
|
|
167
180
|
if (itiInit.getValidationError() === ValidationError.TooShort) {
|
|
168
181
|
return showFormattedError('too short')
|
|
169
182
|
} else {
|
|
@@ -206,16 +219,33 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
|
|
|
206
219
|
|
|
207
220
|
const validateRepeatCountryCode = (itiInit: any) => {
|
|
208
221
|
if (!itiInit) return
|
|
209
|
-
const countryDialCode =
|
|
222
|
+
const countryDialCode = itiRef.current.getSelectedCountryData().dialCode;
|
|
210
223
|
if (unformatNumber(inputValue).startsWith(countryDialCode)) {
|
|
211
224
|
return showFormattedError('repeat country code')
|
|
212
225
|
}
|
|
213
226
|
}
|
|
214
227
|
|
|
228
|
+
// Validation for required empty fields
|
|
229
|
+
const validateRequiredField = () => {
|
|
230
|
+
if (required && (!inputValue || inputValue.trim() === '')) {
|
|
231
|
+
setError('Missing phone number')
|
|
232
|
+
return true
|
|
233
|
+
}
|
|
234
|
+
return false
|
|
235
|
+
}
|
|
215
236
|
|
|
216
237
|
const validateErrors = () => {
|
|
217
|
-
|
|
238
|
+
// If field is empty, only show required field error if applicable
|
|
239
|
+
if (!inputValue || inputValue.trim() === '') {
|
|
240
|
+
if (validateRequiredField()) return
|
|
241
|
+
// Clear any existing errors if field is empty and not required
|
|
242
|
+
if (!required) {
|
|
243
|
+
setError('')
|
|
244
|
+
}
|
|
245
|
+
return
|
|
246
|
+
}
|
|
218
247
|
|
|
248
|
+
// Run validation checks
|
|
219
249
|
if (itiRef.current) isValid(itiRef.current.isValidNumber())
|
|
220
250
|
if (validateOnlyNumbers(itiRef.current)) return
|
|
221
251
|
if (validateTooLongNumber(itiRef.current)) return
|
|
@@ -225,6 +255,52 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
|
|
|
225
255
|
if (validateRepeatCountryCode(itiRef.current)) return
|
|
226
256
|
}
|
|
227
257
|
|
|
258
|
+
// Add listener for form validation to track when validation should be shown
|
|
259
|
+
useEffect(() => {
|
|
260
|
+
const handleInvalid = (event: Event) => {
|
|
261
|
+
const target = event.target as HTMLInputElement
|
|
262
|
+
const phoneNumberContainer = target.closest('.pb_phone_number_input')
|
|
263
|
+
|
|
264
|
+
if (phoneNumberContainer && phoneNumberContainer === wrapperRef.current) {
|
|
265
|
+
const invalidInputName = target.name || target.getAttribute('name')
|
|
266
|
+
if (invalidInputName === name) {
|
|
267
|
+
setFormSubmitted(true)
|
|
268
|
+
// Trigger validation when form is submitted
|
|
269
|
+
validateErrors()
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
document.addEventListener('invalid', handleInvalid, true)
|
|
275
|
+
|
|
276
|
+
return () => {
|
|
277
|
+
document.removeEventListener('invalid', handleInvalid, true)
|
|
278
|
+
}
|
|
279
|
+
}, [name, inputValue])
|
|
280
|
+
|
|
281
|
+
/*
|
|
282
|
+
useImperativeHandle exposes the kit's input element to a parent component via a ref.
|
|
283
|
+
See the Playbook docs for use cases.
|
|
284
|
+
Read: https://react.dev/reference/react/useImperativeHandle
|
|
285
|
+
*/
|
|
286
|
+
useImperativeHandle(ref, () => {
|
|
287
|
+
return {
|
|
288
|
+
clearField() {
|
|
289
|
+
setInputValue("")
|
|
290
|
+
setError("")
|
|
291
|
+
setHasTyped(false)
|
|
292
|
+
setFormSubmitted(false)
|
|
293
|
+
// Only clear validation state if field was required
|
|
294
|
+
if (required) {
|
|
295
|
+
updateValidationState(false)
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
inputNode() {
|
|
299
|
+
return inputRef.current
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
})
|
|
303
|
+
|
|
228
304
|
const getCurrentSelectedData = (itiInit: any, inputValue: string) => {
|
|
229
305
|
return { ...itiInit.getSelectedCountryData(), number: inputValue }
|
|
230
306
|
}
|
|
@@ -232,6 +308,12 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
|
|
|
232
308
|
const handleOnChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
|
|
233
309
|
if (!hasTyped) setHasTyped(true)
|
|
234
310
|
setInputValue(evt.target.value)
|
|
311
|
+
|
|
312
|
+
// Reset form submitted state when user types
|
|
313
|
+
if (formSubmitted) {
|
|
314
|
+
setFormSubmitted(false)
|
|
315
|
+
}
|
|
316
|
+
|
|
235
317
|
let phoneNumberData
|
|
236
318
|
if (formatAsYouType) {
|
|
237
319
|
const formattedPhoneNumberData = getCurrentSelectedData(itiRef.current, evt.target.value)
|
|
@@ -300,7 +382,7 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
|
|
|
300
382
|
dark,
|
|
301
383
|
"data-phone-number": JSON.stringify(selectedData),
|
|
302
384
|
disabled,
|
|
303
|
-
error,
|
|
385
|
+
error: displayError,
|
|
304
386
|
type: 'tel',
|
|
305
387
|
id,
|
|
306
388
|
label,
|
|
@@ -310,7 +392,10 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
|
|
|
310
392
|
value: inputValue
|
|
311
393
|
}
|
|
312
394
|
|
|
313
|
-
let wrapperProps: Record<string, unknown> = {
|
|
395
|
+
let wrapperProps: Record<string, unknown> = {
|
|
396
|
+
className: classes,
|
|
397
|
+
ref: wrapperRef
|
|
398
|
+
}
|
|
314
399
|
|
|
315
400
|
if (!isEmpty(aria)) textInputProps = {...textInputProps, ...ariaProps}
|
|
316
401
|
if (!isEmpty(data)) wrapperProps = {...wrapperProps, ...dataProps}
|
|
@@ -140,14 +140,10 @@ const TextInput = (props: TextInputProps, ref: React.LegacyRef<HTMLInputElement>
|
|
|
140
140
|
formattedValue = value
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
const errorId = error ? `${id}-error` : undefined
|
|
144
|
-
|
|
145
143
|
const textInput = (
|
|
146
144
|
childInput ? React.cloneElement(children, { className: "text_input" }) :
|
|
147
145
|
(<input
|
|
148
146
|
{...domSafeProps(props)}
|
|
149
|
-
aria-describedby={errorId}
|
|
150
|
-
aria-invalid={!!error}
|
|
151
147
|
autoComplete={typeof autoComplete === "string" ? autoComplete : ( autoComplete ? undefined : "off" )}
|
|
152
148
|
className="text_input"
|
|
153
149
|
disabled={disabled}
|
|
@@ -206,20 +202,16 @@ const TextInput = (props: TextInputProps, ref: React.LegacyRef<HTMLInputElement>
|
|
|
206
202
|
{...htmlProps}
|
|
207
203
|
className={css}
|
|
208
204
|
>
|
|
209
|
-
{label &&
|
|
210
|
-
<
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
)}
|
|
205
|
+
{label &&
|
|
206
|
+
<Caption
|
|
207
|
+
className="pb_text_input_kit_label"
|
|
208
|
+
text={label}
|
|
209
|
+
/>
|
|
210
|
+
}
|
|
216
211
|
<div className={`${addOnCss} text_input_wrapper`}>
|
|
217
212
|
{render}
|
|
218
213
|
|
|
219
214
|
{error && <Body
|
|
220
|
-
aria={{ atomic: "true", live: "polite" }}
|
|
221
|
-
htmlOptions={{ role: "alert" }}
|
|
222
|
-
id={errorId}
|
|
223
215
|
status="negative"
|
|
224
216
|
text={error}
|
|
225
217
|
variant={null}
|
|
@@ -9,27 +9,23 @@
|
|
|
9
9
|
|
|
10
10
|
<%= pb_rails("text_input", props: {
|
|
11
11
|
label: "Last Name",
|
|
12
|
-
placeholder: "Enter last name"
|
|
13
|
-
id: "last-name"
|
|
12
|
+
placeholder: "Enter last name"
|
|
14
13
|
}) %>
|
|
15
14
|
|
|
16
15
|
<%= pb_rails("text_input", props: {
|
|
17
16
|
label: "Phone Number",
|
|
18
17
|
type: "phone",
|
|
19
|
-
placeholder: "Enter phone number"
|
|
20
|
-
id: "phone"
|
|
18
|
+
placeholder: "Enter phone number"
|
|
21
19
|
}) %>
|
|
22
20
|
|
|
23
21
|
<%= pb_rails("text_input", props: {
|
|
24
22
|
label: "Email Address",
|
|
25
23
|
type: "email",
|
|
26
|
-
placeholder: "Enter email address"
|
|
27
|
-
id: "email"
|
|
24
|
+
placeholder: "Enter email address"
|
|
28
25
|
}) %>
|
|
29
26
|
|
|
30
27
|
<%= pb_rails("text_input", props: {
|
|
31
28
|
label: "Zip Code",
|
|
32
29
|
type: "number",
|
|
33
|
-
placeholder: "Enter zip code"
|
|
34
|
-
id: "zip"
|
|
30
|
+
placeholder: "Enter zip code"
|
|
35
31
|
}) %>
|
|
@@ -38,7 +38,6 @@ const TextInputDefault = (props) => {
|
|
|
38
38
|
{...props}
|
|
39
39
|
/>
|
|
40
40
|
<TextInput
|
|
41
|
-
id="last-name"
|
|
42
41
|
label="Last Name"
|
|
43
42
|
name="lastName"
|
|
44
43
|
onChange={handleOnChangeFormField}
|
|
@@ -47,7 +46,6 @@ const TextInputDefault = (props) => {
|
|
|
47
46
|
{...props}
|
|
48
47
|
/>
|
|
49
48
|
<TextInput
|
|
50
|
-
id="phone"
|
|
51
49
|
label="Phone Number"
|
|
52
50
|
name="phone"
|
|
53
51
|
onChange={handleOnChangeFormField}
|
|
@@ -57,7 +55,6 @@ const TextInputDefault = (props) => {
|
|
|
57
55
|
{...props}
|
|
58
56
|
/>
|
|
59
57
|
<TextInput
|
|
60
|
-
id="email"
|
|
61
58
|
label="Email Address"
|
|
62
59
|
name="email"
|
|
63
60
|
onChange={handleOnChangeFormField}
|
|
@@ -67,7 +64,6 @@ const TextInputDefault = (props) => {
|
|
|
67
64
|
{...props}
|
|
68
65
|
/>
|
|
69
66
|
<TextInput
|
|
70
|
-
id="zip"
|
|
71
67
|
label="Zip Code"
|
|
72
68
|
name="zip"
|
|
73
69
|
onChange={handleOnChangeFormField}
|
|
@@ -88,7 +84,6 @@ const TextInputDefault = (props) => {
|
|
|
88
84
|
<br />
|
|
89
85
|
|
|
90
86
|
<TextInput
|
|
91
|
-
id="first-name"
|
|
92
87
|
label="First Name"
|
|
93
88
|
onChange={handleOnChangeFirstName}
|
|
94
89
|
placeholder="Enter first name"
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
<%= pb_content_tag(:div, id: nil ) do %>
|
|
2
2
|
<% if object.label.present? %>
|
|
3
|
-
<label for="<%= object.input_options[:id] || object.id %>" >
|
|
4
3
|
<%= pb_rails("caption", props: { text: object.label, dark: object.dark, classname: "pb_text_input_kit_label" }) %>
|
|
5
|
-
</label>
|
|
6
4
|
<% end %>
|
|
7
5
|
<%= content_tag(:div, class: "#{add_on_class} text_input_wrapper") do %>
|
|
8
6
|
<% if content.present? %>
|
|
@@ -17,7 +15,7 @@
|
|
|
17
15
|
<% else %>
|
|
18
16
|
<%= input_tag %>
|
|
19
17
|
<% end %>
|
|
20
|
-
<%= pb_rails("body", props: {dark: object.dark, status: "negative", text: object.error
|
|
18
|
+
<%= pb_rails("body", props: {dark: object.dark, status: "negative", text: object.error}) if object.error %>
|
|
21
19
|
<% end %>
|
|
22
20
|
<% end %>
|
|
23
21
|
|
|
@@ -64,16 +64,10 @@ module Playbook
|
|
|
64
64
|
"#{object.id}-sanitized" if id.present?
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
-
def error_id
|
|
68
|
-
"#{id}-error" if error.present?
|
|
69
|
-
end
|
|
70
|
-
|
|
71
67
|
private
|
|
72
68
|
|
|
73
69
|
def all_input_options
|
|
74
70
|
{
|
|
75
|
-
'aria-describedby': error.present? ? error_id : nil,
|
|
76
|
-
'aria-invalid': error.present?,
|
|
77
71
|
autocomplete: autocomplete == true ? nil : (autocomplete.presence || "off"),
|
|
78
72
|
class: "text_input #{input_options.dig(:classname) || ''}",
|
|
79
73
|
data: validation_data,
|