Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/docs/observed.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ Returns the observed element currently in the DOM for a name, or `null`.
const element = observedElement.getElement("myButton");
```

When multiple elements share a name, an accessibility filter skips ineligible
registrations and returns the first eligible match in registration order.
The same selection applies to pending `waitElement()` and `requestFocus()`
calls; an ineligible earlier registration does not block a later eligible one.

### `waitElement()`

Waits (up to `timeout` ms) for an element with the given name to appear.
Expand Down
14 changes: 11 additions & 3 deletions src/State/ObservedElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,9 @@ export class ObservedElementAPI
delete this._observedById[uid];
}

return el;
if (el) {
return el;
}
}
}

Expand Down Expand Up @@ -615,7 +617,10 @@ export class ObservedElementAPI
!waitingAccessibleElement.conditionTimer
) {
const resolveAccessible = () => {
const element = this.getElement(observedName);
const element = this.getElement(
observedName,
ObservedElementAccessibilities.Accessible
);

if (
element &&
Expand Down Expand Up @@ -644,7 +649,10 @@ export class ObservedElementAPI
!waitingFocusableElement.conditionTimer
) {
const resolveFocusable = () => {
const element = this.getElement(observedName);
const element = this.getElement(
observedName,
ObservedElementAccessibilities.Focusable
);

if (
element &&
Expand Down
186 changes: 186 additions & 0 deletions tests/ObservedElement.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as React from "react";
import {
getTabsterAttribute,
Types,
ObservedElementAccessibilities,
ObservedElementRequestStatuses,
ObservedElementFailureReasons,
} from "tabster";
Expand All @@ -17,6 +18,191 @@ describe("Focusable", () => {
await BroTest.bootstrapTabsterPage({ observed: true });
});

it("should return the first eligible duplicate for the requested accessibility", async () => {
const name = "duplicate";
const observed = getTabsterAttribute({ observed: { names: [name] } });

await new BroTest.BroTest(
<div {...getTabsterAttribute({ root: {} })}>
<button id="inaccessible" aria-hidden="true" {...observed}>
Inaccessible
</button>
<button id="first-eligible" {...observed}>
First eligible
</button>
<button id="second-eligible" {...observed}>
Second eligible
</button>
</div>
)
.eval(
(name, accessible, focusable) => {
const observedElement =
getTabsterTestVariables().observedElement;

return {
any: observedElement?.getElement(name)?.id,
accessible: observedElement?.getElement(
name,
accessible
)?.id,
focusable: observedElement?.getElement(name, focusable)
?.id,
};
},
name,
ObservedElementAccessibilities.Accessible,
ObservedElementAccessibilities.Focusable
)
.check((elements) => {
expect(elements).toEqual({
any: "inaccessible",
accessible: "first-eligible",
focusable: "first-eligible",
});
});
});

it.each([false, true])(
"should focus a duplicate after a hidden registration (late: %s)",
async (late) => {
const name = "duplicate";
const observed = getTabsterAttribute({
observed: { names: [name] },
});

await new BroTest.BroTest(
<div id="root" {...getTabsterAttribute({ root: {} })}>
<button hidden {...observed}>
Hidden
</button>
{!late && (
<button id="eligible" {...observed}>
Eligible
</button>
)}
</div>
)
.eval(
async (name, late) => {
const { observedElement, dom } =
getTabsterTestVariables();
const root = dom?.getElementById(document, "root");

if (!observedElement || !root) {
throw new Error("Missing observed test setup");
}

const request = observedElement.requestFocus(
name,
1000
);
const initialStatus = request.status;

if (late) {
const button = document.createElement("button");
button.id = "eligible";
button.textContent = "Eligible";
button.setAttribute(
"data-tabster",
JSON.stringify({
observed: { names: [name] },
})
);
root.appendChild(button);
}

return {
result: await request.result,
status: request.status,
initialStatus,
};
},
name,
late
)
.check(({ result, status, initialStatus }) => {
expect(result).toBe(true);
expect(status).toBe(
ObservedElementRequestStatuses.Succeeded
);
expect(initialStatus).toBe(
late
? ObservedElementRequestStatuses.Waiting
: ObservedElementRequestStatuses.Succeeded
);
})
.activeElement((el) => {
expect(el?.attributes.id).toBe("eligible");
});
}
);

it.each([
ObservedElementAccessibilities.Accessible,
ObservedElementAccessibilities.Focusable,
])(
"should resolve a pending wait for an eligible duplicate (accessibility: %s)",
async (accessibility) => {
const name = "duplicate";

await new BroTest.BroTest(
<div id="root" {...getTabsterAttribute({ root: {} })}>
<button
aria-hidden="true"
{...getTabsterAttribute({
observed: { names: [name] },
})}
>
Inaccessible
</button>
</div>
)
.eval(
async (name, accessibility) => {
const { observedElement, dom } =
getTabsterTestVariables();
const root = dom?.getElementById(document, "root");

if (!observedElement || !root) {
throw new Error("Missing observed test setup");
}

const request = observedElement.waitElement(
name,
1000,
accessibility
);
const initialStatus = request.status;
const button = document.createElement("button");
button.id = "eligible";
button.setAttribute(
"data-tabster",
JSON.stringify({ observed: { names: [name] } })
);
root.appendChild(button);

return {
id: (await request.result)?.id,
status: request.status,
initialStatus,
};
},
name,
accessibility
)
.check(({ id, status, initialStatus }) => {
expect(id).toBe("eligible");
expect(status).toBe(
ObservedElementRequestStatuses.Succeeded
);
expect(initialStatus).toBe(
ObservedElementRequestStatuses.Waiting
);
});
}
);

it("should forward focus options", async () => {
const name = "test";
const focusOptions: Types.TabsterFocusOptions = {
Expand Down
Loading