@storybook/addon-a11y
Version:
Storybook Addon A11y: Test UI component compliance with WCAG web accessibility standards
1,143 lines (1,120 loc) • 96.6 kB
JavaScript
import {
ADDON_ID,
DOCUMENTATION_DISCREPANCY_LINK,
EVENTS,
PANEL_ID,
PARAM_KEY,
STATUS_TYPE_ID_A11Y,
STATUS_TYPE_ID_COMPONENT_TEST,
VISION_GLOBAL_KEY,
filterDefs,
filters
} from "./_browser-chunks/chunk-7OPMK7TU.js";
import "./_browser-chunks/chunk-4BE7D4DS.js";
// src/manager.tsx
import React8 from "react";
import { Badge as Badge3 } from "storybook/internal/components";
import { addons, types, useAddonState as useAddonState2, useStorybookApi as useStorybookApi3 } from "storybook/manager-api";
// src/components/A11YPanel.tsx
import React6, { useMemo as useMemo3 } from "react";
import { Badge as Badge2, Button as Button4 } from "storybook/internal/components";
import { SyncIcon as SyncIcon2 } from "@storybook/icons";
import { styled as styled5 } from "storybook/theming";
// src/types.ts
var RuleType = {
VIOLATION: "violations",
PASS: "passes",
INCOMPLETION: "incomplete"
};
// src/components/A11yContext.tsx
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
import {
STORY_CHANGED,
STORY_FINISHED,
STORY_HOT_UPDATED,
STORY_RENDER_PHASE_CHANGED
} from "storybook/internal/core-events";
import { HIGHLIGHT, REMOVE_HIGHLIGHT, SCROLL_INTO_VIEW } from "storybook/highlight";
import {
experimental_getStatusStore,
experimental_useStatusStore,
useAddonState,
useChannel,
useGlobals,
useParameter,
useStorybookApi,
useStorybookState
} from "storybook/manager-api";
import { convert, themes } from "storybook/theming";
// src/AccessibilityRuleMaps.ts
var axeRuleMapping_wcag_2_0_a_aa = {
"area-alt": {
title: "<area> alt text",
axeSummary: "Ensure <area> elements of image maps have alternative text",
friendlySummary: "Add alt text to all <area> elements of image maps."
},
"aria-allowed-attr": {
title: "Supported ARIA attributes",
axeSummary: "Ensure an element's role supports its ARIA attributes",
friendlySummary: "Only use ARIA attributes that are permitted for the element's role."
},
"aria-braille-equivalent": {
title: "Braille equivalent",
axeSummary: "Ensure aria-braillelabel and aria-brailleroledescription have a non-braille equivalent",
friendlySummary: "If you use braille ARIA labels, also provide a matching non-braille label."
},
"aria-command-name": {
title: "ARIA command name",
axeSummary: "Ensure every ARIA button, link and menuitem has an accessible name",
friendlySummary: "Every ARIA button, link, or menuitem needs a label or accessible name."
},
"aria-conditional-attr": {
title: "ARIA attribute valid for role",
axeSummary: "Ensure ARIA attributes are used as described in the specification of the element's role",
friendlySummary: "Follow the element role's specification when using ARIA attributes."
},
"aria-deprecated-role": {
title: "Deprecated ARIA role",
axeSummary: "Ensure elements do not use deprecated roles",
friendlySummary: "Don't use deprecated ARIA roles on elements."
},
"aria-hidden-body": {
title: "Hidden body",
axeSummary: 'Ensure aria-hidden="true" is not present on the document <body>',
friendlySummary: 'Never set aria-hidden="true" on the <body> element.'
},
"aria-hidden-focus": {
title: "Hidden element focus",
axeSummary: "Ensure aria-hidden elements are not focusable nor contain focusable elements",
friendlySummary: "Elements marked hidden (aria-hidden) should not be focusable or contain focusable items."
},
"aria-input-field-name": {
title: "ARIA input field name",
axeSummary: "Ensure every ARIA input field has an accessible name",
friendlySummary: "Give each ARIA text input or field a label or accessible name."
},
"aria-meter-name": {
title: "ARIA meter name",
axeSummary: "Ensure every ARIA meter node has an accessible name",
friendlySummary: 'Give each element with role="meter" a label or accessible name.'
},
"aria-progressbar-name": {
title: "ARIA progressbar name",
axeSummary: "Ensure every ARIA progressbar node has an accessible name",
friendlySummary: 'Give each element with role="progressbar" a label or accessible name.'
},
"aria-prohibited-attr": {
title: "ARIA prohibited attributes",
axeSummary: "Ensure ARIA attributes are not prohibited for an element's role",
friendlySummary: "Don't use ARIA attributes that are forbidden for that element's role."
},
"aria-required-attr": {
title: "ARIA required attributes",
axeSummary: "Ensure elements with ARIA roles have all required ARIA attributes",
friendlySummary: "Include all required ARIA attributes for elements with that ARIA role."
},
"aria-required-children": {
title: "ARIA required children",
axeSummary: "Ensure elements with an ARIA role that require child roles contain them",
friendlySummary: "If an ARIA role requires specific child roles, include those child elements."
},
"aria-required-parent": {
title: "ARIA required parent",
axeSummary: "Ensure elements with an ARIA role that require parent roles are contained by them",
friendlySummary: "Place elements with certain ARIA roles inside the required parent role element."
},
"aria-roles": {
title: "ARIA role value",
axeSummary: "Ensure all elements with a role attribute use a valid value",
friendlySummary: "Use only valid values in the role attribute (no typos or invalid roles)."
},
"aria-toggle-field-name": {
title: "ARIA toggle field name",
axeSummary: "Ensure every ARIA toggle field has an accessible name",
friendlySummary: "Every ARIA toggle field (elements with the checkbox, radio, or switch roles) needs an accessible name."
},
"aria-tooltip-name": {
title: "ARIA tooltip name",
axeSummary: "Ensure every ARIA tooltip node has an accessible name",
friendlySummary: 'Give each element with role="tooltip" a descriptive accessible name.'
},
"aria-valid-attr-value": {
title: "ARIA attribute values valid",
axeSummary: "Ensure all ARIA attributes have valid values",
friendlySummary: "Use only valid values for ARIA attributes (no typos or invalid values)."
},
"aria-valid-attr": {
title: "ARIA attribute valid",
axeSummary: "Ensure attributes that begin with aria- are valid ARIA attributes",
friendlySummary: "Use only valid aria-* attributes (make sure the attribute name is correct)."
},
blink: {
title: "<blink> element",
axeSummary: "Ensure <blink> elements are not used",
friendlySummary: "Don't use the deprecated <blink> element."
},
"button-name": {
title: "Button name",
axeSummary: "Ensure buttons have discernible text",
friendlySummary: "Every <button> needs a visible label or accessible name."
},
bypass: {
title: "Navigation bypass",
axeSummary: "Ensure each page has at least one mechanism to bypass navigation and jump to content",
friendlySummary: 'Provide a way to skip repetitive navigation (e.g. a "Skip to content" link).'
},
"color-contrast": {
title: "Color contrast",
axeSummary: "Ensure the contrast between foreground and background text meets WCAG 2 AA minimum thresholds",
friendlySummary: "The color contrast between text and its background meets WCAG AA contrast ratio."
},
"definition-list": {
title: "Definition list structure",
axeSummary: "Ensure <dl> elements are structured correctly",
friendlySummary: "Definition lists (<dl>) should directly contain <dt> and <dd> elements in order."
},
dlitem: {
title: "Definition list items",
axeSummary: "Ensure <dt> and <dd> elements are contained by a <dl>",
friendlySummary: "Ensure <dt> and <dd> elements are contained by a <dl>"
},
"document-title": {
title: "Document title",
axeSummary: "Ensure each HTML document contains a non-empty <title> element",
friendlySummary: "Include a non-empty <title> element for every page."
},
"duplicate-id-aria": {
title: "Unique id",
axeSummary: "Ensure every id attribute value used in ARIA and in labels is unique",
friendlySummary: "Every id used for ARIA or form labels should be unique on the page."
},
"form-field-multiple-labels": {
title: "Multiple form field labels",
axeSummary: "Ensure a form field does not have multiple <label> elements",
friendlySummary: "Don't give a single form field more than one <label>."
},
"frame-focusable-content": {
title: "Focusable frames",
axeSummary: 'Ensure <frame> and <iframe> with focusable content do not have tabindex="-1"',
friendlySummary: `Don't set tabindex="-1" on a <frame> or <iframe> that contains focusable elements.`
},
"frame-title-unique": {
title: "Unique frame title",
axeSummary: "Ensure <iframe> and <frame> elements contain a unique title attribute",
friendlySummary: "Use a unique title attribute for each <frame> or <iframe> on the page."
},
"frame-title": {
title: "Frame title",
axeSummary: "Ensure <iframe> and <frame> elements have an accessible name",
friendlySummary: "Every <frame> and <iframe> needs a title or accessible name."
},
"html-has-lang": {
title: "<html> has lang",
axeSummary: "Ensure every HTML document has a lang attribute",
friendlySummary: "Add a lang attribute to the <html> element."
},
"html-lang-valid": {
title: "<html> lang valid",
axeSummary: "Ensure the <html lang> attribute has a valid value",
friendlySummary: "Use a valid language code in the <html lang> attribute."
},
"html-xml-lang-mismatch": {
title: "HTML and XML lang mismatch",
axeSummary: "Ensure that HTML elements with both lang and xml:lang agree on the page's language",
friendlySummary: "If using both lang and xml:lang on <html>, make sure they are the same language."
},
"image-alt": {
title: "Image alt text",
axeSummary: "Ensure <img> elements have alternative text or a role of none/presentation",
friendlySummary: 'Give every image alt text or mark it as decorative with alt="".'
},
"input-button-name": {
title: "Input button name",
axeSummary: "Ensure input buttons have discernible text",
friendlySummary: 'Give each <input type="button"> or similar a clear label (text or aria-label).'
},
"input-image-alt": {
title: "Input image alt",
axeSummary: 'Ensure <input type="image"> elements have alternative text',
friendlySummary: '<input type="image"> must have alt text describing its image.'
},
label: {
title: "Form label",
axeSummary: "Ensure every form element has a label",
friendlySummary: "Every form field needs an associated label."
},
"link-in-text-block": {
title: "Identifiable links",
axeSummary: "Ensure links are distinguishable from surrounding text without relying on color",
friendlySummary: "Make sure links are obviously identifiable without relying only on color."
},
"link-name": {
title: "Link name",
axeSummary: "Ensure links have discernible text",
friendlySummary: "Give each link meaningful text or an aria-label so its purpose is clear."
},
list: {
title: "List structure",
axeSummary: "Ensure that lists are structured correctly",
friendlySummary: "Use proper list structure. Only use <li> inside <ul> or <ol>."
},
listitem: {
title: "List item",
axeSummary: "Ensure <li> elements are used semantically",
friendlySummary: "Only use <li> tags inside <ul> or <ol> lists."
},
marquee: {
title: "<marquee> element",
axeSummary: "Ensure <marquee> elements are not used",
friendlySummary: "Don't use the deprecated <marquee> element."
},
"meta-refresh": {
title: "<meta> refresh",
axeSummary: 'Ensure <meta http-equiv="refresh"> is not used for delayed refresh',
friendlySummary: 'Avoid auto-refreshing or redirecting pages using <meta http-equiv="refresh">.'
},
"meta-viewport": {
title: "<meta> viewport scaling",
axeSummary: 'Ensure <meta name="viewport"> does not disable text scaling and zooming',
friendlySummary: `Don't disable user zooming in <meta name="viewport"> to allow scaling.`
},
"nested-interactive": {
title: "Nested interactive controls",
axeSummary: "Ensure interactive controls are not nested (nesting causes screen reader/focus issues)",
friendlySummary: "Do not nest interactive elements; it can confuse screen readers and keyboard focus."
},
"no-autoplay-audio": {
title: "Autoplaying video",
axeSummary: "Ensure <video> or <audio> do not autoplay audio > 3 seconds without a control to stop/mute",
friendlySummary: "Don't autoplay audio for more than 3 seconds without providing a way to stop or mute it."
},
"object-alt": {
title: "<object> alt text",
axeSummary: "Ensure <object> elements have alternative text",
friendlySummary: "Provide alternative text or content for <object> elements."
},
"role-img-alt": {
title: 'role="img" alt text',
axeSummary: 'Ensure elements with role="img" have alternative text',
friendlySummary: 'Any element with role="img" needs alt text.'
},
"scrollable-region-focusable": {
title: "Scrollable element focusable",
axeSummary: "Ensure elements with scrollable content are keyboard-accessible",
friendlySummary: "If an area can scroll, ensure it can be focused and scrolled via keyboard."
},
"select-name": {
title: "<select> name",
axeSummary: "Ensure <select> elements have an accessible name",
friendlySummary: "Give each <select> field a label or other accessible name."
},
"server-side-image-map": {
title: "Server-side image map",
axeSummary: "Ensure that server-side image maps are not used",
friendlySummary: "Don't use server-side image maps."
},
"svg-img-alt": {
title: "SVG image alt text",
axeSummary: "Ensure <svg> images/graphics have accessible text",
friendlySummary: 'SVG images with role="img" or similar need a text description.'
},
"td-headers-attr": {
title: "Table headers attribute",
axeSummary: "Ensure each cell in a table using headers only refers to <th> in that table",
friendlySummary: "In tables using the headers attribute, only reference other cells in the same table."
},
"th-has-data-cells": {
title: "<th> has data cell",
axeSummary: "Ensure <th> (or header role) elements have data cells they describe",
friendlySummary: "Every table header (<th> or header role) should correspond to at least one data cell."
},
"valid-lang": {
title: "Valid lang",
axeSummary: "Ensure lang attributes have valid values",
friendlySummary: "Use valid language codes in all lang attributes."
},
"video-caption": {
title: "<video> captions",
axeSummary: "Ensure <video> elements have captions",
friendlySummary: "Provide captions for all <video> content."
}
}, axeRuleMapping_wcag_2_1_a_aa = {
"autocomplete-valid": {
title: "autocomplete attribute valid",
axeSummary: "Ensure the autocomplete attribute is correct and suitable for the form field",
friendlySummary: "Use valid autocomplete values that match the form field's purpose."
},
"avoid-inline-spacing": {
title: "Forced inline spacing",
axeSummary: "Ensure that text spacing set via inline styles can be adjusted with custom CSS",
friendlySummary: "Don't lock in text spacing with forced (!important) inline styles\u2014allow user CSS to adjust text spacing."
}
}, axeRuleMapping_wcag_2_2_a_aa = {
"target-size": {
title: "Touch target size",
axeSummary: "Ensure touch targets have sufficient size and space",
friendlySummary: "Make sure interactive elements are big enough and not too close together for touch."
}
}, axeRuleMapping_best_practices = {
accesskeys: {
title: "Unique accesskey",
axeSummary: "Ensure every accesskey attribute value is unique",
friendlySummary: "Use unique values for all accesskey attributes."
},
"aria-allowed-role": {
title: "Appropriate role value",
axeSummary: "Ensure the role attribute has an appropriate value for the element",
friendlySummary: "ARIA roles should have a valid value for the element."
},
"aria-dialog-name": {
title: "ARIA dialog name",
axeSummary: "Ensure every ARIA dialog and alertdialog has an accessible name",
friendlySummary: "Give each ARIA dialog or alertdialog a title or accessible name."
},
"aria-text": {
title: 'ARIA role="text"',
axeSummary: 'Ensure role="text" is used on elements with no focusable descendants',
friendlySummary: `Only use role="text" on elements that don't contain focusable elements.`
},
"aria-treeitem-name": {
title: "ARIA treeitem name",
axeSummary: "Ensure every ARIA treeitem node has an accessible name",
friendlySummary: "Give each ARIA treeitem a label or accessible name."
},
"empty-heading": {
title: "Empty heading",
axeSummary: "Ensure headings have discernible text",
friendlySummary: "Don't leave heading elements empty or hide them."
},
"empty-table-header": {
title: "Empty table header",
axeSummary: "Ensure table headers have discernible text",
friendlySummary: "Make sure table header cells have visible text."
},
"frame-tested": {
title: "Test all frames",
axeSummary: "Ensure <iframe> and <frame> elements contain the axe-core script",
friendlySummary: "Make sure axe-core is injected into all frames or iframes so they are tested."
},
"heading-order": {
title: "Heading order",
axeSummary: "Ensure the order of headings is semantically correct (no skipping levels)",
friendlySummary: "Use proper heading order (don't skip heading levels)."
},
"image-redundant-alt": {
title: "Redundant image alt text",
axeSummary: "Ensure image alternative text is not repeated as nearby text",
friendlySummary: "Avoid repeating the same information in both an image's alt text and nearby text."
},
"label-title-only": {
title: "Visible form element label",
axeSummary: "Ensure each form element has a visible label (not only title/ARIA)",
friendlySummary: "Every form input needs a visible label (not only a title attribute or hidden text)."
},
"landmark-banner-is-top-level": {
title: "Top-level landmark banner",
axeSummary: "Ensure the banner landmark is at top level (not nested)",
friendlySummary: "Use the banner landmark (e.g. site header) only at the top level of the page, not inside another landmark."
},
"landmark-complementary-is-top-level": {
title: "Top-level <aside>",
axeSummary: "Ensure the complementary landmark (<aside>) is top level",
friendlySummary: 'The complementary landmark <aside> or role="complementary" should be a top-level region, not nested in another landmark.'
},
"landmark-contentinfo-is-top-level": {
title: "Top-level contentinfo",
axeSummary: "Ensure the contentinfo landmark (footer) is top level",
friendlySummary: "Make sure the contentinfo landmark (footer) is at the top level of the page and not contained in another landmark."
},
"landmark-main-is-top-level": {
title: "Top-level main",
axeSummary: "Ensure the main landmark is at top level",
friendlySummary: "The main landmark should be a top-level element and not nested inside another landmark."
},
"landmark-no-duplicate-banner": {
title: "Duplicate banner landmark",
axeSummary: "Ensure the document has at most one banner landmark",
friendlySummary: 'Have only one role="banner" or <header> on a page.'
},
"landmark-no-duplicate-contentinfo": {
title: "Duplicate contentinfo",
axeSummary: "Ensure the document has at most one contentinfo landmark",
friendlySummary: 'Have only one role="contentinfo" or <footer> on a page.'
},
"landmark-no-duplicate-main": {
title: "Duplicate main",
axeSummary: "Ensure the document has at most one main landmark",
friendlySummary: 'Have only one role="main" or <main> on a page.'
},
"landmark-one-main": {
title: "main landmark",
axeSummary: "Ensure the document has a main landmark",
friendlySummary: 'Include a main landmark on each page using a <main> region or role="main".'
},
"landmark-unique": {
title: "Unique landmark",
axeSummary: "Ensure landmarks have a unique role or role/label combination",
friendlySummary: "If you use multiple landmarks of the same type, give them unique labels (names)."
},
"meta-viewport-large": {
title: "Significant viewport scaling",
axeSummary: 'Ensure <meta name="viewport"> can scale a significant amount (e.g. 500%)',
friendlySummary: '<meta name="viewport"> should allow users to significantly scale content.'
},
"page-has-heading-one": {
title: "Has <h1>",
axeSummary: "Ensure the page (or at least one frame) contains a level-one heading",
friendlySummary: "Every page or frame should have at least one <h1> heading."
},
"presentation-role-conflict": {
title: "Presentational content",
axeSummary: 'Ensure elements with role="presentation"/"none" have no ARIA or tabindex',
friendlySummary: `Don't give elements with role="none"/"presentation" any ARIA attributes or a tabindex.`
},
region: {
title: "Landmark regions",
axeSummary: "Ensure all page content is contained by landmarks",
friendlySummary: "Wrap all page content in appropriate landmark regions (<header>, <main>, <footer>, etc.)."
},
"scope-attr-valid": {
title: "scope attribute",
axeSummary: "Ensure the scope attribute is used correctly on tables",
friendlySummary: "Use the scope attribute only on <th> elements, with proper values (col, row, etc.)."
},
"skip-link": {
title: "Skip link",
axeSummary: 'Ensure all "skip" links have a focusable target',
friendlySummary: 'Make sure any "skip to content" link targets an existing, focusable element.'
},
tabindex: {
title: "tabindex values",
axeSummary: "Ensure tabindex attribute values are not greater than 0",
friendlySummary: "Don't use tabindex values greater than 0."
},
"table-duplicate-name": {
title: "Duplicate names for table",
axeSummary: "Ensure the <caption> does not duplicate the summary attribute text",
friendlySummary: "Don't use the same text in both a table's <caption> and its summary attribute."
}
}, axeRuleMapping_wcag_2_x_aaa = {
"color-contrast-enhanced": {
title: "Enhanced color contrast",
axeSummary: "Ensure contrast between text and background meets WCAG 2 AAA enhanced contrast thresholds",
friendlySummary: "Use extra-high contrast for text and background to meet WCAG AAA level."
},
"identical-links-same-purpose": {
title: "Same link name, same purpose",
axeSummary: "Ensure links with the same accessible name serve a similar purpose",
friendlySummary: "If two links have the same text, they should do the same thing (lead to the same content)."
},
"meta-refresh-no-exceptions": {
title: 'No <meta http-equiv="refresh">',
axeSummary: 'Ensure <meta http-equiv="refresh"> is not used for delayed refresh (no exceptions)',
friendlySummary: `Don't auto-refresh or redirect pages using <meta http-equiv="refresh"> even with a delay.`
}
}, axeRuleMapping_experimental = {
"css-orientation-lock": {
title: "CSS orientation lock",
axeSummary: "Ensure content is not locked to a specific display orientation (works in all orientations)",
friendlySummary: "Don't lock content to one screen orientation; support both portrait and landscape modes."
},
"focus-order-semantics": {
title: "Focus order semantic role",
axeSummary: "Ensure elements in the tab order have a role appropriate for interactive content",
friendlySummary: "Ensure elements in the tab order have a role appropriate for interactive content"
},
"hidden-content": {
title: "Hidden content",
axeSummary: "Informs users about hidden content",
friendlySummary: "Display hidden content on the page for test analysis."
},
"label-content-name-mismatch": {
title: "Content name mismatch",
axeSummary: "Ensure elements labeled by their content include that text in their accessible name",
friendlySummary: "If an element's visible text serves as its label, include that text in its accessible name."
},
"p-as-heading": {
title: "No <p> headings",
axeSummary: "Ensure <p> elements aren't styled to look like headings (use real headings)",
friendlySummary: "Don't just style a <p> to look like a heading \u2013 use an actual heading tag for headings."
},
"table-fake-caption": {
title: "Table caption",
axeSummary: "Ensure that tables with a caption use the <caption> element",
friendlySummary: "Use a <caption> element for table captions instead of just styled text."
},
"td-has-header": {
title: "<td> has header",
axeSummary: "Ensure each non-empty data cell in large tables (3\xD73+) has one or more headers",
friendlySummary: "Every data cell in large tables should be associated with at least one header cell."
}
}, axeRuleMapping_deprecated = {
"aria-roledescription": {
title: "aria-roledescription",
axeSummary: "Ensure aria-roledescription is only used on elements with an implicit or explicit role",
friendlySummary: "Only use aria-roledescription on elements that already have a defined role."
}
}, combinedRulesMap = {
...axeRuleMapping_wcag_2_0_a_aa,
...axeRuleMapping_wcag_2_1_a_aa,
...axeRuleMapping_wcag_2_2_a_aa,
...axeRuleMapping_wcag_2_x_aaa,
...axeRuleMapping_best_practices,
...axeRuleMapping_experimental,
...axeRuleMapping_deprecated
};
// src/axeRuleMappingHelper.ts
var getTitleForAxeResult = (axeResult) => combinedRulesMap[axeResult.id]?.title || axeResult.id, getFriendlySummaryForAxeResult = (axeResult) => combinedRulesMap[axeResult.id]?.friendlySummary || axeResult.description;
// src/components/A11yContext.tsx
var unhighlightedSelectors = ["html", "body", "main"], theme = convert(themes.light), colorsByType = {
[RuleType.VIOLATION]: theme.color.negative,
[RuleType.PASS]: theme.color.positive,
[RuleType.INCOMPLETION]: theme.color.warning
}, A11yContext = createContext({
parameters: {},
results: void 0,
highlighted: !1,
toggleHighlight: () => {
},
tab: RuleType.VIOLATION,
handleCopyLink: () => {
},
setTab: () => {
},
setStatus: () => {
},
status: "initial",
error: void 0,
handleManual: () => {
},
discrepancy: null,
selectedItems: /* @__PURE__ */ new Map(),
allExpanded: !1,
toggleOpen: () => {
},
handleCollapseAll: () => {
},
handleExpandAll: () => {
},
handleJumpToElement: () => {
},
handleSelectionChange: () => {
}
}), A11yContextProvider = (props) => {
let parameters = useParameter("a11y", {}), [globals] = useGlobals() ?? [], api = useStorybookApi(), getInitialStatus = useCallback((manual2 = !1) => manual2 ? "manual" : "initial", []), manual = useMemo(() => globals?.a11y?.manual ?? !1, [globals?.a11y?.manual]), a11ySelection = useMemo(() => {
let value = api.getQueryParam("a11ySelection");
return value && api.setQueryParams({ a11ySelection: "" }), value;
}, [api]), [state, setState] = useAddonState(ADDON_ID, {
ui: {
highlighted: !1,
tab: RuleType.VIOLATION
},
results: void 0,
error: void 0,
status: getInitialStatus(manual)
}), { ui, results, error, status } = state, { storyId } = useStorybookState(), currentStoryA11yStatusValue = experimental_useStatusStore(
(allStatuses) => allStatuses[storyId]?.[STATUS_TYPE_ID_A11Y]?.value
);
useEffect(() => experimental_getStatusStore("storybook/component-test").onAllStatusChange(
(statuses, previousStatuses) => {
let current = statuses[storyId]?.[STATUS_TYPE_ID_COMPONENT_TEST], previous = previousStatuses[storyId]?.[STATUS_TYPE_ID_COMPONENT_TEST];
current?.value === "status-value:error" && previous?.value !== "status-value:error" && setState((prev) => ({ ...prev, status: "component-test-error" }));
}
), [setState, storyId]);
let handleToggleHighlight = useCallback(() => {
setState((prev) => ({ ...prev, ui: { ...prev.ui, highlighted: !prev.ui.highlighted } }));
}, [setState]), [selectedItems, setSelectedItems] = useState(() => {
let initialValue = /* @__PURE__ */ new Map();
if (a11ySelection && /^[a-z]+.[a-z-]+.[0-9]+$/.test(a11ySelection)) {
let [type, id] = a11ySelection.split(".");
initialValue.set(`${type}.${id}`, a11ySelection);
}
return initialValue;
}), allExpanded = useMemo(() => results?.[ui.tab]?.every((result) => selectedItems.has(`${ui.tab}.${result.id}`)) ?? !1, [results, selectedItems, ui.tab]), toggleOpen = useCallback(
(event, type, item) => {
event.stopPropagation();
let key = `${type}.${item.id}`;
setSelectedItems((prev) => new Map(prev.delete(key) ? prev : prev.set(key, `${key}.1`)));
},
[]
), handleCollapseAll = useCallback(() => {
setSelectedItems(/* @__PURE__ */ new Map());
}, []), handleExpandAll = useCallback(() => {
setSelectedItems(
(prev) => new Map(
results?.[ui.tab]?.map((result) => {
let key = `${ui.tab}.${result.id}`;
return [key, prev.get(key) ?? `${key}.1`];
}) ?? []
)
);
}, [results, ui.tab]), handleSelectionChange = useCallback((key) => {
let [type, id] = key.split(".");
setSelectedItems((prev) => new Map(prev.set(`${type}.${id}`, key)));
}, []), handleError = useCallback(
(err) => {
setState((prev) => ({ ...prev, status: "error", error: err }));
},
[setState]
), handleResult = useCallback(
(axeResults, id) => {
storyId === id && (setState((prev) => ({ ...prev, status: "ran", results: axeResults })), setTimeout(() => {
setState((prev) => prev.status === "ran" ? { ...prev, status: "ready" } : prev), setSelectedItems((prev) => {
if (prev.size === 1) {
let [key] = prev.values();
document.getElementById(key)?.scrollIntoView({ behavior: "smooth", block: "center" });
}
return prev;
});
}, 900));
},
[storyId, setState, setSelectedItems]
), handleSelect = useCallback(
(itemId, details) => {
let [type, id] = itemId.split("."), { helpUrl, nodes } = results?.[type]?.find((r) => r.id === id) || {}, openedWindow = helpUrl && window.open(helpUrl, "_blank", "noopener,noreferrer");
if (nodes && !openedWindow) {
let index = nodes.findIndex((n) => details.selectors.some((s) => s === String(n.target))) ?? -1;
if (index !== -1) {
let key = `${type}.${id}.${index + 1}`;
setSelectedItems(/* @__PURE__ */ new Map([[`${type}.${id}`, key]])), setTimeout(() => {
document.getElementById(key)?.scrollIntoView({ behavior: "smooth", block: "center" });
}, 100);
}
}
},
[results]
), handleReport = useCallback(
({ reporters }) => {
let a11yReport = reporters.find((r) => r.type === "a11y");
a11yReport && ("error" in a11yReport.result ? handleError(a11yReport.result.error) : handleResult(a11yReport.result, storyId));
},
[handleError, handleResult, storyId]
), handleReset = useCallback(
({ newPhase }) => {
newPhase === "loading" ? setState((prev) => ({
...prev,
results: void 0,
status: manual ? "manual" : "initial"
})) : newPhase === "afterEach" && !manual && setState((prev) => ({ ...prev, status: "running" }));
},
[manual, setState]
), emit = useChannel(
{
[EVENTS.RESULT]: handleResult,
[EVENTS.ERROR]: handleError,
[EVENTS.SELECT]: handleSelect,
[STORY_CHANGED]: () => setSelectedItems(/* @__PURE__ */ new Map()),
[STORY_RENDER_PHASE_CHANGED]: handleReset,
[STORY_FINISHED]: handleReport,
[STORY_HOT_UPDATED]: () => {
setState((prev) => ({ ...prev, status: "running" })), emit(EVENTS.MANUAL, storyId, parameters);
}
},
[handleReset, handleReport, handleSelect, handleError, handleResult, parameters, storyId]
), handleManual = useCallback(() => {
setState((prev) => ({ ...prev, status: "running" })), emit(EVENTS.MANUAL, storyId, parameters);
}, [emit, parameters, setState, storyId]), handleCopyLink = useCallback(async (linkPath) => {
let { createCopyToClipboardFunction } = await import("storybook/internal/components");
await createCopyToClipboardFunction()(`${window.location.origin}${linkPath}`);
}, []), handleJumpToElement = useCallback(
(target) => emit(SCROLL_INTO_VIEW, target),
[emit]
);
useEffect(() => {
setState((prev) => ({ ...prev, status: getInitialStatus(manual) }));
}, [getInitialStatus, manual, setState]);
let isInitial = status === "initial";
useEffect(() => {
a11ySelection && setState((prev) => {
let update = { ...prev.ui, highlighted: !0 }, [type] = a11ySelection.split(".") ?? [];
return type && Object.values(RuleType).includes(type) && (update.tab = type), { ...prev, ui: update };
});
}, [a11ySelection]), useEffect(() => {
if (emit(REMOVE_HIGHLIGHT, `${ADDON_ID}/selected`), emit(REMOVE_HIGHLIGHT, `${ADDON_ID}/others`), !ui.highlighted || isInitial)
return;
let selected = Array.from(selectedItems.values()).flatMap((key) => {
let [type, id, number] = key.split(".");
if (type !== ui.tab)
return [];
let target = results?.[type]?.find((r) => r.id === id)?.nodes[Number(number) - 1]?.target;
return target ? [String(target)] : [];
});
selected.length && emit(HIGHLIGHT, {
id: `${ADDON_ID}/selected`,
priority: 1,
selectors: selected,
styles: {
outline: `1px solid color-mix(in srgb, ${colorsByType[ui.tab]}, transparent 30%)`,
backgroundColor: "transparent"
},
hoverStyles: {
outlineWidth: "2px"
},
focusStyles: {
backgroundColor: "transparent"
},
menu: results?.[ui.tab].map((result) => {
let selectors = result.nodes.flatMap((n) => n.target).map(String).filter((e) => selected.includes(e));
return [
{
id: `${ui.tab}.${result.id}:info`,
title: getTitleForAxeResult(result),
description: getFriendlySummaryForAxeResult(result),
selectors
},
{
id: `${ui.tab}.${result.id}`,
iconLeft: "info",
iconRight: "shareAlt",
title: "Learn how to resolve this violation",
clickEvent: EVENTS.SELECT,
selectors
}
];
})
});
let others = results?.[ui.tab].flatMap((r) => r.nodes.flatMap((n) => n.target).map(String)).filter((e) => ![...unhighlightedSelectors, ...selected].includes(e));
others?.length && emit(HIGHLIGHT, {
id: `${ADDON_ID}/others`,
selectors: others,
styles: {
outline: `1px solid color-mix(in srgb, ${colorsByType[ui.tab]}, transparent 30%)`,
backgroundColor: `color-mix(in srgb, ${colorsByType[ui.tab]}, transparent 60%)`
},
hoverStyles: {
outlineWidth: "2px"
},
focusStyles: {
backgroundColor: "transparent"
},
menu: results?.[ui.tab].map((result) => {
let selectors = result.nodes.flatMap((n) => n.target).map(String).filter((e) => !selected.includes(e));
return [
{
id: `${ui.tab}.${result.id}:info`,
title: getTitleForAxeResult(result),
description: getFriendlySummaryForAxeResult(result),
selectors
},
{
id: `${ui.tab}.${result.id}`,
iconLeft: "info",
iconRight: "shareAlt",
title: "Learn how to resolve this violation",
clickEvent: EVENTS.SELECT,
selectors
}
];
})
});
}, [isInitial, emit, ui.highlighted, results, ui.tab, selectedItems]);
let discrepancy = useMemo(() => {
if (!currentStoryA11yStatusValue)
return null;
if (currentStoryA11yStatusValue === "status-value:success" && results?.violations.length)
return "cliPassedBrowserFailed";
if (currentStoryA11yStatusValue === "status-value:error" && !results?.violations.length) {
if (status === "ready" || status === "ran")
return "browserPassedCliFailed";
if (status === "manual")
return "cliFailedButModeManual";
}
return null;
}, [results?.violations.length, status, currentStoryA11yStatusValue]);
return React.createElement(
A11yContext.Provider,
{
value: {
parameters,
results,
highlighted: ui.highlighted,
toggleHighlight: handleToggleHighlight,
tab: ui.tab,
setTab: useCallback(
(type) => setState((prev) => ({ ...prev, ui: { ...prev.ui, tab: type } })),
[setState]
),
handleCopyLink,
status,
setStatus: useCallback(
(status2) => setState((prev) => ({ ...prev, status: status2 })),
[setState]
),
error,
handleManual,
discrepancy,
selectedItems,
toggleOpen,
allExpanded,
handleCollapseAll,
handleExpandAll,
handleJumpToElement,
handleSelectionChange
},
...props
}
);
}, useA11yContext = () => useContext(A11yContext);
// src/components/Report/Report.tsx
import React3 from "react";
import { Badge, Button as Button2, EmptyTabContent } from "storybook/internal/components";
import { ChevronSmallDownIcon } from "@storybook/icons";
import { styled as styled2 } from "storybook/theming";
// src/components/Report/Details.tsx
import React2, { Fragment, useCallback as useCallback2, useState as useState3 } from "react";
import { Button, Link, SyntaxHighlighter } from "storybook/internal/components";
import { CheckIcon, CopyIcon, LocationIcon } from "@storybook/icons";
// ../../../node_modules/@babel/runtime/helpers/esm/extends.js
function _extends() {
return _extends = Object.assign ? Object.assign.bind() : function(n) {
for (var e = 1; e < arguments.length; e++) {
var t = arguments[e];
for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]);
}
return n;
}, _extends.apply(null, arguments);
}
// ../../../node_modules/@radix-ui/react-tabs/dist/index.mjs
import { forwardRef as $1IHzk$forwardRef, createElement as $1IHzk$createElement, useRef as $1IHzk$useRef, useEffect as $1IHzk$useEffect } from "react";
// ../../../node_modules/@radix-ui/primitive/dist/index.mjs
function $e42e1063c40fb3ef$export$b9ecd428b558ff10(originalEventHandler, ourEventHandler, { checkForDefaultPrevented = !0 } = {}) {
return function(event) {
if (originalEventHandler?.(event), checkForDefaultPrevented === !1 || !event.defaultPrevented) return ourEventHandler?.(event);
};
}
// ../../../node_modules/@radix-ui/react-context/dist/index.mjs
import { createContext as $3bkAK$createContext, useMemo as $3bkAK$useMemo, createElement as $3bkAK$createElement, useContext as $3bkAK$useContext } from "react";
function $c512c27ab02ef895$export$50c7b4e9d9f19c1(scopeName, createContextScopeDeps = []) {
let defaultContexts = [];
function $c512c27ab02ef895$export$fd42f52fd3ae1109(rootComponentName, defaultContext) {
let BaseContext = $3bkAK$createContext(defaultContext), index = defaultContexts.length;
defaultContexts = [
...defaultContexts,
defaultContext
];
function Provider(props) {
let { scope, children, ...context } = props, Context = scope?.[scopeName][index] || BaseContext, value = $3bkAK$useMemo(
() => context,
Object.values(context)
);
return $3bkAK$createElement(Context.Provider, {
value
}, children);
}
function useContext2(consumerName, scope) {
let Context = scope?.[scopeName][index] || BaseContext, context = $3bkAK$useContext(Context);
if (context) return context;
if (defaultContext !== void 0) return defaultContext;
throw new Error(`\`${consumerName}\` must be used within \`${rootComponentName}\``);
}
return Provider.displayName = rootComponentName + "Provider", [
Provider,
useContext2
];
}
let createScope = () => {
let scopeContexts = defaultContexts.map((defaultContext) => $3bkAK$createContext(defaultContext));
return function(scope) {
let contexts = scope?.[scopeName] || scopeContexts;
return $3bkAK$useMemo(
() => ({
[`__scope${scopeName}`]: {
...scope,
[scopeName]: contexts
}
}),
[
scope,
contexts
]
);
};
};
return createScope.scopeName = scopeName, [
$c512c27ab02ef895$export$fd42f52fd3ae1109,
$c512c27ab02ef895$var$composeContextScopes(createScope, ...createContextScopeDeps)
];
}
function $c512c27ab02ef895$var$composeContextScopes(...scopes) {
let baseScope = scopes[0];
if (scopes.length === 1) return baseScope;
let createScope1 = () => {
let scopeHooks = scopes.map(
(createScope) => ({
useScope: createScope(),
scopeName: createScope.scopeName
})
);
return function(overrideScopes) {
let nextScopes1 = scopeHooks.reduce((nextScopes, { useScope, scopeName }) => {
let currentScope = useScope(overrideScopes)[`__scope${scopeName}`];
return {
...nextScopes,
...currentScope
};
}, {});
return $3bkAK$useMemo(
() => ({
[`__scope${baseScope.scopeName}`]: nextScopes1
}),
[
nextScopes1
]
);
};
};
return createScope1.scopeName = baseScope.scopeName, createScope1;
}
// ../../../node_modules/@radix-ui/react-roving-focus/dist/index.mjs
import { forwardRef as $98Iye$forwardRef, createElement as $98Iye$createElement, useRef as $98Iye$useRef, useState as $98Iye$useState, useEffect as $98Iye$useEffect, useCallback as $98Iye$useCallback } from "react";
// ../../../node_modules/@radix-ui/react-collection/dist/index.mjs
import $6vYhU$react from "react";
// ../../../node_modules/@radix-ui/react-compose-refs/dist/index.mjs
import { useCallback as $3vqmr$useCallback } from "react";
function $6ed0406888f73fc4$var$setRef(ref, value) {
typeof ref == "function" ? ref(value) : ref != null && (ref.current = value);
}
function $6ed0406888f73fc4$export$43e446d32b3d21af(...refs) {
return (node) => refs.forEach(
(ref) => $6ed0406888f73fc4$var$setRef(ref, node)
);
}
function $6ed0406888f73fc4$export$c7b2cbe3552a0d05(...refs) {
return $3vqmr$useCallback($6ed0406888f73fc4$export$43e446d32b3d21af(...refs), refs);
}
// ../../../node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot/dist/index.mjs
import { forwardRef as $9IrjX$forwardRef, Children as $9IrjX$Children, isValidElement as $9IrjX$isValidElement, createElement as $9IrjX$createElement, cloneElement as $9IrjX$cloneElement, Fragment as $9IrjX$Fragment } from "react";
var $5e63c961fc1ce211$export$8c6ed5c666ac1360 = $9IrjX$forwardRef((props, forwardedRef) => {
let { children, ...slotProps } = props, childrenArray = $9IrjX$Children.toArray(children), slottable = childrenArray.find($5e63c961fc1ce211$var$isSlottable);
if (slottable) {
let newElement = slottable.props.children, newChildren = childrenArray.map((child) => child === slottable ? $9IrjX$Children.count(newElement) > 1 ? $9IrjX$Children.only(null) : $9IrjX$isValidElement(newElement) ? newElement.props.children : null : child);
return $9IrjX$createElement($5e63c961fc1ce211$var$SlotClone, _extends({}, slotProps, {
ref: forwardedRef
}), $9IrjX$isValidElement(newElement) ? $9IrjX$cloneElement(newElement, void 0, newChildren) : null);
}
return $9IrjX$createElement($5e63c961fc1ce211$var$SlotClone, _extends({}, slotProps, {
ref: forwardedRef
}), children);
});
$5e63c961fc1ce211$export$8c6ed5c666ac1360.displayName = "Slot";
var $5e63c961fc1ce211$var$SlotClone = $9IrjX$forwardRef((props, forwardedRef) => {
let { children, ...slotProps } = props;
return $9IrjX$isValidElement(children) ? $9IrjX$cloneElement(children, {
...$5e63c961fc1ce211$var$mergeProps(slotProps, children.props),
ref: forwardedRef ? $6ed0406888f73fc4$export$43e446d32b3d21af(forwardedRef, children.ref) : children.ref
}) : $9IrjX$Children.count(children) > 1 ? $9IrjX$Children.only(null) : null;
});
$5e63c961fc1ce211$var$SlotClone.displayName = "SlotClone";
var $5e63c961fc1ce211$export$d9f1ccf0bdb05d45 = ({ children }) => $9IrjX$createElement($9IrjX$Fragment, null, children);
function $5e63c961fc1ce211$var$isSlottable(child) {
return $9IrjX$isValidElement(child) && child.type === $5e63c961fc1ce211$export$d9f1ccf0bdb05d45;
}
function $5e63c961fc1ce211$var$mergeProps(slotProps, childProps) {
let overrideProps = {
...childProps
};
for (let propName in childProps) {
let slotPropValue = slotProps[propName], childPropValue = childProps[propName];
/^on[A-Z]/.test(propName) ? slotPropValue && childPropValue ? overrideProps[propName] = (...args) => {
childPropValue(...args), slotPropValue(...args);
} : slotPropValue && (overrideProps[propName] = slotPropValue) : propName === "style" ? overrideProps[propName] = {
...slotPropValue,
...childPropValue
} : propName === "className" && (overrideProps[propName] = [
slotPropValue,
childPropValue
].filter(Boolean).join(" "));
}
return {
...slotProps,
...overrideProps
};
}
// ../../../node_modules/@radix-ui/react-collection/dist/index.mjs
function $e02a7d9cb1dc128c$export$c74125a8e3af6bb2(name) {
let PROVIDER_NAME = name + "CollectionProvider", [createCollectionContext, createCollectionScope] = $c512c27ab02ef895$export$50c7b4e9d9f19c1(PROVIDER_NAME), [CollectionProviderImpl, useCollectionContext] = createCollectionContext(PROVIDER_NAME, {
collectionRef: {
current: null
},
itemMap: /* @__PURE__ */ new Map()
}), CollectionProvider = (props) => {
let { scope, children } = props, ref = $6vYhU$react.useRef(null), itemMap = $6vYhU$react.useRef(/* @__PURE__ */ new Map()).current;
return $6vYhU$react.createElement(CollectionProviderImpl, {
scope,
itemMap,
collectionRef: ref
}, children);
};
Object.assign(CollectionProvider, {
displayName: PROVIDER_NAME
});
let COLLECTION_SLOT_NAME = name + "CollectionSlot", CollectionSlot = $6vYhU$react.forwardRef((props, forwardedRef) => {
let { scope, children } = props, context = useCollectionContext(COLLECTION_SLOT_NAME, scope), composedRefs = $6ed0406888f73fc4$export$c7b2cbe3552a0d05(forwardedRef, context.collectionRef);
return $6vYhU$react.createElement($5e63c961fc1ce211$export$8c6ed5c666ac1360, {
ref: composedRefs
}, children);
});
Object.assign(CollectionSlot, {
displayName: COLLECTION_SLOT_NAME
});
let ITEM_SLOT_NAME = name + "CollectionItemSlot", ITEM_DATA_ATTR = "data-radix-collection-item", CollectionItemSlot = $6vYhU$react.forwardRef((props, forwardedRef) => {
let { scope, children, ...itemData } = props, ref = $6vYhU$react.useRef(null), composedRefs = $6ed0406888f73fc4$export$c7b2cbe3552a0d05(forwardedRef, ref), context = useCollectionContext(ITEM_SLOT_NAME, scope);
return $6vYhU$react.useEffect(() => (context.itemMap.set(ref, {
ref,
...itemData
}), () => void context.itemMap.delete(ref))), $6vYhU$react.createElement($5e63c961fc1ce211$export$8c6ed5c666ac1360, {
[ITEM_DATA_ATTR]: "",
ref: composedRefs
}, children);
});
Object.assign(CollectionItemSlot, {
displayName: ITEM_SLOT_NAME
});
function useCollection(scope) {
let context = useCollectionContext(name + "CollectionConsumer", scope);
return $6vYhU$react.useCallback(() => {
let collectionNode = context.collectionRef.current;
if (!collectionNode) return [];
let orderedNodes = Array.from(collectionNode.querySelectorAll(`[${ITEM_DATA_ATTR}]`));
return Array.from(context.itemMap.values()).sort(
(a, b) => orderedNodes.indexOf(a.ref.current) - orderedNodes.indexOf(b.ref.current)
);
}, [
context.collectionRef,
context.itemMap
]);
}
return [
{
Provider: CollectionProvider,
Slot: CollectionSlot,
ItemSlot: CollectionItemSlot
},
useCollection,
createCollectionScope
];
}
// ../../../node_modules/@radix-ui/react-id/dist/index.mjs
import * as $2AODx$react from "react";
// ../../../node_modules/@radix-ui/react-use-layout-effect/dist/index.mjs
import { useLayoutEffect as $dxlwH$useLayoutEffect } from "react";
var $9f79659886946c16$export$e5c5a5f917a5871c = globalThis?.document ? $dxlwH$useLayoutEffect : () => {
};
// ../../../node_modules/@radix-ui/react-id/dist/index.mjs
var $1746a345f3d73bb7$var$useReactId = $2AODx$react.useId || (() => {
}), $1746a345f3d73bb7$var$count = 0;
function $1746a345f3d73bb7$export$f680877a34711e37(deterministicId) {
let [id, setId] = $2AODx$react.useState($1746a345f3d73bb7$var$useReactId());
return $9f79659886946c16$export$e5c5a5f917a5871c(() => {
deterministicId || setId(
(reactId) => reactId ?? String($1746a345f3d73bb7$var$count++)
);
}, [
deterministicId
]), deterministicId || (id ? `radix-${id}` : "");
}
// ../../../node_modules/@radix-ui/react-primitive/dist/index.mjs
import { forwardRef as $4q5Fq$forwardRef, useEffect as $4q5Fq$useEffect, createElement as $4q5Fq$createElement } from "react";
import { flushSync as $4q5Fq$flushSync } from "react-dom";
// ../../../node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot/dist/index.mjs
import { forwardRef as $9IrjX$forwardRef2, Children as $9IrjX$Children2, isValidElement as $9IrjX$isValidElement2, createElement as $9IrjX$createElement2, cloneElement as $9Ir