playbook_ui 15.5.0 → 15.6.0.pre.alpha.PLAY2611selectinputoptions12951

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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/pb_advanced_table/_advanced_table.scss +96 -6
  3. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_table_props.html.erb +1 -1
  4. data/app/pb_kits/playbook/pb_background/_background.tsx +6 -6
  5. data/app/pb_kits/playbook/pb_background/background.test.js +5 -1
  6. data/app/pb_kits/playbook/pb_background/docs/_background_light.html.erb +1 -1
  7. data/app/pb_kits/playbook/pb_background/docs/_background_light.jsx +0 -1
  8. data/app/pb_kits/playbook/pb_background/docs/_background_light.md +1 -0
  9. data/app/pb_kits/playbook/pb_background/docs/example.yml +2 -2
  10. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_compound_components.html.erb +31 -0
  11. data/app/pb_kits/playbook/pb_draggable/context/index.tsx +156 -6
  12. data/app/pb_kits/playbook/pb_draggable/context/types.ts +8 -3
  13. data/app/pb_kits/playbook/pb_draggable/docs/_draggable_multiple_containers_dropzone.jsx +180 -0
  14. data/app/pb_kits/playbook/pb_draggable/docs/_draggable_multiple_containers_dropzone.md +22 -0
  15. data/app/pb_kits/playbook/pb_draggable/docs/example.yml +3 -2
  16. data/app/pb_kits/playbook/pb_draggable/docs/index.js +2 -1
  17. data/app/pb_kits/playbook/pb_draggable/draggable.test.jsx +77 -1
  18. data/app/pb_kits/playbook/pb_file_upload/_file_upload.scss +4 -4
  19. data/app/pb_kits/playbook/pb_home_address_street/_home_address_street.tsx +34 -22
  20. data/app/pb_kits/playbook/pb_home_address_street/city_emphasis.html.erb +16 -12
  21. data/app/pb_kits/playbook/pb_home_address_street/docs/_home_address_street_default.html.erb +1 -1
  22. data/app/pb_kits/playbook/pb_home_address_street/none_emphasis.html.erb +16 -12
  23. data/app/pb_kits/playbook/pb_home_address_street/street_emphasis.html.erb +16 -12
  24. data/app/pb_kits/playbook/pb_multiple_users/_multiple_users.scss +10 -0
  25. data/app/pb_kits/playbook/pb_multiple_users/_multiple_users.tsx +66 -15
  26. data/app/pb_kits/playbook/pb_multiple_users/docs/_multiple_users_with_tooltip.jsx +42 -0
  27. data/app/pb_kits/playbook/pb_multiple_users/docs/_multiple_users_with_tooltip.md +1 -0
  28. data/app/pb_kits/playbook/pb_multiple_users/docs/example.yml +1 -0
  29. data/app/pb_kits/playbook/pb_multiple_users/docs/index.js +1 -0
  30. data/app/pb_kits/playbook/pb_multiple_users/multiple_users.test.js +25 -0
  31. data/app/pb_kits/playbook/pb_phone_number_input/_phone_number_input.tsx +44 -10
  32. data/app/pb_kits/playbook/pb_phone_number_input/docs/_phone_number_input_validation.html.erb +34 -4
  33. data/app/pb_kits/playbook/pb_phone_number_input/docs/_phone_number_input_validation.jsx +16 -7
  34. data/app/pb_kits/playbook/pb_radio/docs/_radio_error.md +1 -1
  35. data/app/pb_kits/playbook/pb_select/_select.tsx +8 -3
  36. data/app/pb_kits/playbook/pb_select/docs/_select_error.md +1 -1
  37. data/app/pb_kits/playbook/pb_select/docs/_select_input_options.html.erb +16 -0
  38. data/app/pb_kits/playbook/pb_select/docs/_select_input_options.jsx +30 -0
  39. data/app/pb_kits/playbook/pb_select/docs/_select_input_options.md +1 -0
  40. data/app/pb_kits/playbook/pb_select/docs/example.yml +2 -0
  41. data/app/pb_kits/playbook/pb_select/docs/index.js +1 -0
  42. data/app/pb_kits/playbook/pb_select/select.html.erb +2 -2
  43. data/app/pb_kits/playbook/pb_select/select.rb +3 -1
  44. data/app/pb_kits/playbook/pb_select/select.test.js +23 -0
  45. data/app/pb_kits/playbook/pb_table/styles/_vertical_border.scss +49 -0
  46. data/app/pb_kits/playbook/pb_text_input/docs/_text_input_error.md +1 -1
  47. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_error.md +1 -1
  48. data/app/pb_kits/playbook/pb_typeahead/_typeahead.test.jsx +15 -0
  49. data/app/pb_kits/playbook/pb_typeahead/_typeahead.tsx +3 -0
  50. data/app/pb_kits/playbook/pb_typeahead/components/ClearIndicator.tsx +13 -2
  51. data/app/pb_kits/playbook/pb_typeahead/components/MultiValue.tsx +6 -1
  52. data/app/pb_kits/playbook/pb_typeahead/components/ValueContainer.tsx +34 -7
  53. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_input_display.html.erb +30 -0
  54. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_input_display.jsx +37 -0
  55. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_input_display.md +3 -0
  56. data/app/pb_kits/playbook/pb_typeahead/docs/example.yml +2 -0
  57. data/app/pb_kits/playbook/pb_typeahead/docs/index.js +2 -1
  58. data/app/pb_kits/playbook/pb_typeahead/typeahead.rb +6 -1
  59. data/dist/chunks/_typeahead-BXM7QUuy.js +6 -0
  60. data/dist/chunks/{lib-Dk4GKPut.js → lib-CgpqUb6l.js} +2 -2
  61. data/dist/chunks/vendor.js +2 -2
  62. data/dist/playbook-rails-react-bindings.js +1 -1
  63. data/dist/playbook-rails.js +1 -1
  64. data/dist/playbook.css +1 -1
  65. data/lib/playbook/forms/builder/collection_select_field.rb +9 -1
  66. data/lib/playbook/forms/builder/select_field.rb +9 -1
  67. data/lib/playbook/forms/builder/time_zone_select_field.rb +9 -1
  68. data/lib/playbook/version.rb +2 -2
  69. metadata +15 -5
  70. data/app/pb_kits/playbook/pb_bar_graph/BarGraphStyles.scss +0 -58
  71. data/dist/chunks/_typeahead-Bx4QsIEU.js +0 -6
