playbook_ui 15.5.0.pre.alpha.PLAY2581aggressivevalidation12654 → 15.5.0.pre.alpha.draggableask12772

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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_compound_components.html.erb +31 -0
  3. data/app/pb_kits/playbook/pb_dialog/index.js +15 -10
  4. data/app/pb_kits/playbook/pb_draggable/context/index.tsx +28 -0
  5. data/app/pb_kits/playbook/pb_file_upload/_file_upload.scss +4 -4
  6. data/app/pb_kits/playbook/pb_home_address_street/_home_address_street.tsx +34 -22
  7. data/app/pb_kits/playbook/pb_home_address_street/city_emphasis.html.erb +16 -12
  8. data/app/pb_kits/playbook/pb_home_address_street/docs/_home_address_street_default.html.erb +1 -1
  9. data/app/pb_kits/playbook/pb_home_address_street/none_emphasis.html.erb +16 -12
  10. data/app/pb_kits/playbook/pb_home_address_street/street_emphasis.html.erb +16 -12
  11. data/app/pb_kits/playbook/pb_multiple_users/_multiple_users.scss +10 -0
  12. data/app/pb_kits/playbook/pb_multiple_users/_multiple_users.tsx +66 -15
  13. data/app/pb_kits/playbook/pb_multiple_users/docs/_multiple_users_with_tooltip.jsx +42 -0
  14. data/app/pb_kits/playbook/pb_multiple_users/docs/_multiple_users_with_tooltip.md +1 -0
  15. data/app/pb_kits/playbook/pb_multiple_users/docs/example.yml +1 -0
  16. data/app/pb_kits/playbook/pb_multiple_users/docs/index.js +1 -0
  17. data/app/pb_kits/playbook/pb_multiple_users/multiple_users.test.js +25 -0
  18. data/app/pb_kits/playbook/pb_phone_number_input/_phone_number_input.tsx +10 -43
  19. data/app/pb_kits/playbook/pb_typeahead/_typeahead.test.jsx +15 -0
  20. data/app/pb_kits/playbook/pb_typeahead/_typeahead.tsx +3 -0
  21. data/app/pb_kits/playbook/pb_typeahead/components/ClearIndicator.tsx +13 -2
  22. data/app/pb_kits/playbook/pb_typeahead/components/MultiValue.tsx +6 -1
  23. data/app/pb_kits/playbook/pb_typeahead/components/ValueContainer.tsx +34 -7
  24. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_input_display.html.erb +30 -0
  25. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_input_display.jsx +37 -0
  26. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_input_display.md +3 -0
  27. data/app/pb_kits/playbook/pb_typeahead/docs/example.yml +2 -0
  28. data/app/pb_kits/playbook/pb_typeahead/docs/index.js +2 -1
  29. data/app/pb_kits/playbook/pb_typeahead/typeahead.rb +6 -1
  30. data/dist/chunks/{_typeahead-Bbgw3Q5E.js → _typeahead-B4GUo5ik.js} +2 -2
  31. data/dist/chunks/vendor.js +2 -2
  32. data/dist/playbook-rails-react-bindings.js +1 -1
  33. data/dist/playbook-rails.js +1 -1
  34. data/dist/playbook.css +1 -1
  35. data/lib/playbook/version.rb +1 -1
  36. metadata +8 -3
@@ -110,25 +110,13 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
110
110
  const inputRef = useRef<HTMLInputElement | null>(null)
111
111
  const itiRef = useRef<any>(null);
112
112
  const wrapperRef = useRef<HTMLDivElement | null>(null);
113
- const hasBlurredRef = useRef<boolean>(false);
114
- const formSubmittedRef = useRef<boolean>(false);
115
113
  const [inputValue, setInputValue] = useState(value)
116
114
  const [error, setError] = useState(props.error || "")
117
115
  const [dropDownIsOpen, setDropDownIsOpen] = useState(false)
118
116
  const [selectedData, setSelectedData] = useState()
119
117
  const [hasTyped, setHasTyped] = useState(false)
120
- const [hasBlurred, setHasBlurred] = useState(false)
121
118
  const [formSubmitted, setFormSubmitted] = useState(false)
