Skip to content

Commit 55e1d8c

Browse files
committed
[combobox] Revert #783
This PR introduced regression in some cases where the popover doesn't open until the user presses the down or up key after initial input. See #755 for the related issue.
1 parent 03c58dd commit 55e1d8c

File tree

4 files changed

+103
-92
lines changed

4 files changed

+103
-92
lines changed

packages/combobox/__tests__/combobox.test.tsx

Lines changed: 74 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -207,32 +207,32 @@ describe("<Combobox />", () => {
207207
expect(getByTextWithMarkup(optionToSelect)).toBeInTheDocument();
208208
});
209209

210-
it("should *not* open a list when input value changes without text entry", () => {
211-
let optionToSelect = "Eagle Pass, Texas";
212-
213-
function EaglePassSelector() {
214-
let [term, setTerm] = React.useState("");
215-
return (
216-
<div>
217-
<button
218-
type="button"
219-
onClick={() => {
220-
setTerm(optionToSelect);
221-
}}
222-
>
223-
Select Eagle Pass
224-
</button>
225-
<ControlledCombobox term={term} setTerm={setTerm} />
226-
</div>
227-
);
228-
}
229-
230-
let { getByRole, queryByRole } = render(<EaglePassSelector />);
231-
232-
let button = getByRole("button");
233-
userEvent.click(button);
234-
expect(queryByRole("listbox")).toBeFalsy();
235-
});
210+
// it("should *not* open a list when input value changes without text entry", () => {
211+
// let optionToSelect = "Eagle Pass, Texas";
212+
213+
// function EaglePassSelector() {
214+
// let [term, setTerm] = React.useState("");
215+
// return (
216+
// <div>
217+
// <button
218+
// type="button"
219+
// onClick={() => {
220+
// setTerm(optionToSelect);
221+
// }}
222+
// >
223+
// Select Eagle Pass
224+
// </button>
225+
// <ControlledCombobox term={term} setTerm={setTerm} />
226+
// </div>
227+
// );
228+
// }
229+
230+
// let { getByRole, queryByRole } = render(<EaglePassSelector />);
231+
232+
// let button = getByRole("button");
233+
// userEvent.click(button);
234+
// expect(queryByRole("listbox")).toBeFalsy();
235+
// });
236236
});
237237
});
238238

@@ -277,54 +277,54 @@ function BasicCombobox() {
277277
);
278278
}
279279

280-
function ControlledCombobox({
281-
term,
282-
setTerm,
283-
}: {
284-
term: string;
285-
setTerm:
286-
| ((term: string) => void)
287-
| ((setter: (prevTerm: string) => string) => void);
288-
}) {
289-
let results = useCityMatch(term);
290-
291-
function handleChange(event: any) {
292-
setTerm(event.target.value);
293-
}
294-
295-
return (
296-
<div>
297-
<h2>Clientside Search</h2>
298-
<Combobox id="holy-smokes">
299-
<ComboboxInput
300-
aria-label="cool search"
301-
data-testid="input"
302-
name="awesome"
303-
onChange={handleChange}
304-
value={term}
305-
/>
306-
{results ? (
307-
<ComboboxPopover portal={false}>
308-
{results.length === 0 ? (
309-
<p>No results</p>
310-
) : (
311-
<ComboboxList data-testid="list">
312-
{results.slice(0, 10).map((result, index) => (
313-
<ComboboxOption
314-
key={index}
315-
value={`${result.city}, ${result.state}`}
316-
/>
317-
))}
318-
</ComboboxList>
319-
)}
320-
</ComboboxPopover>
321-
) : (
322-
<span>No Results!</span>
323-
)}
324-
</Combobox>
325-
</div>
326-
);
327-
}
280+
// function ControlledCombobox({
281+
// term,
282+
// setTerm,
283+
// }: {
284+
// term: string;
285+
// setTerm:
286+
// | ((term: string) => void)
287+
// | ((setter: (prevTerm: string) => string) => void);
288+
// }) {
289+
// let results = useCityMatch(term);
290+
291+
// function handleChange(event: any) {
292+
// setTerm(event.target.value);
293+
// }
294+
295+
// return (
296+
// <div>
297+
// <h2>Clientside Search</h2>
298+
// <Combobox id="holy-smokes">
299+
// <ComboboxInput
300+
// aria-label="cool search"
301+
// data-testid="input"
302+
// name="awesome"
303+
// onChange={handleChange}
304+
// value={term}
305+
// />
306+
// {results ? (
307+
// <ComboboxPopover portal={false}>
308+
// {results.length === 0 ? (
309+
// <p>No results</p>
310+
// ) : (
311+
// <ComboboxList data-testid="list">
312+
// {results.slice(0, 10).map((result, index) => (
313+
// <ComboboxOption
314+
// key={index}
315+
// value={`${result.city}, ${result.state}`}
316+
// />
317+
// ))}
318+
// </ComboboxList>
319+
// )}
320+
// </ComboboxPopover>
321+
// ) : (
322+
// <span>No Results!</span>
323+
// )}
324+
// </Combobox>
325+
// </div>
326+
// );
327+
// }
328328