@@ -0,0 +1,42 @@
1
+ import React from 'react'
2
+ import MultipleUsers from '../../pb_multiple_users/_multiple_users'
3
+
4
+ const MultipleUsersWithTooltip = (props) => {
5
+ return (
6
+ <div>
7
+ <MultipleUsers
8
+ users={[
9
+ {
10
+ name: 'Patrick Welch',
11
+ imageUrl: 'https://randomuser.me/api/portraits/men/9.jpg',
12
+ tooltip: "Patrick Welch - Online"
13
+ },
14
+ {
15
+ name: 'Lucille Sanchez',
16
+ imageUrl: 'https://randomuser.me/api/portraits/women/6.jpg',
17
+ tooltip: "Lucille Sanchez - Offline"
18
+ },
19
+ {
20
+ name: 'Beverly Reyes',
21
+ imageUrl: 'https://randomuser.me/api/portraits/women/74.jpg',
22
+ tooltip: "Beverly Reyes - Online"
23
+ },
24
+ {
25
+ name: 'Keith Craig',
26
+ imageUrl: 'https://randomuser.me/api/portraits/men/40.jpg',
27
+ tooltip: "Keith Craig - Away"
28
+ },
29
+ {
30
+ name: 'Alicia Cooper',
31
+ imageUrl: 'https://randomuser.me/api/portraits/women/46.jpg',
32
+ tooltip: "Alicia Cooper - Busy"
33
+ },
34
+ ]}
35
+ withTooltip
36
+ {...props}
37
+ />
38
+ </div>
39
+ )
40
+ }
41
+ ``
42
+ export default MultipleUsersWithTooltip
@@ -0,0 +1 @@
1
+ Use the `withTooltip` boolean prop to enable setting user-specific tooltip content via the `tooltip` property in the users array.
@@ -10,6 +10,7 @@ examples:
10
10
  - multiple_users_default: Default
11
11
  - multiple_users_reverse: Reverse
12
12
  - multiple_users_size: Size
13
+ - multiple_users_with_tooltip: With Tooltip
13
14
 
14
15
  swift:
15
16
  - multiple_users_default_swift: Default
@@ -1,3 +1,4 @@
1
1
  export { default as MultipleUsersDefault } from './_multiple_users_default.jsx'
