playbook_ui 15.1.0.pre.alpha.PLAY2468phonenuminputvalidation10993 → 15.1.0.pre.alpha.alphaforaudiences151011000

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/pb_advanced_table/Hooks/useTableState.ts +0 -2
  3. data/app/pb_kits/playbook/pb_date_picker/_date_picker.scss +4 -0
  4. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_stacked_alert.html.erb +16 -16
  5. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_stacked_alert.jsx +2 -1
  6. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_status.html.erb +31 -31
  7. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_status.jsx +4 -3
  8. data/app/pb_kits/playbook/pb_form/docs/_form_form_with_validate.html.erb +1 -1
  9. data/app/pb_kits/playbook/pb_form/pb_form_validation.js +13 -37
  10. data/app/pb_kits/playbook/pb_phone_number_input/_phone_number_input.tsx +25 -198
  11. data/app/pb_kits/playbook/pb_text_input/_text_input.tsx +14 -6
  12. data/app/pb_kits/playbook/pb_text_input/docs/_text_input_default.html.erb +8 -4
  13. data/app/pb_kits/playbook/pb_text_input/docs/_text_input_default.jsx +5 -0
  14. data/app/pb_kits/playbook/pb_text_input/docs/_text_input_default.md +1 -0
  15. data/app/pb_kits/playbook/pb_text_input/text_input.html.erb +3 -1
  16. data/app/pb_kits/playbook/pb_text_input/text_input.rb +6 -0
  17. data/dist/chunks/{_line_graph-C3zTTfo0.js → _line_graph-BnK1i7QI.js} +1 -1
  18. data/dist/chunks/{_typeahead-CfKPIYmd.js → _typeahead-pbS3fEzb.js} +1 -1
  19. data/dist/chunks/{_weekday_stacked-CUTwEQ-P.js → _weekday_stacked-OOFiMFSs.js} +2 -2
  20. data/dist/chunks/pb_form_validation-CleM960_.js +1 -0
  21. data/dist/chunks/vendor.js +1 -1
  22. data/dist/playbook-doc.js +2 -2
  23. data/dist/playbook-rails-react-bindings.js +1 -1
  24. data/dist/playbook-rails.js +1 -1
  25. data/dist/playbook.css +1 -1
  26. data/lib/playbook/version.rb +1 -1
  27. metadata +6 -5
  28. 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: f282ba3da61911cdfdc409d4cf8272cd1a89f0110a381e78c7af5faf80da9faf
4
- data.tar.gz: 5c04ca68a269de660b5fae7f0ef9886e8b8d3545d065f6f91cdf45deba513c83
3
+ metadata.gz: 6ada0b8869b8f7e86b4f9fec2216ae14a2525539440e67b22af9856df81c5261
4
+ data.tar.gz: 3bdbee334dc6ed0c7d937ccb4fe9855e9987cab7b5747e47fe51fb760a573b0c
5
5
  SHA512:
