diff --git a/.changeset/a11y-form-controls.md b/.changeset/a11y-form-controls.md new file mode 100644 index 000000000..3e0e49479 --- /dev/null +++ b/.changeset/a11y-form-controls.md @@ -0,0 +1,5 @@ +--- +'@tiny-design/react': minor +--- + +Improve form-control accessibility. `Form.Item` now generates ids that wire `aria-labelledby`, `aria-describedby`, and `aria-invalid` on the wrapped control automatically, so a label always announces with its input and screen readers hear validation errors. `Cascader` forwards the consumer's ref to the wrapper and pipes `id` and `aria-*` props through to the combobox element. `InputNumber` omits `min`, `max`, `aria-valuemin`, and `aria-valuemax` when the bounds are not finite (previously emitted `Infinity` / `-Infinity` strings) and forwards remaining native input props. DatePicker's weekday header and dim/disabled cell text now use `--ty-color-text-secondary` (with the previous `--ty-*-color-muted` token as fallback) so the picker meets WCAG color-contrast on the popup. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f844c5e32..664f79562 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,3 +40,20 @@ jobs: - name: Test run: pnpm test -- --coverage + + - name: Install browser for Playwright + run: pnpm exec playwright install --with-deps chrome + + - name: Accessibility tests + run: pnpm test:accessibility + + - name: Upload Playwright report + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: | + playwright-report/ + apps/docs/playwright-report/ + apps/docs/test-results/ + if-no-files-found: ignore diff --git a/apps/docs/playwright.accessibility.config.ts b/apps/docs/playwright.accessibility.config.ts new file mode 100644 index 000000000..79d970117 --- /dev/null +++ b/apps/docs/playwright.accessibility.config.ts @@ -0,0 +1,33 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests/accessibility', + outputDir: './test-results/accessibility', + fullyParallel: false, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + reporter: process.env.CI + ? [ + ['html', { outputFolder: 'apps/docs/playwright-report/accessibility', open: 'never' }], + ['list'], + ] + : 'list', + webServer: { + command: 'pnpm exec vite serve --host 127.0.0.1 --port 3004', + url: 'http://127.0.0.1:3004', + reuseExistingServer: !process.env.CI, + timeout: 120_000, + }, + use: { + ...devices['Desktop Chrome'], + baseURL: 'http://127.0.0.1:3004', + channel: 'chrome', + colorScheme: 'light', + deviceScaleFactor: 1, + locale: 'en-US', + timezoneId: 'UTC', + viewport: { width: 1280, height: 900 }, + screenshot: 'only-on-failure', + trace: 'retain-on-failure', + }, +}); diff --git a/apps/docs/tests/accessibility/component-accessibility.spec.ts b/apps/docs/tests/accessibility/component-accessibility.spec.ts new file mode 100644 index 000000000..bc295a9ac --- /dev/null +++ b/apps/docs/tests/accessibility/component-accessibility.spec.ts @@ -0,0 +1,84 @@ +import AxeBuilder from '@axe-core/playwright'; +import { expect, test, type Locator, type Page } from '@playwright/test'; +import { gotoComponent, openFromDemo, previewByTitle, scrollDemoIntoView } from '../visual/helpers'; + +const WCAG_TAGS = ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']; + +let scanId = 0; + +const formatViolations = (violations: Awaited>['violations']) => + violations.map(({ id, impact, description, nodes }) => ({ + id, + impact, + description, + targets: nodes.slice(0, 5).map((node) => node.target.join(' ')), + })); + +const markLocator = async (locator: Locator) => { + const marker = `a11y-scan-${++scanId}`; + await locator.evaluate((node, value) => { + node.setAttribute('data-a11y-scan', value); + }, marker); + + return `[data-a11y-scan="${marker}"]`; +}; + +const scan = async (page: Page, target: Locator | string) => { + const selector = typeof target === 'string' ? target : await markLocator(target); + const results = await new AxeBuilder({ page }) + .withTags(WCAG_TAGS) + .disableRules(['region']) + .include(selector) + .analyze(); + + expect(formatViolations(results.violations)).toEqual([]); +}; + +test.describe('component accessibility checks', () => { + test('form controls and table demos have no WCAG violations', async ({ page }) => { + await gotoComponent(page, 'form'); + await scrollDemoIntoView(page, 'Basic usage'); + await scan(page, previewByTitle(page, 'Basic usage')); + + await gotoComponent(page, 'table'); + await scrollDemoIntoView(page, 'Basic'); + await scan(page, previewByTitle(page, 'Basic')); + + await scrollDemoIntoView(page, 'Row Selection'); + await scan(page, previewByTitle(page, 'Row Selection')); + }); + + test('overlay components have no WCAG violations when open', async ({ page }) => { + await gotoComponent(page, 'modal'); + await openFromDemo(page, 'Basic', 'button'); + await expect(page.locator('.ty-modal__content')).toBeVisible(); + await scan(page, '.ty-modal__content'); + + await gotoComponent(page, 'drawer'); + await openFromDemo(page, 'Basic', 'button'); + await expect(page.locator('.ty-drawer__content')).toBeVisible(); + await scan(page, '.ty-drawer__content'); + + await gotoComponent(page, 'tour'); + await openFromDemo(page, 'Basic', 'button:has-text("Start Tour")'); + await expect(page.locator('.ty-tour')).toBeVisible(); + await scan(page, '.ty-tour'); + }); + + test('select and picker popups have no WCAG violations when open', async ({ page }) => { + await gotoComponent(page, 'select'); + await openFromDemo(page, 'Search', '.ty-select__selector'); + await expect(page.locator('.ty-select__dropdown')).toBeVisible(); + await scan(page, '.ty-select__dropdown'); + + await gotoComponent(page, 'date-picker'); + await openFromDemo(page, 'Date Range', '.ty-date-picker__input'); + await expect(page.locator('.ty-date-picker__dropdown')).toBeVisible(); + await scan(page, '.ty-date-picker__dropdown'); + + await gotoComponent(page, 'cascader'); + await openFromDemo(page, 'Change On Select', '.ty-cascader__selector'); + await expect(page.locator('.ty-cascader__dropdown')).toBeVisible(); + await scan(page, '.ty-cascader__dropdown'); + }); +}); diff --git a/apps/docs/tests/visual/README.md b/apps/docs/tests/visual/README.md index 3bc40dc20..bd9900829 100644 --- a/apps/docs/tests/visual/README.md +++ b/apps/docs/tests/visual/README.md @@ -17,3 +17,10 @@ pnpm test:visual:update The suite targets the docs app and uses the local Chrome channel to avoid storing browser binaries in the repository workflow. + +Accessibility coverage for the same high-risk component families lives in +`apps/docs/tests/accessibility` and runs with: + +```sh +pnpm test:accessibility +``` diff --git a/package.json b/package.json index 2244a11e2..5ad5e3e22 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "build": "turbo run build", "dev": "turbo run dev", "test": "turbo run test", + "test:accessibility": "playwright test -c apps/docs/playwright.accessibility.config.ts", "test:visual": "playwright test -c apps/docs/playwright.config.ts", "test:visual:update": "playwright test -c apps/docs/playwright.config.ts --update-snapshots", "lint": "turbo run lint", @@ -27,11 +28,13 @@ "release": "turbo run build && changeset publish" }, "devDependencies": { + "@axe-core/playwright": "^4.11.3", "@changesets/changelog-github": "^0.6.0", "@changesets/cli": "^2.30.0", "@changesets/get-github-info": "^0.8.0", "@eslint/js": "^9.0.0", "@playwright/test": "^1.59.1", + "autoprefixer": "^10.4.27", "eslint": "^9.0.0", "eslint-plugin-jest": "^28.0.0", "eslint-plugin-jest-dom": "^5.0.0", diff --git a/packages/mcp/src/data/components.json b/packages/mcp/src/data/components.json index 68e36e300..d17fe8cca 100644 --- a/packages/mcp/src/data/components.json +++ b/packages/mcp/src/data/components.json @@ -3521,12 +3521,6 @@ "required": false, "description": "Determine whether always display the control button" }, - { - "name": "children", - "type": "React.ReactNode", - "required": false, - "description": "" - }, { "name": "style", "type": "CSSProperties", diff --git a/packages/react/src/cascader/cascader.tsx b/packages/react/src/cascader/cascader.tsx index 95dd879fb..6afd77fda 100644 --- a/packages/react/src/cascader/cascader.tsx +++ b/packages/react/src/cascader/cascader.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef, useContext, useMemo, useCallback } from 'react'; +import React, { useState, useEffect, useContext, useMemo, useCallback } from 'react'; import classNames from 'classnames'; import { ConfigContext } from '../config-provider/config-context'; import { getPrefixCls } from '../_utils/general'; @@ -6,10 +6,7 @@ import { ArrowDown, ClearIcon } from '../_utils/components'; import Popup from '../popup'; import { CascaderProps, CascaderOption, CascaderValue } from './types'; -const getOptionsByValue = ( - options: CascaderOption[], - value: CascaderValue -): CascaderOption[] => { +const getOptionsByValue = (options: CascaderOption[], value: CascaderValue): CascaderOption[] => { const result: CascaderOption[] = []; let current = options; for (const v of value) { @@ -36,6 +33,11 @@ const Cascader = React.forwardRef((props, ref) => prefixCls: customisedCls, className, style, + id, + 'aria-label': ariaLabel, + 'aria-labelledby': ariaLabelledBy, + 'aria-describedby': ariaDescribedBy, + 'aria-invalid': ariaInvalid, onChange, onDropdownVisibleChange, ...otherProps @@ -44,8 +46,6 @@ const Cascader = React.forwardRef((props, ref) => const configContext = useContext(ConfigContext); const prefixCls = getPrefixCls('cascader', configContext.prefixCls, customisedCls); const cascaderSize = size || configContext.componentSize || 'md'; - const wrapperRef = useRef(null); - const [open, setOpen] = useState(false); const [selectedValue, setSelectedValue] = useState( 'value' in props ? (props.value ?? []) : (defaultValue ?? []) @@ -82,15 +82,18 @@ const Cascader = React.forwardRef((props, ref) => setActiveColumns(cols); }, [options, selectedValue]); - const setDropdownOpen = useCallback((nextOpen: boolean) => { - if (!('open' in props)) { - setOpen(nextOpen); - } - onDropdownVisibleChange?.(nextOpen); - if (!nextOpen) { - setHoveredPath([]); - } - }, [onDropdownVisibleChange, props]); + const setDropdownOpen = useCallback( + (nextOpen: boolean) => { + if (!('open' in props)) { + setOpen(nextOpen); + } + onDropdownVisibleChange?.(nextOpen); + if (!nextOpen) { + setHoveredPath([]); + } + }, + [onDropdownVisibleChange, props] + ); const toggleOpen = () => { if (disabled) return; @@ -117,15 +120,18 @@ const Cascader = React.forwardRef((props, ref) => } }; - const getPathAtLevel = useCallback((level: number): CascaderValue => { - if (hoveredPath.length > level) { - return hoveredPath; - } - if (selectedValue.length > level) { - return selectedValue; - } - return hoveredPath.length > 0 ? hoveredPath : selectedValue; - }, [hoveredPath, selectedValue]); + const getPathAtLevel = useCallback( + (level: number): CascaderValue => { + if (hoveredPath.length > level) { + return hoveredPath; + } + if (selectedValue.length > level) { + return selectedValue; + } + return hoveredPath.length > 0 ? hoveredPath : selectedValue; + }, + [hoveredPath, selectedValue] + ); const handleOptionSelect = (option: CascaderOption, level: number) => { if (option.disabled) return; @@ -198,63 +204,65 @@ const Cascader = React.forwardRef((props, ref) => [`${prefixCls}_has-value`]: allowClear && selectedValue.length > 0 && !disabled, }); - const dropdown = open - ? ( -
-
- {activeColumns.map((columnOptions, level) => ( -
    - {columnOptions.length === 0 ? ( -
  • {notFoundContent}
  • - ) : ( - columnOptions.map((option) => { - const isActive = - (selectedValue[level] === option.value) || - (hoveredPath[level] === option.value); - const hasChildren = !!(option.children?.length); - const itemCls = classNames(`${prefixCls}__menu-item`, { - [`${prefixCls}__menu-item_active`]: isActive, - [`${prefixCls}__menu-item_disabled`]: option.disabled, - }); - return ( -
  • handleOptionSelect(option, level)} - onMouseEnter={() => handleOptionHover(option, level)} - > - {option.label} - {hasChildren && ( - - )} -
  • - ); - }) - )} -
- ))} -
-
- ) - : null; + const dropdown = open ? ( +
+
+ {activeColumns.map((columnOptions, level) => ( +
    + {columnOptions.length === 0 ? ( +
  • {notFoundContent}
  • + ) : ( + columnOptions.map((option) => { + const isActive = + selectedValue[level] === option.value || hoveredPath[level] === option.value; + const hasChildren = !!option.children?.length; + const itemCls = classNames(`${prefixCls}__menu-item`, { + [`${prefixCls}__menu-item_active`]: isActive, + [`${prefixCls}__menu-item_disabled`]: option.disabled, + }); + return ( +
  • handleOptionSelect(option, level)} + onMouseEnter={() => handleOptionHover(option, level)}> + {option.label} + {hasChildren && } +
  • + ); + }) + )} +
+ ))} +
+
+ ) : null; return ( -
+
+ content={dropdown}>
@@ -268,12 +276,13 @@ const Cascader = React.forwardRef((props, ref) => type="button" className={`${prefixCls}__clear`} onClick={handleClear} - aria-label="Clear selection" - > + aria-label="Clear selection"> )} - + + +
diff --git a/packages/react/src/date-picker/style/index.scss b/packages/react/src/date-picker/style/index.scss index b62fbd1c5..fd08625a4 100755 --- a/packages/react/src/date-picker/style/index.scss +++ b/packages/react/src/date-picker/style/index.scss @@ -1,4 +1,4 @@ -@use "../../style/variables" as *; +@use '../../style/variables' as *; $dp: #{$prefix}-date-picker; @@ -20,7 +20,9 @@ $dp: #{$prefix}-date-picker; border-radius: var(--ty-picker-input-radius); background: var(--ty-picker-input-bg); cursor: pointer; - transition: border-color 0.3s, box-shadow 0.3s; + transition: + border-color 0.3s, + box-shadow 0.3s; &:hover { border-color: var(--ty-picker-input-border-hover); @@ -47,7 +49,10 @@ $dp: #{$prefix}-date-picker; min-width: var(--ty-date-picker-input-min-width-md); &::placeholder { - color: var(--ty-input-placeholder, var(--ty-picker-input-color-placeholder, var(--ty-color-text-placeholder))); + color: var( + --ty-input-placeholder, + var(--ty-picker-input-color-placeholder, var(--ty-color-text-placeholder)) + ); opacity: 1; } @@ -226,7 +231,7 @@ $dp: #{$prefix}-date-picker; &__cell-header { font-weight: 400; - color: var(--ty-picker-input-color-muted); + color: var(--ty-color-text-secondary, var(--ty-date-picker-cell-color)); padding: 4px 0; font-size: 12px; } @@ -249,7 +254,7 @@ $dp: #{$prefix}-date-picker; height: var(--ty-date-picker-cell-size); border-radius: var(--ty-date-picker-cell-radius); transition: background 0.2s; - color: var(--ty-date-picker-cell-color-muted); + color: var(--ty-color-text-secondary, var(--ty-date-picker-cell-color-muted)); font-size: var(--ty-date-picker-cell-font-size); } @@ -308,7 +313,7 @@ $dp: #{$prefix}-date-picker; } &__cell_disabled &__cell-inner { - color: var(--ty-date-picker-cell-color-muted); + color: var(--ty-color-text-secondary, var(--ty-date-picker-cell-color-muted)); background: var(--ty-date-picker-cell-disabled-bg); } @@ -317,7 +322,7 @@ $dp: #{$prefix}-date-picker; } &__cell_dim &__cell-inner { - color: var(--ty-date-picker-cell-color-muted); + color: var(--ty-color-text-secondary, var(--ty-date-picker-cell-color-muted)); } &__month-table, @@ -355,7 +360,7 @@ $dp: #{$prefix}-date-picker; &__month-cell.#{$dp}__cell_dim &__month-cell-inner, &__year-cell.#{$dp}__cell_dim &__year-cell-inner { - color: var(--ty-date-picker-cell-color-muted); + color: var(--ty-color-text-secondary, var(--ty-date-picker-cell-color-muted)); font-weight: 400; } diff --git a/packages/react/src/form/__tests__/form.test.tsx b/packages/react/src/form/__tests__/form.test.tsx index a3cc30390..88f0b239b 100644 --- a/packages/react/src/form/__tests__/form.test.tsx +++ b/packages/react/src/form/__tests__/form.test.tsx @@ -61,6 +61,18 @@ describe('
', () => { expect(screen.getByText('username required')).toBeInTheDocument(); }); + it('should associate labels with named controls', () => { + render( + + + + +
+ ); + + expect(screen.getByLabelText('Username')).toBe(screen.getByRole('textbox')); + }); + it('should forward ref to native form element', () => { const ref = React.createRef(); diff --git a/packages/react/src/form/form-item.tsx b/packages/react/src/form/form-item.tsx index 17635f0ad..349634d4d 100755 --- a/packages/react/src/form/form-item.tsx +++ b/packages/react/src/form/form-item.tsx @@ -37,6 +37,12 @@ const FormItem = (props: FormItemProps): JSX.Element => { ); const [hasErrLabel, setHasErrLabel] = useState(false); const errorRef = useRef(null); + const controlUid = React.useId(); + const controlId = name ? `${prefixCls}-${name}-${controlUid.replace(/:/g, '')}` : undefined; + const labelId = label + ? `${controlId ?? `${prefixCls}-${controlUid.replace(/:/g, '')}`}-label` + : undefined; + const errorId = name ? `${controlId}-error` : undefined; const cls = classNames(prefixCls, className, { [`${prefixCls}_has-error`]: !!error, [`${prefixCls}_with-err-label`]: hasErrLabel, @@ -62,7 +68,36 @@ const FormItem = (props: FormItemProps): JSX.Element => { let child: any = children; const prop = getPropName(valuePropName, child && child.type); - const childProps = { [prop]: value, onChange, onBlur }; + const childProps: Record = { [prop]: value, onChange, onBlur }; + const childElement = child as React.ReactElement>; + + if (React.isValidElement(childElement)) { + if (controlId && childElement.props.id === undefined) { + childProps.id = controlId; + } + + if ( + labelId && + childElement.props['aria-label'] === undefined && + childElement.props['aria-labelledby'] === undefined + ) { + childProps['aria-labelledby'] = labelId; + } + + if (error) { + childProps['aria-invalid'] = true; + if (errorId) { + const existingDescribedBy = childElement.props['aria-describedby']; + childProps['aria-describedby'] = [ + typeof existingDescribedBy === 'string' ? existingDescribedBy : undefined, + errorId, + ] + .filter(Boolean) + .join(' '); + } + } + } + child = React.cloneElement(child, childProps); const labelCls = classNames({ @@ -123,7 +158,8 @@ const FormItem = (props: FormItemProps): JSX.Element => {
{notice &&
{notice}
} {helper &&
{helper}
} - setHasErrLabel(false)}> -
{error}
+ setHasErrLabel(false)}> +
+ {error} +
diff --git a/packages/react/src/input-number/__tests__/__snapshots__/input-number.test.tsx.snap b/packages/react/src/input-number/__tests__/__snapshots__/input-number.test.tsx.snap index af904b524..0adda4b68 100644 --- a/packages/react/src/input-number/__tests__/__snapshots__/input-number.test.tsx.snap +++ b/packages/react/src/input-number/__tests__/__snapshots__/input-number.test.tsx.snap @@ -7,13 +7,9 @@ exports[` should match the snapshot 1`] = ` > ', () => { expect(container.querySelector('input')).toBeDisabled(); }); + it('should omit infinite ARIA and number bounds', () => { + const { container } = render(); + const input = container.querySelector('input')!; + expect(input).not.toHaveAttribute('min'); + expect(input).not.toHaveAttribute('max'); + expect(input).not.toHaveAttribute('aria-valuemin'); + expect(input).not.toHaveAttribute('aria-valuemax'); + }); + + it('should render finite ARIA and number bounds', () => { + const { container } = render(); + const input = container.querySelector('input')!; + expect(input).toHaveAttribute('min', '0'); + expect(input).toHaveAttribute('max', '10'); + expect(input).toHaveAttribute('aria-valuemin', '0'); + expect(input).toHaveAttribute('aria-valuemax', '10'); + }); + it('should fire onChange', () => { const fn = jest.fn(); const { container } = render(); diff --git a/packages/react/src/input-number/input-number.tsx b/packages/react/src/input-number/input-number.tsx index 923dd012f..7438a1b69 100755 --- a/packages/react/src/input-number/input-number.tsx +++ b/packages/react/src/input-number/input-number.tsx @@ -42,6 +42,7 @@ const InputNumber = React.forwardRef((props, r className, prefixCls: customisedCls, style, + ...inputProps } = props; const configContext = useContext(ConfigContext); const prefixCls = getPrefixCls('input-number', configContext.prefixCls, customisedCls); @@ -50,14 +51,19 @@ const InputNumber = React.forwardRef((props, r [`${prefixCls}_disabled`]: disabled, [`${prefixCls}_always-controls`]: controls, }); - const resolvedPrecision = precision ?? Math.max(getDecimalPrecision(step), getDecimalPrecision(defaultValue)); + const resolvedPrecision = + precision ?? Math.max(getDecimalPrecision(step), getDecimalPrecision(defaultValue)); const [value, setValue] = useState( 'value' in props ? (props.value as number) : defaultValue ); const hasNumericValue = isFiniteNumber(value); const displayValue = hasNumericValue - ? (resolvedPrecision > 0 ? value.toFixed(resolvedPrecision) : String(value)) + ? resolvedPrecision > 0 + ? value.toFixed(resolvedPrecision) + : String(value) : ''; + const minAttr = Number.isFinite(min) ? min : undefined; + const maxAttr = Number.isFinite(max) ? max : undefined; const inputOnChange = (e: React.ChangeEvent): void => { const raw = Number(e.target.value.trim()); @@ -97,18 +103,19 @@ const InputNumber = React.forwardRef((props, r return (
diff --git a/packages/react/src/input-number/types.ts b/packages/react/src/input-number/types.ts index 1d9ff1e77..d8c569978 100644 --- a/packages/react/src/input-number/types.ts +++ b/packages/react/src/input-number/types.ts @@ -1,7 +1,21 @@ import React from 'react'; import { BaseProps, SizeType } from '../_utils/props'; -export interface InputNumberProps extends BaseProps { +export interface InputNumberProps + extends + BaseProps, + Omit< + React.ComponentPropsWithoutRef<'input'>, + | 'className' + | 'children' + | 'defaultValue' + | 'max' + | 'min' + | 'onChange' + | 'size' + | 'style' + | 'value' + > { min?: number; max?: number; step?: number; @@ -16,5 +30,4 @@ export interface InputNumberProps extends BaseProps { disabled?: boolean; /** Determine whether always display the control button */ controls?: boolean; - children?: React.ReactNode; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4d5793a6d..4c4dbdb90 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: devDependencies: + '@axe-core/playwright': + specifier: ^4.11.3 + version: 4.11.3(playwright-core@1.59.1) '@changesets/changelog-github': specifier: ^0.6.0 version: 0.6.0(encoding@0.1.13) @@ -23,6 +26,9 @@ importers: '@playwright/test': specifier: ^1.59.1 version: 1.59.1 + autoprefixer: + specifier: ^10.4.27 + version: 10.4.27(postcss@8.5.14) eslint: specifier: ^9.0.0 version: 9.39.4(jiti@2.6.1) @@ -370,6 +376,11 @@ packages: '@adobe/css-tools@4.4.4': resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} + '@axe-core/playwright@4.11.3': + resolution: {integrity: sha512-h/kfksv4F0cVIDlKpT4700OehdRgpvuVskuQ2nb7/JmtWUXpe9ftHAPtwyXGvVSsa6SJ64A9ER7Zrzc/sIvC4w==} + peerDependencies: + playwright-core: '>= 1.0.0' + '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} @@ -2145,6 +2156,10 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + axe-core@4.11.4: + resolution: {integrity: sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA==} + engines: {node: '>=4'} + axios@0.18.1: resolution: {integrity: sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g==} deprecated: Critical security vulnerability fixed in v0.21.1. For more information, see https://github.com/axios/axios/pull/3410 @@ -6305,6 +6320,11 @@ snapshots: '@adobe/css-tools@4.4.4': {} + '@axe-core/playwright@4.11.3(playwright-core@1.59.1)': + dependencies: + axe-core: 4.11.4 + playwright-core: 1.59.1 + '@babel/code-frame@7.29.0': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -8064,6 +8084,8 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 + axe-core@4.11.4: {} + axios@0.18.1: dependencies: follow-redirects: 1.5.10