From 4a3b5237612433264cb97de4852341e4024c36f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ari=20Perkki=C3=B6?= Date: Thu, 10 Feb 2022 16:30:11 +0000 Subject: [PATCH] perf: reduce unnecessary work - reduce unnecessary queries when live regions are not tracked - narrow scope of queries when possible --- .storybook/preview.ts | 8 ++++++++ .storybook/utils.ts | 7 +++++++ src/capture-announcements.ts | 20 +++++++++++++------- src/utils.ts | 21 +++++++++++++++++---- 4 files changed, 45 insertions(+), 11 deletions(-) diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 238f78a..4ac815b 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -1,3 +1,6 @@ +import { addons } from '@storybook/addons'; +import { STORY_CHANGED } from '@storybook/core-events'; + import CaptureAnnouncements from '../src'; import prettyDOMWithShadowDOM from './pretty-dom-with-shadow-dom'; import { AnnouncementEvents, SourceCodeUpdateEvents } from './utils'; @@ -8,6 +11,11 @@ CaptureAnnouncements({ onCapture: (text, level) => AnnouncementEvents.emit({ text, level }), }); +addons.getChannel().addListener(STORY_CHANGED, () => { + // Reset captures after a story changes + AnnouncementEvents.clear(); +}); + export const decorators = [ function withSourceCode(Story: StoryFn) { const html = Story(); diff --git a/.storybook/utils.ts b/.storybook/utils.ts index c36bc18..3875228 100644 --- a/.storybook/utils.ts +++ b/.storybook/utils.ts @@ -83,9 +83,11 @@ export function createButtonCycle( type Subscriber = (event: T) => void; class EventBus { subscribers: Subscriber[] = []; + events: EventType[] = []; on(subscriber: Subscriber) { this.subscribers.push(subscriber); + this.events.forEach(subscriber); } off(subscriber: Subscriber) { @@ -94,6 +96,11 @@ class EventBus { emit(event?: EventType) { this.subscribers.forEach(subscriber => subscriber(event)); + this.events.push(event); + } + + clear() { + this.events = []; } } diff --git a/src/capture-announcements.ts b/src/capture-announcements.ts index 4fd5db2..6898120 100644 --- a/src/capture-announcements.ts +++ b/src/capture-announcements.ts @@ -43,6 +43,8 @@ export default function CaptureAnnouncements(options: Options): Restore { * - `textContent` of live region should have changed */ function updateAnnouncements(node: Node) { + if (liveRegions.size === 0) return; + const element = getClosestElement(node); if (!element) return; @@ -93,8 +95,10 @@ export default function CaptureAnnouncements(options: Options): Restore { * Check DOM for live regions and update `liveRegions` store * - TODO: Could be optimized based on appended/updated child */ - function updateLiveRegions() { - for (const liveRegion of getAllLiveRegions(document)) { + function updateLiveRegions(node: Node) { + const context = isElement(node) ? node : document; + + for (const liveRegion of getAllLiveRegions(context)) { addLiveRegion(liveRegion); } } @@ -103,7 +107,6 @@ export default function CaptureAnnouncements(options: Options): Restore { updateAnnouncements(this); } - // https://github.com/facebook/react/blob/9198a5cec0936a21a5ba194a22fcbac03eba5d1d/packages/react-dom/src/client/setTextContent.js#L12-L35 function onNodeValueChange(this: Node) { updateAnnouncements(this); } @@ -112,7 +115,7 @@ export default function CaptureAnnouncements(options: Options): Restore { * Shared handler for methods which mount new nodes on DOM, e.g. appendChild, insertBefore */ function onNodeMount(node: Node) { - updateLiveRegions(); + updateLiveRegions(node); updateAnnouncements(node); } @@ -145,7 +148,7 @@ export default function CaptureAnnouncements(options: Options): Restore { // Previous value was not live region attribute value if (!isAlreadyTracked && liveRegionAttribute) { - return updateLiveRegions(); + return updateLiveRegions(this); } // Value was changed to assertive - announce content immediately @@ -160,7 +163,7 @@ export default function CaptureAnnouncements(options: Options): Restore { } case 'aria-hidden': { - updateLiveRegions(); + updateLiveRegions(this); return updateAnnouncements(this); } @@ -181,7 +184,7 @@ export default function CaptureAnnouncements(options: Options): Restore { const [attribute] = args; if (attribute === 'aria-hidden') { - updateLiveRegions(); + updateLiveRegions(this); updateAnnouncements(this); } } @@ -190,6 +193,8 @@ export default function CaptureAnnouncements(options: Options): Restore { this: Element, ...args: Parameters ) { + if (liveRegions.size === 0) return; + const [node] = args; if (node == null || !isElement(node)) { @@ -241,6 +246,7 @@ function onRemoveAttributeBefore( this: Element, ...args: Parameters ) { + if (liveRegions.size === 0) return; if (!isElement(this)) return; const [attribute] = args; diff --git a/src/utils.ts b/src/utils.ts index 69e389e..ccfcc37 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -20,10 +20,19 @@ const LIVE_REGION_QUERY = [ const HIDDEN_QUERY = '[aria-hidden="true"]'; -export function getAllLiveRegions( - context: Document | Element -): ReturnType { - return context.querySelectorAll(LIVE_REGION_QUERY); +export function getAllLiveRegions(context: Document | Element): Element[] { + const liveRegions = Array.from(context.querySelectorAll(LIVE_REGION_QUERY)); + + // Check whether given `context` is also a live region + if ( + isElement(context) && + resolvePolitenessSetting(context) !== 'off' && + isInDOM(context) + ) { + return liveRegions.concat(context).filter(filterUnique); + } + + return liveRegions; } export function getClosestElement(node: Node): Element | null { @@ -142,3 +151,7 @@ export function getTextContent(node: Node | null): string | null { .join(' ') ); } + +function filterUnique(item: T, index: number, array: T[]): boolean { + return array.indexOf(item) === index; +}