Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1580,6 +1580,56 @@ jobs:
retention-days: 30
if-no-files-found: warn

# The menu-button contract's control role and name, the focus landing on
# Enter, Space, ArrowDown, and ArrowUp opens, and the pointer round trip
# are accessibility-tree and real-browser facts that jsdom cannot observe.
# Run the contract's own fixture proof and the DropdownMenu/MoreMenu
# binding whenever either component's source, the binding's stories, or
# the contract changes. Its own --output directory; the JSON report is
# the head-bound record, uploaded next.
- name: Run Menu Button accessibility contract
id: menu-button-contract
if: ${{ !cancelled() && github.event_name == 'pull_request' }}
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
PLAYWRIGHT_JSON_OUTPUT_FILE: menu-button-contract/report.json
run: |
should_run=false
# DropdownMenu, MoreMenu, and the contract are matched by file name.
# The helper comes from the base, since an older PR head lacks it.
# Exit 1 means out of scope; 0 in scope; 2 unknown, so run.
scope=2
helper="$(mktemp)"
if git fetch --quiet --no-tags --depth=1 origin "$BASE_SHA" \
&& git show "$BASE_SHA:.github/scripts/pr-scope-touches.sh" > "$helper"; then
scope=0
bash "$helper" "$BASE_SHA" \
'^(internal/a11y-spec/src/patterns/menu-button|packages/core/src/DropdownMenu/|packages/core/src/MoreMenu/|apps/storybook/stories/MenuButtonA11y)' || scope=$?
fi
if [ "$scope" != 1 ]; then
should_run=true
fi
echo "should_run=${should_run}" >> "$GITHUB_OUTPUT"
if [ "$should_run" != true ]; then
echo "DropdownMenu, MoreMenu, and the menu-button contract are not in PR scope; skipping menu-button evidence."
exit 0
fi
pnpm exec playwright test \
internal/a11y-spec/src/patterns/menu-button.chromium.spec.ts \
packages/core/src/DropdownMenu/__tests__/MenuButton.a11y.chromium.spec.ts \
--project chromium \
--reporter list,json \
--output menu-button-contract-output

- name: Upload Menu Button accessibility contract evidence
if: ${{ !cancelled() && steps.menu-button-contract.outputs.should_run == 'true' }}
uses: actions/upload-artifact@v7
with:
name: menu-button-contract-evidence-${{ needs.build-storybook.outputs.short_hash }}
path: menu-button-contract/
retention-days: 30
if-no-files-found: warn

# The accordion contract's header role and name, the page Tab step between
# headers, the absence of arrow-key focus movement, and the single-expansion
# sibling collapse are accessibility-tree and real-browser facts that jsdom
Expand Down
42 changes: 42 additions & 0 deletions apps/storybook/stories/MenuButtonA11y.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.

/**
* @file MenuButtonA11y.stories.tsx
* @input The DropdownMenu and MoreMenu menu-button binding states
* @output One checked-in story per state for the Chromium accessibility lane; excluded from the visual matrix
* @position Browser fixtures for component:DropdownMenu AR4 and component:MoreMenu AR1 (MENU_BUTTON_PATTERN)
*/

import type {Meta, StoryObj} from '@storybook/react';
import {renderMenuButtonState} from '../../../packages/core/src/DropdownMenu/__tests__/MenuButton.a11y.renders';
import {MENU_BUTTON_BINDING_STATES} from '../../../packages/core/src/DropdownMenu/__tests__/MenuButton.a11y.states';

const meta = {
title: 'A11y/MenuButton',
tags: ['no-visual'],
parameters: {layout: 'padded'},
} satisfies Meta;

export default meta;
type Story = StoryObj<typeof meta>;

function storyFor(
id: (typeof MENU_BUTTON_BINDING_STATES)[number]['id'],
): Story {
const state = MENU_BUTTON_BINDING_STATES.find(
candidate => candidate.id === id,
);
if (state == null) {
throw new Error(`no Menu Button binding state "${id}"`);
}
return {render: () => renderMenuButtonState(state)};
}

export const DropdownButtonClosed = storyFor('dropdown-button-closed');
export const DropdownButtonOpen = storyFor('dropdown-button-open');
export const DropdownRenderTriggerClosed = storyFor(
'dropdown-render-trigger-closed',
);
export const DropdownButtonDisabled = storyFor('dropdown-button-disabled');
export const MoreMenuClosed = storyFor('more-menu-closed');
export const MoreMenuDisabled = storyFor('more-menu-disabled');
Loading
Loading