playbook_ui 18.2.0 → 18.3.0.pre.rc.1
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_dropdown/_dropdown.tsx +110 -15
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_form.jsx +50 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_form_react.md +9 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_option_creation.html.erb +16 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_option_creation.jsx +27 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_option_creation_rails.md +9 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_option_creation_react.md +9 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_playground.json +17 -1
- data/app/pb_kits/playbook/pb_dropdown/docs/_playground.overrides.json +6 -1
- data/app/pb_kits/playbook/pb_dropdown/docs/example.yml +3 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/index.js +3 -1
- data/app/pb_kits/playbook/pb_dropdown/dropdown.html.erb +2 -2
- data/app/pb_kits/playbook/pb_dropdown/dropdown.rb +14 -0
- data/app/pb_kits/playbook/pb_dropdown/dropdown.test.jsx +39 -0
- data/app/pb_kits/playbook/pb_dropdown/dropdown_creation.test.jsx +414 -0
- data/app/pb_kits/playbook/pb_dropdown/dropdown_form.test.jsx +173 -0
- data/app/pb_kits/playbook/pb_dropdown/dropdown_index.test.js +291 -0
- data/app/pb_kits/playbook/pb_dropdown/hooks/useHandleOnKeydown.tsx +22 -5
- data/app/pb_kits/playbook/pb_dropdown/index.js +94 -30
- data/app/pb_kits/playbook/pb_dropdown/keyboard_accessibility.js +3 -1
- data/app/pb_kits/playbook/pb_dropdown/kit.schema.json +18 -2
- data/app/pb_kits/playbook/pb_dropdown/subcomponents/DropdownContainer.tsx +43 -25
- data/app/pb_kits/playbook/pb_dropdown/subcomponents/DropdownFormFields.tsx +83 -0
- data/app/pb_kits/playbook/pb_dropdown/subcomponents/DropdownOption.tsx +13 -5
- data/app/pb_kits/playbook/pb_dropdown/utilities/optionCreation.ts +21 -0
- data/app/pb_kits/playbook/pb_dropdown/utilities/quickpickDateSync.test.ts +24 -0
- data/app/pb_kits/playbook/pb_filter/Filter/SortMenu.tsx +8 -1
- data/app/pb_kits/playbook/pb_filter/docs/_filter_one_click.html.erb +38 -0
- data/app/pb_kits/playbook/pb_filter/docs/_filter_one_click.jsx +69 -0
- data/app/pb_kits/playbook/pb_filter/docs/_filter_one_click.md +1 -0
- data/app/pb_kits/playbook/pb_filter/docs/example.yml +2 -0
- data/app/pb_kits/playbook/pb_filter/docs/index.js +2 -1
- data/app/pb_kits/playbook/pb_filter/filter.html.erb +4 -4
- data/app/pb_kits/playbook/pb_filter/filter.rb +28 -0
- data/app/pb_kits/playbook/pb_filter/filter.test.js +41 -0
- data/app/pb_kits/playbook/pb_form/docs/_description_react.md +3 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_default_values.jsx +898 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_default_values_react.md +3 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_defaults.jsx +879 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_defaults_react.md +29 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_loading.jsx +75 -0
- data/app/pb_kits/playbook/pb_form/docs/{_form_loading.md → _form_loading_rails.md} +1 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_loading_react.md +1 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_model_values.jsx +147 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_model_values_react.md +16 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_multi_column.jsx +164 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_multi_column_react.md +3 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_validation.jsx +1057 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_validation_react.md +26 -0
- data/app/pb_kits/playbook/pb_form/docs/example.yml +7 -0
- data/app/pb_kits/playbook/pb_full_screen/_full_screen.scss +6 -0
- data/app/pb_kits/playbook/pb_full_screen/_full_screen.tsx +11 -2
- data/app/pb_kits/playbook/pb_full_screen/docs/_full_screen_advanced_table_sticky_header.jsx +73 -0
- data/app/pb_kits/playbook/pb_full_screen/docs/_full_screen_advanced_table_sticky_header.md +1 -0
- data/app/pb_kits/playbook/pb_full_screen/docs/example.yml +1 -0
- data/app/pb_kits/playbook/pb_full_screen/docs/index.js +1 -0
- data/app/pb_kits/playbook/pb_full_screen/full_screen.test.jsx +4 -2
- data/app/pb_kits/playbook/pb_label_pill/_label_pill.tsx +3 -1
- data/app/pb_kits/playbook/pb_label_pill/kit.schema.json +1 -0
- data/app/pb_kits/playbook/pb_legend/_legend.tsx +3 -1
- data/app/pb_kits/playbook/pb_legend/kit.schema.json +1 -0
- data/app/pb_kits/playbook/pb_online_status/_online_status.tsx +3 -1
- data/app/pb_kits/playbook/pb_online_status/kit.schema.json +1 -0
- data/app/pb_kits/playbook/pb_pill/_pill.tsx +3 -1
- data/app/pb_kits/playbook/pb_pill/kit.schema.json +1 -0
- data/app/pb_kits/playbook/pb_progress_step/_progress_step_item.tsx +3 -1
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_required_indicator.md +2 -2
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_rails_required_indicator.html.erb +9 -0
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_rails_required_indicator.md +3 -0
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/example.yml +1 -0
- data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor.html.erb +3 -1
- data/app/pb_kits/playbook/pb_source/_source.tsx +3 -1
- data/app/pb_kits/playbook/pb_source/kit.schema.json +1 -0
- data/app/pb_kits/playbook/pb_stat_change/_stat_change.tsx +3 -1
- data/app/pb_kits/playbook/pb_stat_change/kit.schema.json +1 -0
- data/app/pb_kits/playbook/pb_timeline/_item.tsx +3 -1
- data/app/pb_kits/playbook/pb_typeahead/_typeahead.scss +73 -0
- data/app/pb_kits/playbook/pb_typeahead/_typeahead.test.jsx +158 -12
- data/app/pb_kits/playbook/pb_typeahead/_typeahead.tsx +3 -3
- data/app/pb_kits/playbook/pb_typeahead/components/PillRenderer.tsx +15 -11
- data/app/pb_kits/playbook/pb_typeahead/components/ValueContainer.tsx +29 -30
- data/app/pb_kits/playbook/pb_typeahead/components/usePillDropIndicator.ts +155 -0
- data/app/pb_kits/playbook/pb_typeahead/docs/_playground.json +5 -5
- data/app/pb_kits/playbook/pb_typeahead/docs/_playground.overrides.json +2 -2
- data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_draggable_pills.html.erb +0 -1
- data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_draggable_pills.jsx +0 -1
- data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_draggable_pills_rails.md +3 -3
- data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_draggable_pills_react.md +7 -0
- data/app/pb_kits/playbook/pb_typeahead/kit.schema.json +3 -3
- data/app/pb_kits/playbook/pb_typeahead/typeahead.rb +3 -3
- data/app/pb_kits/playbook/utilities/test/globalProps/onClick.test.js +116 -0
- data/dist/ai/all-schemas.json +53 -14
- data/dist/ai/forms.json +2 -2
- data/dist/ai/index.json +2 -2
- data/dist/ai/kits/dropdown.schema.json +44 -11
- data/dist/ai/kits/label_pill.schema.json +1 -0
- data/dist/ai/kits/legend.schema.json +1 -0
- data/dist/ai/kits/online_status.schema.json +1 -0
- data/dist/ai/kits/pill.schema.json +1 -0
- data/dist/ai/kits/source.schema.json +1 -0
- data/dist/ai/kits/stat_change.schema.json +1 -0
- data/dist/ai/kits/typeahead.schema.json +3 -3
- data/dist/ai/playgrounds/dropdown.json +1 -1
- data/dist/ai/playgrounds/index.json +1 -1
- data/dist/ai/playgrounds/typeahead.json +1 -1
- data/dist/chunks/_pagination-D0hcdUl3.js +1 -0
- data/dist/chunks/{_pb_line_graph-Du7xE9vl.js → _pb_line_graph-BKwbOeqo.js} +1 -1
- data/dist/chunks/{_detail-Bsvl694i.js → _tooltip-DfZEKsnv.js} +2 -2
- data/dist/chunks/_typeahead-CmJ8Ggzz.js +1 -0
- data/dist/chunks/{globalProps-B120NbSJ.js → globalProps-BivjwRew.js} +1 -1
- data/dist/chunks/lib-C50eCpoN.js +29 -0
- data/dist/chunks/{time_picker_helper-CYGg0UQ0.js → time_picker_helper-DfKvHCJu.js} +1 -1
- 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/version.rb +2 -2
- metadata +53 -18
- data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_draggable_pills.md +0 -7
- data/dist/chunks/_table-Bf6LGyL9.js +0 -1
- data/dist/chunks/_typeahead-w6S5Vx4b.js +0 -1
- data/dist/chunks/lib-Buo6zHEw.js +0 -29
- /data/app/pb_kits/playbook/pb_form/docs/{_description.md → _description_rails.md} +0 -0
- /data/app/pb_kits/playbook/pb_form/docs/{_form_default_values.md → _form_default_values_rails.md} +0 -0
- /data/app/pb_kits/playbook/pb_form/docs/{_form_defaults.md → _form_defaults_rails.md} +0 -0
- /data/app/pb_kits/playbook/pb_form/docs/{_form_model_values.md → _form_model_values_rails.md} +0 -0
- /data/app/pb_kits/playbook/pb_form/docs/{_form_multi_column.md → _form_multi_column_rails.md} +0 -0
- /data/app/pb_kits/playbook/pb_form/docs/{_form_validation.md → _form_validation_rails.md} +0 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import React, { useState } from "react"
|
|
2
|
+
import { render, fireEvent, waitFor, screen, act } from "../utilities/test-utils"
|
|
3
|
+
import Dropdown from "./_dropdown"
|
|
4
|
+
|
|
5
|
+
beforeEach(() => jest.useRealTimers())
|
|
6
|
+
|
|
7
|
+
const options = [{ id: "existing", label: "Existing", value: "existing" }]
|
|
8
|
+
const typeQuery = (container, value) => {
|
|
9
|
+
const input = container.querySelector(".dropdown_input")
|
|
10
|
+
fireEvent.change(input, { target: { value } })
|
|
11
|
+
}
|
|
12
|
+
const createdOption = text => ({ id: text, label: text, value: text, __isNew__: true })
|
|
13
|
+
|
|
14
|
+
test("creation enables autocomplete and Enter selects a trimmed value without submitting", () => {
|
|
15
|
+
const onSelect = jest.fn()
|
|
16
|
+
const submit = jest.fn(event => event.preventDefault())
|
|
17
|
+
const { container } = render(
|
|
18
|
+
<form onSubmit={submit}>
|
|
19
|
+
<Dropdown
|
|
20
|
+
allowOptionCreation
|
|
21
|
+
enableFormSupport
|
|
22
|
+
name="tag"
|
|
23
|
+
onSelect={onSelect}
|
|
24
|
+
options={options}
|
|
25
|
+
required
|
|
26
|
+
/>
|
|
27
|
+
</form>
|
|
28
|
+
)
|
|
29
|
+
typeQuery(container, " New tag ")
|
|
30
|
+
expect(screen.getByText('Create "New tag"')).toBeInTheDocument()
|
|
31
|
+
fireEvent.keyDown(container.querySelector(".dropdown_input"), { key: "Enter" })
|
|
32
|
+
expect(onSelect).toHaveBeenCalledWith(createdOption("New tag"))
|
|
33
|
+
expect(new FormData(container.querySelector("form")).get("tag")).toBe("New tag")
|
|
34
|
+
expect(container.querySelector("form").checkValidity()).toBe(true)
|
|
35
|
+
expect(submit).not.toHaveBeenCalled()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test("does not offer whitespace, case-insensitive duplicates, or selected values", () => {
|
|
39
|
+
const { container } = render(
|
|
40
|
+
<Dropdown
|
|
41
|
+
allowOptionCreation
|
|
42
|
+
defaultValue={[createdOption("Selected")]}
|
|
43
|
+
multiSelect
|
|
44
|
+
options={options}
|
|
45
|
+
/>
|
|
46
|
+
)
|
|
47
|
+
for (const value of [" ", " EXISTING ", "selected"]) {
|
|
48
|
+
typeQuery(container, value)
|
|
49
|
+
expect(screen.queryByText(/^Create "/)).toBeNull()
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test("controlled multi-select creation, removal, and clear emit normal selection payloads", () => {
|
|
54
|
+
const changes = jest.fn()
|
|
55
|
+
const Controlled = () => {
|
|
56
|
+
const [value, setValue] = useState([])
|
|
57
|
+
return (
|
|
58
|
+
<Dropdown
|
|
59
|
+
allowOptionCreation
|
|
60
|
+
enableFormSupport
|
|
61
|
+
multiSelect
|
|
62
|
+
name="tags[]"
|
|
63
|
+
onChange={event => {
|
|
64
|
+
changes(event)
|
|
65
|
+
setValue(event.target.value)
|
|
66
|
+
}}
|
|
67
|
+
options={[]}
|
|
68
|
+
value={value}
|
|
69
|
+
/>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
const { container } = render(<form><Controlled /></form>)
|
|
73
|
+
typeQuery(container, "First")
|
|
74
|
+
fireEvent.click(screen.getByText('Create "First"'))
|
|
75
|
+
typeQuery(container, "Second")
|
|
76
|
+
fireEvent.click(screen.getByText('Create "Second"'))
|
|
77
|
+
expect(changes).toHaveBeenLastCalledWith({
|
|
78
|
+
target: {
|
|
79
|
+
name: "tags[]",
|
|
80
|
+
value: [createdOption("First"), createdOption("Second")],
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
expect(new FormData(container.querySelector("form")).getAll("tags[]")).toEqual(["First", "Second"])
|
|
84
|
+
fireEvent.click(container.querySelector(".pb_form_pill_kit .pb_custom_icon"))
|
|
85
|
+
expect(new FormData(container.querySelector("form")).getAll("tags[]")).toEqual(["Second"])
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test.each(["promise", "callback"])("async %s loader settles before creation is offered", async mode => {
|
|
89
|
+
let finish
|
|
90
|
+
const promiseLoader = () => new Promise(resolve => {
|
|
91
|
+
finish = resolve
|
|
92
|
+
})
|
|
93
|
+
const callbackLoader = (_query, callback) => {
|
|
94
|
+
finish = callback
|
|
95
|
+
}
|
|
96
|
+
const loadOptions = jest.fn(mode === "promise" ? promiseLoader : callbackLoader)
|
|
97
|
+
const { container } = render(
|
|
98
|
+
<Dropdown
|
|
99
|
+
allowOptionCreation
|
|
100
|
+
async
|
|
101
|
+
loadOptions={loadOptions}
|
|
102
|
+
options={[]}
|
|
103
|
+
/>
|
|
104
|
+
)
|
|
105
|
+
typeQuery(container, "New")
|
|
106
|
+
expect(screen.queryByText('Create "New"')).toBeNull()
|
|
107
|
+
await waitFor(() => expect(loadOptions).toHaveBeenCalled())
|
|
108
|
+
act(() => finish([]))
|
|
109
|
+
await waitFor(() => expect(screen.getByText('Create "New"')).toBeInTheDocument())
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test("remote duplicates suppress creation and stale results cannot replace a newer query", async () => {
|
|
113
|
+
const callbacks = {}
|
|
114
|
+
const loadOptions = jest.fn((query, callback) => {
|
|
115
|
+
callbacks[query] = callback
|
|
116
|
+
})
|
|
117
|
+
const { container } = render(
|
|
118
|
+
<Dropdown
|
|
119
|
+
allowOptionCreation
|
|
120
|
+
async
|
|
121
|
+
loadOptions={loadOptions}
|
|
122
|
+
options={[]}
|
|
123
|
+
/>
|
|
124
|
+
)
|
|
125
|
+
typeQuery(container, "Old")
|
|
126
|
+
await waitFor(() => expect(callbacks.Old).toBeDefined())
|
|
127
|
+
typeQuery(container, "New")
|
|
128
|
+
await waitFor(() => expect(callbacks.New).toBeDefined())
|
|
129
|
+
act(() => callbacks.New([{ id: "server", label: "New", value: "server" }]))
|
|
130
|
+
act(() => callbacks.Old([]))
|
|
131
|
+
expect(screen.queryByText('Create "New"')).toBeNull()
|
|
132
|
+
expect(screen.getByText("New")).toBeInTheDocument()
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test("composition does not create an option and quickpick ignores creation", () => {
|
|
136
|
+
const onSelect = jest.fn()
|
|
137
|
+
const { container, rerender } = render(
|
|
138
|
+
<Dropdown
|
|
139
|
+
allowOptionCreation
|
|
140
|
+
onSelect={onSelect}
|
|
141
|
+
options={[]}
|
|
142
|
+
/>
|
|
143
|
+
)
|
|
144
|
+
typeQuery(container, "New")
|
|
145
|
+
fireEvent.keyDown(container.querySelector(".dropdown_input"), { key: "Enter", isComposing: true })
|
|
146
|
+
expect(onSelect).not.toHaveBeenCalled()
|
|
147
|
+
rerender(
|
|
148
|
+
<Dropdown
|
|
149
|
+
allowOptionCreation
|
|
150
|
+
variant="quickpick"
|
|
151
|
+
/>
|
|
152
|
+
)
|
|
153
|
+
expect(container.querySelector(".dropdown_input")).toBeNull()
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test.each([undefined, false])("creation %s leaves autocomplete and options unchanged", allowOptionCreation => {
|
|
157
|
+
const { container } = render(
|
|
158
|
+
<Dropdown
|
|
159
|
+
allowOptionCreation={allowOptionCreation}
|
|
160
|
+
options={options}
|
|
161
|
+
/>
|
|
162
|
+
)
|
|
163
|
+
expect(container.querySelector(".dropdown_input")).toBeNull()
|
|
164
|
+
expect(screen.queryByText(/^Create "/)).toBeNull()
|
|
165
|
+
expect(screen.getByText("Existing")).toBeInTheDocument()
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
test("option creation alone does not opt into native form submission", () => {
|
|
169
|
+
const { container } = render(
|
|
170
|
+
<form>
|
|
171
|
+
<Dropdown
|
|
172
|
+
allowOptionCreation
|
|
173
|
+
name="tag"
|
|
174
|
+
options={[]}
|
|
175
|
+
/>
|
|
176
|
+
</form>
|
|
177
|
+
)
|
|
178
|
+
typeQuery(container, "New")
|
|
179
|
+
fireEvent.click(screen.getByText('Create "New"'))
|
|
180
|
+
expect(new FormData(container.querySelector("form")).has("tag")).toBe(false)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
test("composition preserves legacy selection behavior when creation is disabled", () => {
|
|
184
|
+
const onSelect = jest.fn()
|
|
185
|
+
const { container } = render(
|
|
186
|
+
<Dropdown
|
|
187
|
+
autocomplete
|
|
188
|
+
onSelect={onSelect}
|
|
189
|
+
options={options}
|
|
190
|
+
/>
|
|
191
|
+
)
|
|
192
|
+
const input = container.querySelector(".dropdown_input")
|
|
193
|
+
fireEvent.keyDown(input, { key: "ArrowDown" })
|
|
194
|
+
fireEvent.keyDown(input, { key: "Enter", isComposing: true })
|
|
195
|
+
expect(onSelect).toHaveBeenCalledWith(options[0])
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
test.each([undefined, "outside"])("a second Enter preserves a created selection and permits submission with closeOnClick %s", closeOnClick => {
|
|
199
|
+
const onSelect = jest.fn()
|
|
200
|
+
const { container } = render(
|
|
201
|
+
<form>
|
|
202
|
+
<Dropdown allowOptionCreation
|
|
203
|
+
closeOnClick={closeOnClick}
|
|
204
|
+
enableFormSupport
|
|
205
|
+
name="tag"
|
|
206
|
+
onSelect={onSelect}
|
|
207
|
+
options={options}
|
|
208
|
+
/>
|
|
209
|
+
</form>
|
|
210
|
+
)
|
|
211
|
+
const input = container.querySelector(".dropdown_input")
|
|
212
|
+
typeQuery(container, "New tag")
|
|
213
|
+
expect(fireEvent.keyDown(input, { key: "Enter" })).toBe(false)
|
|
214
|
+
expect(onSelect).toHaveBeenCalledTimes(1)
|
|
215
|
+
expect(onSelect).toHaveBeenLastCalledWith(createdOption("New tag"))
|
|
216
|
+
expect(fireEvent.keyDown(input, { key: "Enter" })).toBe(true)
|
|
217
|
+
expect(onSelect).toHaveBeenCalledTimes(1)
|
|
218
|
+
expect(new FormData(container.querySelector("form")).get("tag")).toBe("New tag")
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
test("Enter selects a highlighted option when the query is blank", () => {
|
|
222
|
+
const onSelect = jest.fn()
|
|
223
|
+
const submit = jest.fn(event => event.preventDefault())
|
|
224
|
+
const { container } = render(
|
|
225
|
+
<form onSubmit={submit}>
|
|
226
|
+
<Dropdown
|
|
227
|
+
allowOptionCreation
|
|
228
|
+
name="tag"
|
|
229
|
+
onSelect={onSelect}
|
|
230
|
+
options={options}
|
|
231
|
+
/>
|
|
232
|
+
</form>
|
|
233
|
+
)
|
|
234
|
+
const input = container.querySelector(".dropdown_input")
|
|
235
|
+
fireEvent.click(input)
|
|
236
|
+
expect(container.querySelector(".pb_dropdown_container")).toHaveClass("open")
|
|
237
|
+
fireEvent.keyDown(input, { key: "ArrowDown" })
|
|
238
|
+
expect(fireEvent.keyDown(input, { key: "Enter" })).toBe(false)
|
|
239
|
+
expect(onSelect).toHaveBeenCalledWith(options[0])
|
|
240
|
+
expect(submit).not.toHaveBeenCalled()
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
test("Enter on mount or a whitespace query does not select a static option", () => {
|
|
244
|
+
const onSelect = jest.fn()
|
|
245
|
+
const { container } = render(
|
|
246
|
+
<Dropdown
|
|
247
|
+
allowOptionCreation
|
|
248
|
+
onSelect={onSelect}
|
|
249
|
+
options={options}
|
|
250
|
+
/>
|
|
251
|
+
)
|
|
252
|
+
const input = container.querySelector(".dropdown_input")
|
|
253
|
+
expect(fireEvent.keyDown(input, { key: "Enter" })).toBe(true)
|
|
254
|
+
expect(container.querySelector(".pb_dropdown_container")).toHaveClass("close")
|
|
255
|
+
typeQuery(container, " ")
|
|
256
|
+
expect(fireEvent.keyDown(input, { key: "Enter" })).toBe(true)
|
|
257
|
+
expect(onSelect).not.toHaveBeenCalled()
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
test("Enter confirms the Create row ahead of a partial static match", () => {
|
|
261
|
+
const onSelect = jest.fn()
|
|
262
|
+
const { container } = render(
|
|
263
|
+
<Dropdown
|
|
264
|
+
allowOptionCreation
|
|
265
|
+
onSelect={onSelect}
|
|
266
|
+
options={options}
|
|
267
|
+
/>
|
|
268
|
+
)
|
|
269
|
+
typeQuery(container, "Ex")
|
|
270
|
+
fireEvent.keyDown(container.querySelector(".dropdown_input"), { key: "Enter" })
|
|
271
|
+
expect(onSelect).toHaveBeenCalledWith(createdOption("Ex"))
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
test("closing the menu with a pending query makes Enter available to the form", () => {
|
|
275
|
+
const onSelect = jest.fn()
|
|
276
|
+
const { container } = render(
|
|
277
|
+
<Dropdown
|
|
278
|
+
allowOptionCreation
|
|
279
|
+
onSelect={onSelect}
|
|
280
|
+
options={options}
|
|
281
|
+
/>
|
|
282
|
+
)
|
|
283
|
+
typeQuery(container, "New")
|
|
284
|
+
const input = container.querySelector(".dropdown_input")
|
|
285
|
+
fireEvent.keyDown(input, { key: "Escape" })
|
|
286
|
+
expect(container.querySelector(".pb_dropdown_container")).toHaveClass("close")
|
|
287
|
+
expect(fireEvent.keyDown(input, { key: "Enter" })).toBe(true)
|
|
288
|
+
expect(onSelect).not.toHaveBeenCalled()
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
test("async creation leaves the following Enter available for submission", async () => {
|
|
292
|
+
const onSelect = jest.fn()
|
|
293
|
+
const loadOptions = jest.fn(() => Promise.resolve([]))
|
|
294
|
+
const { container } = render(
|
|
295
|
+
<Dropdown
|
|
296
|
+
allowOptionCreation
|
|
297
|
+
async
|
|
298
|
+
loadOptions={loadOptions}
|
|
299
|
+
onSelect={onSelect}
|
|
300
|
+
options={options}
|
|
301
|
+
/>
|
|
302
|
+
)
|
|
303
|
+
typeQuery(container, "New tag")
|
|
304
|
+
await waitFor(() => expect(screen.getByText('Create "New tag"')).toBeInTheDocument())
|
|
305
|
+
const input = container.querySelector(".dropdown_input")
|
|
306
|
+
expect(fireEvent.keyDown(input, { key: "Enter" })).toBe(false)
|
|
307
|
+
expect(onSelect).toHaveBeenLastCalledWith(createdOption("New tag"))
|
|
308
|
+
expect(fireEvent.keyDown(input, { key: "Enter" })).toBe(true)
|
|
309
|
+
expect(onSelect).toHaveBeenCalledTimes(1)
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
test.each([
|
|
313
|
+
{ async: false, multiSelect: false },
|
|
314
|
+
{ async: false, multiSelect: true },
|
|
315
|
+
{ async: true, multiSelect: false },
|
|
316
|
+
{ async: true, multiSelect: true },
|
|
317
|
+
])("hovering the menu still permits Enter creation with %j", async settings => {
|
|
318
|
+
const onSelect = jest.fn()
|
|
319
|
+
const loadOptions = jest.fn(() => Promise.resolve(options))
|
|
320
|
+
const { container } = render(
|
|
321
|
+
<form>
|
|
322
|
+
<Dropdown
|
|
323
|
+
allowOptionCreation
|
|
324
|
+
async={settings.async}
|
|
325
|
+
closeOnClick="outside"
|
|
326
|
+
enableFormSupport
|
|
327
|
+
loadOptions={settings.async ? loadOptions : undefined}
|
|
328
|
+
multiSelect={settings.multiSelect}
|
|
329
|
+
name="tag"
|
|
330
|
+
onSelect={onSelect}
|
|
331
|
+
options={options}
|
|
332
|
+
/>
|
|
333
|
+
</form>
|
|
334
|
+
)
|
|
335
|
+
typeQuery(container, "Ex")
|
|
336
|
+
await waitFor(() => expect(screen.getByText('Create "Ex"')).toBeInTheDocument())
|
|
337
|
+
fireEvent.mouseEnter(container.querySelector(".pb_dropdown_container"))
|
|
338
|
+
expect(container.querySelector(".pb_dropdown_option_list_focused")).toBeNull()
|
|
339
|
+
|
|
340
|
+
const input = container.querySelector(".dropdown_input")
|
|
341
|
+
expect(fireEvent.keyDown(input, { key: "Enter" })).toBe(false)
|
|
342
|
+
const expectedSelection = settings.multiSelect ? [createdOption("Ex")] : createdOption("Ex")
|
|
343
|
+
expect(onSelect).toHaveBeenCalledTimes(1)
|
|
344
|
+
expect(onSelect).toHaveBeenLastCalledWith(expectedSelection)
|
|
345
|
+
|
|
346
|
+
const fieldName = settings.multiSelect ? "tag[]" : "tag"
|
|
347
|
+
expect(new FormData(container.querySelector("form")).getAll(fieldName)).toEqual(["Ex"])
|
|
348
|
+
expect(fireEvent.keyDown(input, { key: "Enter" })).toBe(true)
|
|
349
|
+
expect(onSelect).toHaveBeenCalledTimes(1)
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
describe.each([false, true])("exact option matching with multiSelect=%s", multiSelect => {
|
|
353
|
+
test.each(["ny", " NY ", "empire"])("keeps the existing option selectable for query %s", query => {
|
|
354
|
+
const onSelect = jest.fn()
|
|
355
|
+
const newYork = { id: "ny", label: "New York", value: "empire" }
|
|
356
|
+
const { container } = render(
|
|
357
|
+
<Dropdown
|
|
358
|
+
allowOptionCreation
|
|
359
|
+
multiSelect={multiSelect}
|
|
360
|
+
onSelect={onSelect}
|
|
361
|
+
options={[newYork]}
|
|
362
|
+
/>
|
|
363
|
+
)
|
|
364
|
+
typeQuery(container, query)
|
|
365
|
+
expect(screen.getByText("New York")).toBeInTheDocument()
|
|
366
|
+
expect(screen.queryByText(/^Create "/)).toBeNull()
|
|
367
|
+
expect(fireEvent.keyDown(container.querySelector(".dropdown_input"), { key: "Enter" })).toBe(false)
|
|
368
|
+
expect(onSelect).toHaveBeenCalledWith(multiSelect ? [newYork] : newYork)
|
|
369
|
+
})
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
test.each([
|
|
373
|
+
["Create row", "New tag", 'Create "New tag"', "New tag", "New tag"],
|
|
374
|
+
["existing option", "Ex", "Existing", "Existing", "existing"],
|
|
375
|
+
])("uncontrolled single-select keeps the %s label and replaces it on the next character", (_kind, query, optionText, label, submitted) => {
|
|
376
|
+
const onSelect = jest.fn()
|
|
377
|
+
const { container } = render(
|
|
378
|
+
<form>
|
|
379
|
+
<Dropdown
|
|
380
|
+
allowOptionCreation
|
|
381
|
+
enableFormSupport
|
|
382
|
+
name="tag"
|
|
383
|
+
onSelect={onSelect}
|
|
384
|
+
options={options}
|
|
385
|
+
/>
|
|
386
|
+
</form>
|
|
387
|
+
)
|
|
388
|
+
const input = container.querySelector(".dropdown_input")
|
|
389
|
+
const form = container.querySelector("form")
|
|
390
|
+
typeQuery(container, query)
|
|
391
|
+
fireEvent.click(screen.getByText(optionText))
|
|
392
|
+
expect(input).toHaveValue(label)
|
|
393
|
+
expect(new FormData(form).get("tag")).toBe(submitted)
|
|
394
|
+
|
|
395
|
+
expect(fireEvent.keyDown(input, { key: "x" })).toBe(false)
|
|
396
|
+
expect(input).toHaveValue("x")
|
|
397
|
+
expect(onSelect).toHaveBeenLastCalledWith(null)
|
|
398
|
+
expect(new FormData(form).get("tag")).toBe("")
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
test("static filtering stays label-only when option creation is disabled", () => {
|
|
402
|
+
const { container } = render(
|
|
403
|
+
<Dropdown
|
|
404
|
+
allowOptionCreation={false}
|
|
405
|
+
autocomplete
|
|
406
|
+
options={[{ id: "ny", label: "New York", value: "empire" }]}
|
|
407
|
+
/>
|
|
408
|
+
)
|
|
409
|
+
typeQuery(container, "ny")
|
|
410
|
+
expect(screen.queryByText("New York")).toBeNull()
|
|
411
|
+
expect(screen.queryByText(/^Create "/)).toBeNull()
|
|
412
|
+
typeQuery(container, "New")
|
|
413
|
+
expect(screen.getByText("New York")).toBeInTheDocument()
|
|
414
|
+
})
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { render, fireEvent, act } from "../utilities/test-utils"
|
|
3
|
+
import Dropdown from "./_dropdown"
|
|
4
|
+
|
|
5
|
+
const options = [{ id: "one", value: "first", label: "First" }, { value: "second", label: "Second" }]
|
|
6
|
+
const formValues = (form, name) => new FormData(form).getAll(name)
|
|
7
|
+
|
|
8
|
+
test("submits ids with a value fallback and normalizes array field names", () => {
|
|
9
|
+
const { container } = render(
|
|
10
|
+
<form>
|
|
11
|
+
<Dropdown
|
|
12
|
+
defaultValue={options}
|
|
13
|
+
enableFormSupport
|
|
14
|
+
multiSelect
|
|
15
|
+
name="tags[]"
|
|
16
|
+
options={options}
|
|
17
|
+
/>
|
|
18
|
+
</form>
|
|
19
|
+
)
|
|
20
|
+
expect(formValues(container.querySelector("form"), "tags[]")).toEqual(["one", "second"])
|
|
21
|
+
expect(container.querySelector('[name="tags[][]"]')).toBeNull()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test("controlled selections and disabled fields are reflected in FormData", () => {
|
|
25
|
+
const { container, rerender } = render(
|
|
26
|
+
<form>
|
|
27
|
+
<Dropdown
|
|
28
|
+
enableFormSupport
|
|
29
|
+
name="tag"
|
|
30
|
+
options={options}
|
|
31
|
+
value={options[0]}
|
|
32
|
+
/>
|
|
33
|
+
</form>
|
|
34
|
+
)
|
|
35
|
+
const form = container.querySelector("form")
|
|
36
|
+
expect(formValues(form, "tag")).toEqual(["one"])
|
|
37
|
+
rerender(
|
|
38
|
+
<form>
|
|
39
|
+
<Dropdown
|
|
40
|
+
enableFormSupport
|
|
41
|
+
name="tag"
|
|
42
|
+
options={options}
|
|
43
|
+
value={options[1]}
|
|
44
|
+
/>
|
|
45
|
+
</form>
|
|
46
|
+
)
|
|
47
|
+
expect(formValues(form, "tag")).toEqual(["second"])
|
|
48
|
+
rerender(
|
|
49
|
+
<form>
|
|
50
|
+
<Dropdown
|
|
51
|
+
disabled
|
|
52
|
+
enableFormSupport
|
|
53
|
+
name="tag"
|
|
54
|
+
options={options}
|
|
55
|
+
value={options[1]}
|
|
56
|
+
/>
|
|
57
|
+
</form>
|
|
58
|
+
)
|
|
59
|
+
expect(formValues(form, "tag")).toEqual([])
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test("required validation shows the message and clears after selecting", () => {
|
|
63
|
+
const { container, getByText } = render(
|
|
64
|
+
<form>
|
|
65
|
+
<Dropdown
|
|
66
|
+
autocomplete
|
|
67
|
+
enableFormSupport
|
|
68
|
+
name="tag"
|
|
69
|
+
options={options}
|
|
70
|
+
required
|
|
71
|
+
validationMessage="Choose a tag"
|
|
72
|
+
/>
|
|
73
|
+
</form>
|
|
74
|
+
)
|
|
75
|
+
const form = container.querySelector("form")
|
|
76
|
+
act(() => { expect(form.checkValidity()).toBe(false) })
|
|
77
|
+
expect(getByText("Choose a tag")).toBeInTheDocument()
|
|
78
|
+
expect(document.activeElement).toBe(container.querySelector(".dropdown_input"))
|
|
79
|
+
fireEvent.click(getByText("First"))
|
|
80
|
+
expect(form.checkValidity()).toBe(true)
|
|
81
|
+
expect(container.querySelector('[role="alert"]')).toBeNull()
|
|
82
|
+
expect(formValues(form, "tag")).toEqual(["one"])
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test("native reset restores defaults and canceled reset preserves the selection", async () => {
|
|
86
|
+
const { container, getByText } = render(
|
|
87
|
+
<form>
|
|
88
|
+
<Dropdown
|
|
89
|
+
defaultValue={options[0]}
|
|
90
|
+
enableFormSupport
|
|
91
|
+
name="tag"
|
|
92
|
+
options={options}
|
|
93
|
+
/>
|
|
94
|
+
</form>
|
|
95
|
+
)
|
|
96
|
+
const form = container.querySelector("form")
|
|
97
|
+
fireEvent.click(getByText("Second"))
|
|
98
|
+
expect(formValues(form, "tag")).toEqual(["second"])
|
|
99
|
+
await act(async () => { form.reset() })
|
|
100
|
+
expect(formValues(form, "tag")).toEqual(["one"])
|
|
101
|
+
fireEvent.click(getByText("Second"))
|
|
102
|
+
form.addEventListener("reset", event => event.preventDefault())
|
|
103
|
+
await act(async () => { form.reset() })
|
|
104
|
+
expect(formValues(form, "tag")).toEqual(["second"])
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test("controlled empty values and blank selections remain invalid", () => {
|
|
108
|
+
const { container, rerender } = render(
|
|
109
|
+
<form>
|
|
110
|
+
<Dropdown
|
|
111
|
+
enableFormSupport
|
|
112
|
+
name="tag"
|
|
113
|
+
required
|
|
114
|
+
value={null}
|
|
115
|
+
/>
|
|
116
|
+
</form>
|
|
117
|
+
)
|
|
118
|
+
act(() => { expect(container.querySelector("form").checkValidity()).toBe(false) })
|
|
119
|
+
expect(container.querySelector('[role="alert"]')).toHaveTextContent("Please fill out this field.")
|
|
120
|
+
rerender(
|
|
121
|
+
<form>
|
|
122
|
+
<Dropdown
|
|
123
|
+
enableFormSupport
|
|
124
|
+
name="tag"
|
|
125
|
+
required
|
|
126
|
+
value={{ label: "None", value: "" }}
|
|
127
|
+
/>
|
|
128
|
+
</form>
|
|
129
|
+
)
|
|
130
|
+
act(() => { expect(container.querySelector("form").checkValidity()).toBe(false) })
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test("reset without a default reports a null single selection", async () => {
|
|
134
|
+
const onSelect = jest.fn()
|
|
135
|
+
const { container } = render(
|
|
136
|
+
<form>
|
|
137
|
+
<Dropdown
|
|
138
|
+
enableFormSupport
|
|
139
|
+
name="tag"
|
|
140
|
+
onSelect={onSelect}
|
|
141
|
+
options={options}
|
|
142
|
+
value={options[0]}
|
|
143
|
+
/>
|
|
144
|
+
</form>
|
|
145
|
+
)
|
|
146
|
+
await act(async () => { container.querySelector("form").reset() })
|
|
147
|
+
expect(onSelect).toHaveBeenLastCalledWith(null)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test.each([undefined, false])("form support %s preserves existing named-field and reset behavior", async enableFormSupport => {
|
|
151
|
+
const onSelect = jest.fn()
|
|
152
|
+
const { container, getByText } = render(
|
|
153
|
+
<form>
|
|
154
|
+
<Dropdown defaultValue={options[0]}
|
|
155
|
+
enableFormSupport={enableFormSupport}
|
|
156
|
+
name="tag"
|
|
157
|
+
onSelect={onSelect}
|
|
158
|
+
options={options}
|
|
159
|
+
required
|
|
160
|
+
/>
|
|
161
|
+
</form>
|
|
162
|
+
)
|
|
163
|
+
const form = container.querySelector("form")
|
|
164
|
+
expect(container.querySelector('input[type="hidden"]')).toBeNull()
|
|
165
|
+
expect(container.querySelector('input[required]')).toBeNull()
|
|
166
|
+
expect(new FormData(form).has("tag")).toBe(false)
|
|
167
|
+
expect(form.checkValidity()).toBe(true)
|
|
168
|
+
fireEvent.click(getByText("Second"))
|
|
169
|
+
onSelect.mockClear()
|
|
170
|
+
await act(async () => { form.reset() })
|
|
171
|
+
expect(onSelect).not.toHaveBeenCalled()
|
|
172
|
+
expect(container.querySelector(".pb_dropdown_trigger")).toHaveTextContent("Second")
|
|
173
|
+
})
|