122
119
  const [hasStartedValidating, setHasStartedValidating] = useState(false)
123
-
124
- // Keep refs in sync with state for use in event listeners
125
- useEffect(() => {
126
- hasBlurredRef.current = hasBlurred
127
- }, [hasBlurred])
128
-
129
- useEffect(() => {
130
- formSubmittedRef.current = formSubmitted
131
- }, [formSubmitted])
132
120
 
133
121
  // Only sync initial error from props, not continuous updates
134
122
  // Once validation starts, internal validation takes over
@@ -155,8 +143,8 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
155
143
  }
156
144
 
157
145
  // Determine which error to display
158
- // Show internal errors only after blur (hasBlurred) or on form submission (formSubmitted)
159
- const shouldShowInternalError = (hasBlurred || formSubmitted) && error
146
+ // Show internal errors on blur (hasTyped) or on form submission (formSubmitted)
147
+ const shouldShowInternalError = (hasTyped || formSubmitted) && required && error
160
148
  const displayError = shouldShowInternalError ? error : ""
161
149
 
162
150
  useEffect(() => {
@@ -271,8 +259,7 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
271
259
  return
272
260
  }
273
261
 
274
- // Only validate if field has been blurred or form has been submitted
275
- if (!hasBlurred && !formSubmitted) return
262
+ if (!hasTyped && !error) return
276
263
 
277
264
  // Run validation checks
278
265
  if (itiRef.current) isValid(itiRef.current.isValidNumber())
@@ -293,7 +280,6 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
293
280
  if (phoneNumberContainer && phoneNumberContainer === wrapperRef.current) {
294
281
  const invalidInputName = target.name || target.getAttribute('name')
295
282
  if (invalidInputName === name) {
296
- formSubmittedRef.current = true
297
283
  setFormSubmitted(true)
298
284
  // Trigger validation when form is submitted
299
285
  validateErrors()
@@ -319,9 +305,6 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
319
305
  setInputValue("")
320
306
  setError("")
321
307
  setHasTyped(false)
322
- hasBlurredRef.current = false
323
- setHasBlurred(false)
324
- formSubmittedRef.current = false
325
308
  setFormSubmitted(false)
326
309
  setHasStartedValidating(false)
327
310
  // Only clear validation state if field was required
@@ -339,7 +322,6 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
339
322
 
340
323
  if (required && isEmpty) {
341
324
  setError('Missing phone number')
342
- formSubmittedRef.current = true
343
325
  setFormSubmitted(true)
344
326
  return 'Missing phone number'
345
327
  }
@@ -396,7 +378,6 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
396
378
 
397
379
  // Set the error state so the validation attribute gets added
398
380
  setError(errorMessage)
399
- formSubmittedRef.current = true
400
381
  setFormSubmitted(true)
401
382
  setHasTyped(true)
402
383
 
@@ -420,7 +401,6 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
420
401
 
421
402
  // Reset form submitted state when user types
422
403
  if (formSubmitted) {
423
- formSubmittedRef.current = false
424
404
  setFormSubmitted(false)
425
405
  }
426
406
 
@@ -436,15 +416,11 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
436
416
 
437
417
  setSelectedData(phoneNumberData)
438
418
  onChange(phoneNumberData)
439
-
440
- // Don't call isValid callback on change - only on blur or form submission
441
- // This prevents triggering validation while typing
442
- // Use refs to get current values in case this is called from event listener
443
- if (hasBlurredRef.current || formSubmittedRef.current) {
444
- isValid(itiRef.current.isValidNumber())
445
- }
419
+ isValid(itiRef.current.isValidNumber())
446
420
 
447
- // Don't validate on change - only validate on blur or form submission
421
+ // Trigger validation after onChange for React Hook Form
422
+ // This ensures validation state is up-to-date
423
+ setTimeout(() => validateErrors(), 0)
448
424
  }
449
425
 
450
426
  // Separating Concerns as React Docs Recommend
@@ -506,12 +482,7 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
506
482
 
507
483
  setSelectedData(phoneNumberData)
508
484
  onChange(phoneNumberData)
509
-
510
- // Don't call isValid callback on change - only on blur or form submission
511
- // Use refs to check current blur state in the event listener (closure issue)
512
- if (hasBlurredRef.current || formSubmittedRef.current) {
513
- isValid(telInputInit.isValidNumber())
514
- }
485
+ isValid(telInputInit.isValidNumber())
515
486
  })
