diff --git a/docs/docs/observed.md b/docs/docs/observed.md index 076e5bff..8636e1e9 100644 --- a/docs/docs/observed.md +++ b/docs/docs/observed.md @@ -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. diff --git a/src/State/ObservedElement.ts b/src/State/ObservedElement.ts index 943bd66d..d7f14bbd 100644 --- a/src/State/ObservedElement.ts +++ b/src/State/ObservedElement.ts @@ -280,7 +280,9 @@ export class ObservedElementAPI delete this._observedById[uid]; } - return el; + if (el) { + return el; + } } } @@ -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 && @@ -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 && diff --git a/tests/ObservedElement.test.tsx b/tests/ObservedElement.test.tsx index 1a766dff..3d5b1e95 100644 --- a/tests/ObservedElement.test.tsx +++ b/tests/ObservedElement.test.tsx @@ -7,6 +7,7 @@ import * as React from "react"; import { getTabsterAttribute, Types, + ObservedElementAccessibilities, ObservedElementRequestStatuses, ObservedElementFailureReasons, } from "tabster"; @@ -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( +
+ + + +
+ ) + .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( +
+ + {!late && ( + + )} +
+ ) + .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( +
+ +
+ ) + .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 = {