playbook_ui 16.2.0.pre.rc.3 → 16.2.0.pre.rc.4

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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_sort.md +1 -1
  3. data/app/pb_kits/playbook/pb_checkbox/_checkbox.scss +1 -1
  4. data/app/pb_kits/playbook/pb_checkbox/_checkbox.tsx +17 -0
  5. data/app/pb_kits/playbook/pb_checkbox/checkbox.html.erb +10 -1
  6. data/app/pb_kits/playbook/pb_checkbox/checkbox.rb +2 -0
  7. data/app/pb_kits/playbook/pb_checkbox/docs/_checkbox_required_indicator.html.erb +6 -0
  8. data/app/pb_kits/playbook/pb_checkbox/docs/_checkbox_required_indicator.jsx +17 -0
  9. data/app/pb_kits/playbook/pb_checkbox/docs/_checkbox_required_indicator.md +3 -0
  10. data/app/pb_kits/playbook/pb_checkbox/docs/example.yml +2 -0
  11. data/app/pb_kits/playbook/pb_checkbox/docs/index.js +1 -0
  12. data/app/pb_kits/playbook/pb_dropdown/_dropdown.tsx +46 -11
  13. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_custom_display_rails.html.erb +1 -1
  14. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_label.html.erb +6 -3
  15. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_label.jsx +1 -0
  16. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_label.md +3 -1
  17. data/app/pb_kits/playbook/pb_dropdown/dropdown.html.erb +11 -5
  18. data/app/pb_kits/playbook/pb_dropdown/dropdown.rb +9 -0
  19. data/app/pb_kits/playbook/pb_dropdown/dropdown_trigger.html.erb +15 -10
  20. data/app/pb_kits/playbook/pb_dropdown/dropdown_trigger.rb +4 -0
  21. data/app/pb_kits/playbook/pb_dropdown/index.js +132 -79
  22. data/app/pb_kits/playbook/pb_dropdown/subcomponents/DropdownTrigger.tsx +16 -0
  23. data/app/pb_kits/playbook/pb_dropdown/utilities/clickOutsideHelper.tsx +6 -0
  24. data/app/pb_kits/playbook/pb_form/docs/_form_with_required_indicator.html.erb +5 -3
  25. data/app/pb_kits/playbook/pb_multi_level_select/_multi_level_select.scss +7 -0
  26. data/app/pb_kits/playbook/pb_multi_level_select/_multi_level_select.tsx +638 -549
  27. data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_label.html.erb +3 -3
  28. data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_label.jsx +4 -7
  29. data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_label.md +3 -0
  30. data/app/pb_kits/playbook/pb_multi_level_select/multi_level_select.test.jsx +4 -4
  31. data/app/pb_kits/playbook/pb_nav/_item.tsx +5 -3
  32. data/app/pb_kits/playbook/pb_nav/_nav.scss +82 -3
  33. data/app/pb_kits/playbook/pb_nav/docs/_collapsible_nav_disabled_item.html.erb +24 -0
  34. data/app/pb_kits/playbook/pb_nav/docs/_collapsible_nav_disabled_item.jsx +87 -0
  35. data/app/pb_kits/playbook/pb_nav/docs/example.yml +2 -6
  36. data/app/pb_kits/playbook/pb_nav/docs/index.js +2 -1
  37. data/app/pb_kits/playbook/pb_nav/item.html.erb +1 -1
  38. data/app/pb_kits/playbook/pb_nav/item.rb +1 -1
  39. data/app/pb_kits/playbook/pb_nav/navTypes.ts +1 -0
  40. data/app/pb_kits/playbook/pb_text_input/text_input.html.erb +10 -10
  41. data/app/pb_kits/playbook/pb_typeahead/_typeahead.test.jsx +29 -1
  42. data/app/pb_kits/playbook/pb_typeahead/_typeahead.tsx +411 -323
  43. data/app/pb_kits/playbook/pb_typeahead/components/Control.tsx +2 -0
  44. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_required_indicator.html.erb +16 -0
  45. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_required_indicator.jsx +23 -0
  46. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_required_indicator.md +3 -0
  47. data/app/pb_kits/playbook/pb_typeahead/docs/example.yml +2 -0
  48. data/app/pb_kits/playbook/pb_typeahead/docs/index.js +22 -21
  49. data/app/pb_kits/playbook/pb_typeahead/typeahead.html.erb +3 -2
  50. data/app/pb_kits/playbook/pb_typeahead/typeahead.rb +3 -1
  51. data/app/pb_kits/playbook/tokens/_colors.scss +3 -0
  52. data/app/pb_kits/playbook/tokens/_colors_accessible.scss +250 -0
  53. data/app/pb_kits/playbook/tokens/exports/_colors.module.scss +10 -0
  54. data/dist/chunks/{_pb_line_graph-CG2X7d4a.js → _pb_line_graph-CC2Ywwix.js} +1 -1
  55. data/dist/chunks/_typeahead-CmMqokN8.js +1 -0
  56. data/dist/chunks/{globalProps-B_OY_vR9.js → globalProps-DYr2qrIf.js} +1 -1
  57. data/dist/chunks/lib-DgqmX9CF.js +29 -0
  58. data/dist/chunks/vendor.js +2 -2
  59. data/dist/playbook-rails-react-bindings.js +1 -1
  60. data/dist/playbook-rails.js +1 -1
  61. data/dist/playbook.css +2 -2
  62. data/dist/reset.css +1 -1
  63. data/lib/playbook/forms/builder/form_field_builder.rb +1 -1
  64. data/lib/playbook/forms/builder/typeahead_field.rb +15 -1
  65. data/lib/playbook/forms/builder.rb +2 -2
  66. data/lib/playbook/version.rb +1 -1
  67. metadata +16 -6
  68. data/dist/chunks/_typeahead-DjDiMPdY.js +0 -1
  69. data/dist/chunks/lib-9vEH4omL.js +0 -29
