playbook_ui 15.3.0.pre.alpha.PLAY2596datestackedcurrentyear12149 → 15.3.0.pre.alpha.PLAY2601advancedtablecustomcellmultiheaderrails12030

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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/pb_advanced_table/Components/RegularTableView.tsx +1 -2
  3. data/app/pb_kits/playbook/pb_advanced_table/Utilities/RowUtils.ts +1 -1
  4. data/app/pb_kits/playbook/pb_advanced_table/_advanced_table.scss +4 -4
  5. data/app/pb_kits/playbook/pb_advanced_table/advanced_table.test.jsx +5 -68
  6. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_background_control_rails.html.erb +0 -4
  7. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_background_control_rails.md +1 -1
  8. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_column_styling.jsx +1 -3
  9. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_column_styling.md +0 -2
  10. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_column_styling_column_headers.jsx +1 -1
  11. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_column_styling_rails.html.erb +0 -1
  12. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_column_styling_rails.md +0 -2
  13. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_padding_control.jsx +1 -9
  14. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_padding_control.md +1 -1
  15. data/app/pb_kits/playbook/pb_advanced_table/flat_advanced_table.js +2 -2
  16. data/app/pb_kits/playbook/pb_advanced_table/index.js +7 -7
  17. data/app/pb_kits/playbook/pb_advanced_table/scss_partials/advanced_table_sticky_mixin.scss +2 -2
  18. data/app/pb_kits/playbook/pb_advanced_table/table_row.rb +3 -32
  19. data/app/pb_kits/playbook/pb_background/background.html.erb +2 -10
  20. data/app/pb_kits/playbook/pb_badge/_badge.tsx +1 -4
  21. data/app/pb_kits/playbook/pb_badge/badge.test.js +0 -13
  22. data/app/pb_kits/playbook/pb_currency/_currency.tsx +7 -20
  23. data/app/pb_kits/playbook/pb_currency/currency.rb +8 -35
  24. data/app/pb_kits/playbook/pb_currency/currency.test.js +0 -47
  25. data/app/pb_kits/playbook/pb_currency/docs/_currency_variants.html.erb +1 -1
  26. data/app/pb_kits/playbook/pb_currency/docs/_currency_variants.jsx +1 -1
  27. data/app/pb_kits/playbook/pb_date_picker/date_picker_helper.ts +4 -16
  28. data/app/pb_kits/playbook/pb_date_picker/docs/example.yml +1 -2
  29. data/app/pb_kits/playbook/pb_date_picker/docs/index.js +1 -2
  30. data/app/pb_kits/playbook/pb_date_stacked/_date_stacked.tsx +4 -6
  31. data/app/pb_kits/playbook/pb_date_stacked/date_stacked.html.erb +3 -2
  32. data/app/pb_kits/playbook/pb_date_stacked/date_stacked.rb +5 -11
  33. data/app/pb_kits/playbook/pb_date_stacked/date_stacked.test.js +9 -26
  34. data/app/pb_kits/playbook/pb_date_stacked/docs/_description.md +1 -1
  35. data/app/pb_kits/playbook/pb_date_stacked/docs/example.yml +0 -2
  36. data/app/pb_kits/playbook/pb_date_stacked/docs/index.js +0 -1
  37. data/app/pb_kits/playbook/pb_dropdown/_dropdown.scss +0 -1
  38. data/app/pb_kits/playbook/pb_dropdown/_dropdown.tsx +6 -111
  39. data/app/pb_kits/playbook/pb_dropdown/docs/example.yml +0 -5
  40. data/app/pb_kits/playbook/pb_dropdown/docs/index.js +1 -5
  41. data/app/pb_kits/playbook/pb_dropdown/dropdown.test.jsx +2 -148
  42. data/app/pb_kits/playbook/pb_dropdown/index.js +1 -1
  43. data/app/pb_kits/playbook/pb_fixed_confirmation_toast/docs/_fixed_confirmation_toast_auto_close.html.erb +1 -15
  44. data/app/pb_kits/playbook/pb_fixed_confirmation_toast/docs/_fixed_confirmation_toast_multi_line.html.erb +8 -9
  45. data/app/pb_kits/playbook/pb_fixed_confirmation_toast/docs/_fixed_confirmation_toast_positions.html.erb +10 -11
  46. data/app/pb_kits/playbook/pb_form/pb_form_validation.js +11 -44
  47. data/app/pb_kits/playbook/pb_form_pill/_form_pill.tsx +1 -1
  48. data/app/pb_kits/playbook/pb_phone_number_input/_phone_number_input.tsx +17 -110
  49. data/app/pb_kits/playbook/pb_typeahead/_typeahead.scss +0 -7
  50. data/app/pb_kits/playbook/pb_typeahead/_typeahead.test.jsx +1 -64
  51. data/app/pb_kits/playbook/pb_typeahead/components/MultiValue.tsx +1 -33
  52. data/dist/chunks/{_line_graph-C-AuMGN2.js → _line_graph-CqE0-dq5.js} +1 -1
  53. data/dist/chunks/_typeahead-3ZAbZUqU.js +6 -0
  54. data/dist/chunks/_weekday_stacked-BFB3mjtE.js +37 -0
  55. data/dist/chunks/{lib-BXBHAZMY.js → lib-CGxXTQ75.js} +1 -1
  56. data/dist/chunks/pb_form_validation-DebqlUKZ.js +1 -0
  57. data/dist/chunks/vendor.js +1 -1
  58. data/dist/playbook-doc.js +1 -1
  59. data/dist/playbook-rails-react-bindings.js +1 -1
  60. data/dist/playbook-rails.js +1 -1
  61. data/dist/playbook.css +1 -1
  62. data/lib/playbook/version.rb +1 -1
  63. metadata +7 -22
  64. data/app/pb_kits/playbook/pb_currency/docs/_currency_variants.md +0 -1
  65. data/app/pb_kits/playbook/pb_date_picker/docs/_date_picker_and_dropdown_range.jsx +0 -38
  66. data/app/pb_kits/playbook/pb_date_picker/docs/_date_picker_and_dropdown_range.md +0 -14
  67. data/app/pb_kits/playbook/pb_date_stacked/docs/_date_stacked_current_year.html.erb +0 -12
  68. data/app/pb_kits/playbook/pb_date_stacked/docs/_date_stacked_current_year.jsx +0 -27
  69. data/app/pb_kits/playbook/pb_date_stacked/docs/_date_stacked_current_year.md +0 -1
  70. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick.jsx +0 -18
  71. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick.md +0 -4
  72. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_default_dates.jsx +0 -18
  73. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_default_dates.md +0 -1
  74. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_range_end.jsx +0 -19
  75. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_range_end.md +0 -1
  76. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_with_date_pickers.jsx +0 -38
  77. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_with_date_pickers.md +0 -14
  78. data/app/pb_kits/playbook/pb_dropdown/quickpick/index.ts +0 -60
  79. data/dist/chunks/_typeahead--38pnHwS.js +0 -6
  80. data/dist/chunks/_weekday_stacked-onVWU89T.js +0 -37
  81. data/dist/chunks/pb_form_validation-BNfSnIUF.js +0 -1