516
487
  }
517
488
  }
@@ -521,16 +492,12 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
521
492
  dark,
522
493
  "data-phone-number": JSON.stringify(selectedData),
523
494
  disabled,
524
- error: displayError || props.error || "",
495
+ error: hasTyped ? error : props.error || displayError,
525
496
  type: 'tel',
526
497
  id,
527
498
  label,
528
499
  name,
529
- onBlur: () => {
530
- hasBlurredRef.current = true
531
- setHasBlurred(true)
532
- validateErrors()
533
- },
500
+ onBlur: validateErrors,
534
501
  onChange: formatAsYouType ? undefined : handleOnChange,
535
502
  value: inputValue
536
503
  }
@@ -276,3 +276,18 @@ test('multi-value badges have tabIndex and focus class when focused', () => {
276
276
  })
277
277
  })
278
278
 
279
+ test('input display none shows number of selected items', () => {
280
+ render(
281
+ <Typeahead
282
+ data={{ testid: 'input-display-none-test' }}
283
+ defaultValue={[options[0], options[1]]}
284
+ inputDisplay="none"
285
+ isMulti
286
+ options={options}
287
+ />
288
+ )
289
+
290
+ const kit = screen.getByTestId('input-display-none-test')
291
+ const inputDisplayDiv = kit.querySelector(".pb_typeahead_selection_count")
292
+ expect(inputDisplayDiv).toHaveTextContent("2 items selected")
293
+ })
@@ -43,6 +43,7 @@ type TypeaheadProps = {
43
43
  error?: string,
44
44
  htmlOptions?: {[key: string]: string | number | boolean | (() => void)},
45
45
  id?: string,
46
+ inputDisplay?: "pills" | "none",
46
47
  label?: string,
47
48
  loadOptions?: string | Noop,
48
49
  getOptionLabel?: string | (() => string),
@@ -89,6 +90,7 @@ const Typeahead = forwardRef<HTMLInputElement, TypeaheadProps>(({
89
90
  getOptionValue,
90
91
  htmlOptions = {},
91
92
  id,
93
+ inputDisplay = "pills",
92
94
  name,
93
95
  loadOptions = noop,
94
96
  marginBottom = "sm",
@@ -238,6 +240,7 @@ const Typeahead = forwardRef<HTMLInputElement, TypeaheadProps>(({
238
240
  getOptionValue: isString(getOptionValue) ? get(window, getOptionValue) : getOptionValue,
239
241
  defaultOptions: true,
240
242
  id: id || uniqueId(),
243
+ inputDisplay: inputDisplay,
241
244
  inline: false,
242
245
  isClearable: true,
243
246
  isSearchable: true,
@@ -7,18 +7,29 @@ type ClearContainerProps = {
7
7
  id: string,
8
8
  },
9
9
  clearValue: () => void,
10
+ innerProps?: any,
10
11
  }
11
12
 
12
- const ClearContainer = (props: ClearContainerProps): React.ReactElement => {
13
- const { selectProps, clearValue } = props
13
+ const ClearContainer = (props: ClearContainerProps | any): React.ReactElement => {
14
+ const { selectProps, clearValue, innerProps } = props
14
15
  useEffect(() => {
15
16
  document.addEventListener(`pb-typeahead-kit-${selectProps.id}:clear`, clearValue)
16
17
  }, [clearValue, selectProps.id])
17
18
 
19
+ // To stop this from bubbling up when inside a dialog or other modal
20
+ const handleMouseDown = (event: React.MouseEvent) => {
21
+ event.stopPropagation()
22
+ innerProps?.onMouseDown?.(event)
23
+ }
24
+
18
25
  return (
19
26
  <components.ClearIndicator
20
27
  className="clear_indicator"
21
28
  {...props}
29
+ innerProps={{
30
+ ...innerProps,
31
+ onMouseDown: handleMouseDown,
32
+ }}
22
33
  />
23
34
  )
24
35
  }
@@ -19,7 +19,12 @@ type Props = {
19
19
  const MultiValue = (props: Props) => {
20
20
  const { removeProps, isFocused } = props
21
21
  const { imageUrl, label } = props.data
22
- const { dark, multiKit, pillColor, truncate, wrapped } = props.selectProps
22
+ const { dark, multiKit, pillColor, truncate, wrapped, inputDisplay } = props.selectProps
23
+
24
+ // If inputDisplay is "none", don't render the pill/badge, just return null (the count handled in ValueContainer file)
25
+ if (inputDisplay === 'none') {
26
+ return null
27
+ }
23
28
 
24
29
  const formPillProps = {
25
30
  marginRight: 'xs',
@@ -1,15 +1,42 @@
1
1
  import React from 'react'
2
2
  import { components } from 'react-select'
3
+ import Body from '../../pb_body/_body'
3
4
 
4
5
  type ValueContainerProps = {
5
- children: React.ReactNode,
6
+ children: React.ReactNode | React.ReactNode[],
7
+ selectProps?: Record<string, unknown>,
8
+ hasValue?: boolean,
6
9
  }
7
10
 
8
- const ValueContainer = (props: ValueContainerProps): React.ReactElement => (
9
- <components.ValueContainer
10
- className="text_input_value_container"
11
- {...props}
12
- />
13
- )
11
+ const ValueContainer = (props: ValueContainerProps | any): React.ReactElement => {
12
+ const { children, selectProps, hasValue } = props
13
+ const inputDisplay = (selectProps as any)?.inputDisplay
14
+ const value = (selectProps as any)?.value
15
+
16
+ // When inputDisplay is "none" and there are selected values, show count text (this is for multi-select only)
17
+ if (inputDisplay === 'none' && hasValue && value) {
18
+ const selectedCount = Array.isArray(value) ? value.length : 0
19
+
20
+ return (
21
+ <components.ValueContainer
22
+ className="text_input_value_container"
23
+ {...props}
24
+ >
25
+ <Body
26
+ className="pb_typeahead_selection_count"
27
+ text={`${selectedCount} item${selectedCount !== 1 ? 's' : ''} selected`}
28
+ />
29
+ {children}
30
+ </components.ValueContainer>
31
+ )
32
+ }
33
+
34
+ return (
35
+ <components.ValueContainer
36
+ className="text_input_value_container"
37
+ {...props}
38
+ />
39
+ )
40
+ }
14
41
 
15
42
  export default ValueContainer
@@ -0,0 +1,30 @@
1
+ <%
2
+ options = [
3
+ { label: 'Orange', value: '#FFA500' },
4
+ { label: 'Red', value: '#FF0000' },
5
+ { label: 'Green', value: '#00FF00' },
6
+ { label: 'Blue', value: '#0000FF' },
7
+ { label: 'Yellow', value: '#FFFF00' },
8
+ { label: 'Purple', value: '#800080' },
9
+ { label: 'Cyan', value: '#00FFFF' },
10
+ { label: 'Magenta', value: '#FF00FF' }
11
+ ]
12
+ %>
13
+
14
+ <%= pb_rails("typeahead", props: {
15
+ id: "typeahead-input-display-none",
16
+ label: "With Input Display None",
17
+ options: options,
18
+ name: :foo,
19
+ input_display: "none",
20
+ })
21
+ %>
22
+ <br/>
23
+ <%= pb_rails("typeahead", props: {
24
+ id: "typeahead-input-display-pills",
25
+ label: "With Input Display Pills (Default)",
26
+ options: options,
27
+ name: :foo,
28
+ pills: true,
29
+ })
30
+ %>
@@ -0,0 +1,37 @@
1
+ import React from 'react'
2
+
3
+ import Typeahead from '../_typeahead'
4
+
5
+ const options = [
6
+ { label: 'Orange', value: '#FFA500' },
7
+ { label: 'Red', value: '#FF0000' },
8
+ { label: 'Green', value: '#00FF00' },
9
+ { label: 'Blue', value: '#0000FF' },
10
+ { label: 'Yellow', value: '#FFFF00' },
11
+ { label: 'Purple', value: '#800080' },
12
+ { label: 'Cyan', value: '#00FFFF' },
13
+ { label: 'Magenta', value: '#FF00FF' }
14
+ ]
15
+
16
+ const TypeaheadInputDisplay = (props) => {
17
+ return (
18
+ <>
19
+ <Typeahead
20
+ inputDisplay="none"
21
+ isMulti
22
+ label="With Input Display None"
23
+ options={options}
24
+ {...props}
25
+ />
26
+ <br/>
27
+ <Typeahead
28
+ isMulti
29
+ label="With Input Display Pills (Default)"
30
+ options={options}
31
+ {...props}
32
+ />
33
+ </>
34
+ )
35
+ }
36
+
37
+ export default TypeaheadInputDisplay
@@ -0,0 +1,3 @@
1
+ Use the `inputDisplay`/`input_display` prop to optionally display only the count in the display as opposed to multiple pills. This prop is set to 'pills' by default.
2
+
3
+ **NOTE**: `inputDisplay`/`input_display` should only be used with typeaheads that allow multi selection.
@@ -5,6 +5,7 @@ examples:
5
5
  - typeahead_default_options: With Default Options
6
6
  - typeahead_with_context: With Context
7
7
  - typeahead_with_pills: With Pills
8
+ - typeahead_input_display: Input Display
8
9
  - typeahead_without_pills: Without Pills (Single Select)
9
10
  - typeahead_with_pills_async: With Pills (Async Data)
10
11
  - typeahead_with_pills_async_users: With Pills (Async Data w/ Users)
@@ -26,6 +27,7 @@ examples:
26
27
  - typeahead_react_hook: React Hook
27
28
  - typeahead_with_highlight: With Highlight
28
29
  - typeahead_with_pills: With Pills
30
+ - typeahead_input_display: Input Display
29
31
  - typeahead_with_pills_async: With Pills (Async Data)
30
32
  - typeahead_with_pills_async_users: With Pills (Async Data w/ Users)
31
33
  - typeahead_with_pills_async_custom_options: With Pills (Async Data w/ Custom Options)
@@ -17,4 +17,5 @@ export { default as TypeaheadReactHook } from './_typeahead_react_hook.jsx'
17
17
  export { default as TypeaheadDisabled } from './_typeahead_disabled.jsx'
18
18
  export { default as TypeaheadPreserveInput } from './_typeahead_preserve_input.jsx'
19
19
  export { default as TypeaheadDefaultValue } from './_typeahead_default_value.jsx'
20
- export { default as TypeaheadCustomOptions } from './_typeahead_custom_options.jsx'
20
+ export { default as TypeaheadCustomOptions } from './_typeahead_custom_options.jsx'
21
+ export { default as TypeaheadInputDisplay } from './_typeahead_input_display.jsx'
@@ -25,6 +25,10 @@ module Playbook
25
25
  prop :is_multi, type: Playbook::Props::Boolean,
26
26
  default: true
27
27
 
28
+ prop :input_display, type: Playbook::Props::Enum,
29
+ values: %w[none pills],
30
+ default: "pills"
31
+
28
32
  prop :pills, type: Playbook::Props::Boolean,
29
33
  default: false
30
34
 
@@ -78,7 +82,7 @@ module Playbook
78
82
  end
79
83
 
80
84
  def is_react?
81
- pills || !is_multi || wrapped
85
+ pills || !is_multi || wrapped || input_display == "none"
82
86
  end
83
87
 
84
88
  def typeahead_react_options
@@ -91,6 +95,7 @@ module Playbook
91
95
  id: id,
92
96
  inline: inline,
93
97
  isMulti: is_multi,
98
+ inputDisplay: input_display,
94
99
  label: label,
95
100
  marginBottom: margin_bottom,
96
101
  multiKit: multi_kit,