Fix React 18 strict mode breaking spotlight search

This code originally held an array of refs. But these refs were unset
just before sorting leading to errors.

For the fix, I've used a callback ref to add/remove the DOM elements
to/from the array in state. This way there's nothing that could possibly
mutate just before sort.
This commit is contained in:
R Midhun Suresh 2024-11-13 00:28:32 +05:30
parent 2f8e98242c
commit 0faf298e05
No known key found for this signature in database
3 changed files with 151 additions and 133 deletions

View file

@ -28,6 +28,12 @@ const checkTabIndexes = (buttons: NodeListOf<HTMLElement>, expectations: number[
expect([...buttons].map((b) => b.tabIndex)).toStrictEqual(expectations);
};
const createButtonElement = (text: string): HTMLButtonElement => {
const button = document.createElement("button");
button.textContent = text;
return button;
};
// give the buttons keys for the fibre reconciler to not treat them all as the same
const button1 = <Button key={1}>a</Button>;
const button2 = <Button key={2}>b</Button>;
@ -123,11 +129,7 @@ describe("RovingTabIndex", () => {
{button2}
<RovingTabIndexWrapper>
{({ onFocus, isActive, ref }) => (
<button
onFocus={onFocus}
tabIndex={isActive ? 0 : -1}
ref={ref as React.RefObject<HTMLButtonElement>}
>
<button onFocus={onFocus} tabIndex={isActive ? 0 : -1} ref={ref}>
.
</button>
)}
@ -147,75 +149,75 @@ describe("RovingTabIndex", () => {
describe("reducer functions as expected", () => {
it("SetFocus works as expected", () => {
const ref1 = React.createRef<HTMLElement>();
const ref2 = React.createRef<HTMLElement>();
const node1 = createButtonElement("Button 1");
const node2 = createButtonElement("Button 2");
expect(
reducer(
{
activeRef: ref1,
refs: [ref1, ref2],
activeNode: node1,
nodes: [node1, node2],
},
{
type: Type.SetFocus,
payload: {
ref: ref2,
node: node2,
},
},
),
).toStrictEqual({
activeRef: ref2,
refs: [ref1, ref2],
activeNode: node2,
nodes: [node1, node2],
});
});
it("Unregister works as expected", () => {
const ref1 = React.createRef<HTMLElement>();
const ref2 = React.createRef<HTMLElement>();
const ref3 = React.createRef<HTMLElement>();
const ref4 = React.createRef<HTMLElement>();
const button1 = createButtonElement("Button 1");
const button2 = createButtonElement("Button 2");
const button3 = createButtonElement("Button 3");
const button4 = createButtonElement("Button 4");
let state: IState = {
refs: [ref1, ref2, ref3, ref4],
nodes: [button1, button2, button3, button4],
};
state = reducer(state, {
type: Type.Unregister,
payload: {
ref: ref2,
node: button2,
},
});
expect(state).toStrictEqual({
refs: [ref1, ref3, ref4],
nodes: [button1, button3, button4],
});
state = reducer(state, {
type: Type.Unregister,
payload: {
ref: ref3,
node: button3,
},
});
expect(state).toStrictEqual({
refs: [ref1, ref4],
nodes: [button1, button4],
});
state = reducer(state, {
type: Type.Unregister,
payload: {
ref: ref4,
node: button4,
},
});
expect(state).toStrictEqual({
refs: [ref1],
nodes: [button1],
});
state = reducer(state, {
type: Type.Unregister,
payload: {
ref: ref1,
node: button1,
},
});
expect(state).toStrictEqual({
refs: [],
nodes: [],
});
});
@ -235,122 +237,122 @@ describe("RovingTabIndex", () => {
);
let state: IState = {
refs: [],
nodes: [],
};
state = reducer(state, {
type: Type.Register,
payload: {
ref: ref1,
node: ref1.current!,
},
});
expect(state).toStrictEqual({
activeRef: ref1,
refs: [ref1],
activeNode: ref1.current,
nodes: [ref1.current],
});
state = reducer(state, {
type: Type.Register,
payload: {
ref: ref2,
node: ref2.current!,
},
});
expect(state).toStrictEqual({
activeRef: ref1,
refs: [ref1, ref2],
activeNode: ref1.current,
nodes: [ref1.current, ref2.current],
});
state = reducer(state, {
type: Type.Register,
payload: {
ref: ref3,
node: ref3.current!,
},
});
expect(state).toStrictEqual({
activeRef: ref1,
refs: [ref1, ref2, ref3],
activeNode: ref1.current,
nodes: [ref1.current, ref2.current, ref3.current],
});
state = reducer(state, {
type: Type.Register,
payload: {
ref: ref4,
node: ref4.current!,
},
});
expect(state).toStrictEqual({
activeRef: ref1,
refs: [ref1, ref2, ref3, ref4],
activeNode: ref1.current,
nodes: [ref1.current, ref2.current, ref3.current, ref4.current],
});
// test that the automatic focus switch works for unmounting
state = reducer(state, {
type: Type.SetFocus,
payload: {
ref: ref2,
node: ref2.current!,
},
});
expect(state).toStrictEqual({
activeRef: ref2,
refs: [ref1, ref2, ref3, ref4],
activeNode: ref2.current,
nodes: [ref1.current, ref2.current, ref3.current, ref4.current],
});
state = reducer(state, {
type: Type.Unregister,
payload: {
ref: ref2,
node: ref2.current!,
},
});
expect(state).toStrictEqual({
activeRef: ref3,
refs: [ref1, ref3, ref4],
activeNode: ref3.current,
nodes: [ref1.current, ref3.current, ref4.current],
});
// test that the insert into the middle works as expected
state = reducer(state, {
type: Type.Register,
payload: {
ref: ref2,
node: ref2.current!,
},
});
expect(state).toStrictEqual({
activeRef: ref3,
refs: [ref1, ref2, ref3, ref4],
activeNode: ref3.current,
nodes: [ref1.current, ref2.current, ref3.current, ref4.current],
});
// test that insertion at the edges works
state = reducer(state, {
type: Type.Unregister,
payload: {
ref: ref1,
node: ref1.current!,
},
});
state = reducer(state, {
type: Type.Unregister,
payload: {
ref: ref4,
node: ref4.current!,
},
});
expect(state).toStrictEqual({
activeRef: ref3,
refs: [ref2, ref3],
activeNode: ref3.current,
nodes: [ref2.current, ref3.current],
});
state = reducer(state, {
type: Type.Register,
payload: {
ref: ref1,
node: ref1.current!,
},
});
state = reducer(state, {
type: Type.Register,
payload: {
ref: ref4,
node: ref4.current!,
},
});
expect(state).toStrictEqual({
activeRef: ref3,
refs: [ref1, ref2, ref3, ref4],
activeNode: ref3.current,
nodes: [ref1.current, ref2.current, ref3.current, ref4.current],
});
});
});