@@ -1,4 +1,4 @@
1
- import React, { useState } from "react"
1
+ import React from "react"
2
2
  import { render, screen, fireEvent } from "../utilities/test-utils"
3
3
 
4
4
  import { Dropdown, Icon, IconCircle } from 'playbook-ui'
@@ -393,150 +393,4 @@ test("applies activeStyle backgroundColor and fontColor when selected", () => {
393
393
  expect(selected).toBeInTheDocument()
394
394
  expect(selected).toHaveClass("bg-bg_light")
395
395
  expect(selected).toHaveClass("font-primary")
396
- })
397
-
398
- test("renders quickpick variant with auto-generated options", () => {
399
- render(
400
- <Dropdown
401
- data={{ testid: testId }}
402
- variant="quickpick"
403
- />
404
- )
405
-
406
- const kit = screen.getByTestId(testId)
407
- expect(kit).toHaveClass('pb_dropdown_quickpick')
408
-
409
- // Check that quickpick options are generated
410
- const options = kit.querySelectorAll('.pb_dropdown_option_list')
411
- expect(options.length).toBe(10)
412
- expect(options[0]).toHaveTextContent("Today")
413
- })
414
-
415
- test("quickpick variant accepts string defaultValue", () => {
416
- render(
417
- <Dropdown
418
- data={{ testid: testId }}
419
- defaultValue="This Month"
420
- variant="quickpick"
421
- />
422
- )
423
-
424
- const kit = screen.getByTestId(testId)
425
- const trigger = kit.querySelector('.pb_dropdown_trigger')
426
-
427
- expect(trigger).toHaveTextContent("This Month")
428
- })
429
-
430
- test("quickpick attaches _dropdownRef to DOM element when id is provided", () => {
431
- render(
432
- <Dropdown
433
- data={{ testid: testId }}
434
- id="test-quickpick"
435
- variant="quickpick"
436
- />
437
- )
438
-
439
- const kit = screen.getByTestId(testId)
440
-
441
- // Check that the element has the _dropdownRef attached
442
- expect(kit._dropdownRef).toBeDefined()
443
- expect(kit._dropdownRef.current).toBeDefined()
444
- expect(kit._dropdownRef.current.clearSelected).toBeDefined()
445
- })
446
-
447
- test("quickpick clears selection when clicking X icon", () => {
448
- render(
449
- <Dropdown
450
- data={{ testid: testId }}
451
- defaultValue="This Week"
452
- variant="quickpick"
453
- />
454
- )
455
-
456
- const kit = screen.getByTestId(testId)
457
- const trigger = kit.querySelector('.pb_dropdown_trigger')
458
-
459
- expect(trigger).toHaveTextContent("This Week")
460
-
461
- const clearIcon = kit.querySelector('[aria-label="times icon"]')
462
- expect(clearIcon).toBeInTheDocument()
463
-
464
- fireEvent.click(clearIcon.parentElement)
465
-
466
- expect(trigger).toHaveTextContent("Select...")
467
- })
468
-
469
- test("quickpick returns date array values when option selected", () => {
470
- const TestComponent = () => {
471
- const [selected, setSelected] = useState(null)
472
- return (
473
- <>
474
- <Dropdown
475
- data={{ testid: testId }}
476
- onSelect={(item) => setSelected(item)}
477
- variant="quickpick"
478
- />
479
- {selected && (
480
- <div data-testid="selected-value">
481
- {JSON.stringify({
482
- label: selected.label,
483
- hasValue: !!selected.value,
484
- isArray: Array.isArray(selected.value),
485
- valueLength: selected.value?.length
486
- })}
487
- </div>
488
- )}
489
- </>
490
- )
491
- }
492
-
493
- render(<TestComponent />)
494
-
495
- const kit = screen.getByTestId(testId)
496
- const options = kit.querySelectorAll('.pb_dropdown_option_list')
497
-
498
- fireEvent.click(options[0])
499
-
500
- const selectedValue = screen.getByTestId('selected-value')
501
- const data = JSON.parse(selectedValue.textContent)
502
-
503
- expect(data.label).toBe("Today")
504
- expect(data.hasValue).toBe(true)
505
- expect(data.isArray).toBe(true)
506
- expect(data.valueLength).toBe(2)
507
- })
508
-
509
- test("quickpick option values are Date objects", () => {
510
- const onSelectMock = jest.fn()
511
-
512
- render(
513
- <Dropdown
514
- data={{ testid: testId }}
515
- onSelect={onSelectMock}
516
- variant="quickpick"
517
- />
518
- )
519
-
520
- const kit = screen.getByTestId(testId)
521
- const options = kit.querySelectorAll('.pb_dropdown_option_list')
522
-
523
- const thisMonthOption = Array.from(options).find(opt => opt.textContent === 'This Month')
524
- fireEvent.click(thisMonthOption)
525
-
526
- const selectedItem = onSelectMock.mock.calls[0][0]
527
-
528
- expect(selectedItem.label).toBe("This Month")
529
- expect(selectedItem.value).toBeDefined()
530
- expect(Array.isArray(selectedItem.value)).toBe(true)
531
- expect(selectedItem.value.length).toBe(2)
532
-
533
- const [startDate, endDate] = selectedItem.value
534
-
535
- expect(startDate instanceof Date).toBe(true)
536
- expect(endDate instanceof Date).toBe(true)
537
-
538
- expect(startDate.getTime()).not.toBeNaN()
539
- expect(endDate.getTime()).not.toBeNaN()
540
-
541
- expect(endDate.getTime()).toBeGreaterThanOrEqual(startDate.getTime())
542
- })
396
+ })
@@ -126,7 +126,7 @@ export default class PbDropdown extends PbEnhancedElement {
126
126
  .label.toString()
127
127
  .toLowerCase();
128
128
 
129
- // hide or show option
129
+ // hide or show option
130
130
  const match = label.includes(lcTerm);
131
131
  opt.style.display = match ? "" : "none";
132
132
  if (match) hasMatch = true
@@ -22,27 +22,13 @@
22
22
  }) %>
