From cc7ef068afe9249f032c91764ba29b51d4b68b36 Mon Sep 17 00:00:00 2001 From: Austin Date: Wed, 7 Oct 2026 14:36:34 -0500 Subject: [PATCH 01/10] fix(ui): deduplicate shadow ancestor events --- .changeset/tidy-shadow-events.md | 2 + docs/shadow-dom-consumer-compatibility.md | 5 +- docs/shadow-dom-isolation-plan.md | 18 +- docs/shadow-dom-rollout-policy.md | 2 +- ...compatibility.shadow-isolation.stories.tsx | 248 +++++++++++++++--- packages/ui/src/lib/shadow-root-host.tsx | 22 +- 6 files changed, 247 insertions(+), 50 deletions(-) create mode 100644 .changeset/tidy-shadow-events.md diff --git a/.changeset/tidy-shadow-events.md b/.changeset/tidy-shadow-events.md new file mode 100644 index 00000000..a845151c --- /dev/null +++ b/.changeset/tidy-shadow-events.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/docs/shadow-dom-consumer-compatibility.md b/docs/shadow-dom-consumer-compatibility.md index d294412f..9f8b177d 100644 --- a/docs/shadow-dom-consumer-compatibility.md +++ b/docs/shadow-dom-consumer-compatibility.md @@ -48,7 +48,7 @@ exercise. They do not establish compatibility for every SDK component. | An internal control resolves light-DOM `aria-labelledby` or `aria-describedby` ID references | Unsupported | The attributes remain present, but reflected element arrays are empty across the boundary in Chromium, Firefox, and Playwright WebKit. Keep referenced nodes in the same tree scope. This DOM evidence is not a substitute for assistive-technology testing. | | A native composed event crosses one shadow boundary | Supported with native retargeting | `EventsRefsAndDomQueriesExposeDifferentConsumerViews` clicks an internal label element and verifies that a light-DOM listener receives the shadow host as `event.target`; `composedPath()` begins with the label and includes the internal button and host. Consumers must not assume an external native listener's target is the internal control. | | A React handler passed to `YouVersionAuthButton` receives its button event | Supported for this public component | The same story verifies that the consumer `onClick` handler receives the internal originating label as `target` and the internal button as `currentTarget`. Consumers may rely on the button current target, not on every event originating at the button itself. This is component-specific evidence, not a package-wide promise for every event prop. | -| A consumer React handler wraps an automatically isolated component in the light DOM | Known limitation; fix required before coordinated release | Real-browser validation in YPE-5950 found that one composed click can invoke the ancestor handler twice: once through the shadow-root portal path with the internal target and once through the application-root path with the event retargeted to the shadow host. Keyboard and focus dispatch need the same shared-host audit. Do not use an ancestor React handler as the only state-toggle seam until YPE-6040 deduplicates dispatch. Native listeners retain the single retargeted-event contract above. | +| A consumer React handler wraps an automatically isolated component in the light DOM | Supported for validated bubbling events | YPE-6040 verifies that one native click, keydown, or focus event invokes the corresponding light-DOM ancestor React handler once. Separate native events remain separate, component callbacks retain their target semantics, and native listeners retain the single retargeted-event contract above. Capture-phase handlers and other event types are not part of this evidence. | | A forwarded `YouVersionAuthButton` ref exposes the internal button | Supported after mount | The ref resolves to the exact internal `HTMLButtonElement`. It remains `null` through the consumer's first layout effect because the shadow root attaches in a passive effect; consumers must handle callback-ref updates or read object refs after a later commit. | | An ordinary document or Storybook-canvas selector finds SDK internals | Unsupported | DOM selector APIs do not cross a shadow boundary. `document.querySelector` and Testing Library queries rooted at the document need explicit open-root traversal. Automation behavior is tool-specific: [Playwright locators pierce open roots by default](https://playwright.dev/docs/locators#locate-in-shadow-dom), except for XPath locators, while closed roots remain inaccessible. | | A consumer traverses an open root and queries after attachment | Supported with timing and access constraints | Wait for the host's open `shadowRoot`, then query within it. The contract depends on the prototype's open-root policy and does not make internals a stable semantic API; prefer public refs, roles, and component callbacks where available. | @@ -64,8 +64,7 @@ exercise. They do not establish compatibility for every SDK component. Automatic isolation changes rendered DOM even when React props stay the same. Consumer selectors and global CSS stop reaching internals; native event targets change at every boundary; external form and ID-reference relationships stop -resolving; forwarded refs become available later; and, pending YPE-6040, -light-DOM ancestor React handlers can receive duplicate dispatch. These are compatibility +resolving; and forwarded refs become available later. These are compatibility and potentially breaking changes, not internal implementation details. An open root permits inspection and mutation by same-page JavaScript, so it is diff --git a/docs/shadow-dom-isolation-plan.md b/docs/shadow-dom-isolation-plan.md index d6024d54..37ec82f5 100644 --- a/docs/shadow-dom-isolation-plan.md +++ b/docs/shadow-dom-isolation-plan.md @@ -82,8 +82,8 @@ functions; assistive-technology checks remain open. | Dialog dismissal and restoration | Coverage exercises Escape, backdrop click, full-viewport hit testing, overlay-only focus, and restoration after both modal nodes unmount. | Validated in Chromium, Firefox, Playwright WebKit, and isolated local Safari 26.6.2 runs | Verify real screen-reader behavior. | | Consumer form participation | Browser coverage verifies that a light-DOM form does not own or serialize a native control inside an SDK shadow root. | Unsupported across tree scopes | Use an explicit component contract if a rollout target requires outer-form participation. | | Consumer labels and ARIA ID references | Browser coverage verifies that external native labels, `aria-labelledby`, and `aria-describedby` relationships do not resolve to controls inside the root. | Unsupported across tree scopes | Keep relationships in one tree scope; verify real assistive technology separately. | -| Consumer events, refs, and automation | Coverage verifies native retargeting, the auth button's React handler and forwarded ref, open-root queries, and effect-driven attachment timing. | Supported with documented constraints | Repeat for each public component selected for rollout. | -| Nested shadow roots | Coverage verifies basic rendering, recursive queries, and event retargeting at each boundary. | Supported for the validated basics | Single-active-peer dismissal is accepted; verify assistive-technology behavior and repeat actual-Safari checks for significant platform changes. | +| Consumer events, refs, and automation | Coverage verifies native retargeting, the auth button's React handler and forwarded ref, open-root queries, effect-driven attachment timing, and one light-DOM ancestor dispatch for each validated bubbling click, keydown, and focus event. | Supported with documented constraints | Repeat for each public component selected for rollout. Capture-phase and other event types are not part of this evidence. | +| Nested shadow roots | Coverage verifies basic rendering, recursive queries, event retargeting at each boundary, and one React ancestor dispatch per scope for a nested click. | Supported for the validated basics | Single-active-peer dismissal is accepted; verify assistive-technology behavior and repeat actual-Safari checks for significant platform changes. | | Realistic same-page usage | YPE-5437 mounts, removes, and re-adds a 12-component mix in Normal and Strict Mode. Chromium, Firefox, Playwright WebKit, and local Safari 26.6.2 coverage verifies exact host counts, rendered scripture content, and one shared stylesheet object across roots and remounts. A production-build comparison found a small warm-run mount-cost difference on one machine. | No shared-host blocker found | Repeat user-visible performance and compatibility checks for each component selected for rollout. | ## YPE-5948 leaf evidence @@ -177,9 +177,17 @@ rollout independently. Real Chromium validation also found that a consumer React handler on a light-DOM ancestor can receive one composed event through both the shadow-root -portal path and the application-root retargeted path. The shared-host correction -and click, keyboard, and focus regression coverage are assigned to YPE-6040 and -must land before YPE-5952 releases the coordinated major version. +portal path and the application-root retargeted path. + +YPE-6040 now keeps one React logical traversal for each native click, keydown, +and focus event without stopping native propagation. The representative auth +button story verifies component callback counts and target semantics, exact +light-DOM ancestor counts, separate repeated clicks, native retargeting, and +the original composed path. The nested-root story retains its existing native +retargeting evidence and adds one React ancestor dispatch per scope. The main +workflow passes Chromium, Firefox, and Playwright WebKit. Because the correction +changes the shared host, YPE-5952 retains the required actual-Safari +release-candidate validation; this evidence makes no assistive-technology claim. ## YPE-5951 Bible reader evidence diff --git a/docs/shadow-dom-rollout-policy.md b/docs/shadow-dom-rollout-policy.md index e9f0ba3f..0516024a 100644 --- a/docs/shadow-dom-rollout-policy.md +++ b/docs/shadow-dom-rollout-policy.md @@ -98,7 +98,7 @@ supported component contract. | Ref is unavailable until the passive-effect shadow mount | Accepted | Document the timing. Component tickets verify forwarded refs where the public component exposes one. | | Native outer-form participation and cross-tree label or ARIA ID references do not work | Accepted | Exclude `Textarea`. Included components keep their accessible relationships inside their root or expose explicit props/callbacks. | | Document-rooted selectors and native event targets change | Accepted | Treat this as a breaking change. Document open-root traversal, role-based automation, public callbacks, and composed-event retargeting. | -| A consumer React handler on a light-DOM ancestor can receive one composed event twice | Follow-up YPE-6040 | The shared portal dispatches once through the shadow-root React listener and again through the application-root listener after native retargeting. Document the limitation for integration-branch consumers and deduplicate shared-host dispatch before YPE-5952 releases the coordinated major version. | +| A consumer React handler on a light-DOM ancestor can receive one composed event twice | Resolved by YPE-6040 | The shared host keeps one React logical traversal for each validated bubbling click, keydown, and focus event while preserving native propagation and retargeting. Distinct native events remain distinct, including repeated clicks. Capture-phase and other event types are outside the validated contract. YPE-5952 repeats actual-Safari validation against the release candidate. | | Consumer global CSS and document-level token overrides do not style internals | Accepted | Supported props and component-owned tokens replace accidental global customization; consumer-supplied children require focused review. | | `rem` still follows the owning document root size | Accepted | Review this sizing input per component. | | Font loading and public `@font-face` names remain document-owned | Accepted | `YouVersionProvider` remains responsible for document fonts, including inside same-origin iframe documents. | diff --git a/packages/ui/src/components/consumer-compatibility.shadow-isolation.stories.tsx b/packages/ui/src/components/consumer-compatibility.shadow-isolation.stories.tsx index db623487..1454ad2c 100644 --- a/packages/ui/src/components/consumer-compatibility.shadow-isolation.stories.tsx +++ b/packages/ui/src/components/consumer-compatibility.shadow-isolation.stories.tsx @@ -50,6 +50,8 @@ async function requireShadowHost(container: ParentNode): Promise { } interface NodeEvidenceOutput extends HTMLOutputElement { + count?: number; + key?: string; observedNode?: EventTarget | null; } @@ -220,6 +222,11 @@ function AutomaticButtonHarness(): React.ReactNode { const buttonRef = useRef(null); const firstLayoutRefState = useRef<'pending' | 'null' | 'resolved'>('pending'); const forwardedRefEvidence = useRef(null); + const ancestorClickEvidence = useRef(null); + const ancestorFocusEvidence = useRef(null); + const ancestorKeyDownEvidence = useRef(null); + const consumerFocusEvidence = useRef(null); + const consumerKeyDownEvidence = useRef(null); const consumerTargetEvidence = useRef(null); const consumerCurrentTargetEvidence = useRef(null); const [firstLayoutResult, setFirstLayoutResult] = useState<'pending' | 'null' | 'resolved'>( @@ -246,37 +253,83 @@ function AutomaticButtonHarness(): React.ReactNode { summary="This example compares the views exposed to React consumers, native event listeners, refs, and DOM selector APIs." title="Events, refs, and test queries" > -
- { - if (consumerTargetEvidence.current) { - consumerTargetEvidence.current.observedNode = event.target; + <> +
{ + if (ancestorClickEvidence.current) { + ancestorClickEvidence.current.count = (ancestorClickEvidence.current.count ?? 0) + 1; } - if (consumerCurrentTargetEvidence.current) { - consumerCurrentTargetEvidence.current.observedNode = event.currentTarget; + }} + onFocus={() => { + if (ancestorFocusEvidence.current) { + ancestorFocusEvidence.current.count = (ancestorFocusEvidence.current.count ?? 0) + 1; } }} - /> - -
+ onKeyDown={() => { + if (ancestorKeyDownEvidence.current) { + ancestorKeyDownEvidence.current.count = + (ancestorKeyDownEvidence.current.count ?? 0) + 1; + } + }} + > + { + if (consumerTargetEvidence.current) { + consumerTargetEvidence.current.observedNode = event.target; + } + if (consumerCurrentTargetEvidence.current) { + consumerCurrentTargetEvidence.current.count = + (consumerCurrentTargetEvidence.current.count ?? 0) + 1; + consumerCurrentTargetEvidence.current.observedNode = event.currentTarget; + } + }} + onFocus={(event) => { + if (consumerFocusEvidence.current) { + consumerFocusEvidence.current.count = + (consumerFocusEvidence.current.count ?? 0) + 1; + consumerFocusEvidence.current.observedNode = event.currentTarget; + } + }} + onKeyDown={(event) => { + if (consumerKeyDownEvidence.current) { + consumerKeyDownEvidence.current.count = + (consumerKeyDownEvidence.current.count ?? 0) + 1; + consumerKeyDownEvidence.current.key = event.key; + consumerKeyDownEvidence.current.observedNode = event.currentTarget; + } + }} + /> + +
+ + ); } export const EventsRefsAndDomQueriesExposeDifferentConsumerViews: Story = { name: 'Events, refs, and DOM queries expose different views', + tags: ['cross-browser'], render: () => , play: async ({ canvasElement }) => { const observer = await waitFor(async () => @@ -306,6 +359,36 @@ export const EventsRefsAndDomQueriesExposeDifferentConsumerViews: Story = { '[data-testid="forwarded-ref-evidence"]', 'forwarded ref evidence not rendered', ); + const ancestorClickEvidence = requireElement( + canvasElement, + '[data-testid="ancestor-click-evidence"]', + 'ancestor click evidence not rendered', + ); + const ancestorFocusEvidence = requireElement( + canvasElement, + '[data-testid="ancestor-focus-evidence"]', + 'ancestor focus evidence not rendered', + ); + const ancestorKeyDownEvidence = requireElement( + canvasElement, + '[data-testid="ancestor-keydown-evidence"]', + 'ancestor keydown evidence not rendered', + ); + const consumerKeyDownEvidence = requireElement( + canvasElement, + '[data-testid="consumer-keydown-evidence"]', + 'consumer keydown evidence not rendered', + ); + const consumerFocusEvidence = requireElement( + canvasElement, + '[data-testid="consumer-focus-evidence"]', + 'consumer focus evidence not rendered', + ); + const outsideFocusTarget = requireElement( + canvasElement, + '[data-testid="outside-focus-target"]', + 'outside focus target not rendered', + ); const consumerTargetEvidence = requireElement( canvasElement, '[data-testid="consumer-target-evidence"]', @@ -326,29 +409,78 @@ export const EventsRefsAndDomQueriesExposeDifferentConsumerViews: Story = { 'div', 'auth button label click target not rendered', ); - let outsideTarget: EventTarget | null = null; - let outsidePath: EventTarget[] = []; - observer.addEventListener( - 'click', - (event) => { - outsideTarget = event.target; - outsidePath = event.composedPath(); - }, - { once: true }, + const outsideClicks: Array<{ + event: Event; + path: EventTarget[]; + target: EventTarget | null; + }> = []; + observer.addEventListener('click', (event) => { + outsideClicks.push({ event, path: event.composedPath(), target: event.target }); + }); + const outsideKeyDowns: Array<{ + path: EventTarget[]; + target: EventTarget | null; + }> = []; + observer.addEventListener('keydown', (event) => { + outsideKeyDowns.push({ path: event.composedPath(), target: event.target }); + }); + const outsideFocusEvents: Array<{ + path: EventTarget[]; + target: EventTarget | null; + }> = []; + observer.addEventListener('focusin', (event) => { + outsideFocusEvents.push({ path: event.composedPath(), target: event.target }); + }); + + outsideFocusTarget.focus(); + button.focus(); + + await expect(root.activeElement).toBe(button); + await expect(ancestorFocusEvidence.count).toBe(1); + await expect(consumerFocusEvidence.count).toBe(1); + await expect(consumerFocusEvidence.observedNode).toBe(button); + await expect(outsideFocusEvents).toHaveLength(1); + await expect(outsideFocusEvents[0]?.target).toBe(host); + await expect(outsideFocusEvents[0]?.path[0]).toBe(button); + await expect(outsideFocusEvents[0]?.path).toContain(host); + + button.dispatchEvent( + new button.ownerDocument.defaultView!.KeyboardEvent('keydown', { + bubbles: true, + composed: true, + key: 'Enter', + }), ); + await expect(ancestorKeyDownEvidence.count).toBe(1); + await expect(consumerKeyDownEvidence.count).toBe(1); + await expect(consumerKeyDownEvidence.key).toBe('Enter'); + await expect(consumerKeyDownEvidence.observedNode).toBe(button); + await expect(outsideKeyDowns).toHaveLength(1); + await expect(outsideKeyDowns[0]?.target).toBe(host); + await expect(outsideKeyDowns[0]?.path[0]).toBe(button); + await expect(outsideKeyDowns[0]?.path).toContain(host); + + await userEvent.click(clickTarget); await userEvent.click(clickTarget); - await expect(outsideTarget).toBe(host); - await expect(outsidePath[0]).toBe(clickTarget); - await expect(outsidePath).toContain(button); - await expect(outsidePath).toContain(host); + await expect(ancestorClickEvidence.count).toBe(2); + await expect(consumerCurrentTargetEvidence.count).toBe(2); + await expect(outsideClicks).toHaveLength(2); + await expect(new Set(outsideClicks.map(({ event }) => event))).toHaveLength(2); + await expect(outsideClicks[1]?.target).toBe(host); + await expect(outsideClicks[1]?.path[0]).toBe(clickTarget); + await expect(outsideClicks[1]?.path).toContain(button); + await expect(outsideClicks[1]?.path).toContain(host); await expect(consumerTargetEvidence.observedNode).toBe(clickTarget); await expect(consumerCurrentTargetEvidence.observedNode).toBe(button); }, }; function NestedRootsHarness(): React.ReactNode { + const lightDomAncestorEvidence = useRef(null); + const outerShadowAncestorEvidence = useRef(null); + return ( - -
- -
-
+
{ + if (lightDomAncestorEvidence.current) { + lightDomAncestorEvidence.current.count = + (lightDomAncestorEvidence.current.count ?? 0) + 1; + } + }} + > + +
{ + if (outerShadowAncestorEvidence.current) { + outerShadowAncestorEvidence.current.count = + (outerShadowAncestorEvidence.current.count ?? 0) + 1; + } + }} + > + +
+
+
+
); } @@ -370,6 +526,16 @@ export const NestedRootsRequireTraversalAndRetargetAtEveryBoundary: Story = { render: () => , play: async ({ canvasElement }) => { const outerHost = await requireShadowHost(canvasElement); + const lightDomAncestorEvidence = requireElement( + canvasElement, + '[data-testid="light-dom-ancestor-evidence"]', + 'light DOM ancestor evidence not rendered', + ); + const outerShadowAncestorEvidence = requireElement( + canvasElement, + '[data-testid="outer-shadow-ancestor-evidence"]', + 'outer shadow ancestor evidence not rendered', + ); const outerRoot = outerHost.shadowRoot!; const outerObserver = await waitForElement( outerRoot, @@ -409,6 +575,8 @@ export const NestedRootsRequireTraversalAndRetargetAtEveryBoundary: Story = { await userEvent.click(button); + await expect(lightDomAncestorEvidence.count).toBe(1); + await expect(outerShadowAncestorEvidence.count).toBe(1); await expect(outerScopeTarget).toBe(innerHost); await expect(documentScopeTarget).toBe(outerHost); await expect(composedPath[0]).toBe(button); diff --git a/packages/ui/src/lib/shadow-root-host.tsx b/packages/ui/src/lib/shadow-root-host.tsx index 33911d87..cf355362 100644 --- a/packages/ui/src/lib/shadow-root-host.tsx +++ b/packages/ui/src/lib/shadow-root-host.tsx @@ -9,6 +9,7 @@ import { useRef, useState, type ReactNode, + type SyntheticEvent, } from 'react'; import { createPortal } from 'react-dom'; @@ -202,6 +203,7 @@ export function ShadowRootHost({ const activePortalIdsRef = useRef(new Set()); const contentWrapperRef = useRef(null); const presentModalIdsRef = useRef(new Set()); + const dispatchedEventsRef = useRef(new WeakSet()); const pendingFocusTargetRef = useRef< (ShadowFocusRestoreSnapshot & { ownerId: string }) | null >(null); @@ -212,6 +214,18 @@ export function ShadowRootHost({ const setHostRef = useCallback((node: HTMLElement | null): void => { hostRef.current = node; }, []); + const stopDuplicateReactPropagation = useCallback((event: SyntheticEvent): void => { + const nativeEvent = event.nativeEvent; + if (!dispatchedEventsRef.current.has(nativeEvent)) { + dispatchedEventsRef.current.add(nativeEvent); + return; + } + + // A portal event follows the React tree once from inside the shadow root, + // then the same native event reaches the application root after retargeting. + // Stop only React's second logical traversal so native propagation remains intact. + event.isPropagationStopped = () => true; + }, []); const hideIfIdle = useCallback( (): void => { @@ -421,7 +435,13 @@ export function ShadowRootHost({ }, [shrinkableBlockHost]); return ( - + {shadowRoot ? createPortal( From c069f26f3d49d05aa0260cf5dd9fd602dc8792db Mon Sep 17 00:00:00 2001 From: Austin Date: Wed, 7 Oct 2026 14:41:37 -0500 Subject: [PATCH 02/10] test(ui): simplify shadow event evidence --- ...compatibility.shadow-isolation.stories.tsx | 195 +++++++----------- 1 file changed, 72 insertions(+), 123 deletions(-) diff --git a/packages/ui/src/components/consumer-compatibility.shadow-isolation.stories.tsx b/packages/ui/src/components/consumer-compatibility.shadow-isolation.stories.tsx index 1454ad2c..c1478ec0 100644 --- a/packages/ui/src/components/consumer-compatibility.shadow-isolation.stories.tsx +++ b/packages/ui/src/components/consumer-compatibility.shadow-isolation.stories.tsx @@ -49,10 +49,24 @@ async function requireShadowHost(container: ParentNode): Promise { }); } -interface NodeEvidenceOutput extends HTMLOutputElement { - count?: number; - key?: string; - observedNode?: EventTarget | null; +interface AutomaticButtonEvidenceOutput extends HTMLOutputElement { + ancestorClickCount?: number; + ancestorFocusCount?: number; + ancestorKeyDownCount?: number; + componentClickCount?: number; + componentClickCurrentTarget?: EventTarget | null; + componentClickTarget?: EventTarget | null; + componentFocusCount?: number; + componentFocusCurrentTarget?: EventTarget | null; + componentKey?: string; + componentKeyDownCount?: number; + componentKeyDownCurrentTarget?: EventTarget | null; + forwardedRefNode?: EventTarget | null; +} + +interface NestedRootsEvidenceOutput extends HTMLOutputElement { + lightDomAncestorClickCount?: number; + outerShadowAncestorClickCount?: number; } interface CompatibilityScenarioProps { @@ -221,14 +235,7 @@ export const FormsAndExternalRelationshipsStopAtTheTreeScope: Story = { function AutomaticButtonHarness(): React.ReactNode { const buttonRef = useRef(null); const firstLayoutRefState = useRef<'pending' | 'null' | 'resolved'>('pending'); - const forwardedRefEvidence = useRef(null); - const ancestorClickEvidence = useRef(null); - const ancestorFocusEvidence = useRef(null); - const ancestorKeyDownEvidence = useRef(null); - const consumerFocusEvidence = useRef(null); - const consumerKeyDownEvidence = useRef(null); - const consumerTargetEvidence = useRef(null); - const consumerCurrentTargetEvidence = useRef(null); + const evidenceRef = useRef(null); const [firstLayoutResult, setFirstLayoutResult] = useState<'pending' | 'null' | 'resolved'>( 'pending', ); @@ -243,7 +250,7 @@ function AutomaticButtonHarness(): React.ReactNode { const receiveButtonRef = useCallback((node: HTMLButtonElement | null): void => { buttonRef.current = node; - if (forwardedRefEvidence.current) forwardedRefEvidence.current.observedNode = node; + if (evidenceRef.current) evidenceRef.current.forwardedRefNode = node; }, []); return ( @@ -257,19 +264,21 @@ function AutomaticButtonHarness(): React.ReactNode {
{ - if (ancestorClickEvidence.current) { - ancestorClickEvidence.current.count = (ancestorClickEvidence.current.count ?? 0) + 1; + if (evidenceRef.current) { + evidenceRef.current.ancestorClickCount = + (evidenceRef.current.ancestorClickCount ?? 0) + 1; } }} onFocus={() => { - if (ancestorFocusEvidence.current) { - ancestorFocusEvidence.current.count = (ancestorFocusEvidence.current.count ?? 0) + 1; + if (evidenceRef.current) { + evidenceRef.current.ancestorFocusCount = + (evidenceRef.current.ancestorFocusCount ?? 0) + 1; } }} onKeyDown={() => { - if (ancestorKeyDownEvidence.current) { - ancestorKeyDownEvidence.current.count = - (ancestorKeyDownEvidence.current.count ?? 0) + 1; + if (evidenceRef.current) { + evidenceRef.current.ancestorKeyDownCount = + (evidenceRef.current.ancestorKeyDownCount ?? 0) + 1; } }} > @@ -278,46 +287,33 @@ function AutomaticButtonHarness(): React.ReactNode { data-testid="isolated-auth-button" mode="signOut" onClick={(event) => { - if (consumerTargetEvidence.current) { - consumerTargetEvidence.current.observedNode = event.target; - } - if (consumerCurrentTargetEvidence.current) { - consumerCurrentTargetEvidence.current.count = - (consumerCurrentTargetEvidence.current.count ?? 0) + 1; - consumerCurrentTargetEvidence.current.observedNode = event.currentTarget; + if (evidenceRef.current) { + evidenceRef.current.componentClickCount = + (evidenceRef.current.componentClickCount ?? 0) + 1; + evidenceRef.current.componentClickTarget = event.target; + evidenceRef.current.componentClickCurrentTarget = event.currentTarget; } }} onFocus={(event) => { - if (consumerFocusEvidence.current) { - consumerFocusEvidence.current.count = - (consumerFocusEvidence.current.count ?? 0) + 1; - consumerFocusEvidence.current.observedNode = event.currentTarget; + if (evidenceRef.current) { + evidenceRef.current.componentFocusCount = + (evidenceRef.current.componentFocusCount ?? 0) + 1; + evidenceRef.current.componentFocusCurrentTarget = event.currentTarget; } }} onKeyDown={(event) => { - if (consumerKeyDownEvidence.current) { - consumerKeyDownEvidence.current.count = - (consumerKeyDownEvidence.current.count ?? 0) + 1; - consumerKeyDownEvidence.current.key = event.key; - consumerKeyDownEvidence.current.observedNode = event.currentTarget; + if (evidenceRef.current) { + evidenceRef.current.componentKeyDownCount = + (evidenceRef.current.componentKeyDownCount ?? 0) + 1; + evidenceRef.current.componentKey = event.key; + evidenceRef.current.componentKeyDownCurrentTarget = event.currentTarget; } }} /> -