2
2
  export { default as MultipleUsersReverse } from './_multiple_users_reverse.jsx'
3
3
  export { default as MultipleUsersSize } from './_multiple_users_size.jsx'
4
+ export { default as MultipleUsersWithTooltip } from './_multiple_users_with_tooltip.jsx'
@@ -49,4 +49,29 @@ test('should render aria-label', () => {
49
49
 
50
50
  const kit = screen.getByTestId(testId)
51
51
  expect(kit).toHaveAttribute('aria-label', testId)
52
+ })
53
+
54
+ test('should render withTooltip prop', () => {
55
+ render(
56
+ <MultipleUsers
57
+ data={{ testid: testId }}
58
+ users={[
59
+ {
60
+ name: 'Patrick Welch',
61
+ imageUrl: 'https://randomuser.me/api/portraits/men/9.jpg',
62
+ tooltip: "Patrick Welch - Online"
63
+ },
64
+ {
65
+ name: 'Lucille Sanchez',
66
+ imageUrl: 'https://randomuser.me/api/portraits/women/6.jpg',
67
+ tooltip: "Lucille Sanchez - Offline"
68
+ },
69
+ ]}
70
+ withTooltip
71
+ />
72
+ )
73
+
74
+ const kit = screen.getByTestId(testId)
75
+ const childWithTooltip = kit.querySelector('.pb_tooltip_kit')
76
+ expect(childWithTooltip).not.toBeNull()
52
77
  })
@@ -110,13 +110,25 @@ 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);
113
115
  const [inputValue, setInputValue] = useState(value)
114
116
  const [error, setError] = useState(props.error || "")
115
117
  const [dropDownIsOpen, setDropDownIsOpen] = useState(false)
116
118
  const [selectedData, setSelectedData] = useState()
117
119
  const [hasTyped, setHasTyped] = useState(false)
120
+ const [hasBlurred, setHasBlurred] = useState(false)
118
121
  const [formSubmitted, setFormSubmitted] = useState(false)
119
122
  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])
120
132
 
121
133
  // Only sync initial error from props, not continuous updates
122
134
  // Once validation starts, internal validation takes over
@@ -143,8 +155,8 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
143
155
  }
144
156
 
145
157
  // Determine which error to display
146
- // Show internal errors on blur (hasTyped) or on form submission (formSubmitted)
147
- const shouldShowInternalError = (hasTyped || formSubmitted) && required && error
158
+ // Show internal errors only after blur (hasBlurred) or on form submission (formSubmitted)
159
+ const shouldShowInternalError = (hasBlurred || formSubmitted) && error
148
160
  const displayError = shouldShowInternalError ? error : ""
149
161
 
