Socket
Book a DemoInstallSign in
Socket

accented

Package Overview
Dependencies
Maintainers
1
Versions
18
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

accented - npm Package Compare versions

Comparing version

to
0.0.0-20250618181418

dist/common/tokens.d.ts

4

dist/accented.d.ts

@@ -1,2 +0,2 @@

import type { AccentedOptions, DisableAccented } from './types';
import type { AccentedOptions, DisableAccented } from './types.ts';
export type { AccentedOptions, DisableAccented };

@@ -28,3 +28,3 @@ /**

*/
export default function accented(options?: AccentedOptions): DisableAccented;
export declare function accented(options?: AccentedOptions): DisableAccented;
//# sourceMappingURL=accented.d.ts.map

@@ -1,15 +0,15 @@

import registerElements from './register-elements.js';
import createDomUpdater from './dom-updater.js';
import createLogger from './logger.js';
import createScanner from './scanner.js';
import setupScrollListeners from './scroll-listeners.js';
import setupResizeListener from './resize-listener.js';
import setupFullscreenListener from './fullscreen-listener.js';
import setupIntersectionObserver from './intersection-observer.js';
import { createDomUpdater } from './dom-updater.js';
import { setupResizeListener as setupFullscreenListener } from './fullscreen-listener.js';
import { setupIntersectionObserver } from './intersection-observer.js';
import { logAndRethrow } from './log-and-rethrow.js';
import { createLogger } from './logger.js';
import { registerElements } from './register-elements.js';
import { setupResizeListener } from './resize-listener.js';
import { createScanner } from './scanner.js';
import { setupScrollListeners } from './scroll-listeners.js';
import { enabled, extendedElementsWithIssues } from './state.js';
import deepMerge from './utils/deep-merge.js';
import validateOptions from './validate-options.js';
import supportsAnchorPositioning from './utils/supports-anchor-positioning.js';
import logAndRethrow from './log-and-rethrow.js';
import { initializeContainingBlockSupportSet } from './utils/containing-blocks.js';
import { deepMerge } from './utils/deep-merge.js';
import { supportsAnchorPositioning } from './utils/supports-anchor-positioning.js';
import { validateOptions } from './validate-options.js';
/**

@@ -40,3 +40,3 @@ * Enables highlighting of elements with accessibility issues.

*/
export default function accented(options = {}) {
export function accented(options = {}) {
validateOptions(options);

@@ -50,7 +50,7 @@ try {

const defaultOutput = {
console: true
console: true,
};
const defaultThrottle = {
wait: 1000,
leading: true
leading: true,
};

@@ -68,3 +68,3 @@ // IMPORTANT: when changing any of the properties or values, also do the following:

throttle: defaultThrottle,
callback: () => { }
callback: () => { },
};

@@ -81,9 +81,13 @@ const { context, axeOptions, name, output, throttle, callback } = deepMerge(defaultOptions, options);

registerElements(name);
const { disconnect: cleanupIntersectionObserver, intersectionObserver } = supportsAnchorPositioning(window) ? {} : setupIntersectionObserver();
const { disconnect: cleanupIntersectionObserver, intersectionObserver } = setupIntersectionObserver();
const cleanupScanner = createScanner(name, context, axeOptions, throttle, callback);
const cleanupDomUpdater = createDomUpdater(name, intersectionObserver);
const cleanupLogger = output.console ? createLogger() : () => { };
const cleanupScrollListeners = supportsAnchorPositioning(window) ? () => { } : setupScrollListeners();
const cleanupResizeListener = supportsAnchorPositioning(window) ? () => { } : setupResizeListener();
const cleanupFullscreenListener = supportsAnchorPositioning(window) ? () => { } : setupFullscreenListener();
const cleanupScrollListeners = setupScrollListeners();
const cleanupResizeListener = supportsAnchorPositioning(window)
? () => { }
: setupResizeListener();
const cleanupFullscreenListener = supportsAnchorPositioning(window)
? () => { }
: setupFullscreenListener();
return () => {

@@ -90,0 +94,0 @@ try {

@@ -1,2 +0,2 @@

export default function createDomUpdater(name: string, intersectionObserver?: IntersectionObserver): () => void;
export declare function createDomUpdater(name: string, intersectionObserver?: IntersectionObserver): () => void;
//# sourceMappingURL=dom-updater.d.ts.map
import { effect } from '@preact/signals-core';
import { primaryColor } from './common/tokens.js';
import { extendedElementsWithIssues, rootNodes } from './state.js';
import areElementsWithIssuesEqual from './utils/are-elements-with-issues-equal.js';
import supportsAnchorPositioning from './utils/supports-anchor-positioning.js';
import { areElementsWithIssuesEqual } from './utils/are-elements-with-issues-equal.js';
import { isDocument, isDocumentFragment, isShadowRoot } from './utils/dom-helpers.js';
import getParent from './utils/get-parent.js';
import { getParent } from './utils/get-parent.js';
import { supportsAnchorPositioning } from './utils/supports-anchor-positioning.js';
const shouldInsertTriggerInsideElement = (element) => {

@@ -27,3 +28,3 @@ /**

};
export default function createDomUpdater(name, intersectionObserver) {
export function createDomUpdater(name, intersectionObserver) {
const attrName = `data-${name}`;

@@ -33,4 +34,4 @@ function getAnchorNames(anchorNameValue) {

.split(',')
.map(anchorName => anchorName.trim())
.filter(anchorName => anchorName.startsWith('--'));
.map((anchorName) => anchorName.trim())
.filter((anchorName) => anchorName.startsWith('--'));
}

@@ -98,3 +99,3 @@ function setAnchorName(elementWithIssues) {

/* OKLCH stuff: https://oklch.com/ */
--${name}-primary-color: oklch(0.5 0.3 0);
--${name}-primary-color: ${primaryColor};
--${name}-secondary-color: oklch(0.98 0 0);

@@ -117,4 +118,4 @@ --${name}-outline-width: 2px;

const newRootNodes = rootNodes.value;
const addedRootNodes = [...newRootNodes].filter(rootNode => !previousRootNodes.has(rootNode));
const removedRootNodes = [...previousRootNodes].filter(rootNode => !newRootNodes.has(rootNode));
const addedRootNodes = [...newRootNodes].filter((rootNode) => !previousRootNodes.has(rootNode));
const removedRootNodes = [...previousRootNodes].filter((rootNode) => !newRootNodes.has(rootNode));
for (const rootNode of addedRootNodes) {

@@ -133,7 +134,7 @@ if (isDocument(rootNode) || (isDocumentFragment(rootNode) && isShadowRoot(rootNode))) {

const disposeOfElementsEffect = effect(() => {
const added = extendedElementsWithIssues.value.filter(elementWithIssues => {
return !previousExtendedElementsWithIssues.some(previousElementWithIssues => areElementsWithIssuesEqual(previousElementWithIssues, elementWithIssues));
const added = extendedElementsWithIssues.value.filter((elementWithIssues) => {
return !previousExtendedElementsWithIssues.some((previousElementWithIssues) => areElementsWithIssuesEqual(previousElementWithIssues, elementWithIssues));
});
const removed = previousExtendedElementsWithIssues.filter(previousElementWithIssues => {
return !extendedElementsWithIssues.value.some(elementWithIssues => areElementsWithIssuesEqual(elementWithIssues, previousElementWithIssues));
const removed = previousExtendedElementsWithIssues.filter((previousElementWithIssues) => {
return !extendedElementsWithIssues.value.some((elementWithIssues) => areElementsWithIssuesEqual(elementWithIssues, previousElementWithIssues));
});

@@ -140,0 +141,0 @@ removeIssues(removed);

@@ -1,3 +0,3 @@

import type { Issue } from '../types';
import type { Signal } from '@preact/signals-core';
import type { Issue } from '../types.ts';
export interface AccentedDialog extends HTMLElement {

@@ -9,3 +9,3 @@ issues: Signal<Array<Issue>> | undefined;

}
declare const _default: () => {
export declare const getAccentedDialog: () => {
new (): {

@@ -360,3 +360,2 @@ "__#1@#abortController": AbortController | undefined;

};
export default _default;
//# sourceMappingURL=accented-dialog.d.ts.map

@@ -1,7 +0,8 @@

import getElementHtml from '../utils/get-element-html.js';
import { accentedUrl } from '../constants.js';
import logAndRethrow from '../log-and-rethrow.js';
import { logAndRethrow } from '../log-and-rethrow.js';
import { getElementHtml } from '../utils/get-element-html.js';
import { isNonEmpty } from '../utils/is-non-empty.js';
// We want Accented to not throw an error in Node, and use static imports,
// so we can't export `class extends HTMLElement` because HTMLElement is not available in Node.
export default () => {
export const getAccentedDialog = () => {
const dialogTemplate = document.createElement('template');

@@ -235,4 +236,4 @@ dialogTemplate.innerHTML = `

constructor() {
super();
try {
super();
this.attachShadow({ mode: 'open' });

@@ -293,5 +294,5 @@ const content = dialogTemplate.content.cloneNode(true);

if (title && impact && description) {
title.textContent = issue.title + ' (' + issue.id + ')';
title.textContent = `${issue.title} (${issue.id})`;
title.href = issue.url;
impact.textContent = 'User impact: ' + issue.impact;
impact.textContent = `User impact: ${issue.impact}`;
impact.setAttribute('data-impact', String(issue.impact));

@@ -302,3 +303,6 @@ const descriptionItems = issue.description.split(/\n\s*/);

const descriptionList = descriptionContent.querySelector('ul');
if (descriptionTitle && descriptionList && descriptionItems.length > 1) {
if (descriptionTitle &&
descriptionList &&
isNonEmpty(descriptionItems) &&
descriptionItems.length > 1) {
descriptionTitle.textContent = descriptionItems[0];

@@ -359,3 +363,5 @@ for (const descriptionItem of descriptionItems.slice(1)) {

const dialog = event.currentTarget;
if (!dialog || typeof dialog.getBoundingClientRect !== 'function' || typeof dialog.close !== 'function') {
if (!dialog ||
typeof dialog.getBoundingClientRect !== 'function' ||
typeof dialog.close !== 'function') {
return;

@@ -362,0 +368,0 @@ }

@@ -1,4 +0,4 @@

import type { AccentedDialog } from './accented-dialog';
import type { Position } from '../types';
import type { Signal } from '@preact/signals-core';
import type { Position } from '../types.ts';
import type { AccentedDialog } from './accented-dialog.ts';
export interface AccentedTrigger extends HTMLElement {

@@ -10,3 +10,3 @@ element: Element | undefined;

}
declare const _default: (name: string) => {
export declare const getAccentedTrigger: (name: string) => {
new (): {

@@ -365,3 +365,2 @@ "__#2@#abortController": AbortController | undefined;

};
export default _default;
//# sourceMappingURL=accented-trigger.d.ts.map
import { effect } from '@preact/signals-core';
import supportsAnchorPositioning from '../utils/supports-anchor-positioning.js';
import logAndRethrow from '../log-and-rethrow.js';
import { logAndRethrow } from '../log-and-rethrow.js';
import { supportsAnchorPositioning } from '../utils/supports-anchor-positioning.js';
// We want Accented to not throw an error in Node, and use static imports,
// so we can't export `class extends HTMLElement` because HTMLElement is not available in Node.
export default (name) => {
export const getAccentedTrigger = (name) => {
const template = document.createElement('template');

@@ -24,4 +24,2 @@ // I initially tried creating a CSSStyelSheet object with styles instead of having a <style> element in the template,

position-visibility: anchors-visible !important;
/* Revert potential effects of white-space: pre; set on a trigger's ancestor. */

@@ -87,4 +85,4 @@ white-space: normal !important;

constructor() {
super();
try {
super();
this.attachShadow({ mode: 'open' });

@@ -119,3 +117,3 @@ const content = template.content.cloneNode(true);

this.#elementMutationObserver.observe(this.element, {
attributes: true
attributes: true,
});

@@ -165,6 +163,6 @@ }

});
this.#disposeOfVisibilityEffect = effect(() => {
this.style.setProperty('visibility', this.visible?.value ? 'visible' : 'hidden', 'important');
});
}
this.#disposeOfVisibilityEffect = effect(() => {
this.style.setProperty('visibility', this.visible?.value ? 'visible' : 'hidden', 'important');
});
}

@@ -171,0 +169,0 @@ }

@@ -1,2 +0,2 @@

export default function setupResizeListener(): () => void;
export declare function setupResizeListener(): () => void;
//# sourceMappingURL=fullscreen-listener.d.ts.map

@@ -1,4 +0,4 @@

import logAndRethrow from './log-and-rethrow.js';
import recalculatePositions from './utils/recalculate-positions.js';
export default function setupResizeListener() {
import { logAndRethrow } from './log-and-rethrow.js';
import { recalculatePositions } from './utils/recalculate-positions.js';
export function setupResizeListener() {
const abortController = new AbortController();

@@ -17,3 +17,2 @@ window.addEventListener('fullscreenchange', () => {

}
;
//# sourceMappingURL=fullscreen-listener.js.map

@@ -1,2 +0,2 @@

export default function setupIntersectionObserver(): {
export declare function setupIntersectionObserver(): {
intersectionObserver: IntersectionObserver;

@@ -3,0 +3,0 @@ disconnect: () => void;

@@ -1,12 +0,18 @@

import logAndRethrow from './log-and-rethrow.js';
import { logAndRethrow } from './log-and-rethrow.js';
import { extendedElementsWithIssues } from './state.js';
import getElementPosition from './utils/get-element-position.js';
export default function setupIntersectionObserver() {
import { getElementPosition } from './utils/get-element-position.js';
import { supportsAnchorPositioning } from './utils/supports-anchor-positioning.js';
export function setupIntersectionObserver() {
const intersectionObserver = new IntersectionObserver((entries) => {
try {
for (const entry of entries) {
const extendedElementWithIssues = extendedElementsWithIssues.value.find(el => el.element === entry.target);
const extendedElementWithIssues = extendedElementsWithIssues.value.find((el) => el.element === entry.target);
if (extendedElementWithIssues) {
// We initially treated setting visibility in the intersection observer
// as a fallback option for browsers that don't support `position-visibility`,
// but then we realized that this `position-visibility` actually works
// in an unexpected way when the container has `overflow: visible`.
// So now we always set visibility in the intersection observer.
extendedElementWithIssues.visible.value = entry.isIntersecting;
if (entry.isIntersecting) {
if (entry.isIntersecting && !supportsAnchorPositioning(window)) {
extendedElementWithIssues.position.value = getElementPosition(entry.target, window);

@@ -25,5 +31,5 @@ }

intersectionObserver.disconnect();
}
},
};
}
//# sourceMappingURL=intersection-observer.js.map

@@ -1,2 +0,2 @@

export default function logAndRethrow(error: unknown): void;
export declare function logAndRethrow(error: unknown): void;
//# sourceMappingURL=log-and-rethrow.d.ts.map
import { issuesUrl } from './constants.js';
export default function logAndRethrow(error) {
console.error(`Accented threw an error (see below). Try updating your browser to the latest version. ` +
`If you’re still seeing the error, file an issue at ${issuesUrl}.`);
export function logAndRethrow(error) {
console.error(`Accented threw an error (see below). Try updating your browser to the latest version. If you’re still seeing the error, file an issue at ${issuesUrl}.`);
throw error;
}
//# sourceMappingURL=log-and-rethrow.js.map

@@ -1,2 +0,2 @@

export default function createLogger(): () => void;
export declare function createLogger(): () => void;
//# sourceMappingURL=logger.d.ts.map
import { effect } from '@preact/signals-core';
import { accentedUrl } from './constants.js';
import { elementsWithIssues, enabled } from './state.js';
import { accentedUrl } from './constants.js';
function filterPropsForOutput(elements) {
return elements.map(({ element, issues }) => ({ element, issues }));
}
export default function createLogger() {
export function createLogger() {
let firstRun = true;

@@ -9,0 +9,0 @@ return effect(() => {

@@ -1,2 +0,2 @@

export default function registerElements(name: string): void;
export declare function registerElements(name: string): void;
//# sourceMappingURL=register-elements.d.ts.map

@@ -1,13 +0,13 @@

import getAccentedTrigger from './elements/accented-trigger.js';
import getAccentedDialog from './elements/accented-dialog.js';
export default function registerElements(name) {
import { getAccentedDialog } from './elements/accented-dialog.js';
import { getAccentedTrigger } from './elements/accented-trigger.js';
export function registerElements(name) {
const elements = [
{
elementName: `${name}-trigger`,
Component: getAccentedTrigger(name)
Component: getAccentedTrigger(name),
},
{
elementName: `${name}-dialog`,
Component: getAccentedDialog()
}
Component: getAccentedDialog(),
},
];

@@ -20,3 +20,2 @@ for (const { elementName, Component } of elements) {

}
;
//# sourceMappingURL=register-elements.js.map

@@ -1,2 +0,2 @@

export default function setupResizeListener(): () => void;
export declare function setupResizeListener(): () => void;
//# sourceMappingURL=resize-listener.d.ts.map

@@ -1,4 +0,4 @@

import logAndRethrow from './log-and-rethrow.js';
import recalculatePositions from './utils/recalculate-positions.js';
export default function setupResizeListener() {
import { logAndRethrow } from './log-and-rethrow.js';
import { recalculatePositions } from './utils/recalculate-positions.js';
export function setupResizeListener() {
const abortController = new AbortController();

@@ -17,3 +17,2 @@ window.addEventListener('resize', () => {

}
;
//# sourceMappingURL=resize-listener.js.map

@@ -1,3 +0,3 @@

import type { AxeOptions, Throttle, Callback, Context } from './types';
export default function createScanner(name: string, context: Context, axeOptions: AxeOptions, throttle: Required<Throttle>, callback: Callback): () => void;
import type { AxeOptions, Callback, Context, Throttle } from './types.ts';
export declare function createScanner(name: string, context: Context, axeOptions: AxeOptions, throttle: Required<Throttle>, callback: Callback): () => void;
//# sourceMappingURL=scanner.d.ts.map
import axe from 'axe-core';
import TaskQueue from './task-queue.js';
import { getAccentedElementNames, issuesUrl } from './constants.js';
import { logAndRethrow } from './log-and-rethrow.js';
import { elementsWithIssues, enabled, extendedElementsWithIssues } from './state.js';
import updateElementsWithIssues from './utils/update-elements-with-issues.js';
import recalculatePositions from './utils/recalculate-positions.js';
import recalculateScrollableAncestors from './utils/recalculate-scrollable-ancestors.js';
import supportsAnchorPositioning from './utils/supports-anchor-positioning.js';
import { getAccentedElementNames, issuesUrl } from './constants.js';
import logAndRethrow from './log-and-rethrow.js';
import createShadowDOMAwareMutationObserver from './utils/shadow-dom-aware-mutation-observer.js';
import getScanContext from './utils/get-scan-context.js';
export default function createScanner(name, context, axeOptions, throttle, callback) {
import { TaskQueue } from './task-queue.js';
import { getScanContext } from './utils/get-scan-context.js';
import { recalculatePositions } from './utils/recalculate-positions.js';
import { recalculateScrollableAncestors } from './utils/recalculate-scrollable-ancestors.js';
import { createShadowDOMAwareMutationObserver } from './utils/shadow-dom-aware-mutation-observer.js';
import { supportsAnchorPositioning } from './utils/supports-anchor-positioning.js';
import { updateElementsWithIssues } from './utils/update-elements-with-issues.js';
export function createScanner(name, context, axeOptions, throttle, callback) {
const axeRunningWindowProp = `__${name}_axe_running__`;

@@ -38,10 +38,7 @@ const win = window;

resultTypes: ['violations'],
...axeOptions
...axeOptions,
});
}
catch (error) {
console.error('Accented: axe-core (the accessibility testing engine) threw an error. ' +
'Check the `axeOptions` property that you’re passing to Accented. ' +
`If you still think it’s a bug in Accented, file an issue at ${issuesUrl}.\n`, error);
result = { violations: [] };
console.error(`Accented: axe-core (the accessibility testing engine) threw an error. Check the \`axeOptions\` property that you’re passing to Accented. If you still think it’s a bug in Accented, file an issue at ${issuesUrl}.\n`, error);
}

@@ -51,3 +48,3 @@ win[axeRunningWindowProp] = false;

const scanDuration = Math.round(scanMeasure.duration);
if (!enabled.value) {
if (!enabled.value || !result) {
return;

@@ -61,3 +58,3 @@ }

win: window,
name
name,
});

@@ -75,4 +72,4 @@ const domUpdateMeasure = performance.measure('dom-update', 'dom-update-start');

// in the scan.
scanContext: scanContext.exclude.length > 0 ? scanContext : scanContext.include
}
scanContext: scanContext.exclude.length > 0 ? scanContext : scanContext.include,
},
});

@@ -87,10 +84,10 @@ }

const accentedElementNames = getAccentedElementNames(name);
const mutationObserver = createShadowDOMAwareMutationObserver(name, mutationList => {
const mutationObserver = createShadowDOMAwareMutationObserver(name, (mutationList) => {
try {
// We're not interested in mutations that are caused exclusively by the custom elements
// introduced by Accented.
const listWithoutAccentedElements = mutationList.filter(mutationRecord => {
const listWithoutAccentedElements = mutationList.filter((mutationRecord) => {
const onlyAccentedElementsAddedOrRemoved = mutationRecord.type === 'childList' &&
[...mutationRecord.addedNodes].every(node => accentedElementNames.includes(node.nodeName.toLowerCase())) &&
[...mutationRecord.removedNodes].every(node => accentedElementNames.includes(node.nodeName.toLowerCase()));
[...mutationRecord.addedNodes].every((node) => accentedElementNames.includes(node.nodeName.toLowerCase())) &&
[...mutationRecord.removedNodes].every((node) => accentedElementNames.includes(node.nodeName.toLowerCase()));
const accentedElementChanged = mutationRecord.type === 'attributes' &&

@@ -114,3 +111,4 @@ accentedElementNames.includes(mutationRecord.target.nodeName.toLowerCase());

const elementsWithAccentedAttributeChanges = listWithoutAccentedElements.reduce((nodes, mutationRecord) => {
if (mutationRecord.type === 'attributes' && mutationRecord.attributeName === `data-${name}`) {
if (mutationRecord.type === 'attributes' &&
mutationRecord.attributeName === `data-${name}`) {
nodes.add(mutationRecord.target);

@@ -120,6 +118,6 @@ }

}, new Set());
const filteredMutationList = listWithoutAccentedElements.filter(mutationRecord => {
const filteredMutationList = listWithoutAccentedElements.filter((mutationRecord) => {
return !elementsWithAccentedAttributeChanges.has(mutationRecord.target);
});
const nodes = filteredMutationList.map(mutationRecord => mutationRecord.target);
const nodes = filteredMutationList.map((mutationRecord) => mutationRecord.target);
taskQueue.addMultiple(nodes);

@@ -135,3 +133,3 @@ }

attributes: true,
characterData: true
characterData: true,
});

@@ -138,0 +136,0 @@ return () => {

@@ -1,2 +0,2 @@

export default function setupScrollListeners(): () => void;
export declare function setupScrollListeners(): () => void;
//# sourceMappingURL=scroll-listeners.d.ts.map
import { effect } from '@preact/signals-core';
import recalculatePositions from './utils/recalculate-positions.js';
import { logAndRethrow } from './log-and-rethrow.js';
import { scrollableAncestors } from './state.js';
import logAndRethrow from './log-and-rethrow.js';
export default function setupScrollListeners() {
import { recalculatePositions } from './utils/recalculate-positions.js';
export function setupScrollListeners() {
const documentAbortController = new AbortController();

@@ -37,3 +37,2 @@ document.addEventListener('scroll', () => {

}
;
//# sourceMappingURL=scroll-listeners.js.map

@@ -1,2 +0,2 @@

import type { ElementWithIssues, ExtendedElementWithIssues } from './types';
import type { ElementWithIssues, ExtendedElementWithIssues } from './types.ts';
export declare const enabled: import("@preact/signals-core").Signal<boolean>;

@@ -3,0 +3,0 @@ export declare const extendedElementsWithIssues: import("@preact/signals-core").Signal<ExtendedElementWithIssues[]>;

@@ -1,11 +0,10 @@

import { signal, computed } from '@preact/signals-core';
import { computed, signal } from '@preact/signals-core';
export const enabled = signal(false);
export const extendedElementsWithIssues = signal([]);
export const elementsWithIssues = computed(() => extendedElementsWithIssues.value.map(extendedElementWithIssues => ({
export const elementsWithIssues = computed(() => extendedElementsWithIssues.value.map((extendedElementWithIssues) => ({
element: extendedElementWithIssues.element,
rootNode: extendedElementWithIssues.rootNode,
issues: extendedElementWithIssues.issues.value
issues: extendedElementWithIssues.issues.value,
})));
export const rootNodes = computed(() => new Set((enabled.value ? [document] : [])
.concat(...(extendedElementsWithIssues.value.map(extendedElementWithIssues => extendedElementWithIssues.rootNode)))));
export const rootNodes = computed(() => new Set((enabled.value ? [document] : []).concat(...extendedElementsWithIssues.value.map((extendedElementWithIssues) => extendedElementWithIssues.rootNode))));
export const scrollableAncestors = computed(() => extendedElementsWithIssues.value.reduce((scrollableAncestors, extendedElementWithIssues) => {

@@ -12,0 +11,0 @@ for (const scrollableAncestor of extendedElementWithIssues.scrollableAncestors.value) {

@@ -1,4 +0,4 @@

import type { Throttle } from './types';
import type { Throttle } from './types.ts';
type TaskCallback<T> = (items: Array<T>) => void;
export default class TaskQueue<T> {
export declare class TaskQueue<T> {
#private;

@@ -5,0 +5,0 @@ constructor(asyncCallback: TaskCallback<T>, throttle: Required<Throttle>);

@@ -1,2 +0,2 @@

export default class TaskQueue {
export class TaskQueue {
#throttle;

@@ -3,0 +3,0 @@ #asyncCallback = null;

@@ -0,4 +1,4 @@

import type { Signal } from '@preact/signals-core';
import type axe from 'axe-core';
import type { Signal } from '@preact/signals-core';
import type { AccentedTrigger } from './elements/accented-trigger';
import type { AccentedTrigger } from './elements/accented-trigger.ts';
export type Throttle = {

@@ -44,3 +44,3 @@ /**

export declare const allowedAxeOptions: readonly ["rules", "runOnly"];
export type AxeOptions = Pick<axe.RunOptions, typeof allowedAxeOptions[number]>;
export type AxeOptions = Pick<axe.RunOptions, (typeof allowedAxeOptions)[number]>;
type CallbackParams = {

@@ -47,0 +47,0 @@ /**

@@ -1,3 +0,3 @@

import type { BaseElementWithIssues } from "../types";
export default function areElementsWithIssuesEqual(elementWithIssues1: BaseElementWithIssues, elementWithIssues2: BaseElementWithIssues): boolean;
import type { BaseElementWithIssues } from '../types.ts';
export declare function areElementsWithIssuesEqual(elementWithIssues1: BaseElementWithIssues, elementWithIssues2: BaseElementWithIssues): boolean;
//# sourceMappingURL=are-elements-with-issues-equal.d.ts.map

@@ -1,5 +0,5 @@

export default function areElementsWithIssuesEqual(elementWithIssues1, elementWithIssues2) {
return elementWithIssues1.element === elementWithIssues2.element
&& elementWithIssues1.rootNode === elementWithIssues2.rootNode;
export function areElementsWithIssuesEqual(elementWithIssues1, elementWithIssues2) {
return (elementWithIssues1.element === elementWithIssues2.element &&
elementWithIssues1.rootNode === elementWithIssues2.rootNode);
}
//# sourceMappingURL=are-elements-with-issues-equal.js.map

@@ -1,3 +0,3 @@

import type { Issue } from '../types';
export default function areIssueSetsEqual(issues1: Array<Issue>, issues2: Array<Issue>): boolean;
import type { Issue } from '../types.ts';
export declare function areIssueSetsEqual(issues1: Array<Issue>, issues2: Array<Issue>): boolean;
//# sourceMappingURL=are-issue-sets-equal.d.ts.map
const issueProps = ['id', 'title', 'description', 'url', 'impact'];
export default function areIssueSetsEqual(issues1, issues2) {
return issues1.length === issues2.length &&
issues1.every(issue1 => Boolean(issues2.find(issue2 => issueProps.every(prop => issue2[prop] === issue1[prop]))));
export function areIssueSetsEqual(issues1, issues2) {
return (issues1.length === issues2.length &&
issues1.every((issue1) => Boolean(issues2.find((issue2) => issueProps.every((prop) => issue2[prop] === issue1[prop])))));
}
//# sourceMappingURL=are-issue-sets-equal.js.map

@@ -38,3 +38,3 @@ /**

{ prop: 'backdropFilter', value: 'blur(1px)' },
{ prop: 'containerType', value: 'size' }
{ prop: 'containerType', value: 'size' },
];

@@ -41,0 +41,0 @@ for (const { prop, value } of propsToTest) {

@@ -1,2 +0,2 @@

export default function contains(ancestor: Node, descendant: Node): boolean;
export declare function contains(ancestor: Node, descendant: Node): boolean;
//# sourceMappingURL=contains.d.ts.map
import { isDocumentFragment, isShadowRoot } from './dom-helpers.js';
export default function contains(ancestor, descendant) {
export function contains(ancestor, descendant) {
if (ancestor.contains(descendant)) {

@@ -4,0 +4,0 @@ return true;

export function deduplicateNodes(nodes) {
return [...new Set(nodes)];
;
}
//# sourceMappingURL=deduplicate-nodes.js.map
type AnyObject = Record<string, any>;
export default function deepMerge(target: AnyObject, source: AnyObject): AnyObject;
export declare function deepMerge(target: AnyObject, source: AnyObject): AnyObject;
export {};
//# sourceMappingURL=deep-merge.d.ts.map

@@ -1,10 +0,11 @@

export default function deepMerge(target, source) {
const isObject = (obj) => typeof obj === 'object' && obj !== null && !Array.isArray(obj);
export function deepMerge(target, source) {
const output = { ...target };
for (const key of Object.keys(source)) {
if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) {
if (!(key in target)) {
output[key] = source[key];
if (isObject(source[key])) {
if (isObject(target[key])) {
output[key] = deepMerge(target[key], source[key]);
}
else {
output[key] = deepMerge(target[key], source[key]);
output[key] = source[key];
}

@@ -11,0 +12,0 @@ }

export function isNode(obj) {
return 'nodeType' in obj && typeof obj.nodeType === 'number' &&
'nodeName' in obj && typeof obj.nodeName === 'string';
return ('nodeType' in obj &&
typeof obj.nodeType === 'number' &&
'nodeName' in obj &&
typeof obj.nodeName === 'string');
}

@@ -5,0 +7,0 @@ export function isNodeList(obj) {

@@ -1,2 +0,2 @@

export default function ensureNonEmpty<T>(arr: T[]): [T, ...T[]];
export declare function ensureNonEmpty<T>(arr: T[]): [T, ...T[]];
//# sourceMappingURL=ensure-non-empty.d.ts.map

@@ -1,4 +0,4 @@

export default function ensureNonEmpty(arr) {
export function ensureNonEmpty(arr) {
if (arr.length === 0) {
throw new Error("Array must not be empty");
throw new Error('Array must not be empty');
}

@@ -5,0 +5,0 @@ return arr;

@@ -1,2 +0,2 @@

export default function getElementHtml(element: Element): string;
export declare function getElementHtml(element: Element): string;
//# sourceMappingURL=get-element-html.d.ts.map

@@ -1,2 +0,2 @@

export default function getElementHtml(element) {
export function getElementHtml(element) {
const outerHtml = element.outerHTML;

@@ -12,4 +12,6 @@ const innerHtml = element.innerHTML;

}
return outerHtml.slice(0, index) + '…' + outerHtml.slice(index + innerHtml.length);
const openingTag = outerHtml.slice(0, index);
const closingTag = outerHtml.slice(index + innerHtml.length);
return `${openingTag}…${closingTag}`;
}
//# sourceMappingURL=get-element-html.js.map

@@ -1,2 +0,2 @@

import type { Position } from '../types';
import type { Position } from '../types.ts';
/**

@@ -10,3 +10,3 @@ * https://github.com/pomerantsev/accented/issues/116

*/
export default function getElementPosition(element: Element, win: Window): Position;
export declare function getElementPosition(element: Element, win: Window): Position;
//# sourceMappingURL=get-element-position.d.ts.map

@@ -0,18 +1,18 @@

import { createsContainingBlock } from './containing-blocks.js';
import { isHtmlElement } from './dom-helpers.js';
import getParent from './get-parent.js';
import { createsContainingBlock } from './containing-blocks.js';
import { getParent } from './get-parent.js';
// https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_display/Containing_block#identifying_the_containing_block
function isContainingBlock(element, win) {
const style = win.getComputedStyle(element);
const { transform, perspective, contain, contentVisibility, containerType, filter, backdropFilter, willChange } = style;
const { transform, perspective, contain, contentVisibility, containerType, filter, backdropFilter, willChange, } = style;
const containItems = contain.split(' ');
const willChangeItems = willChange.split(/\s*,\s*/);
return transform !== 'none'
|| perspective !== 'none'
|| containItems.some((item) => ['layout', 'paint', 'strict', 'content'].includes(item))
|| contentVisibility === 'auto'
|| (createsContainingBlock('containerType') && containerType !== 'normal')
|| (createsContainingBlock('filter') && filter !== 'none')
|| (createsContainingBlock('backdropFilter') && backdropFilter !== 'none')
|| willChangeItems.some((item) => ['transform', 'perspective', 'contain', 'filter', 'backdrop-filter'].includes(item));
return (transform !== 'none' ||
perspective !== 'none' ||
containItems.some((item) => ['layout', 'paint', 'strict', 'content'].includes(item)) ||
contentVisibility === 'auto' ||
(createsContainingBlock('containerType') && containerType !== 'normal') ||
(createsContainingBlock('filter') && filter !== 'none') ||
(createsContainingBlock('backdropFilter') && backdropFilter !== 'none') ||
willChangeItems.some((item) => ['transform', 'perspective', 'contain', 'filter', 'backdrop-filter'].includes(item)));
}

@@ -37,3 +37,3 @@ function getNonInitialContainingBlock(element, win) {

*/
export default function getElementPosition(element, win) {
export function getElementPosition(element, win) {
const nonInitialContainingBlock = getNonInitialContainingBlock(element, win);

@@ -60,17 +60,13 @@ // If an element has a containing block as an ancestor,

}
else {
const elementRect = element.getBoundingClientRect();
const nonInitialContainingBlockRect = nonInitialContainingBlock.getBoundingClientRect();
return {
top: elementRect.top - nonInitialContainingBlockRect.top,
height: elementRect.height,
left: elementRect.left - nonInitialContainingBlockRect.left,
width: elementRect.width
};
}
const elementRect = element.getBoundingClientRect();
const nonInitialContainingBlockRect = nonInitialContainingBlock.getBoundingClientRect();
return {
top: elementRect.top - nonInitialContainingBlockRect.top,
height: elementRect.height,
left: elementRect.left - nonInitialContainingBlockRect.left,
width: elementRect.width,
};
}
else {
return element.getBoundingClientRect();
}
return element.getBoundingClientRect();
}
//# sourceMappingURL=get-element-position.js.map

@@ -1,2 +0,2 @@

export default function getParent(element: Element): Element | null;
export declare function getParent(element: Element): Element | null;
//# sourceMappingURL=get-parent.d.ts.map
import { isDocumentFragment, isShadowRoot } from './dom-helpers.js';
export default function getParent(element) {
export function getParent(element) {
if (element.parentElement) {

@@ -4,0 +4,0 @@ return element.parentElement;

@@ -1,3 +0,3 @@

import type { Context, ScanContext } from '../types';
export default function getScanContext(nodes: Array<Node>, context: Context): ScanContext;
import type { Context, ScanContext } from '../types.ts';
export declare function getScanContext(nodes: Array<Node>, context: Context): ScanContext;
//# sourceMappingURL=get-scan-context.d.ts.map

@@ -1,11 +0,11 @@

import contains from './contains.js';
import { contains } from './contains.js';
import { deduplicateNodes } from './deduplicate-nodes.js';
import isNodeInScanContext from './is-node-in-scan-context.js';
import normalizeContext from './normalize-context.js';
export default function getScanContext(nodes, context) {
import { isNodeInScanContext } from './is-node-in-scan-context.js';
import { normalizeContext } from './normalize-context.js';
export function getScanContext(nodes, context) {
const { include: contextInclude, exclude: contextExclude } = normalizeContext(context);
// Filter only nodes that are included by context (see isNodeInContext above).
const nodesInContext = nodes.filter(node => isNodeInScanContext(node, {
const nodesInContext = nodes.filter((node) => isNodeInScanContext(node, {
include: contextInclude,
exclude: contextExclude
exclude: contextExclude,
}));

@@ -18,5 +18,5 @@ const include = [];

for (const node of nodes) {
const includeDescendants = contextInclude.filter(item => contains(node, item));
const includeDescendants = contextInclude.filter((item) => contains(node, item));
include.push(...includeDescendants);
const excludeDescendants = contextExclude.filter(item => contains(node, item));
const excludeDescendants = contextExclude.filter((item) => contains(node, item));
exclude.push(...excludeDescendants);

@@ -26,5 +26,5 @@ }

include: deduplicateNodes(include),
exclude: deduplicateNodes(exclude)
exclude: deduplicateNodes(exclude),
};
}
//# sourceMappingURL=get-scan-context.js.map

@@ -1,2 +0,2 @@

export default function getScrollableAncestors(element: Element, win: Window): Set<Element>;
export declare function getScrollableAncestors(element: Element, win: Window): Set<Element>;
//# sourceMappingURL=get-scrollable-ancestors.d.ts.map

@@ -1,6 +0,6 @@

import getParent from './get-parent.js';
import { getParent } from './get-parent.js';
const scrollableOverflowValues = new Set(['auto', 'scroll', 'hidden']);
export default function getScrollableAncestors(element, win) {
export function getScrollableAncestors(element, win) {
let currentElement = element;
let scrollableAncestors = new Set();
const scrollableAncestors = new Set();
while (true) {

@@ -12,3 +12,4 @@ currentElement = getParent(currentElement);

const computedStyle = win.getComputedStyle(currentElement);
if (scrollableOverflowValues.has(computedStyle.overflowX) || scrollableOverflowValues.has(computedStyle.overflowY)) {
if (scrollableOverflowValues.has(computedStyle.overflowX) ||
scrollableOverflowValues.has(computedStyle.overflowY)) {
scrollableAncestors.add(currentElement);

@@ -19,3 +20,2 @@ }

}
;
//# sourceMappingURL=get-scrollable-ancestors.js.map

@@ -1,3 +0,3 @@

import type { ScanContext } from '../types';
export default function isNodeInScanContext(node: Node, { include, exclude }: ScanContext): boolean;
import type { ScanContext } from '../types.ts';
export declare function isNodeInScanContext(node: Node, { include, exclude }: ScanContext): boolean;
//# sourceMappingURL=is-node-in-scan-context.d.ts.map
/* Adapted from https://github.com/dequelabs/axe-core/blob/fd6239bfc97ebc904044f93f68d7e49137f744ad/lib/core/utils/is-node-in-context.js */
import contains from './contains.js';
import ensureNonEmpty from './ensure-non-empty.js';
import { contains } from './contains.js';
import { ensureNonEmpty } from './ensure-non-empty.js';
function getDeepest(nodes) {

@@ -13,8 +13,8 @@ let deepest = nodes[0];

}
export default function isNodeInScanContext(node, { include, exclude }) {
const filteredInclude = include.filter(includeNode => contains(includeNode, node));
export function isNodeInScanContext(node, { include, exclude }) {
const filteredInclude = include.filter((includeNode) => contains(includeNode, node));
if (filteredInclude.length === 0) {
return false;
}
const filteredExclude = exclude.filter(excludeNode => contains(excludeNode, node));
const filteredExclude = exclude.filter((excludeNode) => contains(excludeNode, node));
if (filteredExclude.length === 0) {

@@ -21,0 +21,0 @@ return true;

@@ -1,3 +0,3 @@

import type { Context, ScanContext } from '../types';
export default function normalizeContext(context: Context): ScanContext;
import type { Context, ScanContext } from '../types.ts';
export declare function normalizeContext(context: Context): ScanContext;
//# sourceMappingURL=normalize-context.d.ts.map

@@ -0,3 +1,4 @@

import { deduplicateNodes } from './deduplicate-nodes.js';
import { isNode, isNodeList } from './dom-helpers.js';
import { deduplicateNodes } from './deduplicate-nodes.js';
import { isNonEmpty } from './is-non-empty.js';
function recursiveSelectAll(selectors, root) {

@@ -9,2 +10,5 @@ const nodesOnCurrentLevel = root.querySelectorAll(selectors[0]);

const restSelectors = selectors.slice(1);
if (!isNonEmpty(restSelectors)) {
throw new Error('Error: the restSelectors array must not be empty.');
}
const selected = [];

@@ -22,8 +26,6 @@ for (const node of nodesOnCurrentLevel) {

}
else if (isNode(selector)) {
if (isNode(selector)) {
return [selector];
}
else {
return recursiveSelectAll(selector.fromShadowDom, document);
}
return recursiveSelectAll(selector.fromShadowDom, document);
}

@@ -33,3 +35,3 @@ function contextPropToNodes(contextProp) {

if (typeof contextProp === 'object' && (Array.isArray(contextProp) || isNodeList(contextProp))) {
nodes = Array.from(contextProp).map(item => selectorToNodes(item)).flat();
nodes = Array.from(contextProp).flatMap((item) => selectorToNodes(item));
}

@@ -41,3 +43,3 @@ else {

}
export default function normalizeContext(context) {
export function normalizeContext(context) {
let contextInclude = [];

@@ -58,5 +60,5 @@ let contextExclude = [];

include: contextInclude,
exclude: contextExclude
exclude: contextExclude,
};
}
//# sourceMappingURL=normalize-context.js.map

@@ -1,2 +0,2 @@

export default function recalculatePositions(): void;
export declare function recalculatePositions(): void;
//# sourceMappingURL=recalculate-positions.d.ts.map
import { batch } from '@preact/signals-core';
import { logAndRethrow } from '../log-and-rethrow.js';
import { extendedElementsWithIssues } from '../state.js';
import getElementPosition from './get-element-position.js';
import logAndRethrow from '../log-and-rethrow.js';
import { getElementPosition } from './get-element-position.js';
let frameRequested = false;
export default function recalculatePositions() {
export function recalculatePositions() {
if (frameRequested) {

@@ -15,7 +15,7 @@ return;

batch(() => {
extendedElementsWithIssues.value.forEach(({ element, position, visible }) => {
for (const { element, position, visible } of extendedElementsWithIssues.value) {
if (visible.value && element.isConnected) {
position.value = getElementPosition(element, window);
}
});
}
});

@@ -22,0 +22,0 @@ }

@@ -1,2 +0,2 @@

export default function recalculateScrollableAncestors(): void;
export declare function recalculateScrollableAncestors(): void;
//# sourceMappingURL=recalculate-scrollable-ancestors.d.ts.map
import { batch } from '@preact/signals-core';
import { extendedElementsWithIssues } from '../state.js';
import getScrollableAncestors from './get-scrollable-ancestors.js';
export default function recalculateScrollableAncestors() {
import { getScrollableAncestors } from './get-scrollable-ancestors.js';
export function recalculateScrollableAncestors() {
batch(() => {
extendedElementsWithIssues.value.forEach(({ element, scrollableAncestors }) => {
for (const { element, scrollableAncestors } of extendedElementsWithIssues.value) {
if (element.isConnected) {
scrollableAncestors.value = getScrollableAncestors(element, window);
}
});
}
});
}
//# sourceMappingURL=recalculate-scrollable-ancestors.js.map

@@ -1,2 +0,2 @@

export default function createShadowDOMAwareMutationObserver(name: string, callback: MutationCallback): {
export declare function createShadowDOMAwareMutationObserver(name: string, callback: MutationCallback): {
"__#4@#shadowRoots": Set<unknown>;

@@ -3,0 +3,0 @@ "__#4@#options": MutationObserverInit | undefined;

@@ -1,4 +0,4 @@

import { isElement, isDocument, isDocumentFragment } from './dom-helpers.js';
import { getAccentedElementNames } from '../constants.js';
export default function createShadowDOMAwareMutationObserver(name, callback) {
import { isDocument, isDocumentFragment, isElement } from './dom-helpers.js';
export function createShadowDOMAwareMutationObserver(name, callback) {
class ShadowDOMAwareMutationObserver extends MutationObserver {

@@ -10,15 +10,12 @@ #shadowRoots = new Set();

const accentedElementNames = getAccentedElementNames(name);
const childListMutations = mutations
.filter(mutation => mutation.type === 'childList');
const childListMutations = mutations.filter((mutation) => mutation.type === 'childList');
const newElements = childListMutations
.map(mutation => [...mutation.addedNodes])
.flat()
.filter(node => isElement(node))
.filter(node => !accentedElementNames.includes(node.nodeName.toLowerCase()));
.flatMap((mutation) => [...mutation.addedNodes])
.filter((node) => isElement(node))
.filter((node) => !accentedElementNames.includes(node.nodeName.toLowerCase()));
this.#observeShadowRoots(newElements);
const removedElements = childListMutations
.map(mutation => [...mutation.removedNodes])
.flat()
.filter(node => isElement(node))
.filter(node => !accentedElementNames.includes(node.nodeName.toLowerCase()));
.flatMap((mutation) => [...mutation.removedNodes])
.filter((node) => isElement(node))
.filter((node) => !accentedElementNames.includes(node.nodeName.toLowerCase()));
// Mutation observer has no "unobserve" method, so we're simply deleting

@@ -43,9 +40,10 @@ // the elements from the set of shadow roots.

const shadowRoots = elements
.map(element => [...element.querySelectorAll('*')])
.flat()
.filter(element => element.shadowRoot)
.map(element => element.shadowRoot);
.flatMap((element) => [...element.querySelectorAll('*')])
.filter((element) => element.shadowRoot)
.map((element) => element.shadowRoot);
for (const shadowRoot of shadowRoots) {
this.#shadowRoots.add(shadowRoot);
this.observe(shadowRoot, this.#options);
if (shadowRoot) {
this.#shadowRoots.add(shadowRoot);
this.observe(shadowRoot, this.#options);
}
}

@@ -55,6 +53,5 @@ };

const shadowRoots = elements
.map(element => [...element.querySelectorAll('*')])
.flat()
.filter(element => element.shadowRoot)
.map(element => element.shadowRoot);
.flatMap((element) => [...element.querySelectorAll('*')])
.filter((element) => element.shadowRoot)
.map((element) => element.shadowRoot);
for (const shadowRoot of shadowRoots) {

@@ -61,0 +58,0 @@ this.#shadowRoots.delete(shadowRoot);

type WindowWithCSS = Window & {
CSS: typeof CSS;
};
export default function supportsAnchorPositioning(win: WindowWithCSS): boolean;
export declare function supportsAnchorPositioning(win: WindowWithCSS): boolean;
export {};
//# sourceMappingURL=supports-anchor-positioning.d.ts.map

@@ -1,4 +0,4 @@

export default function supportsAnchorPositioning(win) {
export function supportsAnchorPositioning(win) {
return win.CSS.supports('anchor-name: --foo') && win.CSS.supports('position-anchor: --foo');
}
//# sourceMappingURL=supports-anchor-positioning.js.map
import type { AxeResults } from 'axe-core';
import type { ElementWithIssues } from '../types';
export default function transformViolations(violations: typeof AxeResults.violations, name: string): ElementWithIssues[];
import type { ElementWithIssues } from '../types.ts';
export declare function transformViolations(violations: typeof AxeResults.violations, name: string): ElementWithIssues[];
//# sourceMappingURL=transform-violations.d.ts.map

@@ -10,7 +10,7 @@ // This is a list of axe-core violations (their ids) that may be flagged by axe-core

'nested-interactive',
'scrollable-region-focusable' // The Accented trigger might make the content grow such that scrolling is required.
'scrollable-region-focusable', // The Accented trigger might make the content grow such that scrolling is required.
];
function maybeCausedByAccented(violationId, element, name) {
return violationsAffectedByAccentedTriggers.includes(violationId)
&& Boolean(element.querySelector(`${name}-trigger`));
return (violationsAffectedByAccentedTriggers.includes(violationId) &&
Boolean(element.querySelector(`${name}-trigger`)));
}

@@ -21,3 +21,3 @@ function impactCompare(a, b) {

}
export default function transformViolations(violations, name) {
export function transformViolations(violations, name) {
const elementsWithIssues = [];

@@ -40,14 +40,14 @@ for (const violation of violations) {

url: violation.helpUrl,
impact: violation.impact ?? null
impact: violation.impact ?? null,
};
const existingElementIndex = elementsWithIssues.findIndex(elementWithIssues => elementWithIssues.element === element);
if (existingElementIndex === -1) {
const existingElement = elementsWithIssues.find((elementWithIssues) => elementWithIssues.element === element);
if (existingElement === undefined) {
elementsWithIssues.push({
element,
rootNode: element.getRootNode(),
issues: [issue]
issues: [issue],
});
}
else {
elementsWithIssues[existingElementIndex].issues.push(issue);
existingElement.issues.push(issue);
}

@@ -54,0 +54,0 @@ }

@@ -0,5 +1,5 @@

import type { Signal } from '@preact/signals-core';
import type { AxeResults } from 'axe-core';
import type { Signal } from '@preact/signals-core';
import type { ExtendedElementWithIssues, ScanContext } from '../types';
export default function updateElementsWithIssues({ extendedElementsWithIssues, scanContext, violations, win, name }: {
import type { ExtendedElementWithIssues, ScanContext } from '../types.ts';
export declare function updateElementsWithIssues({ extendedElementsWithIssues, scanContext, violations, win, name, }: {
extendedElementsWithIssues: Signal<Array<ExtendedElementWithIssues>>;

@@ -6,0 +6,0 @@ scanContext: ScanContext;

import { batch, signal } from '@preact/signals-core';
import transformViolations from './transform-violations.js';
import areElementsWithIssuesEqual from './are-elements-with-issues-equal.js';
import areIssueSetsEqual from './are-issue-sets-equal.js';
import isNodeInScanContext from './is-node-in-scan-context.js';
import getElementPosition from './get-element-position.js';
import getScrollableAncestors from './get-scrollable-ancestors.js';
import supportsAnchorPositioning from './supports-anchor-positioning.js';
import { areElementsWithIssuesEqual } from './are-elements-with-issues-equal.js';
import { areIssueSetsEqual } from './are-issue-sets-equal.js';
import { isSvgElement } from './dom-helpers.js';
import getParent from './get-parent.js';
import { getElementPosition } from './get-element-position.js';
import { getParent } from './get-parent.js';
import { getScrollableAncestors } from './get-scrollable-ancestors.js';
import { isNodeInScanContext } from './is-node-in-scan-context.js';
import { supportsAnchorPositioning } from './supports-anchor-positioning.js';
import { transformViolations } from './transform-violations.js';
function shouldSkipRender(element) {

@@ -25,13 +25,16 @@ // Skip rendering if the element is inside an SVG:

let count = 0;
export default function updateElementsWithIssues({ extendedElementsWithIssues, scanContext, violations, win, name }) {
export function updateElementsWithIssues({ extendedElementsWithIssues, scanContext, violations, win, name, }) {
const updatedElementsWithIssues = transformViolations(violations, name);
batch(() => {
for (const updatedElementWithIssues of updatedElementsWithIssues) {
const existingElementIndex = extendedElementsWithIssues.value.findIndex(extendedElementWithIssues => areElementsWithIssuesEqual(extendedElementWithIssues, updatedElementWithIssues));
if (existingElementIndex > -1 && extendedElementsWithIssues.value[existingElementIndex] && !areIssueSetsEqual(extendedElementsWithIssues.value[existingElementIndex].issues.value, updatedElementWithIssues.issues)) {
extendedElementsWithIssues.value[existingElementIndex].issues.value = updatedElementWithIssues.issues;
const existingElementIndex = extendedElementsWithIssues.value.findIndex((extendedElementWithIssues) => areElementsWithIssuesEqual(extendedElementWithIssues, updatedElementWithIssues));
if (existingElementIndex > -1 &&
extendedElementsWithIssues.value[existingElementIndex] &&
!areIssueSetsEqual(extendedElementsWithIssues.value[existingElementIndex].issues.value, updatedElementWithIssues.issues)) {
extendedElementsWithIssues.value[existingElementIndex].issues.value =
updatedElementWithIssues.issues;
}
}
const addedElementsWithIssues = updatedElementsWithIssues.filter(updatedElementWithIssues => {
return !extendedElementsWithIssues.value.some(extendedElementWithIssues => areElementsWithIssuesEqual(extendedElementWithIssues, updatedElementWithIssues));
const addedElementsWithIssues = updatedElementsWithIssues.filter((updatedElementWithIssues) => {
return !extendedElementsWithIssues.value.some((extendedElementWithIssues) => areElementsWithIssuesEqual(extendedElementWithIssues, updatedElementWithIssues));
});

@@ -41,6 +44,6 @@ // Only consider an element to be removed in two cases:

// 2. It is within the scan context, but not among updatedElementsWithIssues.
const removedElementsWithIssues = extendedElementsWithIssues.value.filter(extendedElementWithIssues => {
const removedElementsWithIssues = extendedElementsWithIssues.value.filter((extendedElementWithIssues) => {
const isConnected = extendedElementWithIssues.element.isConnected;
const hasNoMoreIssues = isNodeInScanContext(extendedElementWithIssues.element, scanContext)
&& !updatedElementsWithIssues.some(updatedElementWithIssues => areElementsWithIssuesEqual(updatedElementWithIssues, extendedElementWithIssues));
const hasNoMoreIssues = isNodeInScanContext(extendedElementWithIssues.element, scanContext) &&
!updatedElementsWithIssues.some((updatedElementWithIssues) => areElementsWithIssuesEqual(updatedElementWithIssues, extendedElementWithIssues));
return !isConnected || hasNoMoreIssues;

@@ -50,12 +53,12 @@ });

extendedElementsWithIssues.value = [...extendedElementsWithIssues.value]
.filter(extendedElementWithIssues => {
return !removedElementsWithIssues.some(removedElementWithIssues => areElementsWithIssuesEqual(removedElementWithIssues, extendedElementWithIssues));
.filter((extendedElementWithIssues) => {
return !removedElementsWithIssues.some((removedElementWithIssues) => areElementsWithIssuesEqual(removedElementWithIssues, extendedElementWithIssues));
})
.concat(addedElementsWithIssues
.filter(addedElementWithIssues => addedElementWithIssues.element.isConnected)
.map(addedElementWithIssues => {
.filter((addedElementWithIssues) => addedElementWithIssues.element.isConnected)
.map((addedElementWithIssues) => {
const id = count++;
const trigger = win.document.createElement(`${name}-trigger`);
const elementZIndex = parseInt(win.getComputedStyle(addedElementWithIssues.element).zIndex, 10);
if (!isNaN(elementZIndex)) {
const elementZIndex = Number.parseInt(win.getComputedStyle(addedElementWithIssues.element).zIndex, 10);
if (!Number.isNaN(elementZIndex)) {
trigger.style.setProperty('z-index', (elementZIndex + 1).toString(), 'important');

@@ -71,5 +74,5 @@ }

trigger.element = addedElementWithIssues.element;
const scrollableAncestors = supportsAnchorPositioning(win) ?
new Set() :
getScrollableAncestors(addedElementWithIssues.element, win);
const scrollableAncestors = supportsAnchorPositioning(win)
? new Set()
: getScrollableAncestors(addedElementWithIssues.element, win);
const issues = signal(addedElementWithIssues.issues);

@@ -86,6 +89,8 @@ accentedDialog.issues = issues;

scrollableAncestors: signal(scrollableAncestors),
anchorNameValue: addedElementWithIssues.element.style.getPropertyValue('anchor-name')
|| win.getComputedStyle(addedElementWithIssues.element).getPropertyValue('anchor-name'),
anchorNameValue: addedElementWithIssues.element.style.getPropertyValue('anchor-name') ||
win
.getComputedStyle(addedElementWithIssues.element)
.getPropertyValue('anchor-name'),
trigger,
issues
issues,
};

@@ -92,0 +97,0 @@ }));

@@ -1,3 +0,3 @@

import type { AccentedOptions } from './types';
export default function validateOptions(options: AccentedOptions): void;
import type { AccentedOptions } from './types.ts';
export declare function validateOptions(options: AccentedOptions): void;
//# sourceMappingURL=validate-options.d.ts.map
import { allowedAxeOptions } from './types.js';
import { isNode, isNodeList } from './utils/dom-helpers.js';
function isSelector(contextFragment) {
return typeof contextFragment === 'string'
|| isNode(contextFragment)
|| 'fromShadowDom' in contextFragment;
return (typeof contextFragment === 'string' ||
isNode(contextFragment) ||
'fromShadowDom' in contextFragment);
}

@@ -12,9 +12,9 @@ function validateSelector(selector) {

}
else if (isNode(selector)) {
if (isNode(selector)) {
return;
}
else if ('fromShadowDom' in selector) {
if (!Array.isArray(selector.fromShadowDom)
|| selector.fromShadowDom.length < 2 ||
!selector.fromShadowDom.every(item => typeof item === 'string')) {
if ('fromShadowDom' in selector) {
if (!Array.isArray(selector.fromShadowDom) ||
selector.fromShadowDom.length < 2 ||
!selector.fromShadowDom.every((item) => typeof item === 'string')) {
throw new TypeError(`Accented: invalid argument. \`fromShadowDom\` must be an array of strings with at least 2 elements. It’s currently set to ${selector.fromShadowDom}.`);

@@ -24,10 +24,8 @@ }

}
else {
const neverSelector = selector;
throw new TypeError(`Accented: invalid argument. The selector must be one of: string, Node, or an object with a \`fromShadowDom\` property. It’s currently set to ${neverSelector}.`);
}
const neverSelector = selector;
throw new TypeError(`Accented: invalid argument. The selector must be one of: string, Node, or an object with a \`fromShadowDom\` property. It’s currently set to ${neverSelector}.`);
}
function isSelectorList(contextFragment) {
return (typeof contextFragment === 'object' && isNodeList(contextFragment))
|| (Array.isArray(contextFragment) && contextFragment.every(item => isSelector(item)));
return ((typeof contextFragment === 'object' && isNodeList(contextFragment)) ||
(Array.isArray(contextFragment) && contextFragment.every((item) => isSelector(item))));
}

@@ -38,3 +36,3 @@ function validateSelectorList(selectorList) {

}
else if (Array.isArray(selectorList)) {
if (Array.isArray(selectorList)) {
for (const selector of selectorList) {

@@ -65,10 +63,11 @@ validateSelector(selector);

function isContextObject(contextFragment) {
return typeof contextFragment === 'object' && contextFragment !== null
&& ('include' in contextFragment || 'exclude' in contextFragment);
return (typeof contextFragment === 'object' &&
contextFragment !== null &&
('include' in contextFragment || 'exclude' in contextFragment));
}
function validateContextObject(contextObject) {
if ('include' in contextObject) {
if ('include' in contextObject && contextObject.include !== undefined) {
validateContextProp(contextObject.include);
}
if ('exclude' in contextObject) {
if ('exclude' in contextObject && contextObject.exclude !== undefined) {
validateContextProp(contextObject.exclude);

@@ -93,3 +92,3 @@ }

const nameRegex = /^[a-z]([a-z0-9]|-)+$/;
export default function validateOptions(options) {
export function validateOptions(options) {
if (typeof options !== 'object' || options === null) {

@@ -102,3 +101,4 @@ throw new TypeError(`Accented: invalid argument. The options parameter must be an object if provided. It’s currently set to ${options}.`);

}
if (options.throttle.wait !== undefined && (typeof options.throttle.wait !== 'number' || options.throttle.wait < 0)) {
if (options.throttle.wait !== undefined &&
(typeof options.throttle.wait !== 'number' || options.throttle.wait < 0)) {
throw new TypeError(`Accented: invalid argument. \`throttle.wait\` option must be a non-negative number if provided. It’s currently set to ${options.throttle.wait}.`);

@@ -118,3 +118,4 @@ }

}
if (options.name !== undefined && (typeof options.name !== 'string' || !options.name.match(nameRegex))) {
if (options.name !== undefined &&
(typeof options.name !== 'string' || !options.name.match(nameRegex))) {
throw new TypeError(`Accented: invalid argument. \`name\` option must be a string that starts with a lowercase letter and only contains lowercase alphanumeric characters and dashes. It’s currently set to ${options.name}.`);

@@ -126,3 +127,3 @@ }

}
const unsupportedKeys = Object.keys(options.axeOptions).filter(key => !allowedAxeOptions.includes(key));
const unsupportedKeys = Object.keys(options.axeOptions).filter((key) => !allowedAxeOptions.includes(key));
if (unsupportedKeys.length > 0) {

@@ -129,0 +130,0 @@ throw new TypeError(`Accented: invalid argument. \`axeOptions\` contains the following unsupported keys: ${unsupportedKeys.join(', ')}. Valid options are: ${allowedAxeOptions.join(', ')}.`);

{
"name": "accented",
"version": "0.0.0-20250424114613",
"version": "0.0.0-20250618181418",
"description": "Continuous accessibility testing and issue highlighting for web development",

@@ -37,9 +37,11 @@ "type": "module",

"scripts": {
"build": "tsc",
"build": "pnpm copyCommon && tsc",
"copyCommon": "node --import tsx ./scripts/copy-common.ts",
"watchCommon": "node --import tsx ./scripts/watch-common.ts",
"checkBuiltFiles": "node --import tsx ./scripts/check-built-files.ts",
"checkImportsInBuiltFiles": "node ./dist/accented.js",
"typecheckTests": "tsc -p ./tsconfig.test.json",
"watch": "tsc --watch",
"watch": "pnpm copyCommon && (pnpm watchCommon & tsc --watch)",
"test": "node --test --import tsx \"./**/*.test.ts\""
}
}

@@ -12,210 +12,1 @@ # Accented

TODO: example screenshots, without Accented / with Accented.
## Basic usage
* The library can be used in three ways:
* NPM (with a bundler)
* `import accented from 'https://esm.sh/accented';`.
* `import('https://esm.sh/accented').then(({default: accented}) => { accented(); });` (this version will work in the console, unless it violates the content security policy, which shouldn't be the case locally).
* For example, this works on medium.com
## API
### Exports
* `accented`: the default library export. It’s the function that enables the continuous scanning and highlighting
on the page in whose context in was called. Example: `const disable = accented(options)`.
* Parameters: the only parameter is `options`. See [Options](#options).
* Returns: a `disable` function that takes no parameters. When called, disables the scanning and highlighting,
and cleans up any changes that Accented has made to the page.
#### Type exports
The following types are exported for TypeScript consumers:
* `AccentedOptions`: the `options` parameter (see [Options](#options)).
* `DisableAccented`: the type of the function returned by `accented`.
### Options
#### `context`
**Type:** see [documentation](https://www.deque.com/axe/core-documentation/api-documentation/#context-parameter).
**Default:** `document`.
The `context` parameter for `axe.run()`.
Determines what element(s) to scan for accessibility issues.
Accepts a variety of shapes:
* an element reference;
* a selector;
* a `NodeList`;
* an include / exclude object;
* and more.
See documentation: https://www.deque.com/axe/core-documentation/api-documentation/#context-parameter
#### `axeOptions`
**Type:** object.
**Default:** `{}`.
The `options` parameter for `axe.run()`.
Accented only supports two keys of the `options` object:
* `rules`;
* `runOnly`.
Both properties are optional, and both control which accessibility rules your page is tested against.
See documentation: https://www.deque.com/axe/core-documentation/api-documentation/#options-parameter
#### `output`
An object controlling how the results of scans will be presented.
#### `output.console`
**Type:** boolean.
**Default:** `true`.
Whether the list of elements with issues should be printed to the browser console whenever issues are added, removed, or changed.
#### `callback`
**Type:** function.
**Default:** no-op (`() => {}`).
A function that Accented will call after every scan.
It accepts a single `params` object with the following properties:
* `elementsWithIssues`: the most up-to-date array of all elements with accessibility issues.
* `performance`: runtime performance of the last scan. An object:
* `totalBlockingTime`: how long the main thread was blocked by Accented during the last scan, in milliseconds.
It’s further divided into the `scan` and `domUpdate` phases.
* `scan`: how long the `scan` phase took, in milliseconds.
* `domUpdate`: how long the `domUpdate` phase took, in milliseconds.
* `scanContext`: nodes that got scanned. Either an array of nodes,
or an object with `include` and `exclude` properties (if any nodes were excluded).
**Example:**
```
accented({
callback: ({ elementsWithIssues, performance }) => {
console.log('Elements with issues:', elementsWithIssues);
console.log('Total blocking time:', performance.totalBlockingTime);
}
});
```
#### `name`
**Type:** string.
**Default:** `"accented"`.
The character sequence that’s used in various elements, attributes and stylesheets that Accented adds to the page.
You shouldn’t have to use this attribute unless some of the names on your page conflict with what Accented provides by default.
* The data attribute that’s added to elements with issues (default: `data-accented`).
* The custom elements for the button and the dialog that get created for each element with issues
(default: `accented-trigger`, `accented-dialog`).
* The CSS cascade layer containing page-wide Accented-specific styles (default: `accented`).
* The prefix for some of the CSS custom properties used by Accented (default: `--accented-`).
* The window property that’s used to prevent multiple axe-core scans from running simultaneously
(default: `__accented_axe_running__`).
Only lowercase alphanumeric characters and dashes (-) are allowed in the name,
and it must start with a lowercase letter.
**Example:**
```
accented({name: 'my-name'});
```
With the above option provided, the attribute set on elements with issues will be `data-my-name`,
a custom element will be called `my-name-trigger`, and so on.
#### `throttle`
An object controlling when Accented will run its scans.
#### `throttle.wait`
**Type:** number.
**Default:** 1000.
The delay (in milliseconds) after a mutation or after the last Accented scan.
If the page you’re scanning has a lot of nodes,
scanning may take a noticeable time (~ a few hundred milliseconds),
during which time the main thread will be blocked most of the time.
You may want to experiment with this value if your page contents change frequently
or if it has JavaScript-based animations running on the main thread.
#### `throttle.leading`
**Type:** boolean.
**Default:** `true`.
If set to true, the scan runs immediately after a mutation.
In this case, `wait` only applies to subsequent scans,
giving the page at least `wait` milliseconds between the end of the previous scan
and the beginning of the next one.
If set to false, the wait applies to mutations as well,
delaying the output.
This may be useful if you’re expecting bursts of mutations on your page.
### Styling
TODO: Create a separate doc with info on using `:root` and CSS layers to control some aspects of styling.
Documented CSS custom props:
* `--accented-primary-color`
* `--accented-secondary-color`
* `--accented-outline-width`
* `--accented-outline-style`
## Miscellaneous
### Shadow DOM
Highlighting elements inside shadow DOM is not supported yet, see [#25](https://github.com/pomerantsev/accented/issues/25).
### Iframes
Although axe-core is capable of scanning iframes, Accented doesn’t provide that as a special capability.
Instead, if you wish to scan the document in an iframe, initialize Accented inside the iframed document.
There should be no interference between the instances of Accented running in the parent and child documents.
TODO: expand this section and better explain the concepts.
## Frequently asked questions
<!-- TODO: how can this section be better formatted? This probably should be regular sections rather than a Q&A. -->
**Q:** can Accented be used in a CI (continuous integration) environment?
**A:** no, it’s only meant for local development. Accented runs accessibility tests on every state of the page that’s currently in the developer’s browser. However, if you additionally need something for CI, consider using [axe-core](https://www.npmjs.com/package/axe-core) in your automated test suite, either directly, or through wrappers such as [jest-axe](https://www.npmjs.com/package/jest-axe) or [axe-playwright](https://www.npmjs.com/package/axe-playwright).
**Q:** does Accented affect performance?
**A:** TODO: it might (it’s inevitable because it’s on the main thread), but we’ve taken X, Y, and Z measures to make it less noticeable. You can also take A, B, and C steps yourself.
* Only re-running on the changed part of the page.
* Throttling calls and giving the ability to tweak it.
* Providing the ability to select which rules to run, and which elements to run them on.
* TODO: explore axe-core’s internals. Can I make it yield periodically?
import assert from 'node:assert/strict';
import {suite, test} from 'node:test';
import { suite, test } from 'node:test';
import type { Mock } from 'node:test';
import accented from './accented.js';
import { accented } from './accented';

@@ -7,0 +7,0 @@ suite('Accented', () => {

@@ -1,17 +0,16 @@

import registerElements from './register-elements.js';
import createDomUpdater from './dom-updater.js';
import createLogger from './logger.js';
import createScanner from './scanner.js';
import setupScrollListeners from './scroll-listeners.js';
import setupResizeListener from './resize-listener.js';
import setupFullscreenListener from './fullscreen-listener.js';
import setupIntersectionObserver from './intersection-observer.js';
import { createDomUpdater } from './dom-updater.js';
import { setupResizeListener as setupFullscreenListener } from './fullscreen-listener.js';
import { setupIntersectionObserver } from './intersection-observer.js';
import { logAndRethrow } from './log-and-rethrow.js';
import { createLogger } from './logger.js';
import { registerElements } from './register-elements.js';
import { setupResizeListener } from './resize-listener.js';
import { createScanner } from './scanner.js';
import { setupScrollListeners } from './scroll-listeners.js';
import { enabled, extendedElementsWithIssues } from './state.js';
import deepMerge from './utils/deep-merge.js';
import type { AccentedOptions, DisableAccented } from './types';
import validateOptions from './validate-options.js';
import supportsAnchorPositioning from './utils/supports-anchor-positioning.js';
import logAndRethrow from './log-and-rethrow.js';
import type { AccentedOptions, DisableAccented } from './types.ts';
import { initializeContainingBlockSupportSet } from './utils/containing-blocks.js';
import { deepMerge } from './utils/deep-merge.js';
import { supportsAnchorPositioning } from './utils/supports-anchor-positioning.js';
import { validateOptions } from './validate-options.js';

@@ -45,4 +44,3 @@ export type { AccentedOptions, DisableAccented };

*/
export default function accented(options: AccentedOptions = {}): DisableAccented {
export function accented(options: AccentedOptions = {}): DisableAccented {
validateOptions(options);

@@ -52,3 +50,5 @@

if (typeof window === 'undefined' || typeof document === 'undefined') {
console.warn('Accented: this script can only run in the browser, and it’s likely running on the server now. Exiting.');
console.warn(
'Accented: this script can only run in the browser, and it’s likely running on the server now. Exiting.',
);
console.trace();

@@ -59,3 +59,3 @@ return () => {};

const defaultOutput: Required<AccentedOptions['output']> = {
console: true
console: true,
};

@@ -65,3 +65,3 @@

wait: 1000,
leading: true
leading: true,
};

@@ -80,6 +80,9 @@

throttle: defaultThrottle,
callback: () => {}
callback: () => {},
};
const {context, axeOptions, name, output, throttle, callback} = deepMerge(defaultOptions, options);
const { context, axeOptions, name, output, throttle, callback } = deepMerge(
defaultOptions,
options,
);

@@ -90,3 +93,3 @@ if (enabled.value) {

'You are trying to run the Accented library more than once. ' +
'This will likely lead to errors.'
'This will likely lead to errors.',
);

@@ -101,9 +104,14 @@ console.trace();

const {disconnect: cleanupIntersectionObserver, intersectionObserver } = supportsAnchorPositioning(window) ? {} : setupIntersectionObserver();
const { disconnect: cleanupIntersectionObserver, intersectionObserver } =
setupIntersectionObserver();
const cleanupScanner = createScanner(name, context, axeOptions, throttle, callback);
const cleanupDomUpdater = createDomUpdater(name, intersectionObserver);
const cleanupLogger = output.console ? createLogger() : () => {};
const cleanupScrollListeners = supportsAnchorPositioning(window) ? () => {} : setupScrollListeners();
const cleanupResizeListener = supportsAnchorPositioning(window) ? () => {} : setupResizeListener();
const cleanupFullscreenListener = supportsAnchorPositioning(window) ? () => {} : setupFullscreenListener();
const cleanupScrollListeners = setupScrollListeners();
const cleanupResizeListener = supportsAnchorPositioning(window)
? () => {}
: setupResizeListener();
const cleanupFullscreenListener = supportsAnchorPositioning(window)
? () => {}
: setupFullscreenListener();

@@ -110,0 +118,0 @@ return () => {

import { effect } from '@preact/signals-core';
import { primaryColor } from './common/tokens.js';
import { extendedElementsWithIssues, rootNodes } from './state.js';
import type { ExtendedElementWithIssues } from './types';
import areElementsWithIssuesEqual from './utils/are-elements-with-issues-equal.js';
import supportsAnchorPositioning from './utils/supports-anchor-positioning.js';
import type { ExtendedElementWithIssues } from './types.ts';
import { areElementsWithIssuesEqual } from './utils/are-elements-with-issues-equal.js';
import { isDocument, isDocumentFragment, isShadowRoot } from './utils/dom-helpers.js';
import getParent from './utils/get-parent.js';
import { getParent } from './utils/get-parent.js';
import { supportsAnchorPositioning } from './utils/supports-anchor-positioning.js';

@@ -33,13 +34,13 @@ const shouldInsertTriggerInsideElement = (element: Element): boolean => {

export default function createDomUpdater(name: string, intersectionObserver?: IntersectionObserver) {
export function createDomUpdater(name: string, intersectionObserver?: IntersectionObserver) {
const attrName = `data-${name}`;
function getAnchorNames (anchorNameValue: string) {
function getAnchorNames(anchorNameValue: string) {
return anchorNameValue
.split(',')
.map(anchorName => anchorName.trim())
.filter(anchorName => anchorName.startsWith('--'));
.map((anchorName) => anchorName.trim())
.filter((anchorName) => anchorName.startsWith('--'));
}
function setAnchorName (elementWithIssues: ExtendedElementWithIssues) {
function setAnchorName(elementWithIssues: ExtendedElementWithIssues) {
const { element, id, anchorNameValue } = elementWithIssues;

@@ -54,3 +55,3 @@ const anchorNames = getAnchorNames(anchorNameValue);

function removeAnchorName (elementWithIssues: ExtendedElementWithIssues) {
function removeAnchorName(elementWithIssues: ExtendedElementWithIssues) {
const { element, anchorNameValue } = elementWithIssues;

@@ -65,3 +66,3 @@ const anchorNames = getAnchorNames(anchorNameValue);

function setIssues (extendedElementsWithIssues: Array<ExtendedElementWithIssues>) {
function setIssues(extendedElementsWithIssues: Array<ExtendedElementWithIssues>) {
for (const elementWithIssues of extendedElementsWithIssues) {

@@ -87,3 +88,3 @@ if (elementWithIssues.skipRender) {

function removeIssues (extendedElementsWithIssues: Array<ExtendedElementWithIssues>) {
function removeIssues(extendedElementsWithIssues: Array<ExtendedElementWithIssues>) {
for (const elementWithIssues of extendedElementsWithIssues) {

@@ -110,3 +111,3 @@ if (elementWithIssues.skipRender) {

/* OKLCH stuff: https://oklch.com/ */
--${name}-primary-color: oklch(0.5 0.3 0);
--${name}-primary-color: ${primaryColor};
--${name}-secondary-color: oklch(0.98 0 0);

@@ -132,4 +133,6 @@ --${name}-outline-width: 2px;

const newRootNodes = rootNodes.value;
const addedRootNodes = [...newRootNodes].filter(rootNode => !previousRootNodes.has(rootNode));
const removedRootNodes = [...previousRootNodes].filter(rootNode => !newRootNodes.has(rootNode));
const addedRootNodes = [...newRootNodes].filter((rootNode) => !previousRootNodes.has(rootNode));
const removedRootNodes = [...previousRootNodes].filter(
(rootNode) => !newRootNodes.has(rootNode),
);
for (const rootNode of addedRootNodes) {

@@ -149,7 +152,11 @@ if (isDocument(rootNode) || (isDocumentFragment(rootNode) && isShadowRoot(rootNode))) {

const disposeOfElementsEffect = effect(() => {
const added = extendedElementsWithIssues.value.filter(elementWithIssues => {
return !previousExtendedElementsWithIssues.some(previousElementWithIssues => areElementsWithIssuesEqual(previousElementWithIssues, elementWithIssues));
const added = extendedElementsWithIssues.value.filter((elementWithIssues) => {
return !previousExtendedElementsWithIssues.some((previousElementWithIssues) =>
areElementsWithIssuesEqual(previousElementWithIssues, elementWithIssues),
);
});
const removed = previousExtendedElementsWithIssues.filter(previousElementWithIssues => {
return !extendedElementsWithIssues.value.some(elementWithIssues => areElementsWithIssuesEqual(elementWithIssues, previousElementWithIssues));
const removed = previousExtendedElementsWithIssues.filter((previousElementWithIssues) => {
return !extendedElementsWithIssues.value.some((elementWithIssues) =>
areElementsWithIssuesEqual(elementWithIssues, previousElementWithIssues),
);
});

@@ -156,0 +163,0 @@ removeIssues(removed);

@@ -1,6 +0,7 @@

import type { Issue } from '../types';
import type { Signal } from '@preact/signals-core';
import getElementHtml from '../utils/get-element-html.js';
import { accentedUrl } from '../constants.js';
import logAndRethrow from '../log-and-rethrow.js';
import { logAndRethrow } from '../log-and-rethrow.js';
import type { Issue } from '../types.ts';
import { getElementHtml } from '../utils/get-element-html.js';
import { isNonEmpty } from '../utils/is-non-empty.js';

@@ -16,3 +17,3 @@ export interface AccentedDialog extends HTMLElement {

// so we can't export `class extends HTMLElement` because HTMLElement is not available in Node.
export default () => {
export const getAccentedDialog = () => {
const dialogTemplate = document.createElement('template');

@@ -251,7 +252,7 @@ dialogTemplate.innerHTML = `

open: boolean = false;
open = false;
constructor() {
super();
try {
super();
this.attachShadow({ mode: 'open' });

@@ -275,27 +276,39 @@ const content = dialogTemplate.content.cloneNode(true);

this.#abortController = new AbortController();
closeButton?.addEventListener('click', () => {
try {
dialog?.close();
} catch (error) {
logAndRethrow(error);
}
}, { signal: this.#abortController.signal });
closeButton?.addEventListener(
'click',
() => {
try {
dialog?.close();
} catch (error) {
logAndRethrow(error);
}
},
{ signal: this.#abortController.signal },
);
dialog?.addEventListener('click', (event) => {
try {
this.#onDialogClick(event);
} catch (error) {
logAndRethrow(error);
}
}, { signal: this.#abortController.signal });
dialog?.addEventListener(
'click',
(event) => {
try {
this.#onDialogClick(event);
} catch (error) {
logAndRethrow(error);
}
},
{ signal: this.#abortController.signal },
);
dialog?.addEventListener('keydown', (event) => {
try {
if (event.key === 'Escape') {
event.stopPropagation();
dialog?.addEventListener(
'keydown',
(event) => {
try {
if (event.key === 'Escape') {
event.stopPropagation();
}
} catch (error) {
logAndRethrow(error);
}
} catch (error) {
logAndRethrow(error);
}
}, { signal: this.#abortController.signal });
},
{ signal: this.#abortController.signal },
);

@@ -313,6 +326,6 @@ if (this.issues) {

if (title && impact && description) {
title.textContent = issue.title + ' (' + issue.id + ')';
title.textContent = `${issue.title} (${issue.id})`;
title.href = issue.url;
impact.textContent = 'User impact: ' + issue.impact;
impact.textContent = `User impact: ${issue.impact}`;
impact.setAttribute('data-impact', String(issue.impact));

@@ -324,4 +337,9 @@

const descriptionList = descriptionContent.querySelector('ul');
if (descriptionTitle && descriptionList && descriptionItems.length > 1) {
descriptionTitle.textContent = descriptionItems[0]!;
if (
descriptionTitle &&
descriptionList &&
isNonEmpty(descriptionItems) &&
descriptionItems.length > 1
) {
descriptionTitle.textContent = descriptionItems[0];
for (const descriptionItem of descriptionItems.slice(1)) {

@@ -347,10 +365,14 @@ const li = document.createElement('li');

dialog?.addEventListener('close', () => {
try {
this.open = false;
this.dispatchEvent(new Event('close'));
} catch (error) {
logAndRethrow(error);
}
}, { signal: this.#abortController.signal });
dialog?.addEventListener(
'close',
() => {
try {
this.open = false;
this.dispatchEvent(new Event('close'));
} catch (error) {
logAndRethrow(error);
}
},
{ signal: this.#abortController.signal },
);
}

@@ -384,3 +406,7 @@ } catch (error) {

const dialog = event.currentTarget as HTMLDialogElement;
if (!dialog || typeof dialog.getBoundingClientRect !== 'function' || typeof dialog.close !== 'function') {
if (
!dialog ||
typeof dialog.getBoundingClientRect !== 'function' ||
typeof dialog.close !== 'function'
) {
return;

@@ -387,0 +413,0 @@ }

@@ -1,7 +0,7 @@

import type { AccentedDialog } from './accented-dialog';
import type { Position } from '../types';
import { effect } from '@preact/signals-core';
import type { Signal } from '@preact/signals-core';
import supportsAnchorPositioning from '../utils/supports-anchor-positioning.js';
import logAndRethrow from '../log-and-rethrow.js';
import { logAndRethrow } from '../log-and-rethrow.js';
import type { Position } from '../types.ts';
import { supportsAnchorPositioning } from '../utils/supports-anchor-positioning.js';
import type { AccentedDialog } from './accented-dialog.ts';

@@ -17,3 +17,3 @@ export interface AccentedTrigger extends HTMLElement {

// so we can't export `class extends HTMLElement` because HTMLElement is not available in Node.
export default (name: string) => {
export const getAccentedTrigger = (name: string) => {
const template = document.createElement('template');

@@ -37,4 +37,2 @@

position-visibility: anchors-visible !important;
/* Revert potential effects of white-space: pre; set on a trigger's ancestor. */

@@ -110,4 +108,4 @@ white-space: normal !important;

constructor() {
super();
try {
super();
this.attachShadow({ mode: 'open' });

@@ -144,3 +142,3 @@ const content = template.content.cloneNode(true);

this.#elementMutationObserver.observe(this.element, {
attributes: true
attributes: true,
});

@@ -150,33 +148,41 @@ }

this.#abortController = new AbortController();
trigger?.addEventListener('click', (event) => {
try {
// event.preventDefault() ensures that if the issue is within a link,
// the link's default behavior (following the URL) is prevented.
event.preventDefault();
trigger?.addEventListener(
'click',
(event) => {
try {
// event.preventDefault() ensures that if the issue is within a link,
// the link's default behavior (following the URL) is prevented.
event.preventDefault();
// event.stopPropagation() ensures that if there's a click handler on the trigger's ancestor
// (a link, or a button, or anything else), it doesn't get triggered.
event.stopPropagation();
// event.stopPropagation() ensures that if there's a click handler on the trigger's ancestor
// (a link, or a button, or anything else), it doesn't get triggered.
event.stopPropagation();
// We append the dialog when the button is clicked,
// and remove it from the DOM when the dialog is closed.
// This gives us a performance improvement since Axe
// scan time seems to depend on the number of elements in the DOM.
if (this.dialog) {
this.#dialogCloseAbortController = new AbortController();
document.body.append(this.dialog);
this.dialog.showModal();
this.dialog.addEventListener('close', () => {
try {
this.dialog?.remove();
this.#dialogCloseAbortController?.abort();
} catch (error) {
logAndRethrow(error);
}
}, { signal: this.#dialogCloseAbortController.signal });
// We append the dialog when the button is clicked,
// and remove it from the DOM when the dialog is closed.
// This gives us a performance improvement since Axe
// scan time seems to depend on the number of elements in the DOM.
if (this.dialog) {
this.#dialogCloseAbortController = new AbortController();
document.body.append(this.dialog);
this.dialog.showModal();
this.dialog.addEventListener(
'close',
() => {
try {
this.dialog?.remove();
this.#dialogCloseAbortController?.abort();
} catch (error) {
logAndRethrow(error);
}
},
{ signal: this.#dialogCloseAbortController.signal },
);
}
} catch (error) {
logAndRethrow(error);
}
} catch (error) {
logAndRethrow(error);
}
}, { signal: this.#abortController.signal });
},
{ signal: this.#abortController.signal },
);

@@ -193,7 +199,10 @@ if (!supportsAnchorPositioning(window)) {

});
this.#disposeOfVisibilityEffect = effect(() => {
this.style.setProperty('visibility', this.visible?.value ? 'visible' : 'hidden', 'important');
});
}
this.#disposeOfVisibilityEffect = effect(() => {
this.style.setProperty(
'visibility',
this.visible?.value ? 'visible' : 'hidden',
'important',
);
});
}

@@ -200,0 +209,0 @@ } catch (error) {

@@ -1,13 +0,17 @@

import logAndRethrow from './log-and-rethrow.js';
import recalculatePositions from './utils/recalculate-positions.js';
import { logAndRethrow } from './log-and-rethrow.js';
import { recalculatePositions } from './utils/recalculate-positions.js';
export default function setupResizeListener() {
export function setupResizeListener() {
const abortController = new AbortController();
window.addEventListener('fullscreenchange', () => {
try {
recalculatePositions();
} catch (error) {
logAndRethrow(error);
}
}, { signal: abortController.signal });
window.addEventListener(
'fullscreenchange',
() => {
try {
recalculatePositions();
} catch (error) {
logAndRethrow(error);
}
},
{ signal: abortController.signal },
);

@@ -17,2 +21,2 @@ return () => {

};
};
}

@@ -1,21 +0,32 @@

import logAndRethrow from './log-and-rethrow.js';
import { logAndRethrow } from './log-and-rethrow.js';
import { extendedElementsWithIssues } from './state.js';
import getElementPosition from './utils/get-element-position.js';
import { getElementPosition } from './utils/get-element-position.js';
import { supportsAnchorPositioning } from './utils/supports-anchor-positioning.js';
export default function setupIntersectionObserver() {
const intersectionObserver = new IntersectionObserver((entries) => {
try {
for (const entry of entries) {
const extendedElementWithIssues = extendedElementsWithIssues.value.find(el => el.element === entry.target);
if (extendedElementWithIssues) {
extendedElementWithIssues.visible.value = entry.isIntersecting;
if (entry.isIntersecting) {
extendedElementWithIssues.position.value = getElementPosition(entry.target, window);
export function setupIntersectionObserver() {
const intersectionObserver = new IntersectionObserver(
(entries) => {
try {
for (const entry of entries) {
const extendedElementWithIssues = extendedElementsWithIssues.value.find(
(el) => el.element === entry.target,
);
if (extendedElementWithIssues) {
// We initially treated setting visibility in the intersection observer
// as a fallback option for browsers that don't support `position-visibility`,
// but then we realized that this `position-visibility` actually works
// in an unexpected way when the container has `overflow: visible`.
// So now we always set visibility in the intersection observer.
extendedElementWithIssues.visible.value = entry.isIntersecting;
if (entry.isIntersecting && !supportsAnchorPositioning(window)) {
extendedElementWithIssues.position.value = getElementPosition(entry.target, window);
}
}
}
} catch (error) {
logAndRethrow(error);
}
} catch (error) {
logAndRethrow(error);
}
}, { threshold: 0 });
},
{ threshold: 0 },
);

@@ -26,4 +37,4 @@ return {

intersectionObserver.disconnect();
}
},
};
}
import { issuesUrl } from './constants.js';
export default function logAndRethrow(error: unknown) {
export function logAndRethrow(error: unknown) {
console.error(
`Accented threw an error (see below). Try updating your browser to the latest version. ` +
`If you’re still seeing the error, file an issue at ${issuesUrl}.`
`Accented threw an error (see below). Try updating your browser to the latest version. If you’re still seeing the error, file an issue at ${issuesUrl}.`,
);
throw error;
}
import { effect } from '@preact/signals-core';
import { accentedUrl } from './constants.js';
import { elementsWithIssues, enabled } from './state.js';
import { accentedUrl } from './constants.js';
import type { ElementWithIssues } from './types';
import type { ElementWithIssues } from './types.ts';

@@ -10,4 +10,3 @@ function filterPropsForOutput(elements: Array<ElementWithIssues>) {

export default function createLogger() {
export function createLogger() {
let firstRun = true;

@@ -22,6 +21,9 @@

if (elementCount > 0) {
const issueCount = elementsWithIssues.value.reduce((acc, { issues }) => acc + issues.length, 0);
const issueCount = elementsWithIssues.value.reduce(
(acc, { issues }) => acc + issues.length,
0,
);
console.log(
`${issueCount} accessibility issue${issueCount === 1 ? '' : 's'} found in ${elementCount} element${issueCount === 1 ? '' : 's'} (Accented, ${accentedUrl}):\n`,
filterPropsForOutput(elementsWithIssues.value)
filterPropsForOutput(elementsWithIssues.value),
);

@@ -28,0 +30,0 @@ } else {

@@ -1,14 +0,14 @@

import getAccentedTrigger from './elements/accented-trigger.js';
import getAccentedDialog from './elements/accented-dialog.js';
import { getAccentedDialog } from './elements/accented-dialog.js';
import { getAccentedTrigger } from './elements/accented-trigger.js';
export default function registerElements(name: string): void {
export function registerElements(name: string): void {
const elements = [
{
elementName: `${name}-trigger`,
Component: getAccentedTrigger(name)
Component: getAccentedTrigger(name),
},
{
elementName: `${name}-dialog`,
Component: getAccentedDialog()
}
Component: getAccentedDialog(),
},
];

@@ -21,2 +21,2 @@

}
};
}

@@ -1,13 +0,17 @@

import logAndRethrow from './log-and-rethrow.js';
import recalculatePositions from './utils/recalculate-positions.js';
import { logAndRethrow } from './log-and-rethrow.js';
import { recalculatePositions } from './utils/recalculate-positions.js';
export default function setupResizeListener() {
export function setupResizeListener() {
const abortController = new AbortController();
window.addEventListener('resize', () => {
try {
recalculatePositions();
} catch (error) {
logAndRethrow(error);
}
}, { signal: abortController.signal });
window.addEventListener(
'resize',
() => {
try {
recalculatePositions();
} catch (error) {
logAndRethrow(error);
}
},
{ signal: abortController.signal },
);

@@ -17,2 +21,2 @@ return () => {

};
};
}
import axe from 'axe-core';
import TaskQueue from './task-queue.js';
import { getAccentedElementNames, issuesUrl } from './constants.js';
import { logAndRethrow } from './log-and-rethrow.js';
import { elementsWithIssues, enabled, extendedElementsWithIssues } from './state.js';
import type { AxeOptions, Throttle, Callback, Context } from './types';
import updateElementsWithIssues from './utils/update-elements-with-issues.js';
import recalculatePositions from './utils/recalculate-positions.js';
import recalculateScrollableAncestors from './utils/recalculate-scrollable-ancestors.js';
import supportsAnchorPositioning from './utils/supports-anchor-positioning.js';
import { getAccentedElementNames, issuesUrl } from './constants.js';
import logAndRethrow from './log-and-rethrow.js';
import createShadowDOMAwareMutationObserver from './utils/shadow-dom-aware-mutation-observer.js';
import getScanContext from './utils/get-scan-context.js';
import { TaskQueue } from './task-queue.js';
import type { AxeOptions, Callback, Context, Throttle } from './types.ts';
import { getScanContext } from './utils/get-scan-context.js';
import { recalculatePositions } from './utils/recalculate-positions.js';
import { recalculateScrollableAncestors } from './utils/recalculate-scrollable-ancestors.js';
import { createShadowDOMAwareMutationObserver } from './utils/shadow-dom-aware-mutation-observer.js';
import { supportsAnchorPositioning } from './utils/supports-anchor-positioning.js';
import { updateElementsWithIssues } from './utils/update-elements-with-issues.js';
export default function createScanner(name: string, context: Context, axeOptions: AxeOptions, throttle: Required<Throttle>, callback: Callback) {
export function createScanner(
name: string,
context: Context,
axeOptions: AxeOptions,
throttle: Required<Throttle>,
callback: Callback,
) {
const axeRunningWindowProp = `__${name}_axe_running__`;
const win: Record<string, any> = window;
const win = window as unknown as Record<string, boolean>;
const taskQueue = new TaskQueue<Node>(async (nodes) => {

@@ -26,3 +32,2 @@ // We may see errors coming from axe-core when Accented is toggled off and on in qiuck succession,

try {
performance.mark('scan-start');

@@ -34,3 +39,3 @@

let result;
let result: axe.AxeResults | undefined;

@@ -48,12 +53,9 @@ try {

resultTypes: ['violations'],
...axeOptions
...axeOptions,
});
} catch (error) {
console.error(
'Accented: axe-core (the accessibility testing engine) threw an error. ' +
'Check the `axeOptions` property that you’re passing to Accented. ' +
`If you still think it’s a bug in Accented, file an issue at ${issuesUrl}.\n`,
error
`Accented: axe-core (the accessibility testing engine) threw an error. Check the \`axeOptions\` property that you’re passing to Accented. If you still think it’s a bug in Accented, file an issue at ${issuesUrl}.\n`,
error,
);
result = { violations: [] };
}

@@ -65,3 +67,3 @@ win[axeRunningWindowProp] = false;

if (!enabled.value) {
if (!enabled.value || !result) {
return;

@@ -77,3 +79,3 @@ }

win: window,
name
name,
});

@@ -93,4 +95,4 @@

// in the scan.
scanContext: scanContext.exclude.length > 0 ? scanContext : scanContext.include
}
scanContext: scanContext.exclude.length > 0 ? scanContext : scanContext.include,
},
});

@@ -106,11 +108,17 @@ } catch (error) {

const accentedElementNames = getAccentedElementNames(name);
const mutationObserver = createShadowDOMAwareMutationObserver(name, mutationList => {
const mutationObserver = createShadowDOMAwareMutationObserver(name, (mutationList) => {
try {
// We're not interested in mutations that are caused exclusively by the custom elements
// introduced by Accented.
const listWithoutAccentedElements = mutationList.filter(mutationRecord => {
const onlyAccentedElementsAddedOrRemoved = mutationRecord.type === 'childList' &&
[...mutationRecord.addedNodes].every(node => accentedElementNames.includes(node.nodeName.toLowerCase())) &&
[...mutationRecord.removedNodes].every(node => accentedElementNames.includes(node.nodeName.toLowerCase()));
const accentedElementChanged = mutationRecord.type === 'attributes' &&
const listWithoutAccentedElements = mutationList.filter((mutationRecord) => {
const onlyAccentedElementsAddedOrRemoved =
mutationRecord.type === 'childList' &&
[...mutationRecord.addedNodes].every((node) =>
accentedElementNames.includes(node.nodeName.toLowerCase()),
) &&
[...mutationRecord.removedNodes].every((node) =>
accentedElementNames.includes(node.nodeName.toLowerCase()),
);
const accentedElementChanged =
mutationRecord.type === 'attributes' &&
accentedElementNames.includes(mutationRecord.target.nodeName.toLowerCase());

@@ -135,14 +143,20 @@ return !(onlyAccentedElementsAddedOrRemoved || accentedElementChanged);

// leading to extra runs of the mutation observer.
const elementsWithAccentedAttributeChanges = listWithoutAccentedElements.reduce((nodes, mutationRecord) => {
if (mutationRecord.type === 'attributes' && mutationRecord.attributeName === `data-${name}`) {
nodes.add(mutationRecord.target);
}
return nodes;
}, new Set<Node>());
const elementsWithAccentedAttributeChanges = listWithoutAccentedElements.reduce(
(nodes, mutationRecord) => {
if (
mutationRecord.type === 'attributes' &&
mutationRecord.attributeName === `data-${name}`
) {
nodes.add(mutationRecord.target);
}
return nodes;
},
new Set<Node>(),
);
const filteredMutationList = listWithoutAccentedElements.filter(mutationRecord => {
const filteredMutationList = listWithoutAccentedElements.filter((mutationRecord) => {
return !elementsWithAccentedAttributeChanges.has(mutationRecord.target);
});
const nodes = filteredMutationList.map(mutationRecord => mutationRecord.target);
const nodes = filteredMutationList.map((mutationRecord) => mutationRecord.target);
taskQueue.addMultiple(nodes);

@@ -158,3 +172,3 @@ } catch (error) {

attributes: true,
characterData: true
characterData: true,
});

@@ -161,0 +175,0 @@

import { effect } from '@preact/signals-core';
import recalculatePositions from './utils/recalculate-positions.js';
import { logAndRethrow } from './log-and-rethrow.js';
import { scrollableAncestors } from './state.js';
import logAndRethrow from './log-and-rethrow.js';
import { recalculatePositions } from './utils/recalculate-positions.js';
export default function setupScrollListeners() {
export function setupScrollListeners() {
const documentAbortController = new AbortController();
document.addEventListener('scroll', () => {
try {
recalculatePositions();
} catch (error) {
logAndRethrow(error);
}
}, { signal: documentAbortController.signal });
document.addEventListener(
'scroll',
() => {
try {
recalculatePositions();
} catch (error) {
logAndRethrow(error);
}
},
{ signal: documentAbortController.signal },
);

@@ -20,13 +24,17 @@ const disposeOfEffect = effect(() => {

for (const scrollableAncestor of scrollableAncestors.value) {
scrollableAncestor.addEventListener('scroll', () => {
try {
recalculatePositions();
} catch (error) {
logAndRethrow(error);
}
}, { signal: elementAbortController.signal });
scrollableAncestor.addEventListener(
'scroll',
() => {
try {
recalculatePositions();
} catch (error) {
logAndRethrow(error);
}
},
{ signal: elementAbortController.signal },
);
}
return () => {
elementAbortController.abort();
}
};
});

@@ -38,2 +46,2 @@

};
};
}

@@ -1,4 +0,4 @@

import { signal, computed } from '@preact/signals-core';
import { computed, signal } from '@preact/signals-core';
import type { ElementWithIssues, ExtendedElementWithIssues } from './types';
import type { ElementWithIssues, ExtendedElementWithIssues } from './types.ts';

@@ -9,25 +9,28 @@ export const enabled = signal(false);

export const elementsWithIssues = computed<Array<ElementWithIssues>>(() => extendedElementsWithIssues.value.map(extendedElementWithIssues => ({
element: extendedElementWithIssues.element,
rootNode: extendedElementWithIssues.rootNode,
issues: extendedElementWithIssues.issues.value
})));
export const elementsWithIssues = computed<Array<ElementWithIssues>>(() =>
extendedElementsWithIssues.value.map((extendedElementWithIssues) => ({
element: extendedElementWithIssues.element,
rootNode: extendedElementWithIssues.rootNode,
issues: extendedElementWithIssues.issues.value,
})),
);
export const rootNodes = computed<Set<Node>>(() =>
new Set(
(enabled.value ? [document as Node] : [])
.concat(...(extendedElementsWithIssues.value.map(extendedElementWithIssues => extendedElementWithIssues.rootNode)))
)
export const rootNodes = computed<Set<Node>>(
() =>
new Set(
(enabled.value ? [document as Node] : []).concat(
...extendedElementsWithIssues.value.map(
(extendedElementWithIssues) => extendedElementWithIssues.rootNode,
),
),
),
);
export const scrollableAncestors = computed<Set<Element>>(() =>
extendedElementsWithIssues.value.reduce(
(scrollableAncestors, extendedElementWithIssues) => {
for (const scrollableAncestor of extendedElementWithIssues.scrollableAncestors.value) {
scrollableAncestors.add(scrollableAncestor);
}
return scrollableAncestors;
},
new Set<Element>()
)
extendedElementsWithIssues.value.reduce((scrollableAncestors, extendedElementWithIssues) => {
for (const scrollableAncestor of extendedElementWithIssues.scrollableAncestors.value) {
scrollableAncestors.add(scrollableAncestor);
}
return scrollableAncestors;
}, new Set<Element>()),
);
import assert from 'node:assert/strict';
import {mock, suite, test} from 'node:test';
import { mock, suite, test } from 'node:test';
import TaskQueue from './task-queue.js';
import { TaskQueue } from './task-queue';
const wait = (duration: number) => new Promise(resolve => setTimeout(resolve, duration));
const wait = (duration: number) => new Promise((resolve) => setTimeout(resolve, duration));
const createAsyncCallback = (duration: number) => mock.fn(() => new Promise(resolve => setTimeout(resolve, duration)));
const createAsyncCallback = (duration: number) =>
mock.fn(() => new Promise((resolve) => setTimeout(resolve, duration)));

@@ -10,0 +11,0 @@ suite('TaskQueue', () => {

@@ -1,6 +0,6 @@

import type { Throttle } from './types';
import type { Throttle } from './types.ts';
type TaskCallback<T> = (items: Array<T>) => void;
export default class TaskQueue<T> {
export class TaskQueue<T> {
#throttle: Throttle;

@@ -7,0 +7,0 @@ #asyncCallback: TaskCallback<T> | null = null;

@@ -0,4 +1,4 @@

import type { Signal } from '@preact/signals-core';
import type axe from 'axe-core';
import type { Signal } from '@preact/signals-core';
import type { AccentedTrigger } from './elements/accented-trigger';
import type { AccentedTrigger } from './elements/accented-trigger.ts';

@@ -11,3 +11,3 @@ export type Throttle = {

* */
wait?: number,
wait?: number;

@@ -21,4 +21,4 @@ /**

* */
leading?: boolean
}
leading?: boolean;
};

@@ -31,4 +31,4 @@ export type Output = {

* */
console?: boolean
}
console?: boolean;
};

@@ -49,17 +49,17 @@ /**

export type ContextObject = {
include: ContextProp;
exclude?: ContextProp;
} | {
exclude: ContextProp;
include?: ContextProp;
};
export type ContextObject =
| {
include: ContextProp;
exclude?: ContextProp;
}
| {
exclude: ContextProp;
include?: ContextProp;
};
export type Context = ContextProp | ContextObject;
export const allowedAxeOptions = ['rules', 'runOnly'] as const;
export type AxeOptions = Pick<axe.RunOptions, typeof allowedAxeOptions[number]>;
export type AxeOptions = Pick<axe.RunOptions, (typeof allowedAxeOptions)[number]>;

@@ -70,3 +70,3 @@ type CallbackParams = {

* */
elementsWithIssues: Array<ElementWithIssues>,
elementsWithIssues: Array<ElementWithIssues>;

@@ -83,8 +83,8 @@ /**

performance: {
totalBlockingTime: number,
scan: number,
domUpdate: number,
scanContext: ScanContext | Array<Node>
}
}
totalBlockingTime: number;
scan: number;
domUpdate: number;
scanContext: ScanContext | Array<Node>;
};
};

@@ -94,3 +94,2 @@ export type Callback = (params: CallbackParams) => void;

export type AccentedOptions = {
/**

@@ -112,3 +111,3 @@ * The `context` parameter for `axe.run()`.

*/
context?: Context,
context?: Context;

@@ -129,3 +128,3 @@ /**

*/
axeOptions?: AxeOptions,
axeOptions?: AxeOptions;

@@ -147,3 +146,3 @@ /**

*/
name?: string,
name?: string;

@@ -153,3 +152,3 @@ /**

* */
output?: Output,
output?: Output;

@@ -159,3 +158,3 @@ /**

* */
throttle?: Throttle,
throttle?: Throttle;

@@ -167,3 +166,3 @@ /**

* */
callback?: Callback
callback?: Callback;
};

@@ -178,39 +177,39 @@

export type Position = {
left: number,
top: number,
width: number,
height: number
left: number;
top: number;
width: number;
height: number;
};
export type Issue = {
id: string,
title: string,
description: string,
url: string,
impact: axe.ImpactValue
id: string;
title: string;
description: string;
url: string;
impact: axe.ImpactValue;
};
export type BaseElementWithIssues = {
element: HTMLElement | SVGElement,
rootNode: Node
element: HTMLElement | SVGElement;
rootNode: Node;
};
export type ElementWithIssues = BaseElementWithIssues & {
issues: Array<Issue>
issues: Array<Issue>;
};
export type ExtendedElementWithIssues = BaseElementWithIssues & {
issues: Signal<ElementWithIssues['issues']>,
visible: Signal<boolean>,
trigger: AccentedTrigger,
position: Signal<Position>,
skipRender: boolean,
anchorNameValue: string,
scrollableAncestors: Signal<Set<Element>>
id: number
issues: Signal<ElementWithIssues['issues']>;
visible: Signal<boolean>;
trigger: AccentedTrigger;
position: Signal<Position>;
skipRender: boolean;
anchorNameValue: string;
scrollableAncestors: Signal<Set<Element>>;
id: number;
};
export type ScanContext = {
include: Array<Node>,
exclude: Array<Node>
include: Array<Node>;
exclude: Array<Node>;
};

@@ -1,9 +0,11 @@

import type { BaseElementWithIssues } from "../types";
import type { BaseElementWithIssues } from '../types.ts';
export default function areElementsWithIssuesEqual(
export function areElementsWithIssuesEqual(
elementWithIssues1: BaseElementWithIssues,
elementWithIssues2: BaseElementWithIssues
elementWithIssues2: BaseElementWithIssues,
) {
return elementWithIssues1.element === elementWithIssues2.element
&& elementWithIssues1.rootNode === elementWithIssues2.rootNode;
return (
elementWithIssues1.element === elementWithIssues2.element &&
elementWithIssues1.rootNode === elementWithIssues2.rootNode
);
}
import assert from 'node:assert/strict';
import {suite, test} from 'node:test';
import areIssueSetsEqual from './are-issue-sets-equal.js';
import { suite, test } from 'node:test';
import type { Issue } from '../types';
import { areIssueSetsEqual } from './are-issue-sets-equal';

@@ -11,3 +11,3 @@ const issue1: Issue = {

url: 'http://example.com',
impact: 'serious'
impact: 'serious',
};

@@ -20,7 +20,11 @@

url: 'http://example.com',
impact: 'serious'
impact: 'serious',
};
// @ts-expect-error
const issue2Clone: Issue = Object.keys(issue2).reduce((obj, key) => { obj[key] = issue2[key]; return obj; }, {});
const issue2Clone: Issue = Object.keys(issue2).reduce((obj, key) => {
// @ts-expect-error
obj[key] = issue2[key];
return obj;
}, {});

@@ -32,3 +36,3 @@ const issue3: Issue = {

url: 'http://example.com',
impact: 'serious'
impact: 'serious',
};

@@ -35,0 +39,0 @@

@@ -1,10 +0,12 @@

import type { Issue } from '../types';
import type { Issue } from '../types.ts';
const issueProps: Array<keyof Issue> = ['id', 'title', 'description', 'url', 'impact'];
export default function areIssueSetsEqual(issues1: Array<Issue>, issues2: Array<Issue>) {
return issues1.length === issues2.length &&
issues1.every(issue1 => Boolean(issues2.find(issue2 =>
issueProps.every(prop => issue2[prop] === issue1[prop])
)));
export function areIssueSetsEqual(issues1: Array<Issue>, issues2: Array<Issue>) {
return (
issues1.length === issues2.length &&
issues1.every((issue1) =>
Boolean(issues2.find((issue2) => issueProps.every((prop) => issue2[prop] === issue1[prop]))),
)
);
}

@@ -10,3 +10,6 @@ /**

*/
function testContainingBlockCreation<T extends keyof CSSStyleDeclaration>(prop: T, value: CSSStyleDeclaration[T]) {
function testContainingBlockCreation<T extends keyof CSSStyleDeclaration>(
prop: T,
value: CSSStyleDeclaration[T],
) {
const container = document.createElement('div');

@@ -44,3 +47,3 @@ container.style[prop] = value;

type StyleEntry<T extends keyof CSSStyleDeclaration> = {
[K in T]: { prop: K; value: CSSStyleDeclaration[K] }
[K in T]: { prop: K; value: CSSStyleDeclaration[K] };
}[T];

@@ -51,3 +54,3 @@

{ prop: 'backdropFilter', value: 'blur(1px)' },
{ prop: 'containerType', value: 'size' }
{ prop: 'containerType', value: 'size' },
];

@@ -54,0 +57,0 @@

@@ -1,5 +0,5 @@

import { JSDOM } from 'jsdom';
import assert from 'node:assert/strict';
import { suite, test } from 'node:test';
import contains from './contains';
import { JSDOM } from 'jsdom';
import { contains } from './contains';

@@ -6,0 +6,0 @@ suite('contains', () => {

import { isDocumentFragment, isShadowRoot } from './dom-helpers.js';
export default function contains(ancestor: Node, descendant: Node): boolean {
export function contains(ancestor: Node, descendant: Node): boolean {
if (ancestor.contains(descendant)) {

@@ -5,0 +5,0 @@ return true;

export function deduplicateNodes(nodes: Array<Node>): Array<Node> {
return [...new Set(nodes)];;
return [...new Set(nodes)];
}
import assert from 'node:assert/strict';
import { suite, test } from 'node:test';
import deepMerge from './deep-merge';
import { deepMerge } from './deep-merge';

@@ -34,2 +34,9 @@ suite('deepMerge', () => {

});
test('handles merging an object into a string in a logical way', () => {
const target = { a: 'hello' };
const source = { a: { b: 'bye' } };
const result = deepMerge(target, source);
assert.deepEqual(result, { a: { b: 'bye' } });
});
});

@@ -0,14 +1,17 @@

// biome-ignore lint/suspicious/noExplicitAny: I'm not sure how to type this properly
type AnyObject = Record<string, any>;
export default function deepMerge(target: AnyObject, source: AnyObject): AnyObject {
const output = {...target};
const isObject = (obj: unknown): obj is AnyObject =>
typeof obj === 'object' && obj !== null && !Array.isArray(obj);
export function deepMerge(target: AnyObject, source: AnyObject): AnyObject {
const output = { ...target };
for (const key of Object.keys(source)) {
if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) {
if (!(key in target)) {
if (isObject(source[key])) {
if (isObject(target[key])) {
output[key] = deepMerge(target[key], source[key]);
} else {
output[key] = source[key];
} else {
output[key] = deepMerge(target[key], source[key]);
}
}
else {
} else {
output[key] = source[key];

@@ -15,0 +18,0 @@ }

export function isNode(obj: object): obj is Node {
return 'nodeType' in obj && typeof obj.nodeType === 'number' &&
'nodeName' in obj && typeof obj.nodeName === 'string';
return (
'nodeType' in obj &&
typeof obj.nodeType === 'number' &&
'nodeName' in obj &&
typeof obj.nodeName === 'string'
);
}

@@ -5,0 +9,0 @@

@@ -1,6 +0,6 @@

export default function ensureNonEmpty<T>(arr: T[]): [T, ...T[]] {
export function ensureNonEmpty<T>(arr: T[]): [T, ...T[]] {
if (arr.length === 0) {
throw new Error("Array must not be empty");
throw new Error('Array must not be empty');
}
return arr as [T, ...T[]];
}

@@ -1,2 +0,2 @@

export default function getElementHtml(element: Element) {
export function getElementHtml(element: Element) {
const outerHtml = element.outerHTML;

@@ -12,3 +12,5 @@ const innerHtml = element.innerHTML;

}
return outerHtml.slice(0, index) + '…' + outerHtml.slice(index + innerHtml.length);
const openingTag = outerHtml.slice(0, index);
const closingTag = outerHtml.slice(index + innerHtml.length);
return `${openingTag}…${closingTag}`;
}

@@ -1,5 +0,5 @@

import type { Position } from '../types';
import type { Position } from '../types.ts';
import { createsContainingBlock } from './containing-blocks.js';
import { isHtmlElement } from './dom-helpers.js';
import getParent from './get-parent.js';
import { createsContainingBlock } from './containing-blocks.js';
import { getParent } from './get-parent.js';

@@ -9,14 +9,27 @@ // https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_display/Containing_block#identifying_the_containing_block

const style = win.getComputedStyle(element);
const { transform, perspective, contain, contentVisibility, containerType, filter, backdropFilter, willChange } = style;
const {
transform,
perspective,
contain,
contentVisibility,
containerType,
filter,
backdropFilter,
willChange,
} = style;
const containItems = contain.split(' ');
const willChangeItems = willChange.split(/\s*,\s*/);
return transform !== 'none'
|| perspective !== 'none'
|| containItems.some((item) => ['layout', 'paint', 'strict', 'content'].includes(item))
|| contentVisibility === 'auto'
|| (createsContainingBlock('containerType') && containerType !== 'normal')
|| (createsContainingBlock('filter') && filter !== 'none')
|| (createsContainingBlock('backdropFilter') && backdropFilter !== 'none')
|| willChangeItems.some((item) => ['transform', 'perspective', 'contain', 'filter', 'backdrop-filter'].includes(item));
return (
transform !== 'none' ||
perspective !== 'none' ||
containItems.some((item) => ['layout', 'paint', 'strict', 'content'].includes(item)) ||
contentVisibility === 'auto' ||
(createsContainingBlock('containerType') && containerType !== 'normal') ||
(createsContainingBlock('filter') && filter !== 'none') ||
(createsContainingBlock('backdropFilter') && backdropFilter !== 'none') ||
willChangeItems.some((item) =>
['transform', 'perspective', 'contain', 'filter', 'backdrop-filter'].includes(item),
)
);
}

@@ -43,3 +56,3 @@

*/
export default function getElementPosition(element: Element, win: Window): Position {
export function getElementPosition(element: Element, win: Window): Position {
const nonInitialContainingBlock = getNonInitialContainingBlock(element, win);

@@ -65,15 +78,15 @@ // If an element has a containing block as an ancestor,

return { top, left, width, height };
} else {
const elementRect = element.getBoundingClientRect();
const nonInitialContainingBlockRect = nonInitialContainingBlock.getBoundingClientRect();
return {
top: elementRect.top - nonInitialContainingBlockRect.top,
height: elementRect.height,
left: elementRect.left - nonInitialContainingBlockRect.left,
width: elementRect.width
};
}
} else {
return element.getBoundingClientRect();
const elementRect = element.getBoundingClientRect();
const nonInitialContainingBlockRect = nonInitialContainingBlock.getBoundingClientRect();
return {
top: elementRect.top - nonInitialContainingBlockRect.top,
height: elementRect.height,
left: elementRect.left - nonInitialContainingBlockRect.left,
width: elementRect.width,
};
}
return element.getBoundingClientRect();
}
import { isDocumentFragment, isShadowRoot } from './dom-helpers.js';
export default function getParent (element: Element): Element | null {
export function getParent(element: Element): Element | null {
if (element.parentElement) {

@@ -5,0 +5,0 @@ return element.parentElement;

@@ -1,5 +0,5 @@

import { JSDOM } from 'jsdom';
import assert from 'node:assert/strict';
import { suite, test } from 'node:test';
import getScanContext from './get-scan-context';
import { JSDOM } from 'jsdom';
import { getScanContext } from './get-scan-context';

@@ -16,3 +16,3 @@ suite('getScanContext', () => {

include: [],
exclude: []
exclude: [],
});

@@ -31,3 +31,3 @@ });

include: [contextNode],
exclude: []
exclude: [],
});

@@ -49,3 +49,6 @@ });

const mutatedNode = document.querySelector('#mutated-node')!;
const scanContext = getScanContext([mutatedNode], {include: ['.include'], exclude: ['.exclude']});
const scanContext = getScanContext([mutatedNode], {
include: ['.include'],
exclude: ['.exclude'],
});
const innerExclude = document.querySelector('#inner-exclude')!;

@@ -56,3 +59,3 @@ const innerInclude = document.querySelector('#inner-include')!;

include: [mutatedNode, innerInclude],
exclude: [innerExclude]
exclude: [innerExclude],
});

@@ -74,3 +77,6 @@ });

const mutatedNode = document.querySelector('#mutated-node')!;
const scanContext = getScanContext([mutatedNode], {include: ['.include'], exclude: ['.exclude']});
const scanContext = getScanContext([mutatedNode], {
include: ['.include'],
exclude: ['.exclude'],
});
const innerExclude = document.querySelector('#inner-exclude')!;

@@ -81,5 +87,5 @@ const innerInclude = document.querySelector('#inner-include')!;

include: [innerInclude],
exclude: [innerExclude]
exclude: [innerExclude],
});
});
});

@@ -1,19 +0,16 @@

import type { Context, ScanContext } from '../types';
import contains from './contains.js';
import type { Context, ScanContext } from '../types.ts';
import { contains } from './contains.js';
import { deduplicateNodes } from './deduplicate-nodes.js';
import isNodeInScanContext from './is-node-in-scan-context.js';
import normalizeContext from './normalize-context.js';
import { isNodeInScanContext } from './is-node-in-scan-context.js';
import { normalizeContext } from './normalize-context.js';
export default function getScanContext(nodes: Array<Node>, context: Context): ScanContext {
const {
include: contextInclude,
exclude: contextExclude
} = normalizeContext(context);
export function getScanContext(nodes: Array<Node>, context: Context): ScanContext {
const { include: contextInclude, exclude: contextExclude } = normalizeContext(context);
// Filter only nodes that are included by context (see isNodeInContext above).
const nodesInContext = nodes.filter(node =>
const nodesInContext = nodes.filter((node) =>
isNodeInScanContext(node, {
include: contextInclude,
exclude: contextExclude
})
exclude: contextExclude,
}),
);

@@ -29,5 +26,5 @@

for (const node of nodes) {
const includeDescendants = contextInclude.filter(item => contains(node, item));
const includeDescendants = contextInclude.filter((item) => contains(node, item));
include.push(...includeDescendants);
const excludeDescendants = contextExclude.filter(item => contains(node, item));
const excludeDescendants = contextExclude.filter((item) => contains(node, item));
exclude.push(...excludeDescendants);

@@ -38,4 +35,4 @@ }

include: deduplicateNodes(include),
exclude: deduplicateNodes(exclude)
exclude: deduplicateNodes(exclude),
};
}

@@ -1,8 +0,8 @@

import getParent from './get-parent.js';
import { getParent } from './get-parent.js';
const scrollableOverflowValues = new Set(['auto', 'scroll', 'hidden']);
export default function getScrollableAncestors (element: Element, win: Window) {
export function getScrollableAncestors(element: Element, win: Window) {
let currentElement: Element | null = element;
let scrollableAncestors = new Set<Element>();
const scrollableAncestors = new Set<Element>();
while (true) {

@@ -14,3 +14,6 @@ currentElement = getParent(currentElement);

const computedStyle = win.getComputedStyle(currentElement);
if (scrollableOverflowValues.has(computedStyle.overflowX) || scrollableOverflowValues.has(computedStyle.overflowY)) {
if (
scrollableOverflowValues.has(computedStyle.overflowX) ||
scrollableOverflowValues.has(computedStyle.overflowY)
) {
scrollableAncestors.add(currentElement);

@@ -20,2 +23,2 @@ }

return scrollableAncestors;
};
}

@@ -0,5 +1,5 @@

import assert from 'node:assert/strict';
import { suite, test } from 'node:test';
import { JSDOM } from 'jsdom';
import assert from 'node:assert/strict';
import {suite, test} from 'node:test';
import isNodeInScanContext from './is-node-in-scan-context';
import { isNodeInScanContext } from './is-node-in-scan-context';

@@ -6,0 +6,0 @@ suite('isNodeInScanContext', () => {

/* Adapted from https://github.com/dequelabs/axe-core/blob/fd6239bfc97ebc904044f93f68d7e49137f744ad/lib/core/utils/is-node-in-context.js */
import type { ScanContext } from '../types';
import contains from './contains.js';
import ensureNonEmpty from './ensure-non-empty.js';
import type { ScanContext } from '../types.ts';
import { contains } from './contains.js';
import { ensureNonEmpty } from './ensure-non-empty.js';

@@ -17,8 +17,8 @@ function getDeepest(nodes: [Node, ...Node[]]): Node {

export default function isNodeInScanContext(node: Node, { include, exclude }: ScanContext): boolean {
const filteredInclude = include.filter(includeNode => contains(includeNode, node));
export function isNodeInScanContext(node: Node, { include, exclude }: ScanContext): boolean {
const filteredInclude = include.filter((includeNode) => contains(includeNode, node));
if (filteredInclude.length === 0) {
return false;
}
const filteredExclude = exclude.filter(excludeNode => contains(excludeNode, node));
const filteredExclude = exclude.filter((excludeNode) => contains(excludeNode, node));
if (filteredExclude.length === 0) {

@@ -25,0 +25,0 @@ return true;

@@ -1,5 +0,5 @@

import { JSDOM } from 'jsdom';
import assert from 'node:assert/strict';
import { suite, test } from 'node:test';
import normalizeContext from './normalize-context';
import { JSDOM } from 'jsdom';
import { normalizeContext } from './normalize-context';

@@ -14,3 +14,3 @@ suite('normalizeContext', () => {

include: [document],
exclude: []
exclude: [],
});

@@ -27,3 +27,3 @@ });

include: [element],
exclude: []
exclude: [],
});

@@ -46,3 +46,3 @@ });

include: matchingElements,
exclude: []
exclude: [],
});

@@ -63,3 +63,3 @@ });

include: [],
exclude: []
exclude: [],
});

@@ -81,3 +81,3 @@ });

include: Array.from(matchingElements),
exclude: []
exclude: [],
});

@@ -102,3 +102,3 @@ });

}
const normalizedContext = normalizeContext({fromShadowDom: ['.host', '.matches']});
const normalizedContext = normalizeContext({ fromShadowDom: ['.host', '.matches'] });

@@ -108,5 +108,5 @@ assert.equal(matchingElements.length, 2);

include: matchingElements,
exclude: []
exclude: [],
});
});
});

@@ -1,7 +0,11 @@

import type { Context, ContextProp, Selector, ScanContext } from '../types';
import type { Context, ContextProp, ScanContext, Selector } from '../types.ts';
import { deduplicateNodes } from './deduplicate-nodes.js';
import { isNode, isNodeList } from './dom-helpers.js';
import { deduplicateNodes } from './deduplicate-nodes.js';
import { isNonEmpty } from './is-non-empty.js';
function recursiveSelectAll(selectors: Array<string>, root: Document | ShadowRoot): Array<Node> {
const nodesOnCurrentLevel = root.querySelectorAll(selectors[0]!);
function recursiveSelectAll(
selectors: [string, ...string[]],
root: Document | ShadowRoot,
): Array<Node> {
const nodesOnCurrentLevel = root.querySelectorAll(selectors[0]);
if (selectors.length === 1) {

@@ -11,2 +15,5 @@ return Array.from(nodesOnCurrentLevel);

const restSelectors: Array<string> = selectors.slice(1);
if (!isNonEmpty(restSelectors)) {
throw new Error('Error: the restSelectors array must not be empty.');
}
const selected = [];

@@ -24,7 +31,7 @@ for (const node of nodesOnCurrentLevel) {

return recursiveSelectAll([selector], document);
} else if (isNode(selector)) {
}
if (isNode(selector)) {
return [selector];
} else {
return recursiveSelectAll(selector.fromShadowDom, document);
}
return recursiveSelectAll(selector.fromShadowDom, document);
}

@@ -35,3 +42,3 @@

if (typeof contextProp === 'object' && (Array.isArray(contextProp) || isNodeList(contextProp))) {
nodes = Array.from(contextProp).map(item => selectorToNodes(item)).flat();
nodes = Array.from(contextProp).flatMap((item) => selectorToNodes(item));
} else {

@@ -43,3 +50,3 @@ nodes = selectorToNodes(contextProp);

export default function normalizeContext(context: Context): ScanContext {
export function normalizeContext(context: Context): ScanContext {
let contextInclude: Array<Node> = [];

@@ -60,4 +67,4 @@ let contextExclude: Array<Node> = [];

include: contextInclude,
exclude: contextExclude
exclude: contextExclude,
};
}
import { batch } from '@preact/signals-core';
import { logAndRethrow } from '../log-and-rethrow.js';
import { extendedElementsWithIssues } from '../state.js';
import getElementPosition from './get-element-position.js';
import logAndRethrow from '../log-and-rethrow.js';
import { getElementPosition } from './get-element-position.js';
let frameRequested = false;
export default function recalculatePositions() {
export function recalculatePositions() {
if (frameRequested) {

@@ -17,7 +17,7 @@ return;

batch(() => {
extendedElementsWithIssues.value.forEach(({ element, position, visible }) => {
for (const { element, position, visible } of extendedElementsWithIssues.value) {
if (visible.value && element.isConnected) {
position.value = getElementPosition(element, window);
}
});
}
});

@@ -24,0 +24,0 @@ } catch (error) {

import { batch } from '@preact/signals-core';
import { extendedElementsWithIssues } from '../state.js';
import getScrollableAncestors from './get-scrollable-ancestors.js';
import { getScrollableAncestors } from './get-scrollable-ancestors.js';
export default function recalculateScrollableAncestors() {
export function recalculateScrollableAncestors() {
batch(() => {
extendedElementsWithIssues.value.forEach(({ element, scrollableAncestors }) => {
for (const { element, scrollableAncestors } of extendedElementsWithIssues.value) {
if (element.isConnected) {
scrollableAncestors.value = getScrollableAncestors(element, window);
}
});
}
});
}

@@ -1,5 +0,5 @@

import { isElement, isDocument, isDocumentFragment } from './dom-helpers.js';
import { getAccentedElementNames } from '../constants.js';
import { isDocument, isDocumentFragment, isElement } from './dom-helpers.js';
export default function createShadowDOMAwareMutationObserver (name: string, callback: MutationCallback) {
export function createShadowDOMAwareMutationObserver(name: string, callback: MutationCallback) {
class ShadowDOMAwareMutationObserver extends MutationObserver {

@@ -13,10 +13,8 @@ #shadowRoots = new Set();

const accentedElementNames = getAccentedElementNames(name);
const childListMutations = mutations
.filter(mutation => mutation.type === 'childList')
const childListMutations = mutations.filter((mutation) => mutation.type === 'childList');
const newElements = childListMutations
.map(mutation => [...mutation.addedNodes])
.flat()
.filter(node => isElement(node))
.filter(node => !accentedElementNames.includes(node.nodeName.toLowerCase()));
.flatMap((mutation) => [...mutation.addedNodes])
.filter((node) => isElement(node))
.filter((node) => !accentedElementNames.includes(node.nodeName.toLowerCase()));

@@ -26,6 +24,5 @@ this.#observeShadowRoots(newElements);

const removedElements = childListMutations
.map(mutation => [...mutation.removedNodes])
.flat()
.filter(node => isElement(node))
.filter(node => !accentedElementNames.includes(node.nodeName.toLowerCase()));
.flatMap((mutation) => [...mutation.removedNodes])
.filter((node) => isElement(node))
.filter((node) => !accentedElementNames.includes(node.nodeName.toLowerCase()));

@@ -55,19 +52,19 @@ // Mutation observer has no "unobserve" method, so we're simply deleting

const shadowRoots = elements
.map(element => [...element.querySelectorAll('*')])
.flat()
.filter(element => element.shadowRoot)
.map(element => element.shadowRoot!);
.flatMap((element) => [...element.querySelectorAll('*')])
.filter((element) => element.shadowRoot)
.map((element) => element.shadowRoot);
for (const shadowRoot of shadowRoots) {
this.#shadowRoots.add(shadowRoot);
this.observe(shadowRoot, this.#options);
if (shadowRoot) {
this.#shadowRoots.add(shadowRoot);
this.observe(shadowRoot, this.#options);
}
}
}
};
#deleteShadowRoots = (elements: Array<Element | Document | DocumentFragment>) => {
const shadowRoots = elements
.map(element => [...element.querySelectorAll('*')])
.flat()
.filter(element => element.shadowRoot)
.map(element => element.shadowRoot!);
.flatMap((element) => [...element.querySelectorAll('*')])
.filter((element) => element.shadowRoot)
.map((element) => element.shadowRoot);

@@ -77,3 +74,3 @@ for (const shadowRoot of shadowRoots) {

}
}
};
}

@@ -80,0 +77,0 @@

type WindowWithCSS = Window & {
CSS: typeof CSS
}
CSS: typeof CSS;
};
export default function supportsAnchorPositioning(win: WindowWithCSS) {
export function supportsAnchorPositioning(win: WindowWithCSS) {
return win.CSS.supports('anchor-name: --foo') && win.CSS.supports('position-anchor: --foo');
}
import assert from 'node:assert/strict';
import {suite, test} from 'node:test';
import transformViolations from './transform-violations';
import { suite, test } from 'node:test';
import { transformViolations } from './transform-violations';

@@ -15,3 +15,3 @@ import type { AxeResults } from 'axe-core';

tags: [],
impact: 'serious'
impact: 'serious',
};

@@ -25,13 +25,13 @@

tags: [],
impact: 'serious'
impact: 'serious',
};
const getRootNode = (): Node => ({} as Node);
const getRootNode = (): Node => ({}) as Node;
// @ts-expect-error element is not HTMLElement
const element1: HTMLElement = {getRootNode};
const element1: HTMLElement = { getRootNode };
// @ts-expect-error element is not HTMLElement
const element2: HTMLElement = {getRootNode};
const element2: HTMLElement = { getRootNode };
// @ts-expect-error element is not HTMLElement
const element3: HTMLElement = {getRootNode};
const element3: HTMLElement = { getRootNode };

@@ -42,3 +42,3 @@ const commonNodeProps = {

all: [],
none: []
none: [],
};

@@ -50,3 +50,3 @@

target: ['div'],
failureSummary: 'summary1'
failureSummary: 'summary1',
};

@@ -58,3 +58,3 @@

target: ['div'],
failureSummary: 'summary2'
failureSummary: 'summary2',
};

@@ -66,3 +66,3 @@

target: ['div'],
failureSummary: 'summary3'
failureSummary: 'summary3',
};

@@ -74,3 +74,3 @@

...commonViolationProps1,
nodes: [node1]
nodes: [node1],
};

@@ -88,11 +88,13 @@ const elementsWithIssues = transformViolations([violation], 'accented');

...commonViolationProps1,
nodes: [node1, node2]
nodes: [node1, node2],
};
const violation2: Violation = {
...commonViolationProps2,
nodes: [node1, node3]
nodes: [node1, node3],
};
const elementsWithIssues = transformViolations([violation1, violation2], 'accented');
assert.equal(elementsWithIssues.length, 3);
const elementWithTwoIssues = elementsWithIssues.find(elementWithIssues => elementWithIssues.element === element1);
const elementWithTwoIssues = elementsWithIssues.find(
(elementWithIssues) => elementWithIssues.element === element1,
);
assert.equal(elementWithTwoIssues?.issues.length, 2);

@@ -107,7 +109,7 @@ });

target: ['iframe', 'div'],
failureSummary: 'summary1'
failureSummary: 'summary1',
};
const violation: Violation = {
...commonViolationProps1,
nodes: [node]
nodes: [node],
};

@@ -125,7 +127,7 @@

target: [['div', 'div']],
failureSummary: 'summary1'
failureSummary: 'summary1',
};
const violation: Violation = {
...commonViolationProps1,
nodes: [node]
nodes: [node],
};

@@ -132,0 +134,0 @@

import type { AxeResults, ImpactValue } from 'axe-core';
import type { Issue, ElementWithIssues } from '../types';
import type { ElementWithIssues, Issue } from '../types.ts';

@@ -13,8 +13,10 @@ // This is a list of axe-core violations (their ids) that may be flagged by axe-core

'nested-interactive',
'scrollable-region-focusable' // The Accented trigger might make the content grow such that scrolling is required.
'scrollable-region-focusable', // The Accented trigger might make the content grow such that scrolling is required.
];
function maybeCausedByAccented(violationId: string, element: HTMLElement, name: string) {
return violationsAffectedByAccentedTriggers.includes(violationId)
&& Boolean(element.querySelector(`${name}-trigger`));
return (
violationsAffectedByAccentedTriggers.includes(violationId) &&
Boolean(element.querySelector(`${name}-trigger`))
);
}

@@ -27,3 +29,3 @@

export default function transformViolations(violations: typeof AxeResults.violations, name: string) {
export function transformViolations(violations: typeof AxeResults.violations, name: string) {
const elementsWithIssues: Array<ElementWithIssues> = [];

@@ -49,13 +51,15 @@

url: violation.helpUrl,
impact: violation.impact ?? null
impact: violation.impact ?? null,
};
const existingElementIndex = elementsWithIssues.findIndex(elementWithIssues => elementWithIssues.element === element);
if (existingElementIndex === -1) {
const existingElement = elementsWithIssues.find(
(elementWithIssues) => elementWithIssues.element === element,
);
if (existingElement === undefined) {
elementsWithIssues.push({
element,
rootNode: element.getRootNode(),
issues: [issue]
issues: [issue],
});
} else {
elementsWithIssues[existingElementIndex]!.issues.push(issue);
existingElement.issues.push(issue);
}

@@ -62,0 +66,0 @@ }

@@ -1,7 +0,7 @@

import {suite, test} from 'node:test';
import assert from 'node:assert/strict';
import { suite, test } from 'node:test';
import type { Signal } from '@preact/signals-core';
import { signal } from '@preact/signals-core';
import type { ExtendedElementWithIssues, Issue } from '../types';
import updateElementsWithIssues from './update-elements-with-issues';
import { updateElementsWithIssues } from './update-elements-with-issues';

@@ -18,5 +18,5 @@ import type { AxeResults, ImpactValue } from 'axe-core';

style: {
setProperty: () => {}
setProperty: () => {},
},
dataset: {}
dataset: {},
}),

@@ -29,13 +29,13 @@ contains: () => true,

direction: 'ltr',
getPropertyValue: () => 'none'
getPropertyValue: () => 'none',
}),
// @ts-expect-error we're missing a lot of properties
CSS: {
supports: () => true
}
}
supports: () => true,
},
};
const getBoundingClientRect = () => ({});
const getRootNode = (): Node => ({} as Node);
const getRootNode = (): Node => ({}) as Node;

@@ -46,13 +46,13 @@ const baseElement = {

style: {
getPropertyValue: () => ''
getPropertyValue: () => '',
},
closest: () => null,
}
};
// @ts-expect-error element is not HTMLElement
const element1: HTMLElement = {...baseElement, isConnected: true};
const element1: HTMLElement = { ...baseElement, isConnected: true };
// @ts-expect-error element is not HTMLElement
const element2: HTMLElement = {...baseElement, isConnected: true};
const element2: HTMLElement = { ...baseElement, isConnected: true };
// @ts-expect-error element is not HTMLElement
const element3: HTMLElement = {...baseElement, isConnected: false};
const element3: HTMLElement = { ...baseElement, isConnected: false };

@@ -68,3 +68,3 @@ // @ts-expect-error rootNode is not Node

top: 0,
height: 100
height: 100,
});

@@ -81,3 +81,3 @@

none: [],
target: ['div']
target: ['div'],
};

@@ -105,3 +105,3 @@

tags: [],
impact: 'serious' as ImpactValue
impact: 'serious' as ImpactValue,
};

@@ -112,3 +112,3 @@

id: 'id1',
nodes: [node1]
nodes: [node1],
};

@@ -119,3 +119,3 @@

id: 'id2',
nodes: [node2]
nodes: [node2],
};

@@ -126,3 +126,3 @@

id: 'id3',
nodes: [node2]
nodes: [node2],
};

@@ -133,3 +133,3 @@

id: 'id4',
nodes: [node3]
nodes: [node3],
};

@@ -141,3 +141,3 @@

url: 'http://example.com',
impact: 'serious'
impact: 'serious',
} as const;

@@ -147,3 +147,3 @@

id: 'id1',
...commonIssueProps
...commonIssueProps,
};

@@ -153,3 +153,3 @@

id: 'id2',
...commonIssueProps
...commonIssueProps,
};

@@ -159,3 +159,3 @@

id: 'id3',
...commonIssueProps
...commonIssueProps,
};

@@ -165,4 +165,4 @@

include: [win.document],
exclude: []
}
exclude: [],
};

@@ -182,3 +182,3 @@ suite('updateElementsWithIssues', () => {

scrollableAncestors,
issues: signal([issue1])
issues: signal([issue1]),
},

@@ -195,4 +195,4 @@ {

scrollableAncestors,
issues: signal([issue2])
}
issues: signal([issue2]),
},
]);

@@ -204,3 +204,3 @@ updateElementsWithIssues({

win,
name: 'accented'
name: 'accented',
});

@@ -226,3 +226,3 @@ assert.equal(extendedElementsWithIssues.value.length, 2);

scrollableAncestors,
issues: signal([issue1])
issues: signal([issue1]),
},

@@ -239,4 +239,4 @@ {

scrollableAncestors,
issues: signal([issue2])
}
issues: signal([issue2]),
},
]);

@@ -248,3 +248,3 @@ updateElementsWithIssues({

win,
name: 'accented'
name: 'accented',
});

@@ -270,3 +270,3 @@ assert.equal(extendedElementsWithIssues.value.length, 2);

scrollableAncestors,
issues: signal([issue1])
issues: signal([issue1]),
},

@@ -283,4 +283,4 @@ {

scrollableAncestors,
issues: signal([issue2, issue3])
}
issues: signal([issue2, issue3]),
},
]);

@@ -292,3 +292,3 @@ updateElementsWithIssues({

win,
name: 'accented'
name: 'accented',
});

@@ -314,4 +314,4 @@ assert.equal(extendedElementsWithIssues.value.length, 2);

scrollableAncestors,
issues: signal([issue1])
}
issues: signal([issue1]),
},
]);

@@ -323,3 +323,3 @@ updateElementsWithIssues({

win,
name: 'accented'
name: 'accented',
});

@@ -345,4 +345,4 @@ assert.equal(extendedElementsWithIssues.value.length, 2);

scrollableAncestors,
issues: signal([issue1])
}
issues: signal([issue1]),
},
]);

@@ -354,3 +354,3 @@ updateElementsWithIssues({

win,
name: 'accented'
name: 'accented',
});

@@ -373,3 +373,3 @@ assert.equal(extendedElementsWithIssues.value.length, 1);

scrollableAncestors,
issues: signal([issue1])
issues: signal([issue1]),
},

@@ -386,4 +386,4 @@ {

scrollableAncestors,
issues: signal([issue2])
}
issues: signal([issue2]),
},
]);

@@ -395,3 +395,3 @@ updateElementsWithIssues({

win,
name: 'accented'
name: 'accented',
});

@@ -398,0 +398,0 @@ assert.equal(extendedElementsWithIssues.value.length, 1);

@@ -1,19 +0,18 @@

import type { AxeResults } from 'axe-core';
import type { Signal } from '@preact/signals-core';
import { batch, signal } from '@preact/signals-core';
import type { ExtendedElementWithIssues, ScanContext } from '../types';
import transformViolations from './transform-violations.js';
import areElementsWithIssuesEqual from './are-elements-with-issues-equal.js';
import areIssueSetsEqual from './are-issue-sets-equal.js';
import isNodeInScanContext from './is-node-in-scan-context.js';
import type { AccentedTrigger } from '../elements/accented-trigger';
import type { AccentedDialog } from '../elements/accented-dialog';
import getElementPosition from './get-element-position.js';
import getScrollableAncestors from './get-scrollable-ancestors.js';
import supportsAnchorPositioning from './supports-anchor-positioning.js';
import type { AxeResults } from 'axe-core';
import type { AccentedDialog } from '../elements/accented-dialog.ts';
import type { AccentedTrigger } from '../elements/accented-trigger.ts';
import type { ExtendedElementWithIssues, ScanContext } from '../types.ts';
import { areElementsWithIssuesEqual } from './are-elements-with-issues-equal.js';
import { areIssueSetsEqual } from './are-issue-sets-equal.js';
import { isSvgElement } from './dom-helpers.js';
import getParent from './get-parent.js';
import { getElementPosition } from './get-element-position.js';
import { getParent } from './get-parent.js';
import { getScrollableAncestors } from './get-scrollable-ancestors.js';
import { isNodeInScanContext } from './is-node-in-scan-context.js';
import { supportsAnchorPositioning } from './supports-anchor-positioning.js';
import { transformViolations } from './transform-violations.js';
function shouldSkipRender(element: Element): boolean {
// Skip rendering if the element is inside an SVG:

@@ -36,3 +35,3 @@ // https://github.com/pomerantsev/accented/issues/62

export default function updateElementsWithIssues({
export function updateElementsWithIssues({
extendedElementsWithIssues,

@@ -42,9 +41,9 @@ scanContext,

win,
name
name,
}: {
extendedElementsWithIssues: Signal<Array<ExtendedElementWithIssues>>,
scanContext: ScanContext,
violations: typeof AxeResults.violations,
win: Window & { CSS: typeof CSS },
name: string
extendedElementsWithIssues: Signal<Array<ExtendedElementWithIssues>>;
scanContext: ScanContext;
violations: typeof AxeResults.violations;
win: Window & { CSS: typeof CSS };
name: string;
}) {

@@ -55,10 +54,23 @@ const updatedElementsWithIssues = transformViolations(violations, name);

for (const updatedElementWithIssues of updatedElementsWithIssues) {
const existingElementIndex = extendedElementsWithIssues.value.findIndex(extendedElementWithIssues => areElementsWithIssuesEqual(extendedElementWithIssues, updatedElementWithIssues));
if (existingElementIndex > -1 && extendedElementsWithIssues.value[existingElementIndex] && !areIssueSetsEqual(extendedElementsWithIssues.value[existingElementIndex].issues.value, updatedElementWithIssues.issues)) {
extendedElementsWithIssues.value[existingElementIndex].issues.value = updatedElementWithIssues.issues;
const existingElementIndex = extendedElementsWithIssues.value.findIndex(
(extendedElementWithIssues) =>
areElementsWithIssuesEqual(extendedElementWithIssues, updatedElementWithIssues),
);
if (
existingElementIndex > -1 &&
extendedElementsWithIssues.value[existingElementIndex] &&
!areIssueSetsEqual(
extendedElementsWithIssues.value[existingElementIndex].issues.value,
updatedElementWithIssues.issues,
)
) {
extendedElementsWithIssues.value[existingElementIndex].issues.value =
updatedElementWithIssues.issues;
}
}
const addedElementsWithIssues = updatedElementsWithIssues.filter(updatedElementWithIssues => {
return !extendedElementsWithIssues.value.some(extendedElementWithIssues => areElementsWithIssuesEqual(extendedElementWithIssues, updatedElementWithIssues));
const addedElementsWithIssues = updatedElementsWithIssues.filter((updatedElementWithIssues) => {
return !extendedElementsWithIssues.value.some((extendedElementWithIssues) =>
areElementsWithIssuesEqual(extendedElementWithIssues, updatedElementWithIssues),
);
});

@@ -69,52 +81,65 @@

// 2. It is within the scan context, but not among updatedElementsWithIssues.
const removedElementsWithIssues = extendedElementsWithIssues.value.filter(extendedElementWithIssues => {
const isConnected = extendedElementWithIssues.element.isConnected;
const hasNoMoreIssues = isNodeInScanContext(extendedElementWithIssues.element, scanContext)
&& !updatedElementsWithIssues.some(updatedElementWithIssues => areElementsWithIssuesEqual(updatedElementWithIssues, extendedElementWithIssues));
return !isConnected || hasNoMoreIssues;
});
const removedElementsWithIssues = extendedElementsWithIssues.value.filter(
(extendedElementWithIssues) => {
const isConnected = extendedElementWithIssues.element.isConnected;
const hasNoMoreIssues =
isNodeInScanContext(extendedElementWithIssues.element, scanContext) &&
!updatedElementsWithIssues.some((updatedElementWithIssues) =>
areElementsWithIssuesEqual(updatedElementWithIssues, extendedElementWithIssues),
);
return !isConnected || hasNoMoreIssues;
},
);
if (addedElementsWithIssues.length > 0 || removedElementsWithIssues.length > 0) {
extendedElementsWithIssues.value = [...extendedElementsWithIssues.value]
.filter(extendedElementWithIssues => {
return !removedElementsWithIssues.some(removedElementWithIssues => areElementsWithIssuesEqual(removedElementWithIssues, extendedElementWithIssues));
.filter((extendedElementWithIssues) => {
return !removedElementsWithIssues.some((removedElementWithIssues) =>
areElementsWithIssuesEqual(removedElementWithIssues, extendedElementWithIssues),
);
})
.concat(addedElementsWithIssues
.filter(addedElementWithIssues => addedElementWithIssues.element.isConnected)
.map(addedElementWithIssues => {
const id = count++;
const trigger = win.document.createElement(`${name}-trigger`) as AccentedTrigger;
const elementZIndex = parseInt(win.getComputedStyle(addedElementWithIssues.element).zIndex, 10);
if (!isNaN(elementZIndex)) {
trigger.style.setProperty('z-index', (elementZIndex + 1).toString(), 'important');
}
trigger.style.setProperty('position-anchor', `--${name}-anchor-${id}`, 'important');
trigger.dataset.id = id.toString();
const accentedDialog = win.document.createElement(`${name}-dialog`) as AccentedDialog;
trigger.dialog = accentedDialog;
const position = getElementPosition(addedElementWithIssues.element, win);
trigger.position = signal(position);
trigger.visible = signal(true);
trigger.element = addedElementWithIssues.element;
const scrollableAncestors = supportsAnchorPositioning(win) ?
new Set<HTMLElement>() :
getScrollableAncestors(addedElementWithIssues.element, win);
const issues = signal(addedElementWithIssues.issues);
accentedDialog.issues = issues;
accentedDialog.element = addedElementWithIssues.element;
return {
id,
element: addedElementWithIssues.element,
skipRender: shouldSkipRender(addedElementWithIssues.element),
rootNode: addedElementWithIssues.rootNode,
visible: trigger.visible,
position: trigger.position,
scrollableAncestors: signal(scrollableAncestors),
anchorNameValue:
addedElementWithIssues.element.style.getPropertyValue('anchor-name')
|| win.getComputedStyle(addedElementWithIssues.element).getPropertyValue('anchor-name'),
trigger,
issues
};
})
.concat(
addedElementsWithIssues
.filter((addedElementWithIssues) => addedElementWithIssues.element.isConnected)
.map((addedElementWithIssues) => {
const id = count++;
const trigger = win.document.createElement(`${name}-trigger`) as AccentedTrigger;
const elementZIndex = Number.parseInt(
win.getComputedStyle(addedElementWithIssues.element).zIndex,
10,
);
if (!Number.isNaN(elementZIndex)) {
trigger.style.setProperty('z-index', (elementZIndex + 1).toString(), 'important');
}
trigger.style.setProperty('position-anchor', `--${name}-anchor-${id}`, 'important');
trigger.dataset.id = id.toString();
const accentedDialog = win.document.createElement(`${name}-dialog`) as AccentedDialog;
trigger.dialog = accentedDialog;
const position = getElementPosition(addedElementWithIssues.element, win);
trigger.position = signal(position);
trigger.visible = signal(true);
trigger.element = addedElementWithIssues.element;
const scrollableAncestors = supportsAnchorPositioning(win)
? new Set<HTMLElement>()
: getScrollableAncestors(addedElementWithIssues.element, win);
const issues = signal(addedElementWithIssues.issues);
accentedDialog.issues = issues;
accentedDialog.element = addedElementWithIssues.element;
return {
id,
element: addedElementWithIssues.element,
skipRender: shouldSkipRender(addedElementWithIssues.element),
rootNode: addedElementWithIssues.rootNode,
visible: trigger.visible,
position: trigger.position,
scrollableAncestors: signal(scrollableAncestors),
anchorNameValue:
addedElementWithIssues.element.style.getPropertyValue('anchor-name') ||
win
.getComputedStyle(addedElementWithIssues.element)
.getPropertyValue('anchor-name'),
trigger,
issues,
};
}),
);

@@ -121,0 +146,0 @@ }

@@ -1,9 +0,18 @@

import type { Selector, SelectorList, ContextProp, ContextObject, AccentedOptions, Context } from './types';
import { allowedAxeOptions } from './types.js';
import type {
AccentedOptions,
Context,
ContextObject,
ContextProp,
Selector,
SelectorList,
} from './types.ts';
import { isNode, isNodeList } from './utils/dom-helpers.js';
function isSelector(contextFragment: Context): contextFragment is Selector {
return typeof contextFragment === 'string'
|| isNode(contextFragment)
|| 'fromShadowDom' in contextFragment;
return (
typeof contextFragment === 'string' ||
isNode(contextFragment) ||
'fromShadowDom' in contextFragment
);
}

@@ -14,21 +23,29 @@

return;
} else if (isNode(selector)) {
}
if (isNode(selector)) {
return;
} else if ('fromShadowDom' in selector) {
if (!Array.isArray(selector.fromShadowDom)
|| selector.fromShadowDom.length < 2 ||
!selector.fromShadowDom.every(item => typeof item === 'string')
}
if ('fromShadowDom' in selector) {
if (
!Array.isArray(selector.fromShadowDom) ||
selector.fromShadowDom.length < 2 ||
!selector.fromShadowDom.every((item) => typeof item === 'string')
) {
throw new TypeError(`Accented: invalid argument. \`fromShadowDom\` must be an array of strings with at least 2 elements. It’s currently set to ${selector.fromShadowDom}.`);
throw new TypeError(
`Accented: invalid argument. \`fromShadowDom\` must be an array of strings with at least 2 elements. It’s currently set to ${selector.fromShadowDom}.`,
);
}
return;
} else {
const neverSelector: never = selector;
throw new TypeError(`Accented: invalid argument. The selector must be one of: string, Node, or an object with a \`fromShadowDom\` property. It’s currently set to ${neverSelector}.`);
}
const neverSelector: never = selector;
throw new TypeError(
`Accented: invalid argument. The selector must be one of: string, Node, or an object with a \`fromShadowDom\` property. It’s currently set to ${neverSelector}.`,
);
}
function isSelectorList(contextFragment: Context): contextFragment is SelectorList {
return (typeof contextFragment === 'object' && isNodeList(contextFragment))
|| (Array.isArray(contextFragment) && contextFragment.every(item => isSelector(item)));
return (
(typeof contextFragment === 'object' && isNodeList(contextFragment)) ||
(Array.isArray(contextFragment) && contextFragment.every((item) => isSelector(item)))
);
}

@@ -39,3 +56,4 @@

return;
} else if (Array.isArray(selectorList)) {
}
if (Array.isArray(selectorList)) {
for (const selector of selectorList) {

@@ -46,3 +64,5 @@ validateSelector(selector);

const neverSelectorList: never = selectorList;
throw new TypeError(`Accented: invalid argument. The selector list must either be a NodeList or an array. It’s currently set to ${neverSelectorList}.`);
throw new TypeError(
`Accented: invalid argument. The selector list must either be a NodeList or an array. It’s currently set to ${neverSelectorList}.`,
);
}

@@ -62,3 +82,5 @@ }

const neverContext: never = context;
throw new TypeError(`Accented: invalid argument. The context property must either be a selector or a selector list. It’s currently set to ${neverContext}.`);
throw new TypeError(
`Accented: invalid argument. The context property must either be a selector or a selector list. It’s currently set to ${neverContext}.`,
);
}

@@ -68,12 +90,15 @@ }

function isContextObject(contextFragment: Context): contextFragment is ContextObject {
return typeof contextFragment === 'object' && contextFragment !== null
&& ('include' in contextFragment || 'exclude' in contextFragment);
return (
typeof contextFragment === 'object' &&
contextFragment !== null &&
('include' in contextFragment || 'exclude' in contextFragment)
);
}
function validateContextObject(contextObject: ContextObject) {
if ('include' in contextObject) {
validateContextProp(contextObject.include!);
if ('include' in contextObject && contextObject.include !== undefined) {
validateContextProp(contextObject.include);
}
if ('exclude' in contextObject) {
validateContextProp(contextObject.exclude!);
if ('exclude' in contextObject && contextObject.exclude !== undefined) {
validateContextProp(contextObject.exclude);
}

@@ -89,3 +114,5 @@ }

const neverContext: never = context;
throw new TypeError(`Accented: invalid context argument. It’s currently set to ${neverContext}.`);
throw new TypeError(
`Accented: invalid context argument. It’s currently set to ${neverContext}.`,
);
}

@@ -99,12 +126,21 @@ }

export default function validateOptions(options: AccentedOptions) {
export function validateOptions(options: AccentedOptions) {
if (typeof options !== 'object' || options === null) {
throw new TypeError(`Accented: invalid argument. The options parameter must be an object if provided. It’s currently set to ${options}.`);
throw new TypeError(
`Accented: invalid argument. The options parameter must be an object if provided. It’s currently set to ${options}.`,
);
}
if (options.throttle !== undefined) {
if (typeof options.throttle !== 'object' || options.throttle === null) {
throw new TypeError(`Accented: invalid argument. \`throttle\` option must be an object if provided. It’s currently set to ${options.throttle}.`);
throw new TypeError(
`Accented: invalid argument. \`throttle\` option must be an object if provided. It’s currently set to ${options.throttle}.`,
);
}
if (options.throttle.wait !== undefined && (typeof options.throttle.wait !== 'number' || options.throttle.wait < 0)) {
throw new TypeError(`Accented: invalid argument. \`throttle.wait\` option must be a non-negative number if provided. It’s currently set to ${options.throttle.wait}.`);
if (
options.throttle.wait !== undefined &&
(typeof options.throttle.wait !== 'number' || options.throttle.wait < 0)
) {
throw new TypeError(
`Accented: invalid argument. \`throttle.wait\` option must be a non-negative number if provided. It’s currently set to ${options.throttle.wait}.`,
);
}

@@ -114,21 +150,38 @@ }

if (typeof options.output !== 'object' || options.output === null) {
throw new TypeError(`Accented: invalid argument. \`output\` option must be an object if provided. It’s currently set to ${options.output}.`);
throw new TypeError(
`Accented: invalid argument. \`output\` option must be an object if provided. It’s currently set to ${options.output}.`,
);
}
if (options.output.console !== undefined && typeof options.output.console !== 'boolean') {
console.warn(`Accented: invalid argument. \`output.console\` option is expected to be a boolean. It’s currently set to ${options.output.console}.`);
console.warn(
`Accented: invalid argument. \`output.console\` option is expected to be a boolean. It’s currently set to ${options.output.console}.`,
);
}
}
if (options.callback !== undefined && typeof options.callback !== 'function') {
throw new TypeError(`Accented: invalid argument. \`callback\` option must be a function if provided. It’s currently set to ${options.callback}.`);
throw new TypeError(
`Accented: invalid argument. \`callback\` option must be a function if provided. It’s currently set to ${options.callback}.`,
);
}
if (options.name !== undefined && (typeof options.name !== 'string' || !options.name.match(nameRegex))) {
throw new TypeError(`Accented: invalid argument. \`name\` option must be a string that starts with a lowercase letter and only contains lowercase alphanumeric characters and dashes. It’s currently set to ${options.name}.`);
if (
options.name !== undefined &&
(typeof options.name !== 'string' || !options.name.match(nameRegex))
) {
throw new TypeError(
`Accented: invalid argument. \`name\` option must be a string that starts with a lowercase letter and only contains lowercase alphanumeric characters and dashes. It’s currently set to ${options.name}.`,
);
}
if (options.axeOptions !== undefined) {
if (typeof options.axeOptions !== 'object' || options.axeOptions === null) {
throw new TypeError(`Accented: invalid argument. \`axeOptions\` option must be an object if provided. It’s currently set to ${options.axeOptions}.`);
throw new TypeError(
`Accented: invalid argument. \`axeOptions\` option must be an object if provided. It’s currently set to ${options.axeOptions}.`,
);
}
const unsupportedKeys = Object.keys(options.axeOptions).filter(key => !(allowedAxeOptions as unknown as Array<string>).includes(key));
const unsupportedKeys = Object.keys(options.axeOptions).filter(
(key) => !(allowedAxeOptions as unknown as Array<string>).includes(key),
);
if (unsupportedKeys.length > 0) {
throw new TypeError(`Accented: invalid argument. \`axeOptions\` contains the following unsupported keys: ${unsupportedKeys.join(', ')}. Valid options are: ${allowedAxeOptions.join(', ')}.`);
throw new TypeError(
`Accented: invalid argument. \`axeOptions\` contains the following unsupported keys: ${unsupportedKeys.join(', ')}. Valid options are: ${allowedAxeOptions.join(', ')}.`,
);
}

@@ -135,0 +188,0 @@ }

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet