diff --git a/packages/combobox/examples/simulated-change.example.js b/packages/combobox/examples/simulated-change.example.js
new file mode 100644
index 000000000..5fb92f057
--- /dev/null
+++ b/packages/combobox/examples/simulated-change.example.js
@@ -0,0 +1,91 @@
+import * as React from "react";
+import {
+ Combobox,
+ ComboboxInput,
+ ComboboxList,
+ ComboboxOption,
+ ComboboxPopover,
+} from "@reach/combobox";
+import { useCityMatch } from "./utils";
+import "@reach/combobox/styles.css";
+
+let name = "Simulated Change";
+
+function Example() {
+ let [term, setTerm] = React.useState("Detroit");
+ let [selection, setSelection] = React.useState("");
+ let results = useCityMatch(term);
+ let ref = React.useRef();
+
+ const handleChange = (event) => {
+ setTerm(event.target.value);
+ };
+
+ const handleSelect = (value) => {
+ setSelection(value);
+ setTerm("");
+ };
+
+ const handleSimulateChange = () => {
+ setTerm("New York");
+ };
+
+ return (
+
+
Clientside Search
+
+ This example tests that changes to the controlled value of Combobox
+ don't expand it unless we are actually typing. The initial value and
+ programmatically set value here shouldn't open the Popover.
+
+
Selection: {selection}
+
Term: {term}
+
+
+ Set value programmatically
+
+
+
+
+ {results && (
+
+ {results.length === 0 && (
+
+ No Results{" "}
+ {
+ setTerm("");
+ ref.current.focus();
+ }}
+ >
+ clear
+
+
+ )}
+
+ {results.slice(0, 10).map((result, index) => (
+
+ ))}
+
+
+ Add a record
+
+
+ )}
+
+
+ );
+}
+
+Example.story = { name };
+export const Comp = Example;
+export default { title: "Combobox" };
diff --git a/packages/combobox/src/index.tsx b/packages/combobox/src/index.tsx
index 8bb5db349..e88c71505 100644
--- a/packages/combobox/src/index.tsx
+++ b/packages/combobox/src/index.tsx
@@ -70,10 +70,11 @@ const CLEAR = "CLEAR";
// User is typing
const CHANGE = "CHANGE";
-// Initial input value change handler for syncing user state with state machine
-// Prevents initial change from sending the user to the NAVIGATING state
+// Any input change that is not triggered by an actual onChange event.
+// For example an initial value or a controlled value that was changed.
+// Prevents sending the user to the NAVIGATING state
// https://github.com/reach/reach-ui/issues/464
-const INITIAL_CHANGE = "INITIAL_CHANGE";
+const SIMULATED_CHANGE = "SIMULATED_CHANGE";
// User is navigating w/ the keyboard
const NAVIGATE = "NAVIGATE";
@@ -107,7 +108,7 @@ const stateChart: StateChart = {
[BLUR]: IDLE,
[CLEAR]: IDLE,
[CHANGE]: SUGGESTING,
- [INITIAL_CHANGE]: IDLE,
+ [SIMULATED_CHANGE]: IDLE,
[FOCUS]: SUGGESTING,
[NAVIGATE]: NAVIGATING,
[OPEN_WITH_BUTTON]: SUGGESTING,
@@ -160,7 +161,7 @@ const reducer: Reducer = (data: StateData, event: MachineEvent) => {
let nextState = { ...data, lastEventType: event.type };
switch (event.type) {
case CHANGE:
- case INITIAL_CHANGE:
+ case SIMULATED_CHANGE:
return {
...nextState,
navigationValue: null,
@@ -428,11 +429,8 @@ export const ComboboxInput = React.forwardRef(function ComboboxInput(
forwardedRef
) {
// https://github.com/reach/reach-ui/issues/464
- let { current: initialControlledValue } = React.useRef(controlledValue);
- let controlledValueChangedRef = React.useRef(false);
- useUpdateEffect(() => {
- controlledValueChangedRef.current = true;
- }, [controlledValue]);
+ // https://github.com/reach/reach-ui/issues/755
+ let inputValueChangedRef = React.useRef(false);
let {
data: { navigationValue, value, lastEventType },
@@ -471,16 +469,13 @@ export const ComboboxInput = React.forwardRef(function ComboboxInput(
(value: ComboboxValue) => {
if (value.trim() === "") {
transition(CLEAR);
- } else if (
- value === initialControlledValue &&
- !controlledValueChangedRef.current
- ) {
- transition(INITIAL_CHANGE, { value });
+ } else if (!inputValueChangedRef.current) {
+ transition(SIMULATED_CHANGE, { value });
} else {
transition(CHANGE, { value });
}
},
- [initialControlledValue, transition]
+ [transition]
);
React.useEffect(() => {
@@ -495,6 +490,9 @@ export const ComboboxInput = React.forwardRef(function ComboboxInput(
) {
handleValueChange(controlledValue!);
}
+ // After we handled the changed value, we need to make sure the next
+ // controlled change won't trigger a CHANGE event. (instead of a SIMULATED_CHANGE)
+ inputValueChangedRef.current = false;
}, [controlledValue, handleValueChange, isControlled, value]);
// [*]... and when controlled, we don't trigger handleValueChange as the
@@ -502,6 +500,7 @@ export const ComboboxInput = React.forwardRef(function ComboboxInput(
// onChange prop
function handleChange(event: React.ChangeEvent) {
let { value } = event.target;
+ inputValueChangedRef.current = true;
if (!isControlled) {
handleValueChange(value);
}
@@ -1331,7 +1330,7 @@ type State = "IDLE" | "SUGGESTING" | "NAVIGATING" | "INTERACTING";
type MachineEventType =
| "CLEAR"
| "CHANGE"
- | "INITIAL_CHANGE"
+ | "SIMULATED_CHANGE"
| "NAVIGATE"
| "SELECT_WITH_KEYBOARD"
| "SELECT_WITH_CLICK"
@@ -1363,7 +1362,7 @@ interface StateData {
type MachineEvent =
| { type: "BLUR" }
| { type: "CHANGE"; value: ComboboxValue }
- | { type: "INITIAL_CHANGE"; value: ComboboxValue }
+ | { type: "SIMULATED_CHANGE"; value: ComboboxValue }
| { type: "CLEAR" }
| { type: "CLOSE_WITH_BUTTON" }
| { type: "ESCAPE" }