150
162
  useEffect(() => {
@@ -259,7 +271,9 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
259
271
  return
260
272
  }
261
273
 
262
- if (!hasTyped && !error) return
274
+ // Only validate if field has been blurred or form has been submitted
275
+ // Use refs here since state updates are async and we need current values
276
+ if (!hasBlurredRef.current && !formSubmittedRef.current) return
263
277
 
264
278
  // Run validation checks
265
279
  if (itiRef.current) isValid(itiRef.current.isValidNumber())
@@ -280,6 +294,7 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
280
294
  if (phoneNumberContainer && phoneNumberContainer === wrapperRef.current) {
281
295
  const invalidInputName = target.name || target.getAttribute('name')
282
296
  if (invalidInputName === name) {
297
+ formSubmittedRef.current = true
283
298
  setFormSubmitted(true)
284
299
  // Trigger validation when form is submitted
285
300
  validateErrors()
@@ -305,6 +320,9 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
305
320
  setInputValue("")
306
321
  setError("")
307
322
  setHasTyped(false)
323
+ hasBlurredRef.current = false
324
+ setHasBlurred(false)
325
+ formSubmittedRef.current = false
308
326
  setFormSubmitted(false)
309
327
  setHasStartedValidating(false)
310
328
  // Only clear validation state if field was required
@@ -322,6 +340,7 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
322
340
 
323
341
  if (required && isEmpty) {
324
342
  setError('Missing phone number')
343
+ formSubmittedRef.current = true
325
344
  setFormSubmitted(true)
326
345
  return 'Missing phone number'
327
346
  }
@@ -378,6 +397,7 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
378
397
 
379
398
  // Set the error state so the validation attribute gets added
380
399
  setError(errorMessage)
400
+ formSubmittedRef.current = true
381
401
  setFormSubmitted(true)
382
402
  setHasTyped(true)
383
403
 
@@ -401,6 +421,7 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
401
421
 
402
422
  // Reset form submitted state when user types
403
423
  if (formSubmitted) {
424
+ formSubmittedRef.current = false
404
425
  setFormSubmitted(false)
405
426
  }
406
427
 
@@ -416,11 +437,15 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
416
437
 
417
438
  setSelectedData(phoneNumberData)
418
439
  onChange(phoneNumberData)
419
- isValid(itiRef.current.isValidNumber())
440
+
441
+ // Don't call isValid callback on change - only on blur or form submission
442
+ // This prevents triggering validation while typing
443
+ // Use refs to get current values in case this is called from event listener
444
+ if (hasBlurredRef.current || formSubmittedRef.current) {
445
+ isValid(itiRef.current.isValidNumber())
446
+ }
420
447
 
421
- // Trigger validation after onChange for React Hook Form
422
- // This ensures validation state is up-to-date
423
- setTimeout(() => validateErrors(), 0)
448
+ // Don't validate on change - only validate on blur or form submission
424
449
  }
425
450
 
426
451
  // Separating Concerns as React Docs Recommend
@@ -482,7 +507,12 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
482
507
 
483
508
  setSelectedData(phoneNumberData)
484
509
  onChange(phoneNumberData)
485
- isValid(telInputInit.isValidNumber())
510
+
511
+ // Don't call isValid callback on change - only on blur or form submission
512
+ // Use refs to check current blur state in the event listener (closure issue)
513
+ if (hasBlurredRef.current || formSubmittedRef.current) {
514
+ isValid(telInputInit.isValidNumber())
515
+ }
486
516
  })
487
517
  }
488
518
  }
@@ -492,12 +522,16 @@ const PhoneNumberInput = (props: PhoneNumberInputProps, ref?: React.Ref<unknown>
492
522
  dark,
493
523
  "data-phone-number": JSON.stringify(selectedData),
494
524
  disabled,
495
- error: hasTyped ? error : props.error || displayError,
525
+ error: displayError || props.error || "",
496
526
  type: 'tel',
497
527
  id,
498
528
  label,
499
529
  name,
500
- onBlur: validateErrors,
530
+ onBlur: () => {
531
+ hasBlurredRef.current = true
532
+ setHasBlurred(true)
533
+ validateErrors()
534
+ },
501
535
  onChange: formatAsYouType ? undefined : handleOnChange,
502
536
  value: inputValue
503
537
  }
@@ -1,12 +1,42 @@
1
1
  <form id="example-form-validation" action="" method="get">
2
- <%= pb_rails("phone_number_input", props: { error: "Missing phone number", id: "validation", initial_country: "af", value: "", required: true }) %>
2
+ <%= pb_rails("phone_number_input", props: {
3
+ id: "validation",
4
+ initial_country: "af",
5
+ value: "",
6
+ required: true
7
+ }) %>
3
8
  <%= pb_rails("button", props: {html_type: "submit", text: "Save Phone Number"}) %>
4
9
  </form>
5
10
 
6
11
  <%= javascript_tag do %>
7
12
  document.addEventListener('DOMContentLoaded', function () {
8
- document.querySelector('#example-form-validation').addEventListener('submit', function (e) {
9
- if (e.target.querySelectorAll('[error]:not([error=""])').length > 0) e.preventDefault();
10
- })
13
+ const form = document.querySelector('#example-form-validation');
14
+
15
+ // Wait for React component to mount
16
+ function waitForComponent() {
17
+ const phoneInput = form.querySelector('#validation');
18
+
19
+ if (!phoneInput) {
20
+ setTimeout(waitForComponent, 100);
21
+ return;
22
+ }
23
+
24
+ // Wait for intl-tel-input to initialize, then focus and blur to trigger validation
25
+ setTimeout(function() {
26
+ phoneInput.focus({ preventScroll: true });
27
+ setTimeout(function() {
28
+ phoneInput.blur();
29
+ }, 100);
30
+ }, 500);
31
+ }
32
+
33
+ waitForComponent();
34
+
35
+ // Prevent form submission if there are validation errors
36
+ form.addEventListener('submit', function (e) {
37
+ if (e.target.querySelectorAll('[error]:not([error=""])').length > 0) {
38
+ e.preventDefault();
39
+ }
40
+ });
11
41
  })
