playbook_ui 15.1.0.pre.alpha.PLAY2468phonenuminputvalidation11082 → 15.1.0.pre.alpha.PLAY2468phonenuminputvalidation11148
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_form/docs/_form_form_with_validate.html.erb +11 -0
- data/app/pb_kits/playbook/pb_form/pb_form_validation.js +13 -37
- data/app/pb_kits/playbook/pb_icon_circle/_icon_circle.tsx +2 -2
- data/app/pb_kits/playbook/pb_icon_stat_value/_icon_stat_value.scss +15 -21
- data/app/pb_kits/playbook/pb_icon_stat_value/_icon_stat_value.tsx +6 -5
- data/app/pb_kits/playbook/pb_icon_stat_value/icon_stat_value.html.erb +2 -0
- data/app/pb_kits/playbook/pb_icon_stat_value/icon_stat_value.rb +11 -3
- data/app/pb_kits/playbook/pb_icon_stat_value/icon_stat_value.test.js +9 -8
- data/app/pb_kits/playbook/pb_phone_number_input/_phone_number_input.tsx +15 -124
- data/app/pb_kits/playbook/pb_typeahead/_typeahead.tsx +39 -1
- data/app/pb_kits/playbook/pb_typeahead/typeahead.rb +2 -0
- data/dist/chunks/{_line_graph-Cw2C1Nuk.js → _line_graph-ghx4wYQu.js} +1 -1
- data/dist/chunks/{_typeahead-DslCE4I8.js → _typeahead-DUcfjQGg.js} +1 -1
- data/dist/chunks/{_weekday_stacked-Cs0EoUlF.js → _weekday_stacked-tkzbOLCo.js} +2 -2
- data/dist/chunks/pb_form_validation-CleM960_.js +1 -0
- data/dist/chunks/vendor.js +1 -1
- data/dist/playbook-doc.js +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/version.rb +1 -1
- metadata +6 -6
- data/dist/chunks/pb_form_validation-D1VURgVg.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: 0d8f257689482817a53d451705d8410896d0bcd1ade7e19e468f507cf9792d7f
|
|
4
|
+
data.tar.gz: 03be064207b87ddc0fe3a1fcf5cfdd3b1a0db19176b851d36f469a93788d9c0f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a92fc50f91a2a4c1e7c800f102458ce09e44a3397797ecc2a3115c8896c05c1750d353f37537b6a23e769f5f965561becca597d52d965e0da361dbd8de49d84d
|
|
7
|
+
data.tar.gz: 6edf98e6ecfffa46bbdc840c2e6592b4d9980f733e7683aed808fda2a7886c6f2209df3c2067b80ed99d7098affe108f930407007b78148203b0f0c08e2c2f85
|
|
@@ -21,6 +21,15 @@
|
|
|
21
21
|
]
|
|
22
22
|
%>
|
|
23
23
|
|
|
24
|
+
<%
|
|
25
|
+
example_typeahead_options = [
|
|
26
|
+
{ label: 'Orange', value: '#FFA500' },
|
|
27
|
+
{ label: 'Red', value: '#FF0000' },
|
|
28
|
+
{ label: 'Green', value: '#00FF00' },
|
|
29
|
+
{ label: 'Blue', value: '#0000FF' },
|
|
30
|
+
]
|
|
31
|
+
%>
|
|
32
|
+
|
|
24
33
|
<% treeData = [{
|
|
25
34
|
label: "Power Home Remodeling",
|
|
26
35
|
value: "Power Home Remodeling",
|
|
@@ -89,6 +98,8 @@
|
|
|
89
98
|
|
|
90
99
|
<%= pb_form_with(scope: :example, method: :get, url: "", validate: true) do |form| %>
|
|
91
100
|
<%= 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." } } %>
|
|
101
|
+
<%= form.typeahead :example_typeahead_validation_react, props: { options: example_typeahead_options, pills: true, label: "Example Typeahead (React Rendered)", placeholder: "Search for a user", required: true, validation: { message: "Please select a color." } } %>
|
|
102
|
+
<%= form.typeahead :example_typeahead_validation_react_2, props: { options: example_typeahead_options, pills: true, label: "Example Typeahead 2 (React Rendered)", placeholder: "Search for a user", required: true } %>
|
|
92
103
|
<%= form.text_field :example_text_field_validation, props: { label: true, required: true } %>
|
|
93
104
|
<%= form.phone_number_field :example_phone_number_field_validation, props: { label: "Example phone field", hidden_inputs: true, required: true } %>
|
|
94
105
|
<%= form.email_field :example_email_field_validation, props: { label: true, required: true } %>
|
|
@@ -2,13 +2,12 @@ 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
|
|
11
|
-
const PHONE_NUMBER_VALIDATION_ERROR_SELECTOR = '[data-pb-phone-validation-error="true"]'
|
|
9
|
+
const FORM_SELECTOR = 'form[data-pb-form-validation="true"]'
|
|
10
|
+
const REQUIRED_FIELDS_SELECTOR = 'input[required],textarea[required],select[required]'
|
|
12
11
|
|
|
13
12
|
const FIELD_EVENTS = [
|
|
14
13
|
'change',
|
|
@@ -23,24 +22,12 @@ class PbFormValidation extends PbEnhancedElement {
|
|
|
23
22
|
|
|
24
23
|
connect() {
|
|
25
24
|
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
|
-
|
|
30
25
|
FIELD_EVENTS.forEach((e) => {
|
|
31
26
|
field.addEventListener(e, debounce((event) => {
|
|
32
27
|
this.validateFormField(event)
|
|
33
28
|
}, 250), false)
|
|
34
29
|
})
|
|
35
30
|
})
|
|
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
|
-
})
|
|
44
31
|
}
|
|
45
32
|
|
|
46
33
|
validateFormField(event) {
|
|
@@ -58,25 +45,20 @@ class PbFormValidation extends PbEnhancedElement {
|
|
|
58
45
|
|
|
59
46
|
showValidationMessage(target) {
|
|
60
47
|
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')
|
|
65
48
|
|
|
66
49
|
// ensure clean error message state
|
|
67
50
|
this.clearError(target)
|
|
68
|
-
|
|
51
|
+
parentElement.closest(KIT_SELECTOR).classList.add('error')
|
|
69
52
|
|
|
70
|
-
//
|
|
71
|
-
|
|
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
|
|
53
|
+
// set the error message element
|
|
54
|
+
const errorMessageContainer = this.errorMessageContainer
|
|
76
55
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
56
|
+
if (target.dataset.message) target.setCustomValidity(target.dataset.message)
|
|
57
|
+
|
|
58
|
+
errorMessageContainer.innerHTML = target.validationMessage
|
|
59
|
+
|
|
60
|
+
// add the error message element to the dom tree
|
|
61
|
+
parentElement.appendChild(errorMessageContainer)
|
|
80
62
|
}
|
|
81
63
|
|
|
82
64
|
clearError(target) {
|
|
@@ -86,12 +68,6 @@ class PbFormValidation extends PbEnhancedElement {
|
|
|
86
68
|
if (errorMessageContainer) errorMessageContainer.remove()
|
|
87
69
|
}
|
|
88
70
|
|
|
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
|
-
|
|
95
71
|
get errorMessageContainer() {
|
|
96
72
|
const errorContainer = document.createElement('div')
|
|
97
73
|
const kitClassName = ERROR_MESSAGE_SELECTOR.replace(/\./, '')
|
|
@@ -3,7 +3,7 @@ import React from 'react'
|
|
|
3
3
|
import classnames from 'classnames'
|
|
4
4
|
|
|
5
5
|
import { buildAriaProps, buildCss, buildDataProps, buildHtmlProps } from '../utilities/props'
|
|
6
|
-
import { globalProps } from '../utilities/globalProps'
|
|
6
|
+
import { globalProps, GlobalProps } from '../utilities/globalProps'
|
|
7
7
|
|
|
8
8
|
import Icon from '../pb_icon/_icon'
|
|
9
9
|
|
|
@@ -26,7 +26,7 @@ type IconCircleProps = {
|
|
|
26
26
|
| "orange"
|
|
27
27
|
| "green"
|
|
28
28
|
| "lighter",
|
|
29
|
-
}
|
|
29
|
+
} & GlobalProps
|
|
30
30
|
|
|
31
31
|
const IconCircle = (props: IconCircleProps) => {
|
|
32
32
|
const {
|
|
@@ -3,51 +3,45 @@
|
|
|
3
3
|
@import "../tokens/spacing";
|
|
4
4
|
@import "../pb_icon_circle/icon_circle";
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
.pb_icon_stat_value_kit_horizontal,
|
|
7
|
+
.pb_icon_stat_value_kit_vertical
|
|
8
|
+
{
|
|
7
9
|
display: flex;
|
|
8
10
|
align-items: baseline;
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
&.pb_icon_stat_value_kit_vertical {
|
|
11
13
|
flex-direction: column;
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
&.text_align_center {
|
|
14
16
|
align-items: center;
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
.pb_title_kit,
|
|
19
|
+
.pb_body_kit,
|
|
20
|
+
.pb_caption_kit_md {
|
|
19
21
|
text-align: center;
|
|
20
22
|
}
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
&.text_align_right {
|
|
24
26
|
align-items: flex-end;
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
.pb_title_kit,
|
|
29
|
+
.pb_body_kit,
|
|
30
|
+
.pb_caption_kit_md {
|
|
29
31
|
text-align: right;
|
|
30
32
|
}
|
|
31
33
|
}
|
|
32
|
-
|
|
33
|
-
[class^=pb_icon_circle] {
|
|
34
|
-
margin-bottom: $space-xs;
|
|
35
|
-
}
|
|
36
34
|
}
|
|
37
35
|
|
|
38
|
-
|
|
36
|
+
&.pb_icon_stat_value_kit_horizontal {
|
|
39
37
|
align-items: center;
|
|
40
38
|
|
|
41
|
-
|
|
39
|
+
&.text_align_center {
|
|
42
40
|
justify-content: center;
|
|
43
41
|
}
|
|
44
42
|
|
|
45
|
-
|
|
43
|
+
&.text_align_right {
|
|
46
44
|
justify-content: flex-end;
|
|
47
45
|
}
|
|
48
|
-
|
|
49
|
-
[class^=pb_icon_circle] {
|
|
50
|
-
margin-right: $space-sm;
|
|
51
|
-
}
|
|
52
46
|
}
|
|
53
47
|
}
|
|
@@ -2,7 +2,7 @@ import React from 'react'
|
|
|
2
2
|
import classnames from 'classnames'
|
|
3
3
|
|
|
4
4
|
import { buildAriaProps, buildCss, buildDataProps, buildHtmlProps } from '../utilities/props'
|
|
5
|
-
import { globalProps } from '../utilities/globalProps'
|
|
5
|
+
import { globalProps, GlobalProps } from '../utilities/globalProps'
|
|
6
6
|
|
|
7
7
|
import Body from '../pb_body/_body'
|
|
8
8
|
import Caption from '../pb_caption/_caption'
|
|
@@ -33,8 +33,7 @@ type IconStatValueProps = {
|
|
|
33
33
|
| "yellow"
|
|
34
34
|
| "orange"
|
|
35
35
|
| "green"
|
|
36
|
-
|
|
37
|
-
}
|
|
36
|
+
} & GlobalProps
|
|
38
37
|
|
|
39
38
|
const IconStatValue = (props: IconStatValueProps): React.ReactElement => {
|
|
40
39
|
const {
|
|
@@ -50,13 +49,13 @@ const IconStatValue = (props: IconStatValueProps): React.ReactElement => {
|
|
|
50
49
|
text = '',
|
|
51
50
|
unit = '',
|
|
52
51
|
value = 0,
|
|
53
|
-
variant = '
|
|
52
|
+
variant = 'default',
|
|
54
53
|
} = props
|
|
55
54
|
const ariaProps = buildAriaProps(aria)
|
|
56
55
|
const dataProps = buildDataProps(data)
|
|
57
56
|
const htmlProps = buildHtmlProps(htmlOptions)
|
|
58
57
|
const classes = classnames(
|
|
59
|
-
buildCss('pb_icon_stat_value_kit', orientation
|
|
58
|
+
buildCss('pb_icon_stat_value_kit', orientation), globalProps(props),
|
|
60
59
|
className
|
|
61
60
|
)
|
|
62
61
|
const titleSize = function(size: "sm" | "md" | "lg") {
|
|
@@ -101,6 +100,8 @@ const IconStatValue = (props: IconStatValueProps): React.ReactElement => {
|
|
|
101
100
|
<IconCircle
|
|
102
101
|
dark={dark}
|
|
103
102
|
icon={icon}
|
|
103
|
+
marginBottom={orientation == 'vertical' ? 'xs' : undefined}
|
|
104
|
+
marginRight={orientation == 'horizontal' ? 'sm' : undefined}
|
|
104
105
|
size={size}
|
|
105
106
|
variant={variant}
|
|
106
107
|
/>
|
|
@@ -9,8 +9,8 @@ module Playbook
|
|
|
9
9
|
values: %w[sm md lg],
|
|
10
10
|
default: "sm"
|
|
11
11
|
prop :variant, type: Playbook::Props::Enum,
|
|
12
|
-
values: %w[default royal blue purple teal red yellow green orange
|
|
13
|
-
default: "
|
|
12
|
+
values: %w[default royal blue purple teal red yellow green orange],
|
|
13
|
+
default: "default"
|
|
14
14
|
|
|
15
15
|
prop :orientation, type: Playbook::Props::Enum,
|
|
16
16
|
values: %w[vertical horizontal],
|
|
@@ -25,7 +25,7 @@ module Playbook
|
|
|
25
25
|
prop :value
|
|
26
26
|
|
|
27
27
|
def classname
|
|
28
|
-
generate_classname("pb_icon_stat_value_kit", orientation
|
|
28
|
+
generate_classname("pb_icon_stat_value_kit", orientation)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
def value_string
|
|
@@ -41,6 +41,14 @@ module Playbook
|
|
|
41
41
|
3
|
|
42
42
|
end
|
|
43
43
|
end
|
|
44
|
+
|
|
45
|
+
def icon_margin_right
|
|
46
|
+
orientation === "horizontal" && "sm"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def icon_margin_bottom
|
|
50
|
+
orientation === "vertical" && "xs"
|
|
51
|
+
end
|
|
44
52
|
end
|
|
45
53
|
end
|
|
46
54
|
end
|
|
@@ -18,7 +18,7 @@ describe("IconStatValue Kit", () => {
|
|
|
18
18
|
)
|
|
19
19
|
|
|
20
20
|
const kit = screen.getByTestId(testId)
|
|
21
|
-
expect(kit).toHaveClass("
|
|
21
|
+
expect(kit).toHaveClass("pb_icon_stat_value_kit_horizontal")
|
|
22
22
|
})
|
|
23
23
|
|
|
24
24
|
test("renders icon", () => {
|
|
@@ -99,9 +99,10 @@ describe("IconStatValue Kit", () => {
|
|
|
99
99
|
value={64.18}
|
|
100
100
|
/>
|
|
101
101
|
)
|
|
102
|
-
|
|
102
|
+
const size = sizeProp === "sm" ? "3" : sizeProp === "md" ? "2" : "1"
|
|
103
103
|
const kit = screen.getByTestId(testId)
|
|
104
|
-
|
|
104
|
+
const title = kit.querySelector(".pb_title_kit")
|
|
105
|
+
expect(title).toHaveClass(`pb_title_${size}`)
|
|
105
106
|
|
|
106
107
|
cleanup()
|
|
107
108
|
})
|
|
@@ -115,8 +116,7 @@ describe("IconStatValue Kit", () => {
|
|
|
115
116
|
"teal",
|
|
116
117
|
"red",
|
|
117
118
|
"yellow",
|
|
118
|
-
"green"
|
|
119
|
-
"lighter"].forEach(
|
|
119
|
+
"green"].forEach(
|
|
120
120
|
(colorProp) => {
|
|
121
121
|
render(
|
|
122
122
|
<IconStatValue
|
|
@@ -128,9 +128,10 @@ describe("IconStatValue Kit", () => {
|
|
|
128
128
|
variant={colorProp}
|
|
129
129
|
/>
|
|
130
130
|
)
|
|
131
|
-
|
|
131
|
+
|
|
132
132
|
const kit = screen.getByTestId(testId)
|
|
133
|
-
|
|
133
|
+
const iconCircle = kit.querySelector(`.pb_icon_circle_kit_size_sm_${colorProp}`)
|
|
134
|
+
expect(iconCircle).toBeInTheDocument()
|
|
134
135
|
|
|
135
136
|
cleanup()
|
|
136
137
|
})
|
|
@@ -149,7 +150,7 @@ describe("IconStatValue Kit", () => {
|
|
|
149
150
|
)
|
|
150
151
|
|
|
151
152
|
const kit = screen.getByTestId(testId)
|
|
152
|
-
expect(kit).toHaveClass("
|
|
153
|
+
expect(kit).toHaveClass("pb_icon_stat_value_kit_vertical")
|
|
153
154
|
})
|
|
154
155
|
|
|
155
156
|
})
|
|
@@ -110,54 +110,18 @@ 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
113
|
const [inputValue, setInputValue] = useState(value)
|
|
115
114
|
const [error, setError] = useState(props.error || "")
|
|
116
115
|
const [dropDownIsOpen, setDropDownIsOpen] = useState(false)
|
|
117
116
|
const [selectedData, setSelectedData] = useState()
|
|
118
117
|
const [hasTyped, setHasTyped] = useState(false)
|
|
119
|
-
const [formSubmitted, setFormSubmitted] = useState(false)
|
|
120
|
-
const [hasStartedValidating, setHasStartedValidating] = useState(false)
|
|
121
118
|
|
|
122
|
-
// Only sync initial error from props, not continuous updates
|
|
123
|
-
// Once validation starts, internal validation takes over
|
|
124
119
|
useEffect(() => {
|
|
125
|
-
if (
|
|
126
|
-
setError(props.error)
|
|
127
|
-
// If there's an initial error from props, mark as submitted so it shows
|
|
128
|
-
if (props.error) {
|
|
129
|
-
setFormSubmitted(true)
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}, [props.error, hasStartedValidating])
|
|
133
|
-
|
|
134
|
-
// Function to update validation state on the wrapper element
|
|
135
|
-
// Only applies when input is required
|
|
136
|
-
const updateValidationState = (hasError: boolean) => {
|
|
137
|
-
if (wrapperRef.current && required) {
|
|
138
|
-
if (hasError) {
|
|
139
|
-
wrapperRef.current.setAttribute('data-pb-phone-validation-error', 'true')
|
|
140
|
-
} else {
|
|
141
|
-
wrapperRef.current.removeAttribute('data-pb-phone-validation-error')
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Determine which error to display
|
|
147
|
-
// Show internal errors on blur (hasTyped) or on form submission (formSubmitted)
|
|
148
|
-
const shouldShowInternalError = (hasTyped || formSubmitted) && required && error
|
|
149
|
-
const displayError = shouldShowInternalError ? error : ""
|
|
150
|
-
|
|
151
|
-
useEffect(() => {
|
|
152
|
-
const hasError = error.length > 0
|
|
153
|
-
if (hasError) {
|
|
120
|
+
if ((error ?? '').length > 0) {
|
|
154
121
|
onValidate(false)
|
|
155
122
|
} else {
|
|
156
123
|
onValidate(true)
|
|
157
124
|
}
|
|
158
|
-
|
|
159
|
-
// Update validation state whenever error changes
|
|
160
|
-
updateValidationState(hasError)
|
|
161
125
|
}, [error, onValidate])
|
|
162
126
|
|
|
163
127
|
const unformatNumber = (formattedNumber: any) => {
|
|
@@ -208,7 +172,7 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
|
|
|
208
172
|
}
|
|
209
173
|
|
|
210
174
|
const validateUnhandledError = (itiInit: any) => {
|
|
211
|
-
if (!
|
|
175
|
+
if (!itiInit) return
|
|
212
176
|
if (itiInit.getValidationError() === ValidationError.SomethingWentWrong) {
|
|
213
177
|
if (inputValue.length === 1) {
|
|
214
178
|
return showFormattedError('too short')
|
|
@@ -237,9 +201,8 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
|
|
|
237
201
|
}
|
|
238
202
|
}
|
|
239
203
|
|
|
240
|
-
// Validation for required empty fields
|
|
241
204
|
const validateRequiredField = () => {
|
|
242
|
-
if (
|
|
205
|
+
if (!inputValue || inputValue.trim() === '') {
|
|
243
206
|
setError('Missing phone number')
|
|
244
207
|
return true
|
|
245
208
|
}
|
|
@@ -247,22 +210,14 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
|
|
|
247
210
|
}
|
|
248
211
|
|
|
249
212
|
const validateErrors = () => {
|
|
250
|
-
//
|
|
251
|
-
if (!hasStartedValidating) {
|
|
252
|
-
setHasStartedValidating(true)
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// If field is empty, only show required field error if applicable
|
|
213
|
+
// If field is empty, show error message
|
|
256
214
|
if (!inputValue || inputValue.trim() === '') {
|
|
257
215
|
if (validateRequiredField()) return
|
|
258
|
-
// Clear any existing errors if field is empty and not required
|
|
259
|
-
if (!required) {
|
|
260
|
-
setError('')
|
|
261
|
-
}
|
|
262
216
|
return
|
|
263
217
|
}
|
|
264
218
|
|
|
265
|
-
|
|
219
|
+
if (!hasTyped && !error) return
|
|
220
|
+
|
|
266
221
|
if (itiRef.current) isValid(itiRef.current.isValidNumber())
|
|
267
222
|
if (validateOnlyNumbers(itiRef.current)) return
|
|
268
223
|
if (validateTooLongNumber(itiRef.current)) return
|
|
@@ -272,29 +227,6 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
|
|
|
272
227
|
if (validateRepeatCountryCode(itiRef.current)) return
|
|
273
228
|
}
|
|
274
229
|
|
|
275
|
-
// Add listener for form validation to track when validation should be shown
|
|
276
|
-
useEffect(() => {
|
|
277
|
-
const handleInvalid = (event: Event) => {
|
|
278
|
-
const target = event.target as HTMLInputElement
|
|
279
|
-
const phoneNumberContainer = target.closest('.pb_phone_number_input')
|
|
280
|
-
|
|
281
|
-
if (phoneNumberContainer && phoneNumberContainer === wrapperRef.current) {
|
|
282
|
-
const invalidInputName = target.name || target.getAttribute('name')
|
|
283
|
-
if (invalidInputName === name) {
|
|
284
|
-
setFormSubmitted(true)
|
|
285
|
-
// Trigger validation when form is submitted
|
|
286
|
-
validateErrors()
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
document.addEventListener('invalid', handleInvalid, true)
|
|
292
|
-
|
|
293
|
-
return () => {
|
|
294
|
-
document.removeEventListener('invalid', handleInvalid, true)
|
|
295
|
-
}
|
|
296
|
-
}, [name, inputValue])
|
|
297
|
-
|
|
298
230
|
/*
|
|
299
231
|
useImperativeHandle exposes the kit's input element to a parent component via a ref.
|
|
300
232
|
See the Playbook docs for use cases.
|
|
@@ -306,12 +238,6 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
|
|
|
306
238
|
setInputValue("")
|
|
307
239
|
setError("")
|
|
308
240
|
setHasTyped(false)
|
|
309
|
-
setFormSubmitted(false)
|
|
310
|
-
setHasStartedValidating(false)
|
|
311
|
-
// Only clear validation state if field was required
|
|
312
|
-
if (required) {
|
|
313
|
-
updateValidationState(false)
|
|
314
|
-
}
|
|
315
241
|
},
|
|
316
242
|
inputNode() {
|
|
317
243
|
return inputRef.current
|
|
@@ -321,15 +247,13 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
|
|
|
321
247
|
// Run validation and return error message or true
|
|
322
248
|
const isEmpty = !inputValue || inputValue.trim() === ''
|
|
323
249
|
|
|
324
|
-
if (required && isEmpty) {
|
|
325
|
-
setError('Missing phone number')
|
|
326
|
-
setFormSubmitted(true)
|
|
327
|
-
return 'Missing phone number'
|
|
328
|
-
}
|
|
329
|
-
|
|
330
250
|
if (isEmpty) {
|
|
331
|
-
|
|
332
|
-
|
|
251
|
+
// Show missing phone number error
|
|
252
|
+
const errorMessage = 'Missing phone number'
|
|
253
|
+
setError(errorMessage)
|
|
254
|
+
setHasTyped(true)
|
|
255
|
+
// Only return error for React Hook Form if field is required
|
|
256
|
+
return required ? errorMessage : true
|
|
333
257
|
}
|
|
334
258
|
|
|
335
259
|
if (!itiRef.current) {
|
|
@@ -342,7 +266,6 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
|
|
|
342
266
|
const countryName = itiRef.current.getSelectedCountryData().name
|
|
343
267
|
const errorMessage = `Invalid ${countryName} phone number (repeat country code)`
|
|
344
268
|
setError(errorMessage)
|
|
345
|
-
setFormSubmitted(true)
|
|
346
269
|
setHasTyped(true)
|
|
347
270
|
return errorMessage
|
|
348
271
|
}
|
|
@@ -352,7 +275,6 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
|
|
|
352
275
|
const countryName = itiRef.current.getSelectedCountryData().name
|
|
353
276
|
const errorMessage = `Invalid ${countryName} phone number (enter numbers only)`
|
|
354
277
|
setError(errorMessage)
|
|
355
|
-
setFormSubmitted(true)
|
|
356
278
|
setHasTyped(true)
|
|
357
279
|
return errorMessage
|
|
358
280
|
}
|
|
@@ -373,9 +295,7 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
|
|
|
373
295
|
errorMessage = `Invalid ${countryName} phone number`
|
|
374
296
|
}
|
|
375
297
|
|
|
376
|
-
// Set the error state so the validation attribute gets added
|
|
377
298
|
setError(errorMessage)
|
|
378
|
-
setFormSubmitted(true)
|
|
379
299
|
setHasTyped(true)
|
|
380
300
|
|
|
381
301
|
return errorMessage
|
|
@@ -395,12 +315,6 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
|
|
|
395
315
|
const handleOnChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
|
|
396
316
|
if (!hasTyped) setHasTyped(true)
|
|
397
317
|
setInputValue(evt.target.value)
|
|
398
|
-
|
|
399
|
-
// Reset form submitted state when user types
|
|
400
|
-
if (formSubmitted) {
|
|
401
|
-
setFormSubmitted(false)
|
|
402
|
-
}
|
|
403
|
-
|
|
404
318
|
let phoneNumberData
|
|
405
319
|
if (formatAsYouType) {
|
|
406
320
|
const formattedPhoneNumberData = getCurrentSelectedData(itiRef.current, evt.target.value)
|
|
@@ -409,28 +323,8 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
|
|
|
409
323
|
phoneNumberData = getCurrentSelectedData(itiRef.current, evt.target.value)
|
|
410
324
|
}
|
|
411
325
|
setSelectedData(phoneNumberData)
|
|
412
|
-
|
|
413
|
-
// Check if this is React Hook Form by checking if onChange expects target format
|
|
414
|
-
const isReactHookForm = onChange.toString().includes("target")
|
|
415
|
-
if (isReactHookForm) {
|
|
416
|
-
// For React Hook Form, pass the event with modified target value
|
|
417
|
-
onChange({
|
|
418
|
-
...evt,
|
|
419
|
-
target: {
|
|
420
|
-
...evt.target,
|
|
421
|
-
name,
|
|
422
|
-
value: phoneNumberData
|
|
423
|
-
}
|
|
424
|
-
} as any)
|
|
425
|
-
} else {
|
|
426
|
-
onChange(phoneNumberData)
|
|
427
|
-
}
|
|
428
|
-
|
|
326
|
+
onChange(phoneNumberData)
|
|
429
327
|
isValid(itiRef.current.isValidNumber())
|
|
430
|
-
|
|
431
|
-
// Trigger validation after onChange for React Hook Form
|
|
432
|
-
// This ensures validation state is up-to-date
|
|
433
|
-
setTimeout(() => validateErrors(), 0)
|
|
434
328
|
}
|
|
435
329
|
|
|
436
330
|
// Separating Concerns as React Docs Recommend
|
|
@@ -489,7 +383,7 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
|
|
|
489
383
|
dark,
|
|
490
384
|
"data-phone-number": JSON.stringify(selectedData),
|
|
491
385
|
disabled,
|
|
492
|
-
error:
|
|
386
|
+
error: hasTyped ? error : props.error,
|
|
493
387
|
type: 'tel',
|
|
494
388
|
id,
|
|
495
389
|
label,
|
|
@@ -499,10 +393,7 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
|
|
|
499
393
|
value: inputValue
|
|
500
394
|
}
|
|
501
395
|
|
|
502
|
-
let wrapperProps: Record<string, unknown> = {
|
|
503
|
-
className: classes,
|
|
504
|
-
ref: wrapperRef
|
|
505
|
-
}
|
|
396
|
+
let wrapperProps: Record<string, unknown> = { className: classes }
|
|
506
397
|
|
|
507
398
|
if (!isEmpty(aria)) textInputProps = {...textInputProps, ...ariaProps}
|
|
508
399
|
if (!isEmpty(data)) wrapperProps = {...wrapperProps, ...dataProps}
|