@@ -1,12 +1,12 @@
1
- import React, { useState, useEffect, forwardRef, useRef} from 'react'
2
- import Select from 'react-select'
3
- import AsyncSelect from 'react-select/async'
4
- import CreateableSelect from 'react-select/creatable'
5
- import AsyncCreateableSelect from 'react-select/async-creatable'
6
- import { get, isString, uniqueId } from '../utilities/object'
1
+ import React, {useState, useEffect, forwardRef, useRef} from "react"
2
+ import Select from "react-select"
3
+ import AsyncSelect from "react-select/async"
4
+ import CreateableSelect from "react-select/creatable"
5
+ import AsyncCreateableSelect from "react-select/async-creatable"
6
+ import {get, isString, uniqueId} from "../utilities/object"
7
7
 
8
- import { globalProps, GlobalProps } from '../utilities/globalProps'
9
- import classnames from 'classnames'
8
+ import {globalProps, GlobalProps} from "../utilities/globalProps"
9
+ import classnames from "classnames"
10
10
 
11
11
  import {
12
12
  Control,
@@ -21,8 +21,8 @@ import {
21
21
 
22
22
  import * as kitComponents from "./components"
23
23
 
24
- import { noop, buildDataProps, buildHtmlProps } from '../utilities/props'
25
- import { GenericObject, Noop } from '../types'
24
+ import {noop, buildDataProps, buildHtmlProps} from "../utilities/props"
25
+ import {GenericObject, Noop} from "../types"
26
26
 
27
27
  /**
28
28
  * @typedef {object} Props
@@ -33,32 +33,55 @@ import { GenericObject, Noop } from '../types'
33
33
  */
34
34
 
35
35
  type TypeaheadProps = {
36
- async?: boolean,
37
- className?: string,
38
- components?: GenericObject,
39
- createable?: boolean,
40
- dark?: boolean,
41
- data?: { [key: string]: string },
42
- disabled?: boolean,
43
- error?: string,
44
- htmlOptions?: {[key: string]: string | number | boolean | (() => void)},
45
- id?: string,
46
- inputDisplay?: "pills" | "none",
47
- label?: string,
48
- loadOptions?: string | Noop,
49
- getOptionLabel?: string | (() => string),
50
- getOptionValue?: string | (() => string),
51
- name?: string,
52
- options?: Array<{ label: string; value?: string }>,
53
- marginBottom?: "none" | "xxs" | "xs" | "sm" | "md" | "lg" | "xl",
54
- pillColor?: "primary" | "neutral" | "success" | "warning" | "error" | "info" | "data_1" | "data_2" | "data_3" | "data_4" | "data_5" | "data_6" | "data_7" | "data_8" | "windows" | "siding" | "roofing" | "doors" | "gutters" | "solar" | "insulation" | "accessories",
55
- onChange?: any,
56
- optionsByContext?: Record<string, Array<{ label: string; value?: string }>>
57
- required?: boolean,
58
- validation?: { message: string },
59
- searchContextSelector?: string,
60
- clearOnContextChange?: boolean,
61
- preserveSearchInput?: boolean,
36
+ async?: boolean
37
+ className?: string
38
+ components?: GenericObject
39
+ createable?: boolean
40
+ dark?: boolean
41
+ data?: {[key: string]: string}
42
+ disabled?: boolean
43
+ error?: string
44
+ htmlOptions?: {[key: string]: string | number | boolean | (() => void)}
45
+ id?: string
46
+ inputDisplay?: "pills" | "none"
47
+ label?: string
48
+ loadOptions?: string | Noop
49
+ getOptionLabel?: string | (() => string)
50
+ getOptionValue?: string | (() => string)
51
+ name?: string
52
+ options?: Array<{label: string; value?: string}>
53
+ marginBottom?: "none" | "xxs" | "xs" | "sm" | "md" | "lg" | "xl"
54
+ pillColor?:
55
+ | "primary"
56
+ | "neutral"
57
+ | "success"
58
+ | "warning"
59
+ | "error"
60
+ | "info"
61
+ | "data_1"
62
+ | "data_2"
63
+ | "data_3"
64
+ | "data_4"
65
+ | "data_5"
66
+ | "data_6"
67
+ | "data_7"
68
+ | "data_8"
69
+ | "windows"
70
+ | "siding"
71
+ | "roofing"
72
+ | "doors"
73
+ | "gutters"
74
+ | "solar"
75
+ | "insulation"
76
+ | "accessories"
77
+ onChange?: any
78
+ optionsByContext?: Record<string, Array<{label: string; value?: string}>>
79
+ required?: boolean
80
+ requiredIndicator?: boolean
81
+ validation?: {message: string}
82
+ searchContextSelector?: string
83
+ clearOnContextChange?: boolean
84
+ preserveSearchInput?: boolean
62
85
  } & GlobalProps
63
86
 
64
87
  export type SelectValueType = {
@@ -70,329 +93,391 @@ export type SelectValueType = {
70
93
  }
71
94
 
72
95
  type TagOnChangeValues = {
73
- action?: string,
74
- option?: SelectValueType,
75
- removedValue?: string,
96
+ action?: string
97
+ option?: SelectValueType
98
+ removedValue?: string
76
99
  }
77
100
 
78
101
  /**
79
102
  * @constant {React.ReactComponent} Typeahead
80
103
  * @param {TypeaheadProps} props - props as described at https://react-select.com/props
81
104
  */
82
- const Typeahead = forwardRef<HTMLInputElement, TypeaheadProps>(({
83
- async,
84
- className,
85
- components = {},
86
- createable,
87
- error = "",
88
- data = {},
89
- disabled = false,
90
- getOptionLabel,
91
- getOptionValue,
92
- htmlOptions = {},
93
- id,
94
- inputDisplay = "pills",
95
- name,
96
- loadOptions = noop,
97
- marginBottom = "sm",
98
- pillColor,
99
- onChange,
100
- optionsByContext = {},
101
- searchContextSelector,
102
- required = false,
103
- validation,
104
- clearOnContextChange = false,
105
- preserveSearchInput = false, // Default to false to maintain backward compatibility
106
- ...props
107
- }: TypeaheadProps) => {
108
- // State to manage the input value when preserveSearchInput is true
109
- const [inputValue, setInputValue] = useState("")
110
- // State to track if form has been submitted to control validation display for react rendered rails kit
111
- const [formSubmitted, setFormSubmitted] = useState(false)
112
- // State to track if user has made a selection (to disable defaultValue focus behavior)
113
- const [hasUserSelected, setHasUserSelected] = useState(false)
114
-
115
- // If preserveSearchInput is true, we need to control the input value
116
- const handleInputChange = preserveSearchInput
117
- ? (newValue: string, actionMeta: {action: string}) => {
118
- // Only update the input value for certain actions
119
- if (actionMeta.action === 'input-change') {
120
- setInputValue(newValue)
121
- } else if (actionMeta.action === 'menu-close' && !props.value) {
122
- // Don't clear the input when the menu closes without a selection
123
- // unless the component is controlled and has a value
124
- } else if (actionMeta.action === 'set-value') {
125
- // When an option is selected, clear the input
126
- setInputValue('')
105
+ const Typeahead = forwardRef<HTMLInputElement, TypeaheadProps>(
106
+ ({
107
+ async,
108
+ className,
109
+ components = {},
110
+ createable,
111
+ error = "",
112
+ data = {},
113
+ disabled = false,
114
+ getOptionLabel,
115
+ getOptionValue,
116
+ htmlOptions = {},
117
+ id,
118
+ inputDisplay = "pills",
119
+ name,
120
+ label,
121
+ loadOptions = noop,
122
+ marginBottom = "sm",
123
+ pillColor,
124
+ onChange,
125
+ optionsByContext = {},
126
+ searchContextSelector,
127
+ required = false,
128
+ requiredIndicator = false,
129
+ validation,
130
+ clearOnContextChange = false,
131
+ preserveSearchInput = false, // Default to false to maintain backward compatibility
132
+ ...props
133
+ }: TypeaheadProps) => {
134
+ // State to manage the input value when preserveSearchInput is true
135
+ const [inputValue, setInputValue] = useState("")
136
+ // State to track if form has been submitted to control validation display for react rendered rails kit
137
+ const [formSubmitted, setFormSubmitted] = useState(false)
138
+ // State to track if user has made a selection (to disable defaultValue focus behavior)
139
+ const [hasUserSelected, setHasUserSelected] = useState(false)
140
+
141
+ // If preserveSearchInput is true, we need to control the input value
142
+ const handleInputChange = preserveSearchInput
143
+ ? (newValue: string, actionMeta: {action: string}) => {
144
+ // Only update the input value for certain actions
145
+ if (actionMeta.action === "input-change") {
146
+ setInputValue(newValue)
147
+ } else if (actionMeta.action === "menu-close" && !props.value) {
148
+ // Don't clear the input when the menu closes without a selection
149
+ // unless the component is controlled and has a value
150
+ } else if (actionMeta.action === "set-value") {
151
+ // When an option is selected, clear the input
152
+ setInputValue("")
153
+ }
154
+
155
+ // If the original onInputChange was provided, call it too
156
+ if (props.onInputChange) {
157
+ return props.onInputChange(newValue, actionMeta)
158
+ }
159
+ return newValue
127
160
  }
161
+ : props.onInputChange
162
+
163
+ // Handle blur events if we're preserving input
164
+ const handleBlur = preserveSearchInput
165
+ ? (event: React.FocusEvent<HTMLInputElement>) => {
166
+ // Do not clear input on blur - the value is preserved in our state
167
+ if (props.onBlur) {
168
+ props.onBlur(event)
169
+ }
170
+ }
171
+ : props.onBlur
172
+
173
+ // Create a ref to access React Select instance
174
+ const selectRef = useRef<any>(null)
175
+
176
+ // Helper function to flatten grouped options if custom groups are used
177
+ const flattenOptions = (options: any[]): any[] => {
178
+ if (!options) return []
128
179
 
129
- // If the original onInputChange was provided, call it too
130
- if (props.onInputChange) {
131
- return props.onInputChange(newValue, actionMeta)
180
+ return options.reduce((acc, option) => {
181
+ if (option.options && Array.isArray(option.options)) {
182
+ return [...acc, ...option.options]
132
183
  }
133
- return newValue
134
- }
135
- : props.onInputChange
136
-
137
- // Handle blur events if we're preserving input
138
- const handleBlur = preserveSearchInput
139
- ? (event: React.FocusEvent<HTMLInputElement>) => {
140
- // Do not clear input on blur - the value is preserved in our state
141
- if (props.onBlur) {
142
- props.onBlur(event)
184
+ return [...acc, option]
185
+ }, [])
186
+ }
187
+
188
+ // Configure focus on selected option using React Select's API
189
+ const handleMenuOpen = () => {
190
+ setTimeout(() => {
191
+ let currentValue = props.value || props.defaultValue
192
+
193
+ // Handle react rendered rails version which passes arrays even for single selects
194
+ if (Array.isArray(currentValue) && currentValue.length > 0) {
195
+ currentValue = currentValue[0]
143
196
  }
144
- }
145
- : props.onBlur
146
-
147
- // Create a ref to access React Select instance
148
- const selectRef = useRef<any>(null)
149
-
150
- // Helper function to flatten grouped options if custom groups are used
151
- const flattenOptions = (options: any[]): any[] => {
152
- if (!options) return []
153
-
154
- return options.reduce((acc, option) => {
155
- if (option.options && Array.isArray(option.options)) {
156
- return [...acc, ...option.options]
157
- }
158
- return [...acc, option]
159
- }, [])
160
- }
161
-
162
- // Configure focus on selected option using React Select's API
163
- const handleMenuOpen = () => {
164
- setTimeout(() => {
165
- let currentValue = props.value || props.defaultValue
166
-
167
- // Handle react rendered rails version which passes arrays even for single selects
168
- if (Array.isArray(currentValue) && currentValue.length > 0) {
169
- currentValue = currentValue[0]
170
- }
171
-
172
- // Only apply custom focus if user has NOT made a selection yet
173
- if (currentValue && selectRef.current && !hasUserSelected && !props.isMulti) {
174
-
175
- const options = props.options
176
- if (options) {
177
- // Flatten grouped options to find the matching option and find matching option
178
- const flatOptions = flattenOptions(options)
179
-
180
- const targetOption = flatOptions.find((option: any) => {
181
- const optionValue = props.getOptionValue ? props.getOptionValue(option) : option.value
182
- const currentOptionValue = props.getOptionValue ? props.getOptionValue(currentValue) : currentValue.value
183
- return optionValue === currentOptionValue
184
- })
185
-
186
- if (targetOption) {
187
- // Use React Select's internal state to set focused option
188
- if (selectRef.current && selectRef.current.setState) {
189
- selectRef.current.setState({
190
- focusedOption: targetOption,
191
- focusedValue: null
192
- })
193
-
194
- // Handle scrolling so selected option is visible
195
- setTimeout(() => {
196
- if (selectRef.current && selectRef.current.menuListRef) {
197
- const menuElement = selectRef.current.menuListRef
198
- // Find the focused option using React Select's class
199
- const focusedElement = menuElement.querySelector('.typeahead-kit-select__option--is-focused')
200
-
201
- if (focusedElement) {
202
- const optionTop = focusedElement.offsetTop
203
- const optionHeight = focusedElement.offsetHeight
204
- const menuHeight = menuElement.clientHeight
205
-
206
- // Set the menu's scrollTop to position the selected option in the middle
207
- const scrollToMiddle = optionTop - (menuHeight / 2) + (optionHeight / 2)
208
- menuElement.scrollTop = Math.max(0, scrollToMiddle)
197
+
198
+ // Only apply custom focus if user has NOT made a selection yet
199
+ if (
200
+ currentValue &&
201
+ selectRef.current &&
202
+ !hasUserSelected &&
203
+ !props.isMulti
204
+ ) {
205
+ const options = props.options
206
+ if (options) {
207
+ // Flatten grouped options to find the matching option and find matching option
208
+ const flatOptions = flattenOptions(options)
209
+
210
+ const targetOption = flatOptions.find((option: any) => {
211
+ const optionValue = props.getOptionValue
212
+ ? props.getOptionValue(option)
213
+ : option.value
214
+ const currentOptionValue = props.getOptionValue
215
+ ? props.getOptionValue(currentValue)
216
+ : currentValue.value
217
+ return optionValue === currentOptionValue
218
+ })
219
+
220
+ if (targetOption) {
221
+ // Use React Select's internal state to set focused option
222
+ if (selectRef.current && selectRef.current.setState) {
223
+ selectRef.current.setState({
224
+ focusedOption: targetOption,
225
+ focusedValue: null,
226
+ })
227
+
228
+ // Handle scrolling so selected option is visible
229
+ setTimeout(() => {
230
+ if (selectRef.current && selectRef.current.menuListRef) {
231
+ const menuElement = selectRef.current.menuListRef
232
+ // Find the focused option using React Select's class
233
+ const focusedElement = menuElement.querySelector(
234
+ ".typeahead-kit-select__option--is-focused",
235
+ )
236
+
237
+ if (focusedElement) {
238
+ const optionTop = focusedElement.offsetTop
239
+ const optionHeight = focusedElement.offsetHeight
240
+ const menuHeight = menuElement.clientHeight
241
+
242
+ // Set the menu's scrollTop to position the selected option in the middle
243
+ const scrollToMiddle =
244
+ optionTop - menuHeight / 2 + optionHeight / 2
245
+ menuElement.scrollTop = Math.max(0, scrollToMiddle)
246
+ }
209
247
  }
210
- }
211
- }, 20)
248
+ }, 20)
249
+ }
212
250
  }
213
251
  }
214
252
  }
215
- }
216
- }, 0)
217
-
218
- // Call original onMenuOpen if provided
219
- if (props.onMenuOpen) {
220
- props.onMenuOpen()
221
- }
222
- }
223
-
224
- const selectProps = {
225
- cacheOptions: true,
226
- required,
227
- components: {
228
- Control,
229
- ClearIndicator,
230
- IndicatorsContainer,
231
- IndicatorSeparator: null as null,
232
- MenuList,
233
- MultiValue,
234
- Option,
235
- Placeholder,
236
- ValueContainer,
237
- ...components
238
- },
239
- loadOptions: isString(loadOptions) ? get(window, loadOptions) : loadOptions,
240
- getOptionLabel: isString(getOptionLabel) ? get(window, getOptionLabel) : getOptionLabel,
241
- getOptionValue: isString(getOptionValue) ? get(window, getOptionValue) : getOptionValue,
242
- defaultOptions: true,
243
- id: id || uniqueId(),
244
- inputDisplay: inputDisplay,
245
- inline: false,
246
- isClearable: true,
247
- isSearchable: true,
248
- name,
249
- multiKit: '',
250
- onCreateOption: null as null,
251
- plusIcon: false,
252
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
253
- onMultiValueClick: (_option: SelectValueType): any => undefined,
254
- pillColor: pillColor,
255
- ...(preserveSearchInput ? { inputValue } : {}),
256
- onInputChange: handleInputChange,
257
- onBlur: handleBlur,
258
- onMenuOpen: handleMenuOpen,
259
- ...props,
260
- }
261
-
262
- const [contextValue, setContextValue] = useState("")
263
-
264
- // Add listener for form validation to track when validation should be shown (needed for react rendered rails kit)
265
- useEffect(() => {
266
- const handleInvalid = (event: Event) => {
267
- const target = event.target as HTMLInputElement
268
- const typeaheadContainer = target.closest('[data-pb-react-component="Typeahead"]')
269
-
270
- if (typeaheadContainer) {
271
- // Check if this invalid event is specifically for our typeahead by comparing names so we do not have to require ids
272
- const invalidInputName = target.name || target.getAttribute('name')
273
- if (invalidInputName === name) {
274
- setFormSubmitted(true)
275
- }
253
+ }, 0)
254
+
255
+ // Call original onMenuOpen if provided
256
+ if (props.onMenuOpen) {
257
+ props.onMenuOpen()
276
258
  }
277
259
  }
278
- document.addEventListener('invalid', handleInvalid, true)
279
-
280
- return () => {
281
- document.removeEventListener('invalid', handleInvalid, true)
260
+
261
+ const selectProps = {
262
+ cacheOptions: true,
263
+ components: {
264
+ Control,
265
+ ClearIndicator,
266
+ IndicatorsContainer,
267
+ IndicatorSeparator: null as null,
268
+ MenuList,
269
+ MultiValue,
270
+ Option,
271
+ Placeholder,
272
+ ValueContainer,
273
+ ...components,
274
+ },
275
+ loadOptions: isString(loadOptions)
276
+ ? get(window, loadOptions)
277
+ : loadOptions,
278
+ getOptionLabel: isString(getOptionLabel)
279
+ ? get(window, getOptionLabel)
280
+ : getOptionLabel,
281
+ getOptionValue: isString(getOptionValue)
282
+ ? get(window, getOptionValue)
283
+ : getOptionValue,
284
+ defaultOptions: true,
285
+ id: id || uniqueId(),
286
+ inputDisplay: inputDisplay,
287
+ inline: false,
288
+ isClearable: true,
289
+ isSearchable: true,
290
+ label: label,
291
+ name,
292
+ multiKit: "",
293
+ onCreateOption: null as null,
294
+ plusIcon: false,
295
+ ...(inputDisplay === 'none' ? { hideSelectedOptions: false, closeMenuOnSelect: false } : {}),
296
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
297
+ onMultiValueClick: (_option: SelectValueType): any => undefined,
298
+ pillColor: pillColor,
299
+ ...(preserveSearchInput ? {inputValue} : {}),
300
+ onInputChange: handleInputChange,
301
+ onBlur: handleBlur,
302
+ onMenuOpen: handleMenuOpen,
303
+ required,
304
+ requiredIndicator: requiredIndicator,
305
+ ...props,
282
306
  }
283
- }, [name])
284
307
 
285
- // Add listener for clearing
286
- useEffect(() => {
287
- const handleClear = () => {
288
- if (preserveSearchInput) {
289
- setInputValue('')
308
+ const [contextValue, setContextValue] = useState("")
309
+
310
+ // Add listener for form validation to track when validation should be shown (needed for react rendered rails kit)
311
+ useEffect(() => {
312
+ const handleInvalid = (event: Event) => {
313
+ const target = event.target as HTMLInputElement
314
+ const typeaheadContainer = target.closest(
315
+ '[data-pb-react-component="Typeahead"]',
316
+ )
317
+
318
+ if (typeaheadContainer) {
319
+ // Check if this invalid event is specifically for our typeahead by comparing names so we do not have to require ids
320
+ const invalidInputName = target.name || target.getAttribute("name")
321
+ if (invalidInputName === name) {
322
+ setFormSubmitted(true)
323
+ }
324
+ }
290
325
  }
291
- }
326
+ document.addEventListener("invalid", handleInvalid, true)
292
327
 
293
- document.addEventListener(`pb-typeahead-kit-${selectProps.id}:clear`, handleClear)
328
+ return () => {
329
+ document.removeEventListener("invalid", handleInvalid, true)
330
+ }
331
+ }, [name])
294
332
 
295
- return () => {
296
- document.removeEventListener(`pb-typeahead-kit-${selectProps.id}:clear`, handleClear)
297
- }
298
- }, [selectProps.id, preserveSearchInput])
299
-
300
- useEffect(() => {
301
- if (searchContextSelector) {
302
- const searchContextElement = document.getElementById(searchContextSelector)
303
-
304
- setContextValue((searchContextElement as HTMLInputElement)?.value)
305
- const handleContextChange = (e: Event) => {
306
- const target = e.target as HTMLInputElement;
307
- setContextValue(target.value);
308
- if (clearOnContextChange) {
309
- document.dispatchEvent(new CustomEvent(`pb-typeahead-kit-${selectProps.id}:clear`))
310
- if (preserveSearchInput) {
311
- setInputValue('')
312
- }
333
+ // Add listener for clearing
334
+ useEffect(() => {
335
+ const handleClear = () => {
336
+ if (preserveSearchInput) {
337
+ setInputValue("")
313
338
  }
314
339
  }
315
340
 
316
- if (searchContextElement) searchContextElement.addEventListener('change', handleContextChange)
341
+ document.addEventListener(
342
+ `pb-typeahead-kit-${selectProps.id}:clear`,
343
+ handleClear,
344
+ )
317
345
 
318
346
  return () => {
319
- if (searchContextElement) searchContextElement.removeEventListener('change', handleContextChange)
347
+ document.removeEventListener(
348
+ `pb-typeahead-kit-${selectProps.id}:clear`,
349
+ handleClear,
350
+ )
320
351
  }
321
- }
322
- }, [searchContextSelector, clearOnContextChange, selectProps.id, preserveSearchInput])
323
-
324
- const contextArray = optionsByContext[contextValue]
325
- if (Array.isArray(contextArray) && contextArray.length > 0) {
326
- selectProps.options = contextArray
327
- }
328
-
329
- const Tag = (
330
- createable
331
- ? (async ? AsyncCreateableSelect : CreateableSelect)
332
- : (async ? AsyncSelect : Select)
333
- )
334
-
335
- const handleOnChange = (_data: SelectValueType, { action, option, removedValue }: TagOnChangeValues) => {
336
- if (onChange) {
337
- const isReactHookForm = onChange.toString().includes("target")
338
- if (isReactHookForm) {
339
- onChange({ target: { name, value: _data } })
340
- } else {
341
- onChange(_data)
352
+ }, [selectProps.id, preserveSearchInput])
353
+
354
+ useEffect(() => {
355
+ if (searchContextSelector) {
356
+ const searchContextElement = document.getElementById(
357
+ searchContextSelector,
358
+ )
359
+
360
+ setContextValue((searchContextElement as HTMLInputElement)?.value)
361
+ const handleContextChange = (e: Event) => {
362
+ const target = e.target as HTMLInputElement
363
+ setContextValue(target.value)
364
+ if (clearOnContextChange) {
365
+ document.dispatchEvent(
366
+ new CustomEvent(`pb-typeahead-kit-${selectProps.id}:clear`),
367
+ )
368
+ if (preserveSearchInput) {
369
+ setInputValue("")
370
+ }
371
+ }
372
+ }
373
+
374
+ if (searchContextElement)
375
+ searchContextElement.addEventListener("change", handleContextChange)
376
+
377
+ return () => {
378
+ if (searchContextElement)
379
+ searchContextElement.removeEventListener(
380
+ "change",
381
+ handleContextChange,
382
+ )
383
+ }
342
384
  }
385
+ }, [
386
+ searchContextSelector,
387
+ clearOnContextChange,
388
+ selectProps.id,
389
+ preserveSearchInput,
390
+ ])
391
+
392
+ const contextArray = optionsByContext[contextValue]
393
+ if (Array.isArray(contextArray) && contextArray.length > 0) {
394
+ selectProps.options = contextArray
343
395
  }
344
396
 
345
- // Reset form submitted state when a selection is made (this is all for react rendered rails kit)
346
- if (action === 'select-option') {
347
- setFormSubmitted(false)
348
- // Mark that user has made a selection to disable default value focus behavior
349
- setHasUserSelected(true)
350
- }
397
+ const Tag = createable
398
+ ? async
399
+ ? AsyncCreateableSelect
400
+ : CreateableSelect
401
+ : async
402
+ ? AsyncSelect
403
+ : Select
404
+
405
+ const handleOnChange = (
406
+ _data: SelectValueType,
407
+ {action, option, removedValue}: TagOnChangeValues,
408
+ ) => {
409
+ if (onChange) {
410
+ const isReactHookForm = onChange.toString().includes("target")
411
+ if (isReactHookForm) {
412
+ onChange({target: {name, value: _data}})
413
+ } else {
414
+ onChange(_data)
415
+ }
416
+ }
351
417
 
352
- // If a value is selected and we're preserving input on blur, clear the input
353
- if (action === 'select-option' && preserveSearchInput) {
354
- setInputValue('')
355
- }
418
+ // Reset form submitted state when a selection is made (this is all for react rendered rails kit)
419
+ if (action === "select-option") {
420
+ setFormSubmitted(false)
421
+ // Mark that user has made a selection to disable default value focus behavior
422
+ setHasUserSelected(true)
423
+ }
356
424
 
357
- if (action === 'select-option') {
358
- if (selectProps.onMultiValueClick && option) selectProps.onMultiValueClick(option)
359
- const multiValueClearEvent = new CustomEvent(`pb-typeahead-kit-${selectProps.id}-result-option-select`, { detail: option ? option : _data })
360
- document.dispatchEvent(multiValueClearEvent)
361
- }
362
- if (action === 'remove-value' || action === 'pop-value') {
363
- const multiValueRemoveEvent = new CustomEvent(`pb-typeahead-kit-${selectProps.id}-result-option-remove`, { detail: removedValue })
364
- document.dispatchEvent(multiValueRemoveEvent)
365
- }
366
- if (action === 'clear') {
367
- const multiValueClearEvent = new CustomEvent(`pb-typeahead-kit-${selectProps.id}-result-clear`)
368
- document.dispatchEvent(multiValueClearEvent)
369
- // If preserving input on blur, also clear input on explicit clear
370
- if (preserveSearchInput) {
371
- setInputValue('')
425
+ // If a value is selected and we're preserving input on blur, clear the input
426
+ if (action === "select-option" && preserveSearchInput) {
427
+ setInputValue("")
428
+ }
429
+
430
+ if (action === "select-option") {
431
+ if (selectProps.onMultiValueClick && option)
432
+ selectProps.onMultiValueClick(option)
433
+ const multiValueClearEvent = new CustomEvent(
434
+ `pb-typeahead-kit-${selectProps.id}-result-option-select`,
435
+ {detail: option ? option : _data},
436
+ )
437
+ document.dispatchEvent(multiValueClearEvent)
438
+ }
439
+ if (action === "remove-value" || action === "pop-value") {
440
+ const multiValueRemoveEvent = new CustomEvent(
441
+ `pb-typeahead-kit-${selectProps.id}-result-option-remove`,
442
+ {detail: removedValue},
443
+ )
444
+ document.dispatchEvent(multiValueRemoveEvent)
445
+ }
446
+ if (action === "clear") {
447
+ const multiValueClearEvent = new CustomEvent(
448
+ `pb-typeahead-kit-${selectProps.id}-result-clear`,
449
+ )
450
+ document.dispatchEvent(multiValueClearEvent)
451
+ // If preserving input on blur, also clear input on explicit clear
452
+ if (preserveSearchInput) {
453
+ setInputValue("")
454
+ }
372
455
  }
373
456
  }
374
- }
375
457
 
376
- const filteredProps: TypeaheadProps = {...props}
377
- delete filteredProps.truncate
458
+ const filteredProps: TypeaheadProps = {...props}
459
+ delete filteredProps.truncate
378
460
 
379
- const dataProps = buildDataProps(data)
380
- const htmlProps = buildHtmlProps(htmlOptions)
381
- const classes = classnames(
382
- 'pb_typeahead_kit react-select',
383
- `mb_${marginBottom}`,
384
- globalProps(filteredProps),
385
- className
386
- )
461
+ const dataProps = buildDataProps(data)
462
+ const htmlProps = buildHtmlProps(htmlOptions)
463
+ const classes = classnames(
464
+ "pb_typeahead_kit react-select",
465
+ `mb_${marginBottom}`,
466
+ globalProps(filteredProps),
467
+ className
468
+ )
387
469
 
388
- const inlineClass = selectProps.inline ? 'inline' : null
470
+ const inlineClass = selectProps.inline ? "inline" : null
389
471
 
390
- const shouldShowValidationError = required &&
391
- formSubmitted
472
+ const shouldShowValidationError = required && formSubmitted
392
473
 
393
- const errorDisplay = error || (shouldShowValidationError ? validation?.message || "Please fill out this field." : "")
474
+ const errorDisplay =
475
+ error ||
476
+ (shouldShowValidationError
477
+ ? validation?.message || "Please fill out this field."
478
+ : "")
394
479
 
395
- return (
480
+ return (
396
481
  <div
397
482
  {...dataProps}
398
483
  {...htmlProps}
@@ -407,12 +492,15 @@ const Typeahead = forwardRef<HTMLInputElement, TypeaheadProps>(({
407
492
  {...selectProps}
408
493
  />
409
494
  </div>
410
- )
411
- })
495
+ )
496
+ },
497
+ )
412
498
 
413
499
  Object.keys(kitComponents).forEach((k) => {
414
- (Typeahead as GenericObject)[k] = (kitComponents as {[key: string]: unknown})[k]
500
+ (Typeahead as GenericObject)[k] = (
501
+ kitComponents as {[key: string]: unknown}
502
+ )[k]
415
503
  })
416
504
 
417
- Typeahead.displayName = 'Typeahead'
505
+ Typeahead.displayName = "Typeahead"
418
506
  export default Typeahead