23
23
 
24
24
  <script>
25
- // Hide toasts immediately
26
- const hideAutoToasts = () => {
27
- const toastAuto = document.getElementById('toast-auto-close');
28
- const toastAutoCloseable = document.getElementById('toast-auto-close-closeable');
29
- if (toastAuto) toastAuto.style.display = 'none';
30
- if (toastAutoCloseable) toastAutoCloseable.style.display = 'none';
31
- }
32
- hideAutoToasts();
33
-
34
- // Handle various page load/restore events
35
- window.addEventListener('pageshow', hideAutoToasts)
36
- document.addEventListener('turbolinks:load', hideAutoToasts)
37
- document.addEventListener('turbo:load', hideAutoToasts)
38
-
39
25
  document.addEventListener('DOMContentLoaded', () => {
40
26
  // Initialize toast elements and buttons
41
27
  const toasts = {
42
28
  '#toast-auto-close': document.querySelector("#toast-auto-close"),
43
29
  '#toast-auto-close-closeable': document.querySelector("#toast-auto-close-closeable")
44
30
  }
45
-
31
+
46
32
  const buttons = {
47
33
  '#toast-auto-close': document.querySelector("button[data-toast='#toast-auto-close']"),
48
34
  '#toast-auto-close-closeable': document.querySelector("button[data-toast='#toast-auto-close-closeable']")
@@ -24,6 +24,7 @@
24
24
  horizontal: "center"
25
25
  }) %>
26
26
 
27
+
27
28
  <script type="text/javascript">
28
29
  const multitoasts = document.querySelectorAll(".multitoast-to-hide")
29
30
  const multibuttons = document.querySelectorAll("button[data-multitoast]")
@@ -34,15 +35,6 @@
34
35
  })
35
36
  }
36
37
 
37
- // Hide toasts immediately
38
- hideMultiToasts()
39
-
40
- // Handle various page load/restore events
41
- window.addEventListener('pageshow', hideMultiToasts)
42
- document.addEventListener('DOMContentLoaded', hideMultiToasts)
43
- document.addEventListener('turbolinks:load', hideMultiToasts)
44
- document.addEventListener('turbo:load', hideMultiToasts)
45
-
46
38
  multibuttons.forEach((button) => {
47
39
  button.onclick = () => {
48
40
  hideMultiToasts()
@@ -54,3 +46,10 @@
54
46
  }
55
47
  })
56
48
  </script>
49
+
50
+ <!-- hiding toast on page load -->
51
+ <style>
52
+ #toast-long, #toast-short {
53
+ display: none;
54
+ }
55
+ </style>
@@ -69,28 +69,27 @@
69
69
  const toasts = document.querySelectorAll(".toast-to-hide")
70
70
  const buttons = document.querySelectorAll("button[data-toast]")
71
71
 
72
- const hidePositionToasts = () => {
72
+ const hideToasts = () => {
73
73
  toasts.forEach((toast) => {
74
74
  toast.style.display = "none"
75
75
  })
76
76
  }
77
77
 
78
- // Hide toasts immediately
79
- hidePositionToasts()
80
-
81
- // Handle various page load/restore events
82
- window.addEventListener('pageshow', hidePositionToasts)
83
- document.addEventListener('DOMContentLoaded', hidePositionToasts)
84
- document.addEventListener('turbolinks:load', hidePositionToasts)
85
- document.addEventListener('turbo:load', hidePositionToasts)
86
-
87
78
  buttons.forEach((button) => {
88
79
  button.onclick = () => {
89
- hidePositionToasts()
80
+ hideToasts()
90
81
  let toast = document.querySelector(button.getAttribute("data-toast"))
82
+
91
83
  if (toast) {
92
84
  toast.style.display = "flex"
93
85
  }
94
86
  }
95
87
  })
96
88
  </script>
89
+
90
+ <!-- hiding toast on page load -->
91
+ <style>
92
+ #toast-top-center, #toast-top-right, #toast-top-left, #toast-bottom-center, #toast-bottom-right, #toast-bottom-left {
93
+ display: none;
94
+ }
95
+ </style>
@@ -8,13 +8,13 @@ const ERROR_MESSAGE_SELECTOR = '.pb_body_kit_negative'
8
8
  // Validation selectors