12
42
  <% end %>
@@ -10,8 +10,19 @@ const PhoneNumberInputValidation = (props) => {
10
10
  const [showFormErrors, setShowFormErrors] = useState(false);
11
11
  const [phoneNumber, setPhoneNumber] = useState("");
12
12
  const [countryCode, setCountryCode] = useState("af");
13
+ const [isValid, setIsValid] = useState(false);
14
+ const [hasInteracted, setHasInteracted] = useState(false);
15
+
16
+ // Start with initial error - will be cleared on blur if valid
17
+ const initialError = (
18
+ <>
19
+ <Icon icon="warning" /> Missing phone number.
20
+ </>
21
+ );
13
22
 
14
23
  const handleOnValidate = (valid) => {
24
+ setIsValid(valid);
25
+ setHasInteracted(true);
15
26
  setFormErrors(
16
27
  valid ? "" : "Please correct the fields below and try again."
17
28
  );
@@ -23,18 +34,16 @@ const PhoneNumberInputValidation = (props) => {
23
34
  };
24
35
 
25
36
  const handleOnSubmit = (e) => {
26
- if (showFormErrors) e.preventDefault()
37
+ if (!isValid) e.preventDefault()
27
38
  }
28
39
 
29
40
  useEffect(() => {
30
41
  setShowFormErrors(formErrors.length > 0);
31
42
  }, [formErrors]);
32
43
 
33
- const error = (
34
- <>
35
- <Icon icon="warning" /> Missing phone number.
36
- </>
37
- )
44
+ // Only show error prop initially, or if invalid after interaction
45
+ // Clear error prop once valid (component handles validation on blur)
46
+ const shouldShowError = !hasInteracted || (hasInteracted && !isValid);
38
47
 
39
48
  return (
40
49
  <form
@@ -50,7 +59,7 @@ const PhoneNumberInputValidation = (props) => {
50
59
  />
51
60
  )}
52
61
  <PhoneNumberInput
53
- error={error}
62
+ error={shouldShowError ? initialError : undefined}
54
63
  id="validation"
55
64
  initialCountry={countryCode}
56
65
  onChange={handleOnChange}
@@ -1 +1 @@
1
- Error shows that the radio option must be selected or is invalid (ie when used in a form it signals a user to fix an error).
1
+ Error shows that the radio option must be selected or is invalid (i.e. when used in a form it signals a user to fix an error).
@@ -29,6 +29,7 @@ type SelectProps = {
29
29
  id?: string,
30
30
  includeBlank?: string,
31
31
  inline?: boolean,
32
+ inputOptions?: {[key: string]: string | number | boolean | (() => void)},
32
33
  label?: string,
33
34
  margin: string,
34
35
  marginBottom: string,
@@ -63,6 +64,7 @@ const Select = ({
63
64
  label,
64
65
  htmlOptions = {},
65
66
  inline = false,
67
+ inputOptions = {},
66
68
  multiple = false,
67
69
  name,
68
70
  onChange = () => undefined,
@@ -94,14 +96,17 @@ const Select = ({
94
96
  const angleDown = getAllIcons()["angleDown"].icon as unknown as { [key: string]: SVGElement }
95
97
 
96
98
  const selectWrapperClass = classnames(buildCss('pb_select_kit_wrapper'), { error }, className)
99
+ const selectId = (inputOptions?.id as string) || name
100
+
97
101
  const selectBody =(() =>{
98
102
  if (children) return children
99
103
  return (
100
104
  <select
101
105
  {...htmlOptions}
102
106
  {...domSafeProps(props)}
107
+ {...inputOptions}
103
108
  disabled={disabled}
104
- id={name}
109
+ id={selectId}
105
110
  multiple={multiple}
106
111
  name={name}
107
112
  onChange={onChange}
@@ -125,7 +130,7 @@ const Select = ({
125
130
  {label &&
126
131
  <label
127
132
  className="pb_select_kit_label"
128
- htmlFor={name}
133
+ htmlFor={selectId}
129
134
  >
130
135
  <Caption
131
136
  dark={props.dark}
@@ -135,7 +140,7 @@ const Select = ({
135
140
  }
136
141
  <label
137
142
  className={selectWrapperClass}
138
- htmlFor={name}
143
+ htmlFor={selectId}
139
144
  >
140
145
  {selectBody}
141
146
  { multiple !== true ?
@@ -1 +1 @@
1
- Select w/ Error shows that the radio option must be selected or is invalid (ie when used in a form it signals a user to fix an error).
1
+ Select w/ Error shows that an option must be selected or is invalid (i.e. when used in a form it signals a user to fix an error).
@@ -0,0 +1,16 @@
1
+ <%= pb_rails("select", props: {
2
+ label: "Favorite Food",
3
+ name: "favorite_food",
4
+ options: [
5
+ { value: "pizza", value_text: "Pizza" },
6
+ { value: "tacos", value_text: "Tacos" },
7
+ { value: "sushi", value_text: "Sushi" }
8
+ ],
9
+ input_options: {
10
+ 'aria-label': "Select your favorite food",
11
+ class: "custom-select-class",
12
+ data: { controller: "search", action: "change->search#filter" },
13
+ id: "favorite-food-select"
14
+ }
15
+ }) %>
16
+
@@ -0,0 +1,30 @@
1
+ import React from 'react'
2
+
3
+ import Select from '../_select'
4
+
5
+ const SelectInputOptions = (props) => {
6
+ const options = [
7
+ { value: 'pizza', text: 'Pizza' },
8
+ { value: 'tacos', text: 'Tacos' },
9
+ { value: 'sushi', text: 'Sushi' },
10
+ ]
11
+
12
+ return (
13
+ <>
14
+ <Select
15
+ inputOptions={{
16
+ 'aria-label': 'Select your favorite food',
17
+ className: 'custom-select-class',
18
+ id: 'favorite-food-select',
19
+ }}
20
+ label="Favorite Food"
21
+ name="favorite_food"
22
+ options={options}
23
+ {...props}
24
+ />
25
+ </>
26
+ )
27
+ }
28
+
29
+ export default SelectInputOptions
30
+
@@ -0,0 +1 @@
1
+ Use the `input_options` / `inputOptions` prop to pass additional attributes directly to the underlying `<select>` element instead of the outer wrapper. This is useful for applying data attributes, custom IDs, or other HTML attributes that need to be on the select element itself.
@@ -15,6 +15,7 @@ examples:
15
15
  - select_inline_compact: Select Inline Compact
16
16
  - select_attributes: Select W/ Attributes
17
17
  - select_multiple: Select Multiple
18
+ - select_input_options: Input Options
18
19
 
19
20
 
20
21
 
@@ -33,6 +34,7 @@ examples:
33
34
  - select_inline_compact: Select Inline Compact
34
35
  - select_multiple: Select Multiple
35
36
  - select_react_hook: React Hook
37
+ - select_input_options: Input Options
36
38
 
37
39
  swift:
38
40
  - select_default_swift: Default
@@ -12,3 +12,4 @@ export { default as SelectInlineCompact } from './_select_inline_compact.jsx'
12
12
  export { default as SelectMultiple } from './_select_multiple.jsx'
13
13
  export { default as SelectReactHook } from './_select_react_hook.jsx'
14
14
  export { default as SelectCustomSelectSubheaders } from './_select_custom_select_subheaders.jsx'
15
+ export { default as SelectInputOptions } from './_select_input_options.jsx'
@@ -2,11 +2,11 @@
2
2
  id: nil,
3
3
  class: object.classnames ) do %>
4
4
  <% if object.label %>
5
- <label class="pb_select_kit_label" for="<%= object.name %>">
5
+ <label class="pb_select_kit_label" for="<%= object.input_options[:id] || object.name %>">
6
6
  <%= pb_rails("caption", props: { text: object.label, dark: object.dark }) %>
7
7
  </label>
8
8
  <% end %>
9
- <label class="<%= object.select_wrapper_class %>" for="<%= object.name %>">
9
+ <label class="<%= object.select_wrapper_class %>" for="<%= object.input_options[:id] || object.name %>">
10
10
  <% if content.present? %>
11
11
  <%= content %>
12
12
  <%= pb_rails("body", props: { status: "negative", text: object.error }) %>
@@ -14,6 +14,8 @@ module Playbook
14
14
  prop :error
15
15
  prop :include_blank
16
16
  prop :inline, type: Playbook::Props::Boolean, default: false
17
+ prop :input_options, type: Playbook::Props::HashProp,
18
+ default: {}
17
19
  prop :label
18
20
  prop :multiple, type: Playbook::Props::Boolean, default: false
19
21
  prop :name
@@ -38,7 +40,7 @@ module Playbook
38
40
  multiple: multiple,
39
41
  onchange: onchange,
40
42
  include_blank: include_blank,
41
- }.merge(attributes)
43
+ }.merge(attributes).merge(input_options)
42
44
  end
43
45
 
44
46
  def classname
@@ -65,4 +65,27 @@ test('returns multiple variant', () => {
65
65
  const selectElement = kit.querySelector('select');
66
66
 
67
67
  expect(selectElement).toHaveAttribute('multiple', '');
68
+ });
69
+
70
+ test('inputOptions are passed to select element', () => {
71
+ render(
72
+ <Select
73
+ data={{ testid: testId }}
74
+ inputOptions={{
75
+ id: 'custom-select-id',
76
+ className: 'custom-select-class',
77
+ 'aria-label': 'Custom aria label',
78
+ }}
79
+ label="Favorite Food"
80
+ name="food"
81
+ options={options}
82
+ />
83
+ )
84
+
85
+ const kit = screen.getByTestId(testId)
86
+ const selectElement = kit.querySelector('select')
87
+
88
+ expect(selectElement).toHaveAttribute('id', 'custom-select-id')
89
+ expect(selectElement).toHaveClass('custom-select-class')
90
+ expect(selectElement).toHaveAttribute('aria-label', 'Custom aria label')
68
91
  });
@@ -8,6 +8,25 @@
8
8
  border-right: 1px solid $border_light !important;
9
9
  }
10
10
 
11
+ // **Advanced Table** specific rules to eliminate double borders when vertical-border is active
12
+ .pb_advanced_table &,
13
+ &[data-vertical-border="true"] {
14
+ // Remove first column box-shadow (preserve border-right in Chrome and use CSS var to respect column group border colors)
15
+ .table-header-cells:first-child,
16
+ .table-header-cells-custom:first-child,
17
+ td:first-child,
18
+ .pb_table_td:first-child,
19
+ .checkbox-cell.checkbox-cell-header:first-child {
20
+ box-shadow: none !important;
21
+ border-right: 1px solid var(--column-border-color, #{$border_light}) !important;
22
+ }
23
+
24
+ .pb_table_td:nth-child(2) {
25
+ box-shadow: none !important;
26
+ }
27
+ }
28
+ // --- End Advanced Table First Column Code Section ---
29
+
11
30
  @media screen and (min-width: $screen-xs-min) {
12
31
  tr:hover, .pb_table_tr:hover {
13
32
  td:last-child, .pb_table_td:last-child {
@@ -63,5 +82,35 @@
63
82
  }
64
83
  }
65
84
  }
85
+
86
+ // Dark mode support for advanced tables
87
+ .pb_advanced_table.dark & {
88
+ td, th, .pb_table_td, .pb_table_th {
89
+ border-right: 1px solid $border_dark !important;
90
+ }
91
+
92
+ thead tr th {
93
+ border-right: 1px solid $border_dark !important;
94
+ }
95
+
96
+ &[data-vertical-border="true"] {
97
+ .table-header-cells:first-child,
98
+ .table-header-cells-custom:first-child,
99
+ td:first-child,
100
+ .pb_table_td:first-child,
101
+ .checkbox-cell.checkbox-cell-header:first-child {
102
+ border-right: 1px solid var(--column-border-color, #{$border_dark}) !important;
103
+ }
104
+ }
105
+
106
+ @media screen and (min-width: $screen-xs-min) {
107
+ tr:hover, .pb_table_tr:hover {
108
+ td:last-child, .pb_table_td:last-child {
109
+ border-right-color: darken($border_dark, 10%) !important;
110
+ }
111
+ }
112
+ }
113
+ }
114
+ // --- End Advanced Table Dark Mode Code Section ---
66
115
  }
67
116
  }
@@ -1 +1 @@
1
- Text Input w/ Error shows that the radio option must be selected or is invalid (ie when used in a form it signals a user to fix an error).
1
+ Text Input w/ Error shows that the input must be filled out (i.e. when used in a form it signals a user to fix an error).
@@ -1 +1 @@
1
- Textarea w/ Error shows that the radio option must be selected or is invalid (ie when used in a form it signals a user to fix an error).
1
+ Textarea w/ Error shows that the input must be filled out (i.e. when used in a form it signals a user to fix an error).
@@ -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,