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.
- checksums.yaml +4 -4
- data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_sort.md +1 -1
- data/app/pb_kits/playbook/pb_checkbox/_checkbox.scss +1 -1
- data/app/pb_kits/playbook/pb_checkbox/_checkbox.tsx +17 -0
- data/app/pb_kits/playbook/pb_checkbox/checkbox.html.erb +10 -1
- data/app/pb_kits/playbook/pb_checkbox/checkbox.rb +2 -0
- data/app/pb_kits/playbook/pb_checkbox/docs/_checkbox_required_indicator.html.erb +6 -0
- data/app/pb_kits/playbook/pb_checkbox/docs/_checkbox_required_indicator.jsx +17 -0
- data/app/pb_kits/playbook/pb_checkbox/docs/_checkbox_required_indicator.md +3 -0
- data/app/pb_kits/playbook/pb_checkbox/docs/example.yml +2 -0
- data/app/pb_kits/playbook/pb_checkbox/docs/index.js +1 -0
- data/app/pb_kits/playbook/pb_dropdown/_dropdown.tsx +46 -11
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_custom_display_rails.html.erb +1 -1
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_label.html.erb +6 -3
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_label.jsx +1 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_label.md +3 -1
- data/app/pb_kits/playbook/pb_dropdown/dropdown.html.erb +11 -5
- data/app/pb_kits/playbook/pb_dropdown/dropdown.rb +9 -0
- data/app/pb_kits/playbook/pb_dropdown/dropdown_trigger.html.erb +15 -10
- data/app/pb_kits/playbook/pb_dropdown/dropdown_trigger.rb +4 -0
- data/app/pb_kits/playbook/pb_dropdown/index.js +132 -79
- data/app/pb_kits/playbook/pb_dropdown/subcomponents/DropdownTrigger.tsx +16 -0
- data/app/pb_kits/playbook/pb_dropdown/utilities/clickOutsideHelper.tsx +6 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_with_required_indicator.html.erb +5 -3
- data/app/pb_kits/playbook/pb_multi_level_select/_multi_level_select.scss +7 -0
- data/app/pb_kits/playbook/pb_multi_level_select/_multi_level_select.tsx +638 -549
- 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 +4 -7
- data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_label.md +3 -0
- data/app/pb_kits/playbook/pb_multi_level_select/multi_level_select.test.jsx +4 -4
- data/app/pb_kits/playbook/pb_nav/_item.tsx +5 -3
- data/app/pb_kits/playbook/pb_nav/_nav.scss +82 -3
- data/app/pb_kits/playbook/pb_nav/docs/_collapsible_nav_disabled_item.html.erb +24 -0
- data/app/pb_kits/playbook/pb_nav/docs/_collapsible_nav_disabled_item.jsx +87 -0
- data/app/pb_kits/playbook/pb_nav/docs/example.yml +2 -6
- data/app/pb_kits/playbook/pb_nav/docs/index.js +2 -1
- data/app/pb_kits/playbook/pb_nav/item.html.erb +1 -1
- data/app/pb_kits/playbook/pb_nav/item.rb +1 -1
- data/app/pb_kits/playbook/pb_nav/navTypes.ts +1 -0
- data/app/pb_kits/playbook/pb_text_input/text_input.html.erb +10 -10
- data/app/pb_kits/playbook/pb_typeahead/_typeahead.test.jsx +29 -1
- data/app/pb_kits/playbook/pb_typeahead/_typeahead.tsx +411 -323
- data/app/pb_kits/playbook/pb_typeahead/components/Control.tsx +2 -0
- data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_required_indicator.html.erb +16 -0
- data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_required_indicator.jsx +23 -0
- data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_required_indicator.md +3 -0
- data/app/pb_kits/playbook/pb_typeahead/docs/example.yml +2 -0
- data/app/pb_kits/playbook/pb_typeahead/docs/index.js +22 -21
- data/app/pb_kits/playbook/pb_typeahead/typeahead.html.erb +3 -2
- data/app/pb_kits/playbook/pb_typeahead/typeahead.rb +3 -1
- data/app/pb_kits/playbook/tokens/_colors.scss +3 -0
- data/app/pb_kits/playbook/tokens/_colors_accessible.scss +250 -0
- data/app/pb_kits/playbook/tokens/exports/_colors.module.scss +10 -0
- data/dist/chunks/{_pb_line_graph-CG2X7d4a.js → _pb_line_graph-CC2Ywwix.js} +1 -1
- data/dist/chunks/_typeahead-CmMqokN8.js +1 -0
- data/dist/chunks/{globalProps-B_OY_vR9.js → globalProps-DYr2qrIf.js} +1 -1
- data/dist/chunks/lib-DgqmX9CF.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 +2 -2
- data/dist/reset.css +1 -1
- data/lib/playbook/forms/builder/form_field_builder.rb +1 -1
- data/lib/playbook/forms/builder/typeahead_field.rb +15 -1
- data/lib/playbook/forms/builder.rb +2 -2
- data/lib/playbook/version.rb +1 -1
- metadata +16 -6
- data/dist/chunks/_typeahead-DjDiMPdY.js +0 -1
- data/dist/chunks/lib-9vEH4omL.js +0 -29
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
import Select from
|
|
3
|
-
import AsyncSelect from
|
|
4
|
-
import CreateableSelect from
|
|
5
|
-
import AsyncCreateableSelect from
|
|
6
|
-
import {
|
|
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 {
|
|
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 {
|
|
25
|
-
import {
|
|
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?: {
|
|
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<{
|
|
53
|
-
marginBottom?: "none" | "xxs" | "xs" | "sm" | "md" | "lg" | "xl"
|
|
54
|
-
pillColor?:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
130
|
-
if (
|
|
131
|
-
return
|
|
180
|
+
return options.reduce((acc, option) => {
|
|
181
|
+
if (option.options && Array.isArray(option.options)) {
|
|
182
|
+
return [...acc, ...option.options]
|
|
132
183
|
}
|
|
133
|
-
return
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
}
|
|
248
|
+
}, 20)
|
|
249
|
+
}
|
|
212
250
|
}
|
|
213
251
|
}
|
|
214
252
|
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
328
|
+
return () => {
|
|
329
|
+
document.removeEventListener("invalid", handleInvalid, true)
|
|
330
|
+
}
|
|
331
|
+
}, [name])
|
|
294
332
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
341
|
+
document.addEventListener(
|
|
342
|
+
`pb-typeahead-kit-${selectProps.id}:clear`,
|
|
343
|
+
handleClear,
|
|
344
|
+
)
|
|
317
345
|
|
|
318
346
|
return () => {
|
|
319
|
-
|
|
347
|
+
document.removeEventListener(
|
|
348
|
+
`pb-typeahead-kit-${selectProps.id}:clear`,
|
|
349
|
+
handleClear,
|
|
350
|
+
)
|
|
320
351
|
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
358
|
-
if (
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
377
|
-
|
|
458
|
+
const filteredProps: TypeaheadProps = {...props}
|
|
459
|
+
delete filteredProps.truncate
|
|
378
460
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
470
|
+
const inlineClass = selectProps.inline ? "inline" : null
|
|
389
471
|
|
|
390
|
-
|
|
391
|
-
formSubmitted
|
|
472
|
+
const shouldShowValidationError = required && formSubmitted
|
|
392
473
|
|
|
393
|
-
|
|
474
|
+
const errorDisplay =
|
|
475
|
+
error ||
|
|
476
|
+
(shouldShowValidationError
|
|
477
|
+
? validation?.message || "Please fill out this field."
|
|
478
|
+
: "")
|
|
394
479
|
|
|
395
|
-
|
|
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] = (
|
|
500
|
+
(Typeahead as GenericObject)[k] = (
|
|
501
|
+
kitComponents as {[key: string]: unknown}
|
|
502
|
+
)[k]
|
|
415
503
|
})
|
|
416
504
|
|
|
417
|
-
Typeahead.displayName =
|
|
505
|
+
Typeahead.displayName = "Typeahead"
|
|
418
506
|
export default Typeahead
|