9
9
  const FORM_SELECTOR = 'form[data-pb-form-validation="true"]'
10
10
  const REQUIRED_FIELDS_SELECTOR = 'input[required],textarea[required],select[required]'
11
- const PHONE_NUMBER_VALIDATION_ERROR_SELECTOR = '[data-pb-phone-validation-error="true"]'
12
11
 
13
12
  const FIELD_EVENTS = [
14
13
  'change',
15
14
  'valid',
16
15
  'invalid',
17
16
  ]
17
+
18
18
  class PbFormValidation extends PbEnhancedElement {
19
19
  static get selector() {
20
20
  return FORM_SELECTOR
@@ -22,27 +22,12 @@ class PbFormValidation extends PbEnhancedElement {
22
22
 
23
23
  connect() {
24
24
  this.formValidationFields.forEach((field) => {
25
- // Skip phone number inputs - they handle their own validation
26
- const isPhoneNumberInput = field.closest('.pb_phone_number_input')
27
- if (isPhoneNumberInput) return
28
-
29
25
  FIELD_EVENTS.forEach((e) => {
30
26
  field.addEventListener(e, debounce((event) => {
31
27
  this.validateFormField(event)
32
28
  }, 250), false)
33
29
  })
34
30
  })
35
-
36
- // Add event listener to check for phone number validation errors
37
- this.element.addEventListener('submit', (event) => {
38
- // Use setTimeout to ensure React state updates have completed
39
- setTimeout(() => {
40
- if (this.hasPhoneNumberValidationErrors()) {
41
- event.preventDefault()
42
- return false
43
- }
44
- }, 0)
45
- })
46
31
  }
47
32
 
48
33
  validateFormField(event) {
@@ -60,58 +45,40 @@ class PbFormValidation extends PbEnhancedElement {
60
45
 
61
46
  showValidationMessage(target) {
62
47
  const { parentElement } = target
63
- const kitElement = parentElement.closest(KIT_SELECTOR)
64
-
65
- // FIX: Add null check for kitElement
66
- if (!kitElement) return
67
-
68
- // Check if this is a phone number input
69
- const isPhoneNumberInput = kitElement.classList.contains('pb_phone_number_input')
70
48
 
71
49
  // ensure clean error message state
72
50
  this.clearError(target)
73
- kitElement.classList.add('error')
51
+ parentElement.closest(KIT_SELECTOR).classList.add('error')
74
52
 
75
- // Only add error message if it's NOT a phone number input
76
- if (!isPhoneNumberInput) {
77
- // set the error message element
78
- const errorMessageContainer = this.errorMessageContainer
53
+ // set the error message element
54
+ const errorMessageContainer = this.errorMessageContainer
79
55
 
80
- if (target.dataset.message) target.setCustomValidity(target.dataset.message)
56
+ if (target.dataset.message) target.setCustomValidity(target.dataset.message)
81
57
 
82
- errorMessageContainer.innerHTML = target.validationMessage
58
+ errorMessageContainer.innerHTML = target.validationMessage
83
59
 
84
- // add the error message element to the dom tree
85
- parentElement.appendChild(errorMessageContainer)
86
- }
60
+ // add the error message element to the dom tree
61
+ parentElement.appendChild(errorMessageContainer)
87
62
  }
88
63
 
89
64
  clearError(target) {
90
65
  const { parentElement } = target
91
- const kitElement = parentElement.closest(KIT_SELECTOR)
92
- // Remove error class from kit element
93
- if (kitElement) kitElement.classList.remove('error')
94
- // Remove error message from parent element
66
+ parentElement.closest(KIT_SELECTOR).classList.remove('error')
95
67
  const errorMessageContainer = parentElement.querySelector(ERROR_MESSAGE_SELECTOR)
96
68
  if (errorMessageContainer) errorMessageContainer.remove()
97
69
  }
98
70
 
99
- // Check if there are phone number input errors
100
- hasPhoneNumberValidationErrors() {
101
- const phoneNumberErrors = this.element.querySelectorAll(PHONE_NUMBER_VALIDATION_ERROR_SELECTOR)
102
- return phoneNumberErrors.length > 0
103
- }
104
-
105
71
  get errorMessageContainer() {
106
72
  const errorContainer = document.createElement('div')
107
73
  const kitClassName = ERROR_MESSAGE_SELECTOR.replace(/\./, '')
108
74
  errorContainer.classList.add(kitClassName)
109
75
  return errorContainer
110
76
  }
77
+
111
78
  get formValidationFields() {
112
79
  return this._formValidationFields =
113
80
  this._formValidationFields || this.element.querySelectorAll(REQUIRED_FIELDS_SELECTOR)
114
81
  }
115
82
  }
116
83
 
117
- window.PbFormValidation = PbFormValidation
84
+ window.PbFormValidation = PbFormValidation
@@ -9,7 +9,7 @@ import { buildDataProps, buildHtmlProps } from '../utilities/props'
9
9
 
10
10
  type FormPillProps = {
11
11
  className?: string,
12
- htmlOptions?: {[key: string]: string | number | boolean | (() => void) | ((event: any) => void) | any},
12
+ htmlOptions?: {[key: string]: string | number | boolean | (() => void)},
13
13
  id?: string,
14
14
  text: string,
15
15
  name?: string,
@@ -55,6 +55,7 @@ const formatToGlobalCountryName = (countryName: string) => {
55
55
 
56
56
  const formatAllCountries = () => {
57
57
  const countryData = intlTelInput.getCountryData()
58
+
58
59
  for (let i = 0; i < countryData.length; i++) {
59
60
  const country = countryData[i]
60
61
  country.name = formatToGlobalCountryName(country.name)
@@ -109,54 +110,18 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
109
110
 
110
111
  const inputRef = useRef<HTMLInputElement | null>(null)
111
112
  const itiRef = useRef<any>(null);
112
- const wrapperRef = useRef<HTMLDivElement | null>(null);
113
113
  const [inputValue, setInputValue] = useState(value)
114
114
  const [error, setError] = useState(props.error || "")
115
115
  const [dropDownIsOpen, setDropDownIsOpen] = useState(false)
116
116
  const [selectedData, setSelectedData] = useState()
117
117
  const [hasTyped, setHasTyped] = useState(false)
118
- const [formSubmitted, setFormSubmitted] = useState(false)
119
- const [hasStartedValidating, setHasStartedValidating] = useState(false)
120
-
121
- // Only sync initial error from props, not continuous updates
122
- // Once validation starts, internal validation takes over
123
- useEffect(() => {
124
- if (props.error && !hasStartedValidating) {
125
- setError(props.error)
126
- // If there's an initial error from props, mark as submitted so it shows
127
- if (props.error) {
128
- setFormSubmitted(true)
129
- }
130
- }
131
- }, [props.error, hasStartedValidating])
132
-
133
- // Function to update validation state on the wrapper element
134
- // Only applies when input is required
135
- const updateValidationState = (hasError: boolean) => {
136
- if (wrapperRef.current && required) {
137
- if (hasError) {
138
- wrapperRef.current.setAttribute('data-pb-phone-validation-error', 'true')
139
- } else {
140
- wrapperRef.current.removeAttribute('data-pb-phone-validation-error')
141
- }
142
- }
143
- }
144
-
145
- // Determine which error to display
146
- // Show internal errors on blur (hasTyped) or on form submission (formSubmitted)
147
- const shouldShowInternalError = (hasTyped || formSubmitted) && required && error
148
- const displayError = shouldShowInternalError ? error : ""
149
118
 
150
119
  useEffect(() => {
151
- const hasError = (error ?? '').length > 0
152
- if (hasError) {
120
+ if ((error ?? '').length > 0) {
153
121
  onValidate(false)
154
122
  } else {
155
123
  onValidate(true)
156
124
  }
157
-
158
- // Update validation state whenever error changes
159
- updateValidationState(hasError)
160
125
  }, [error, onValidate])
161
126
 
162
127
  const unformatNumber = (formattedNumber: any) => {
@@ -172,7 +137,6 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
172
137
 
173
138
  const validateTooLongNumber = (itiInit: any) => {
174
139
  if (!itiInit) return
175
-
176
140
  if (itiInit.getValidationError() === ValidationError.TooLong) {
177
141
  return showFormattedError('too long')
178
142
  } else {
@@ -182,11 +146,13 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
182
146
 
183
147
  const validateTooShortNumber = (itiInit: any) => {
184
148
  if (!itiInit) return
149
+
185
150
  // If field is empty, don't show "too short" error
186
151
  if (!inputValue || inputValue.trim() === '') {
187
152
  setError('')
188
153
  return false
189
154
  }
155
+
190
156
  if (itiInit.getValidationError() === ValidationError.TooShort) {
191
157
  return showFormattedError('too short')
192
158
  } else {
@@ -206,7 +172,7 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
206
172
  }
207
173
 
208
174
  const validateUnhandledError = (itiInit: any) => {
209
- if (!required || !itiInit) return
175
+ if (!itiInit) return
210
176
  if (itiInit.getValidationError() === ValidationError.SomethingWentWrong) {
211
177
  if (inputValue.length === 1) {
212
178
  return showFormattedError('too short')
@@ -218,6 +184,7 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
218
184
  }
219
185
  }
220
186
  }
187
+
221
188
  const validateMissingAreaCode = (itiInit: any) => {
222
189
  if (!itiInit) return
223
190
  if (itiInit.getValidationError() === ValidationError.MissingAreaCode) {
@@ -234,9 +201,8 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
234
201
  }
235
202
  }
236
203
 
237
- // Validation for required empty fields
238
204
  const validateRequiredField = () => {
239
- if (required && (!inputValue || inputValue.trim() === '')) {
205
+ if (!inputValue || inputValue.trim() === '') {
240
206
  setError('Missing phone number')
241
207
  return true
242
208
  }
@@ -244,24 +210,14 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
244
210
  }
245
211
 
246
212
  const validateErrors = () => {
247
- // Signal validation has started, so prop errors won't override internal validation
248
- if (!hasStartedValidating) {
249
- setHasStartedValidating(true)
250
- }
251
-
252
- // If field is empty, only show required field error if applicable
213
+ // If field is empty, show error message
253
214
  if (!inputValue || inputValue.trim() === '') {
254
215
  if (validateRequiredField()) return
255
- // Clear any existing errors if field is empty and not required
256
- if (!required) {
257
- setError('')
258
- }
259
216
  return
260
217
  }
261
218
 
262
- if (!hasTyped && !error) return
219
+ if (!hasTyped && !error) return
263
220
 
264
- // Run validation checks
265
221
  if (itiRef.current) isValid(itiRef.current.isValidNumber())
266
222
  if (validateOnlyNumbers(itiRef.current)) return
267
223
  if (validateTooLongNumber(itiRef.current)) return
@@ -271,29 +227,6 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
271
227
  if (validateRepeatCountryCode(itiRef.current)) return
272
228
  }
273
229
 
274
- // Add listener for form validation to track when validation should be shown
275
- useEffect(() => {
276
- const handleInvalid = (event: Event) => {
277
- const target = event.target as HTMLInputElement
278
- const phoneNumberContainer = target.closest('.pb_phone_number_input')
279
-
280
- if (phoneNumberContainer && phoneNumberContainer === wrapperRef.current) {
281
- const invalidInputName = target.name || target.getAttribute('name')
282
- if (invalidInputName === name) {
283
- setFormSubmitted(true)
284
- // Trigger validation when form is submitted
285
- validateErrors()
286
- }
287
- }
288
- }
289
-
290
- document.addEventListener('invalid', handleInvalid, true)
291
-
292
- return () => {
293
- document.removeEventListener('invalid', handleInvalid, true)
294
- }
295
- }, [name, inputValue])
296
-
297
230
  /*
298
231
  useImperativeHandle exposes the kit's input element to a parent component via a ref.
299
232
  See the Playbook docs for use cases.
@@ -305,12 +238,6 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
305
238
  setInputValue("")
306
239
  setError("")
307
240
  setHasTyped(false)
308
- setFormSubmitted(false)
309
- setHasStartedValidating(false)
310
- // Only clear validation state if field was required
311
- if (required) {
312
- updateValidationState(false)
313
- }
314
241
  },
315
242
  inputNode() {
316
243
  return inputRef.current
@@ -320,12 +247,6 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
320
247
  // Run validation and return error message or true
321
248
  const isEmpty = !inputValue || inputValue.trim() === ''
322
249
 
323
- if (required && isEmpty) {
324
- setError('Missing phone number')
325
- setFormSubmitted(true)
326
- return 'Missing phone number'
327
- }
328
-
329
250
  if (isEmpty) {
330
251
  // Show missing phone number error
331
252
  const errorMessage = 'Missing phone number'
@@ -345,7 +266,6 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
345
266
  const countryName = itiRef.current.getSelectedCountryData().name
346
267
  const errorMessage = `Invalid ${countryName} phone number (repeat country code)`
347
268
  setError(errorMessage)
348
- setFormSubmitted(true)
349
269
  setHasTyped(true)
350
270
  return errorMessage
351
271
  }
@@ -355,7 +275,6 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
355
275
  const countryName = itiRef.current.getSelectedCountryData().name
356
276
  const errorMessage = `Invalid ${countryName} phone number (enter numbers only)`
357
277
  setError(errorMessage)
358
- setFormSubmitted(true)
359
278
  setHasTyped(true)
360
279
  return errorMessage
361
280
  }
@@ -376,9 +295,7 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
376
295
  errorMessage = `Invalid ${countryName} phone number`
377
296
  }
378
297
 
379
- // Set the error state so the validation attribute gets added
380
298
  setError(errorMessage)
381
- setFormSubmitted(true)
382
299
  setHasTyped(true)
383
300
 
384
301
  return errorMessage
@@ -397,16 +314,11 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
397
314
 
398
315
  const handleOnChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
399
316
  if (!hasTyped) setHasTyped(true)
400
- setInputValue(evt.target.value)
401
317
 
402
- // Reset form submitted state when user types
403
- if (formSubmitted) {
404
- setFormSubmitted(false)
405
- }
318
+ setInputValue(evt.target.value)
406
319
 
407
320
  let phoneNumberData
408
321
 
409
- // Handle formatAsYouType with input event
410
322
  if (formatAsYouType) {
411
323
  const formattedPhoneNumberData = getCurrentSelectedData(itiRef.current, evt.target.value)
412
324
  phoneNumberData = {...formattedPhoneNumberData, number: unformatNumber(formattedPhoneNumberData.number)}
@@ -417,15 +329,12 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
417
329
  setSelectedData(phoneNumberData)
418
330
  onChange(phoneNumberData)
419
331
  isValid(itiRef.current.isValidNumber())
420
-
421
- // Trigger validation after onChange for React Hook Form
422
- // This ensures validation state is up-to-date
423
- setTimeout(() => validateErrors(), 0)
424
332
  }
425
333
 
426
334
  // Separating Concerns as React Docs Recommend
427
335
  // This also Fixes things for our react_component rendering on the Rails Side
428
336
  useEffect(formatAllCountries, [])
337
+
429
338
  // If an initial country is not specified, the "globe" icon will show
430
339
  // Always set a country
431
340
  const fallbackCountry =
@@ -466,9 +375,9 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
466
375
  inputRef.current.addEventListener("open:countrydropdown", () => setDropDownIsOpen(true))
467
376
  inputRef.current.addEventListener("close:countrydropdown", () => setDropDownIsOpen(false))
468
377
 
469
- // Handle formatAsYouType with input event
470
- if (formatAsYouType) {
471
- inputRef.current.addEventListener("input", (evt: Event) => {
378
+ // Handle formatAsYouType with input event
379
+ if (formatAsYouType) {
380
+ inputRef.current.addEventListener("input", (evt: Event) => {
472
381
  const target = evt.target as HTMLInputElement
473
382
  const formattedValue = target.value
474
383
 
@@ -487,12 +396,13 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
487
396
  }
488
397
  }
489
398
  }, [])
399
+
490
400
  let textInputProps: {[key: string]: any} = {
491
401
  className: dropDownIsOpen ? 'dropdown_open' : '',
492
402
  dark,
493
403
  "data-phone-number": JSON.stringify(selectedData),
494
404
  disabled,
495
- error: hasTyped ? error : props.error || displayError,
405
+ error: hasTyped ? error : props.error,
496
406
  type: 'tel',
497
407
  id,
498
408
  label,
@@ -502,10 +412,7 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
502
412
  value: inputValue
503
413
  }
504
414
 
505
- let wrapperProps: Record<string, unknown> = {
506
- className: classes,
507
- ref: wrapperRef
508
- }
415
+ let wrapperProps: Record<string, unknown> = { className: classes }
509
416
 
510
417
  if (!isEmpty(aria)) textInputProps = {...textInputProps, ...ariaProps}
511
418
  if (!isEmpty(data)) wrapperProps = {...wrapperProps, ...dataProps}