329329
function useCityMatch(term: string) {
330330
return term.trim() === ""

packages/combobox/examples/index.story.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export { Example as Controlled } from "./controlled.example.js";
55
export { Example as LotsOfElements } from "./lots-of-elements.example.js";
66
export { Example as NoPopover } from "./no-popover.example.js";
77
export { Example as OpenOnFocus } from "./open-on-focus.example.js";
8-
export { Example as SimulatedChange } from "./simulated-change.example.js";
8+
// export { Example as SimulatedChange } from "./simulated-change.example.js";
99
export { Example as TokenInput } from "./token-input.example.js";
1010
export { Example as WithButton } from "./with-button.example.js";
1111
export { Example as WithCustomSelectDataTs } from "./with-custom-select-data.example.tsx";

packages/combobox/examples/simulated-change.example.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@ import {
99
import { useCityMatch } from "./utils";
1010
import "@reach/combobox/styles.css";
1111

12+
/**
13+
* TODO: This example is buggy at the moment. The example itself and the bug it
14+
* fixed was introduced in #783, but I merged it before testing thoroughly
15+
* enough and it introduced what I believe are actually a more harmful
16+
* regression in some cases where the popover doesn't open until the user
17+
* presses the down or up key after initial input. Leaving the example and
18+
* related test in place as a TODO.
19+
* See https://github.com/reach/reach-ui/issues/755
20+
*/
21+
1222
let name = "Simulated Change";
1323

1424
function Example() {

packages/combobox/src/index.tsx

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,10 @@ const CLEAR = "CLEAR";
7070
// User is typing
7171
const CHANGE = "CHANGE";
7272

73-
// Any input change that is not triggered by an actual onChange event.
74-
// For example an initial value or a controlled value that was changed.
75-
// Prevents sending the user to the NAVIGATING state
73+
// Initial input value change handler for syncing user state with state machine
74+
// Prevents initial change from sending the user to the NAVIGATING state
7675
// https://github.com/reach/reach-ui/issues/464
77-
const SIMULATED_CHANGE = "SIMULATED_CHANGE";
76+
const INITIAL_CHANGE = "INITIAL_CHANGE";
7877

7978
// User is navigating w/ the keyboard
8079
const NAVIGATE = "NAVIGATE";
@@ -108,7 +107,7 @@ const stateChart: StateChart = {
108107
[BLUR]: IDLE,
109108
[CLEAR]: IDLE,
110109
[CHANGE]: SUGGESTING,
111-
[SIMULATED_CHANGE]: IDLE,
110+
[INITIAL_CHANGE]: IDLE,
112111
[FOCUS]: SUGGESTING,
113112
[NAVIGATE]: NAVIGATING,
114113
[OPEN_WITH_BUTTON]: SUGGESTING,
@@ -161,7 +160,7 @@ const reducer: Reducer = (data: StateData, event: MachineEvent) => {
161160
let nextState = { ...data, lastEventType: event.type };
162161
switch (event.type) {
163162
case CHANGE:
164-
case SIMULATED_CHANGE:
163+
case INITIAL_CHANGE:
165164
return {
166165
...nextState,
167166
navigationValue: null,
@@ -429,8 +428,11 @@ export const ComboboxInput = React.forwardRef(function ComboboxInput(
429428
forwardedRef
430429
) {
431430
// https://github.com/reach/reach-ui/issues/464
432-
// https://github.com/reach/reach-ui/issues/755
433-
let inputValueChangedRef = React.useRef(false);
431+
let { current: initialControlledValue } = React.useRef(controlledValue);
432+
let controlledValueChangedRef = React.useRef(false);
433+
useUpdateEffect(() => {
434+
controlledValueChangedRef.current = true;
435+
}, [controlledValue]);
434436

435437
let {
436438
data: { navigationValue, value, lastEventType },
@@ -469,13 +471,16 @@ export const ComboboxInput = React.forwardRef(function ComboboxInput(
469471
(value: ComboboxValue) => {
470472
if (value.trim() === "") {
471473
transition(CLEAR);
472-
} else if (!inputValueChangedRef.current) {
473-
transition(SIMULATED_CHANGE, { value });
474+
} else if (
475+
value === initialControlledValue &&
476+
!controlledValueChangedRef.current
477+
) {
478+
transition(INITIAL_CHANGE, { value });
474479
} else {
475480
transition(CHANGE, { value });
476481
}
477482
},
478-
[transition]
483+
[initialControlledValue, transition]
479484
);
480485

481486
React.useEffect(() => {
@@ -490,17 +495,13 @@ export const ComboboxInput = React.forwardRef(function ComboboxInput(
490495
) {
491496
handleValueChange(controlledValue!);
492497
}
493-
// After we handled the changed value, we need to make sure the next
494-
// controlled change won't trigger a CHANGE event. (instead of a SIMULATED_CHANGE)
495-
inputValueChangedRef.current = false;
496498
}, [controlledValue, handleValueChange, isControlled, value]);
497499

498500
// [*]... and when controlled, we don't trigger handleValueChange as the
499501
// user types, instead the developer controls it with the normal input
500502
// onChange prop
501503
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
502504
let { value } = event.target;
503-
inputValueChangedRef.current = true;
504505
if (!isControlled) {
505506
handleValueChange(value);
506507
}
@@ -1330,7 +1331,7 @@ type State = "IDLE" | "SUGGESTING" | "NAVIGATING" | "INTERACTING";
13301331
type MachineEventType =
13311332
| "CLEAR"
13321333
| "CHANGE"
1333-
| "SIMULATED_CHANGE"
1334+
| "INITIAL_CHANGE"
13341335
| "NAVIGATE"
13351336
| "SELECT_WITH_KEYBOARD"
13361337
| "SELECT_WITH_CLICK"
@@ -1362,7 +1363,7 @@ interface StateData {
13621363
type MachineEvent =
13631364
| { type: "BLUR" }
13641365
| { type: "CHANGE"; value: ComboboxValue }
1365-
| { type: "SIMULATED_CHANGE"; value: ComboboxValue }
1366+
| { type: "INITIAL_CHANGE"; value: ComboboxValue }
13661367
| { type: "CLEAR" }
13671368
| { type: "CLOSE_WITH_BUTTON" }
13681369
| { type: "ESCAPE" }

0 commit comments

Comments
 (0)