playbook_ui 16.2.0.pre.rc.2 → 16.2.0.pre.rc.3
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.
- checksums.yaml +4 -4
- data/app/pb_kits/playbook/pb_checkbox/_checkbox.scss +1 -1
- data/app/pb_kits/playbook/pb_checkbox/_checkbox.tsx +0 -17
- data/app/pb_kits/playbook/pb_checkbox/checkbox.html.erb +1 -10
- data/app/pb_kits/playbook/pb_checkbox/checkbox.rb +0 -2
- data/app/pb_kits/playbook/pb_checkbox/docs/example.yml +0 -2
- data/app/pb_kits/playbook/pb_checkbox/docs/index.js +0 -1
- data/app/pb_kits/playbook/pb_dropdown/_dropdown.tsx +11 -46
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_label.html.erb +3 -6
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_label.jsx +0 -1
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_label.md +1 -3
- data/app/pb_kits/playbook/pb_dropdown/dropdown.html.erb +4 -10
- data/app/pb_kits/playbook/pb_dropdown/dropdown.rb +0 -9
- data/app/pb_kits/playbook/pb_dropdown/dropdown_trigger.html.erb +2 -7
- data/app/pb_kits/playbook/pb_dropdown/dropdown_trigger.rb +0 -4
- data/app/pb_kits/playbook/pb_dropdown/index.js +73 -125
- data/app/pb_kits/playbook/pb_dropdown/subcomponents/DropdownTrigger.tsx +0 -16
- data/app/pb_kits/playbook/pb_dropdown/utilities/clickOutsideHelper.tsx +0 -6
- data/app/pb_kits/playbook/pb_form/docs/_form_with_required_indicator.html.erb +3 -5
- data/app/pb_kits/playbook/pb_multi_level_select/_multi_level_select.scss +0 -7
- data/app/pb_kits/playbook/pb_multi_level_select/_multi_level_select.tsx +549 -638
- data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_label.html.erb +3 -3
- data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_label.jsx +7 -4
- data/app/pb_kits/playbook/pb_multi_level_select/multi_level_select.test.jsx +4 -4
- data/app/pb_kits/playbook/pb_text_input/text_input.html.erb +10 -10
- data/app/pb_kits/playbook/pb_typeahead/_typeahead.tsx +323 -410
- data/app/pb_kits/playbook/pb_typeahead/components/Control.tsx +0 -2
- data/app/pb_kits/playbook/pb_typeahead/docs/example.yml +0 -2
- data/app/pb_kits/playbook/pb_typeahead/docs/index.js +21 -22
- data/app/pb_kits/playbook/pb_typeahead/typeahead.html.erb +2 -3
- data/app/pb_kits/playbook/pb_typeahead/typeahead.rb +1 -3
- data/dist/chunks/{_pb_line_graph-DuJNCf7N.js → _pb_line_graph-CG2X7d4a.js} +1 -1
- data/dist/chunks/_typeahead-DjDiMPdY.js +1 -0
- data/dist/chunks/{globalProps-Bc-FVsRt.js → globalProps-B_OY_vR9.js} +1 -1
- data/dist/chunks/lib-9vEH4omL.js +29 -0
- data/dist/chunks/vendor.js +2 -2
- data/dist/playbook-rails-react-bindings.js +1 -1
- data/dist/playbook-rails.js +1 -1
- data/dist/playbook.css +1 -1
- data/lib/playbook/forms/builder/form_field_builder.rb +1 -1
- data/lib/playbook/forms/builder/typeahead_field.rb +1 -15
- data/lib/playbook/forms/builder.rb +2 -2
- data/lib/playbook/version.rb +1 -1
- metadata +5 -12
- data/app/pb_kits/playbook/pb_checkbox/docs/_checkbox_required_indicator.html.erb +0 -6
- data/app/pb_kits/playbook/pb_checkbox/docs/_checkbox_required_indicator.jsx +0 -17
- data/app/pb_kits/playbook/pb_checkbox/docs/_checkbox_required_indicator.md +0 -3
- data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_label.md +0 -3
- data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_required_indicator.html.erb +0 -16
- data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_required_indicator.jsx +0 -23
- data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_required_indicator.md +0 -3
- data/dist/chunks/_typeahead-BKSzddAX.js +0 -1
- data/dist/chunks/lib-BwX82vim.js +0 -29
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import React, {useState, useEffect, forwardRef, useRef} from
|
|
2
|
-
import Select from
|
|
3
|
-
import AsyncSelect from
|
|
4
|
-
import CreateableSelect from
|
|
5
|
-
import AsyncCreateableSelect from
|
|
6
|
-
import {get, isString, uniqueId} from
|
|
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
|
|
9
|
-
import classnames from
|
|
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
|
|
25
|
-
import {GenericObject, Noop} from
|
|
24
|
+
import { noop, buildDataProps, buildHtmlProps } from '../utilities/props'
|
|
25
|
+
import { GenericObject, Noop } from '../types'
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* @typedef {object} Props
|
|
@@ -33,55 +33,32 @@ 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?:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
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,
|
|
85
62
|
} & GlobalProps
|
|
86
63
|
|
|
87
64
|
export type SelectValueType = {
|
|
@@ -93,390 +70,329 @@ export type SelectValueType = {
|
|
|
93
70
|
}
|
|
94
71
|
|
|
95
72
|
type TagOnChangeValues = {
|
|
96
|
-
action?: string
|
|
97
|
-
option?: SelectValueType
|
|
98
|
-
removedValue?: string
|
|
73
|
+
action?: string,
|
|
74
|
+
option?: SelectValueType,
|
|
75
|
+
removedValue?: string,
|
|
99
76
|
}
|
|
100
77
|
|
|
101
78
|
/**
|
|
102
79
|
* @constant {React.ReactComponent} Typeahead
|
|
103
80
|
* @param {TypeaheadProps} props - props as described at https://react-select.com/props
|
|
104
81
|
*/
|
|
105
|
-
const Typeahead = forwardRef<HTMLInputElement, TypeaheadProps>(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
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('')
|
|
160
127
|
}
|
|
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 []
|
|
179
128
|
|
|
180
|
-
|
|
181
|
-
if (
|
|
182
|
-
return
|
|
129
|
+
// If the original onInputChange was provided, call it too
|
|
130
|
+
if (props.onInputChange) {
|
|
131
|
+
return props.onInputChange(newValue, actionMeta)
|
|
183
132
|
}
|
|
184
|
-
return
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (Array.isArray(currentValue) && currentValue.length > 0) {
|
|
195
|
-
currentValue = currentValue[0]
|
|
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)
|
|
196
143
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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)
|
|
247
209
|
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
210
|
+
}
|
|
211
|
+
}, 20)
|
|
250
212
|
}
|
|
251
213
|
}
|
|
252
214
|
}
|
|
253
|
-
}, 0)
|
|
254
|
-
|
|
255
|
-
// Call original onMenuOpen if provided
|
|
256
|
-
if (props.onMenuOpen) {
|
|
257
|
-
props.onMenuOpen()
|
|
258
215
|
}
|
|
216
|
+
}, 0)
|
|
217
|
+
|
|
218
|
+
// Call original onMenuOpen if provided
|
|
219
|
+
if (props.onMenuOpen) {
|
|
220
|
+
props.onMenuOpen()
|
|
259
221
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
}
|
|
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)
|
|
323
275
|
}
|
|
324
276
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
}
|
|
277
|
+
}
|
|
278
|
+
document.addEventListener('invalid', handleInvalid, true)
|
|
279
|
+
|
|
280
|
+
return () => {
|
|
281
|
+
document.removeEventListener('invalid', handleInvalid, true)
|
|
282
|
+
}
|
|
283
|
+
}, [name])
|
|
331
284
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
285
|
+
// Add listener for clearing
|
|
286
|
+
useEffect(() => {
|
|
287
|
+
const handleClear = () => {
|
|
288
|
+
if (preserveSearchInput) {
|
|
289
|
+
setInputValue('')
|
|
338
290
|
}
|
|
291
|
+
}
|
|
339
292
|
|
|
340
|
-
|
|
341
|
-
`pb-typeahead-kit-${selectProps.id}:clear`,
|
|
342
|
-
handleClear,
|
|
343
|
-
)
|
|
293
|
+
document.addEventListener(`pb-typeahead-kit-${selectProps.id}:clear`, handleClear)
|
|
344
294
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
)
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
}
|
|
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('')
|
|
370
312
|
}
|
|
371
313
|
}
|
|
314
|
+
}
|
|
372
315
|
|
|
373
|
-
|
|
374
|
-
searchContextElement.addEventListener("change", handleContextChange)
|
|
316
|
+
if (searchContextElement) searchContextElement.addEventListener('change', handleContextChange)
|
|
375
317
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
searchContextElement.removeEventListener(
|
|
379
|
-
"change",
|
|
380
|
-
handleContextChange,
|
|
381
|
-
)
|
|
382
|
-
}
|
|
318
|
+
return () => {
|
|
319
|
+
if (searchContextElement) searchContextElement.removeEventListener('change', handleContextChange)
|
|
383
320
|
}
|
|
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
|
|
394
321
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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)
|
|
415
342
|
}
|
|
343
|
+
}
|
|
416
344
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
+
}
|
|
423
351
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
+
}
|
|
428
356
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
}
|
|
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('')
|
|
454
372
|
}
|
|
455
373
|
}
|
|
374
|
+
}
|
|
456
375
|
|
|
457
|
-
|
|
458
|
-
|
|
376
|
+
const filteredProps: TypeaheadProps = {...props}
|
|
377
|
+
delete filteredProps.truncate
|
|
459
378
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
+
)
|
|
468
387
|
|
|
469
|
-
|
|
388
|
+
const inlineClass = selectProps.inline ? 'inline' : null
|
|
470
389
|
|
|
471
|
-
|
|
390
|
+
const shouldShowValidationError = required &&
|
|
391
|
+
formSubmitted
|
|
472
392
|
|
|
473
|
-
|
|
474
|
-
error ||
|
|
475
|
-
(shouldShowValidationError
|
|
476
|
-
? validation?.message || "Please fill out this field."
|
|
477
|
-
: "")
|
|
393
|
+
const errorDisplay = error || (shouldShowValidationError ? validation?.message || "Please fill out this field." : "")
|
|
478
394
|
|
|
479
|
-
|
|
395
|
+
return (
|
|
480
396
|
<div
|
|
481
397
|
{...dataProps}
|
|
482
398
|
{...htmlProps}
|
|
@@ -491,15 +407,12 @@ const Typeahead = forwardRef<HTMLInputElement, TypeaheadProps>(
|
|
|
491
407
|
{...selectProps}
|
|
492
408
|
/>
|
|
493
409
|
</div>
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
)
|
|
410
|
+
)
|
|
411
|
+
})
|
|
497
412
|
|
|
498
413
|
Object.keys(kitComponents).forEach((k) => {
|
|
499
|
-
(Typeahead as GenericObject)[k] = (
|
|
500
|
-
kitComponents as {[key: string]: unknown}
|
|
501
|
-
)[k]
|
|
414
|
+
(Typeahead as GenericObject)[k] = (kitComponents as {[key: string]: unknown})[k]
|
|
502
415
|
})
|
|
503
416
|
|
|
504
|
-
Typeahead.displayName =
|
|
417
|
+
Typeahead.displayName = 'Typeahead'
|
|
505
418
|
export default Typeahead
|