playbook_ui 16.1.0.pre.alpha.PLAY276913866 → 16.1.0.pre.alpha.play2724typeaheadindicator13970

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