6
- metadata.gz: 3869b47c55c537d86d46e33849016d975bcb8184d7f8c2529b68a556087c349a7e6438bfdb07e5c1e8ecd8e99daae22251919181714d8520b3df7e91cff67c48
7
- data.tar.gz: 55243c0bbbafd08287ac9cb8524bfca5a76730222e3d459be8c9cfaf8afdf752eae4ed5ab787ce94c3d5b82d28e6caee59028b71dc5feb88b9f73df247ee8d08
6
+ metadata.gz: 87f028d5f2cba6bcf56e2ea6054e48a67179ca1192e45710b19096607102981a442aa5ed5e653d5e70b25d180cda4b4c7d008d3a1a1956983d8cd5dd13a2ca03
7
+ data.tar.gz: 58335f67e22a6055a4b631f211b99f6801f68ea9cb2f9bc13ba6dc7145caa067a626d4f57e03e8981b2420f81e8964e93f09ca9ebbe9c84a46579e6f892bd8f7
@@ -146,8 +146,6 @@ export function useTableState({
146
146
 
147
147
  // Pagination configuration
148
148
  const paginationInitializer = useMemo(() => {
149
- if (!pagination) return {};
150
-
151
149
  return {
152
150
  getPaginationRowModel: getPaginationRowModel(),
153
151
  paginateExpandedRows: false,
@@ -19,6 +19,10 @@
19
19
  @import "./sass_partials/calendar_input_icon";
20
20
  }
21
21
 
22
+ label {
23
+ display: block !important;
24
+ }
25
+
22
26
  &:focus,
23
27
  :focus-within {
24
28
  div.cal_icon_wrapper,
@@ -3,12 +3,12 @@
3
3
  <%= pb_rails("button", props: { text: "Delete Status", data: {"open-dialog": "dialog-stacked-delete"}, margin_right: "md" }) %>
4
4
 
5
5
 
6
- <%= pb_rails("dialog", props: {
7
- id:"dialog-stacked-default",
6
+ <%= pb_rails("dialog", props: {
7
+ id:"dialog-stacked-default",
8
8
  status: "default",
9
- size: "sm",
10
- title: "Are you sure?",
11
- text: "Text explaining why there is an alert",
9
+ size: "sm",
10
+ title: "Are you sure?",
11
+ text: "Text explaining why there is an alert",
12
12
  }) do %>
13
13
  <%= pb_rails("dialog/dialog_footer") do %>
14
14
  <%= pb_rails("flex", props: { orientation: "column", padding_x:"md", padding: "sm" }) do %>
@@ -18,12 +18,12 @@
18
18
  <% end %>
19
19
  <% end %>
20
20
 
21
- <%= pb_rails("dialog", props: {
22
- id:"dialog-stacked-caution",
21
+ <%= pb_rails("dialog", props: {
22
+ id:"dialog-stacked-caution",
23
23
  status: "caution",
24
- size: "sm",
25
- title: "Are you sure?",
26
- text: "This is the action you will be taking",
24
+ size: "sm",
25
+ title: "Are you sure?",
26
+ text: "This is the action you will be taking",
27
27
  }) do %>
28
28
  <%= pb_rails("dialog/dialog_footer") do %>
29
29
  <%= pb_rails("flex", props: { orientation: "column", padding_x:"md", padding: "sm" }) do %>
@@ -33,16 +33,16 @@
33
33
  <% end %>
34
34
  <% end %>
35
35
 
36
- <%= pb_rails("dialog", props: {
37
- id:"dialog-stacked-delete",
36
+ <%= pb_rails("dialog", props: {
37
+ id:"dialog-stacked-delete",
38
38
  status: "delete",
39
- size: "sm",
40
- title: "Delete",
41
- text: "You are about to delete ...",
39
+ size: "sm",
40
+ title: "Delete",
41
+ text: "You are about to delete ...",
42
42
  }) do %>
43
43
  <%= pb_rails("dialog/dialog_footer") do %>
44
44
  <%= pb_rails("flex", props: { orientation: "column", padding_x:"md", padding: "sm" }) do %>
45
- <%= pb_rails("button", props: { text: "Yes, Action", full_width: true }) %>
45
+ <%= pb_rails("button", props: { text: "Yes, Action", variant: "danger", full_width: true }) %>
46
46
  <%= pb_rails("button", props: { text: "No, Cancel", variant: "secondary", full_width: true, margin_top: "sm", data: {"close-dialog": "dialog-stacked-delete" } }) %>
47
47
  <% end %>
48
48
  <% end %>
@@ -51,7 +51,7 @@ const DialogStackedAlert = () => {
51
51
  return (
52
52
  <div>
53
53
  <Flex
54
- rowGap="xs"
54
+ rowGap="xs"
55
55
  wrap
56
56
  >
57
57
  <Button
@@ -93,6 +93,7 @@ const DialogStackedAlert = () => {
93
93
  <Button
94
94
  fullWidth
95
95
  onClick={dialog.toggle}
96
+ variant= {dialog.status == "delete" ? "danger" : "primary"}
96
97
  >
97
98
  {dialog.buttonOneText}
98
99
  </Button>
@@ -8,12 +8,12 @@
8
8
  <%= pb_rails("button", props: { text: "Success Status", data: {"open-dialog": "dialog-status-success"}, margin_right: "md" }) %>
9
9
  <% end %>
10
10
 
11
- <%= pb_rails("dialog", props: {
12
- id:"dialog-status-default",
11
+ <%= pb_rails("dialog", props: {
12
+ id:"dialog-status-default",
13
13
  status: "default",
14
- size: "status_size",
15
- title: "Are you sure?",
16
- text: "Text explaining why there is an alert",
14
+ size: "status_size",
15
+ title: "Are you sure?",
16
+ text: "Text explaining why there is an alert",
17
17
  }) do %>
18
18
  <%= pb_rails("dialog/dialog_footer") do %>
19
19
  <%= pb_rails("flex", props: { spacing:"between", padding_x:"md", padding_bottom:"md", padding: "sm" }) do %>
@@ -23,12 +23,12 @@
23
23
  <% end %>
24
24
  <% end %>
25
25
 
26
- <%= pb_rails("dialog", props: {
27
- id:"dialog-status-info",
26
+ <%= pb_rails("dialog", props: {
27
+ id:"dialog-status-info",
28
28
  status: "info",
29
- size: "status_size",
30
- title: "Information",
31
- text: "Text explaining why there is an alert",
29
+ size: "status_size",
30
+ title: "Information",
31
+ text: "Text explaining why there is an alert",
32
32
  }) do %>
33
33
  <%= pb_rails("dialog/dialog_footer") do %>
34
34
  <%= pb_rails("flex", props: { spacing:"between", padding_x:"md", padding_bottom:"md", padding: "sm" }) do %>
@@ -37,12 +37,12 @@
37
37
  <% end %>
38
38
  <% end %>
39
39
 
40
- <%= pb_rails("dialog", props: {
41
- id:"dialog-status-caution",
40
+ <%= pb_rails("dialog", props: {
41
+ id:"dialog-status-caution",
42
42
  status: "caution",
43
- size: "status_size",
44
- title: "Are you Sure?",
45
- text: "This is the action you will be taking",
43
+ size: "status_size",
44
+ title: "Are you Sure?",
45
+ text: "This is the action you will be taking",
46
46
  }) do %>
47
47
  <%= pb_rails("dialog/dialog_footer") do %>
48
48
  <%= pb_rails("flex", props: { spacing:"between", padding_x:"md", padding_bottom:"md", padding: "sm" }) do %>
@@ -52,27 +52,27 @@
52
52
  <% end %>
53
53
  <% end %>
54
54
 
55
- <%= pb_rails("dialog", props: {
56
- id:"dialog-status-delete",
55
+ <%= pb_rails("dialog", props: {
56
+ id:"dialog-status-delete",
57
57
  status: "delete",
58
- size: "status_size",
59
- title: "Delete",
60
- text: "You are about to delete ...",
58
+ size: "status_size",
59
+ title: "Delete",
60
+ text: "You are about to delete ...",
61
61
  }) do %>
62
62
  <%= pb_rails("dialog/dialog_footer") do %>
63
63
  <%= pb_rails("flex", props: { spacing:"between", padding_x:"md", padding_bottom:"md", padding: "sm" }) do %>
64
- <%= pb_rails("button", props: { text: "Yes, Delete" }) %>
64
+ <%= pb_rails("button", props: { text: "Yes, Delete", variant: "danger" }) %>
65
65
  <%= pb_rails("button", props: { text: "No, Cancel", variant: "secondary", data: {"close-dialog": "dialog-status-delete" } }) %>
66
66
  <% end %>
67
67
  <% end %>
68
68
  <% end %>
69
69
 
70
- <%= pb_rails("dialog", props: {
71
- id:"dialog-status-error",
70
+ <%= pb_rails("dialog", props: {
71
+ id:"dialog-status-error",
72
72
  status: "error",
73
- size: "status_size",
74
- title: "Error Message",
75
- text: "Text explaining the error",
73
+ size: "status_size",
74
+ title: "Error Message",
75
+ text: "Text explaining the error",
76
76
  }) do %>
77
77
  <%= pb_rails("dialog/dialog_footer") do %>
78
78
  <%= pb_rails("flex", props: { spacing:"between", padding_x:"md", padding_bottom:"md", padding: "sm" }) do %>
@@ -81,12 +81,12 @@
81
81
  <% end %>
82
82
  <% end %>
83
83
 
84
- <%= pb_rails("dialog", props: {
85
- id:"dialog-status-success",
84
+ <%= pb_rails("dialog", props: {
85
+ id:"dialog-status-success",
86
86
  status: "success",
87
- size: "status_size",
88
- title: "Success!",
89
- text: "Text explaining what is successful",
87
+ size: "status_size",
88
+ title: "Success!",
89
+ text: "Text explaining what is successful",
90
90
  }) do %>
91
91
  <%= pb_rails("dialog/dialog_footer") do %>
92
92
  <%= pb_rails("flex", props: { spacing:"between", padding_x:"md", padding_bottom:"md", padding: "sm" }) do %>
@@ -83,8 +83,8 @@ const DialogStatus = () => {
83
83
 
84
84
  return (
85
85
  <div>
86
- <Flex
87
- rowGap="xs"
86
+ <Flex
87
+ rowGap="xs"
88
88
  wrap
89
89
  >
90
90
  <Button
@@ -117,7 +117,7 @@ const DialogStatus = () => {
117
117
  >
118
118
  {"Success Status"}
119
119
  </Button>
120
- <Button
120
+ <Button
121
121
  marginRight="md"
122
122
  onClick={toggleErrorAlert}
123
123
  >
@@ -152,6 +152,7 @@ const DialogStatus = () => {
152
152
  <Button
153
153
  onClick={dialog.toggle}
154
154
  paddingRight="xl"
155
+ variant={dialog.status == "delete" ? "danger" : "primary"}
155
156
  >
156
157
  {dialog.buttonOneText}
157
158
  </Button>
@@ -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, required: true } %>
93
+ <%= form.phone_number_field :example_phone_number_field_validation, props: { label: "Example phone field", hidden_inputs: 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,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 = '[class^="pb_"][class*="_kit"]'
6
- const ERROR_MESSAGE_SELECTOR = '.pb_body_kit_negative'
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 = '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"]'
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
- kitElement.classList.add('error')
51
+ parentElement.closest(KIT_SELECTOR).classList.add('error')
69
52
 
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
53
+ // set the error message element
54
+ const errorMessageContainer = this.errorMessageContainer
76
55
 
77
- // add the error message element to the dom tree
78
- parentElement.appendChild(errorMessageContainer)
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(/\./, '')
@@ -110,43 +110,38 @@ 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
- const [error, setError] = useState("")
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
-
121
- // Function to update validation state on the wrapper element
122
- // Only applies when input is required
123
- const updateValidationState = (hasError: boolean) => {
124
- if (wrapperRef.current && required) {
125
- if (hasError) {
126
- wrapperRef.current.setAttribute('data-pb-phone-validation-error', 'true')
127
- } else {
128
- wrapperRef.current.removeAttribute('data-pb-phone-validation-error')
129
- }
130
- }
131
- }
132
-
133
- // Determine which error to display
134
- // Show internal errors on blur (hasTyped) or on form submission (formSubmitted)
135
- const shouldShowInternalError = (hasTyped || formSubmitted) && required && error
136
- const displayError = props.error || (shouldShowInternalError ? error : "")
137
118
 
138
119
  useEffect(() => {
139
- const hasError = error.length > 0
140
- if (hasError) {
120
+ if ((error ?? '').length > 0) {
141
121
  onValidate(false)
142
122
  } else {
143
123
  onValidate(true)
144
124
  }
145
-
146
- // Update validation state whenever error changes
147
- updateValidationState(hasError)
148
125
  }, [error, onValidate])
149
126
 
127
+ /*
128
+ useImperativeHandle exposes the kit's input element to a parent component via a ref.
129
+ See the Playbook docs for use cases.
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
+ })
144
+
150
145
  const unformatNumber = (formattedNumber: any) => {
151
146
  return formattedNumber.replace(/\D/g, "")
152
147
  }
@@ -169,13 +164,6 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
169
164
 
170
165
  const validateTooShortNumber = (itiInit: any) => {
171
166
  if (!itiInit) return
172
-
173
- // If field is empty, don't show "too short" error
174
- if (!inputValue || inputValue.trim() === '') {
175
- setError('')
176
- return false
177
- }
178
-
179
167
  if (itiInit.getValidationError() === ValidationError.TooShort) {
180
168
  return showFormattedError('too short')
181
169
  } else {
@@ -218,33 +206,16 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
218
206
 
219
207
  const validateRepeatCountryCode = (itiInit: any) => {
220
208
  if (!itiInit) return
221
- const countryDialCode = itiRef.current.getSelectedCountryData().dialCode;
209
+ const countryDialCode = itiInit.getSelectedCountryData().dialCode;
222
210
  if (unformatNumber(inputValue).startsWith(countryDialCode)) {
223
211
  return showFormattedError('repeat country code')
224
212
  }
225
213
  }
226
214
 
227
- // Validation for required empty fields
228
- const validateRequiredField = () => {
229
- if (required && (!inputValue || inputValue.trim() === '')) {
230
- setError('Missing phone number')
231
- return true
232
- }
233
- return false
234
- }
235
215
 
236
216
  const validateErrors = () => {
237
- // If field is empty, only show required field error if applicable
238
- if (!inputValue || inputValue.trim() === '') {
239
- if (validateRequiredField()) return
240
- // Clear any existing errors if field is empty and not required
241
- if (!required) {
242
- setError('')
243
- }
244
- return
245
- }
217
+ if (!hasTyped && !error) return
246
218
 
247
- // Run validation checks
248
219
  if (itiRef.current) isValid(itiRef.current.isValidNumber())
249
220
  if (validateOnlyNumbers(itiRef.current)) return
250
221
  if (validateTooLongNumber(itiRef.current)) return
@@ -254,121 +225,6 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
254
225
  if (validateRepeatCountryCode(itiRef.current)) return
255
226
  }
256
227
 
257
- // Add listener for form validation to track when validation should be shown
258
- useEffect(() => {
259
- const handleInvalid = (event: Event) => {
260
- const target = event.target as HTMLInputElement
261
- const phoneNumberContainer = target.closest('.pb_phone_number_input')
262
-
263
- if (phoneNumberContainer && phoneNumberContainer === wrapperRef.current) {
264
- const invalidInputName = target.name || target.getAttribute('name')
265
- if (invalidInputName === name) {
266
- setFormSubmitted(true)
267
- // Trigger validation when form is submitted
268
- validateErrors()
269
- }
270
- }
271
- }
272
-
273
- document.addEventListener('invalid', handleInvalid, true)
274
-
275
- return () => {
276
- document.removeEventListener('invalid', handleInvalid, true)
277
- }
278
- }, [name, inputValue])
279
-
280
- /*
281
- useImperativeHandle exposes the kit's input element to a parent component via a ref.
282
- See the Playbook docs for use cases.
283
- Read: https://react.dev/reference/react/useImperativeHandle
284
- */
285
- useImperativeHandle(ref, () => {
286
- return {
287
- clearField() {
288
- setInputValue("")
289
- setError("")
290
- setHasTyped(false)
291
- setFormSubmitted(false)
292
- // Only clear validation state if field was required
293
- if (required) {
294
- updateValidationState(false)
295
- }
296
- },
297
- inputNode() {
298
- return inputRef.current
299
- },
300
- // Expose validation method for React Hook Form
301
- validate() {
302
- // Run validation and return error message or true
303
- const isEmpty = !inputValue || inputValue.trim() === ''
304
-
305
- if (required && isEmpty) {
306
- setError('Missing phone number')
307
- setFormSubmitted(true)
308
- return 'Missing phone number'
309
- }
310
-
311
- if (isEmpty) {
312
- setError('')
313
- return true
314
- }
315
-
316
- if (!itiRef.current) {
317
- return true
318
- }
319
-
320
- // Check for repeat country code first
321
- const countryDialCode = itiRef.current.getSelectedCountryData().dialCode;
322
- if (unformatNumber(inputValue).startsWith(countryDialCode)) {
323
- const countryName = itiRef.current.getSelectedCountryData().name
324
- const errorMessage = `Invalid ${countryName} phone number (repeat country code)`
325
- setError(errorMessage)
326
- setFormSubmitted(true)
327
- setHasTyped(true)
328
- return errorMessage
329
- }
330
-
331
- // Check if it only contains valid characters
332
- if (!containOnlyNumbers(inputValue)) {
333
- const countryName = itiRef.current.getSelectedCountryData().name
334
- const errorMessage = `Invalid ${countryName} phone number (enter numbers only)`
335
- setError(errorMessage)
336
- setFormSubmitted(true)
337
- setHasTyped(true)
338
- return errorMessage
339
- }
340
-
341
- // Check if valid number
342
- if (!itiRef.current.isValidNumber()) {
343
- const countryName = itiRef.current.getSelectedCountryData().name
344
- const validationError = itiRef.current.getValidationError()
345
- let errorMessage = ''
346
-
347
- if (validationError === ValidationError.TooShort) {
348
- errorMessage = `Invalid ${countryName} phone number (too short)`
349
- } else if (validationError === ValidationError.TooLong) {
350
- errorMessage = `Invalid ${countryName} phone number (too long)`
351
- } else if (validationError === ValidationError.MissingAreaCode) {
352
- errorMessage = `Invalid ${countryName} phone number (missing area code)`
353
- } else {
354
- errorMessage = `Invalid ${countryName} phone number`
355
- }
356
-
357
- // Set the error state so the validation attribute gets added
358
- setError(errorMessage)
359
- setFormSubmitted(true)
360
- setHasTyped(true)
361
-
362
- return errorMessage
363
- }
364
-
365
- // Clear error if valid
366
- setError('')
367
- return true
368
- }
369
- }
370
- })
371
-
372
228
  const getCurrentSelectedData = (itiInit: any, inputValue: string) => {
373
229
  return { ...itiInit.getSelectedCountryData(), number: inputValue }
374
230
  }
@@ -376,12 +232,6 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
376
232
  const handleOnChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
377
233
  if (!hasTyped) setHasTyped(true)
378
234
  setInputValue(evt.target.value)
379
-
380
- // Reset form submitted state when user types
381
- if (formSubmitted) {
382
- setFormSubmitted(false)
383
- }
384
-
385
235
  let phoneNumberData
386
236
  if (formatAsYouType) {
387
237
  const formattedPhoneNumberData = getCurrentSelectedData(itiRef.current, evt.target.value)
@@ -390,28 +240,8 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
390
240
  phoneNumberData = getCurrentSelectedData(itiRef.current, evt.target.value)
391
241
  }
392
242
  setSelectedData(phoneNumberData)
393
-
394
- // Check if this is React Hook Form by checking if onChange expects target format
395
- const isReactHookForm = onChange.toString().includes("target")
396
- if (isReactHookForm) {
397
- // For React Hook Form, pass the event with modified target value
398
- onChange({
399
- ...evt,
400
- target: {
401
- ...evt.target,
402
- name,
403
- value: phoneNumberData
404
- }
405
- } as any)
406
- } else {
407
- onChange(phoneNumberData)
408
- }
409
-
243
+ onChange(phoneNumberData)
410
244
  isValid(itiRef.current.isValidNumber())
411
-
412
- // Trigger validation after onChange for React Hook Form
413
- // This ensures validation state is up-to-date
414
- setTimeout(() => validateErrors(), 0)
415
245
  }
416
246
 
417
247
  // Separating Concerns as React Docs Recommend
@@ -470,7 +300,7 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
470
300
  dark,
471
301
  "data-phone-number": JSON.stringify(selectedData),
472
302
  disabled,
473
- error: displayError,
303
+ error,
474
304
  type: 'tel',
475
305
  id,
476
306
  label,
@@ -480,10 +310,7 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
480
310
  value: inputValue
481
311
  }
482
312
 
483
- let wrapperProps: Record<string, unknown> = {
484
- className: classes,
485
- ref: wrapperRef
486
- }
313
+ let wrapperProps: Record<string, unknown> = { className: classes }
487
314
 
488
315
  if (!isEmpty(aria)) textInputProps = {...textInputProps, ...ariaProps}
489
316
  if (!isEmpty(data)) wrapperProps = {...wrapperProps, ...dataProps}