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.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/pb_button/docs/_button_managed_disabled.html.erb +31 -0
  3. data/app/pb_kits/playbook/pb_button/docs/_button_managed_disabled.md +7 -0
  4. data/app/pb_kits/playbook/pb_button/docs/_button_managed_disabled_helper.html.erb +21 -0
  5. data/app/pb_kits/playbook/pb_button/docs/_button_managed_disabled_helper.md +7 -0
  6. data/app/pb_kits/playbook/pb_button/docs/example.yml +2 -0
  7. data/app/pb_kits/playbook/pb_button/index.js +99 -0
  8. data/app/pb_kits/playbook/pb_date_picker/_date_picker.scss +0 -4
  9. data/app/pb_kits/playbook/pb_form/docs/_form_form_with_validate.html.erb +1 -1
  10. data/app/pb_kits/playbook/pb_form/pb_form_validation.js +37 -13
  11. data/app/pb_kits/playbook/pb_phone_number_input/_phone_number_input.tsx +109 -24
  12. data/app/pb_kits/playbook/pb_text_input/_text_input.tsx +6 -14
  13. data/app/pb_kits/playbook/pb_text_input/docs/_text_input_default.html.erb +4 -8
  14. data/app/pb_kits/playbook/pb_text_input/docs/_text_input_default.jsx +0 -5
  15. data/app/pb_kits/playbook/pb_text_input/text_input.html.erb +1 -3
  16. data/app/pb_kits/playbook/pb_text_input/text_input.rb +0 -6
  17. data/dist/chunks/{_line_graph-BnK1i7QI.js → _line_graph-BLndveyW.js} +1 -1
  18. data/dist/chunks/{_typeahead-pbS3fEzb.js → _typeahead-C8u0HdSd.js} +1 -1
  19. data/dist/chunks/{_weekday_stacked-x-syST1P.js → _weekday_stacked-Ckwpc_D2.js} +1 -1
  20. data/dist/chunks/pb_form_validation-D1VURgVg.js +1 -0
  21. data/dist/chunks/vendor.js +1 -1
  22. data/dist/menu.yml +1 -1
  23. data/dist/playbook-doc.js +2 -2
  24. data/dist/playbook-rails-react-bindings.js +1 -1
  25. data/dist/playbook-rails.js +1 -1
  26. data/dist/playbook.css +1 -1
  27. data/lib/playbook/version.rb +1 -1
  28. metadata +11 -7
  29. data/app/pb_kits/playbook/pb_text_input/docs/_text_input_default.md +0 -1
  30. 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: 035454cb65ebb23e13c71c6e36d3873a2a33a02844f64b859986cfa996791ac1
4
- data.tar.gz: 18f3d951507cd95d6b141fabaa2ab49d7307a6b373b6a32ba8b50295b4888648
3
+ metadata.gz: 72cfd26d4607165f040c9d27f0ce24d777167b2f2f584228fdfeaf336b423687
4
+ data.tar.gz: 699ecdaa5c3077dc49bb8c09a2ce16c21498ddce86d838a11392536b2bec4399
5
5
  SHA512:
6
- metadata.gz: 2213e36a01dd1a174e83a9dd8ac269c1a5de115885bf786f8896c36cedc9e0d66d7334037db9ec01c778d1957fd8e19c1fe44eeeddba7c8bacddfa4f7975f4cf
7
- data.tar.gz: afb376f3f3b31f525ec7edf64fdd413769f8a819ec2fee999a389f0041a2a2f8aca832a04eea1fdf44a77cf2ec43a7bc118f1eafc35eb3df434b4f9a31735ef7
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
+
@@ -19,10 +19,6 @@
19
19
  @import "./sass_partials/calendar_input_icon";
20
20
  }
21
21
 
22
- label {
23
- display: block !important;
24
- }
25
-
26
22
  &:focus,
27
23
  :focus-within {
28
24
  div.cal_icon_wrapper,
@@ -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 = '[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]'
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
- parentElement.closest(KIT_SELECTOR).classList.add('error')
52
-
53
- // set the error message element
54
- const errorMessageContainer = this.errorMessageContainer
68
+ kitElement.classList.add('error')
55
69
 
56
- if (target.dataset.message) target.setCustomValidity(target.dataset.message)
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
- errorMessageContainer.innerHTML = target.validationMessage
59
-
60
- // add the error message element to the dom tree
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(props.error)
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
- if ((error ?? '').length > 0) {
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
- 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
- })
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 = itiInit.getSelectedCountryData().dialCode;
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
- if (!hasTyped && !error) return
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> = { className: classes }
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
- <label htmlFor={id}>
211
- <Caption className="pb_text_input_kit_label"
212
- text={label}
213
- />
214
- </label>
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, id: object.error_id, aria: { atomic: "true", live: "polite" }, html_options: { role: "alert" }}) if 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,