Skip to content
Open
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
7 changes: 7 additions & 0 deletions .changeset/add-evo-menu.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@ebay/skin": patch
"@evo-web/marko": patch
"@evo-web/react": patch
---

Add EvoMenu to React and align its accessible naming API with Marko using `a11yText` and `a11yLabelId`.
1 change: 1 addition & 0 deletions .claude/skills/evo-app-migrate-react/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ When migrating a listed component, read the linked file completely and apply the
- `ebay-filter-chip`: [evo-filter-chip.md](components/evo-filter-chip.md)
- `ebay-icon-button`: [evo-icon-button.md](components/evo-icon-button.md)
- `ebay-lightbox-dialog`: [evo-dialog.md](components/evo-dialog.md)
- `ebay-menu`: [evo-menu.md](components/evo-menu.md)
- `ebay-progress-spinner`: [evo-progress-spinner.md](components/evo-progress-spinner.md)
- `ebay-tabs`: [evo-tabs.md](components/evo-tabs.md)
- `ebay-textbox`: [evo-input.md](components/evo-input.md) / [evo-textarea.md](components/evo-textarea.md)
Expand Down
128 changes: 128 additions & 0 deletions .claude/skills/evo-app-migrate-react/components/evo-menu.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# ebay-menu → evo-menu

EvoMenu uses an explicit compound-component API. Wrap all navigable items in `EvoMenuItems`, and use the item and group component that matches each item's behavior.

## Command menu, badge, separator, and footer

**Before:**

```tsx
import {
EbayMenu,
EbayMenuItem,
EbayMenuSeparator,
} from "@ebay/ui-core-react/ebay-menu";

<EbayMenu onSelect={(_, { index }) => handleSelect(index)}>
<EbayMenuItem badgeNumber={5} badgeAriaLabel="5 unread messages">
Messages
</EbayMenuItem>
<EbayMenuSeparator />
<EbayMenuItem>Settings</EbayMenuItem>
</EbayMenu>;
```

**After:**

```tsx
import { EvoButton } from "@evo-web/react/button";
import {
EvoMenu,
EvoMenuFooter,
EvoMenuItem,
EvoMenuItemBadge,
EvoMenuItems,
EvoMenuSeparator,
} from "@evo-web/react/menu";

<EvoMenu>
<EvoMenuItems a11yText="Actions">
<EvoMenuItem onSelect={handleMessagesSelect}>
Messages
<EvoMenuItemBadge number={5} a11yText="5 unread messages" />
</EvoMenuItem>
<EvoMenuSeparator />
<EvoMenuItem onSelect={handleSettingsSelect}>Settings</EvoMenuItem>
</EvoMenuItems>
<EvoMenuFooter>
<EvoButton priority="tertiary">Apply</EvoButton>
</EvoMenuFooter>
</EvoMenu>;
```

`EvoMenuFooter` is optional. When footer content is needed, place it next to `EvoMenuItems` and use `EvoButton` directly instead of a menu-specific footer button.

## Radio menu

**Before:**

```tsx
<EbayMenu
type="radio"
checked={1}
onChange={(_, { index }) => handleSortChange(index)}
>
<EbayMenuItem value="price">Price</EbayMenuItem>
<EbayMenuItem value="distance">Distance</EbayMenuItem>
</EbayMenu>
```

**After:**

```tsx
<EvoMenu>
<EvoMenuItems a11yText="Sort by">
<EvoMenuRadioGroup
defaultSelected="distance"
onSelectedChange={handleSortChange}
>
<EvoMenuRadioItem value="price">Price</EvoMenuRadioItem>
<EvoMenuRadioItem value="distance">Distance</EvoMenuRadioItem>
</EvoMenuRadioGroup>
</EvoMenuItems>
</EvoMenu>
```

Use `selected` instead of `defaultSelected` when the radio group is controlled. Selection callbacks now receive the selected item value rather than its child index.

## Checkbox menu

**Before:**

```tsx
<EbayMenu
type="checkbox"
onChange={(_, { checkedValues }) => handleFilterChange(checkedValues)}
>
<EbayMenuItem value="shipping" checked>
Free shipping
</EbayMenuItem>
<EbayMenuItem value="returns">Free returns</EbayMenuItem>
</EbayMenu>
```

**After:**

```tsx
<EvoMenu>
<EvoMenuItems a11yText="Filters">
<EvoMenuCheckboxGroup
defaultSelected={["shipping"]}
onSelectedChange={handleFilterChange}
>
<EvoMenuCheckboxItem value="shipping">Free shipping</EvoMenuCheckboxItem>
<EvoMenuCheckboxItem value="returns">Free returns</EvoMenuCheckboxItem>
</EvoMenuCheckboxGroup>
</EvoMenuItems>
</EvoMenu>
```

Use `selected` instead of `defaultSelected` when the checkbox group is controlled. Radio and checkbox item `value` props are required; selection never falls back to child indexes.

## Additional changes

- Use `a11yText` on `EvoMenuItems` for an accessible name, or use `a11yLabelId` when an external element labels the menu. Do not use both.
- Move plain-menu `onSelect` to each `EvoMenuItem`; move parent `onClick` handlers to the applicable items.
- Move `onKeyDown` to `EvoMenuItems` when needed. It receives the native keyboard event without legacy index or checked callback data.
- Remove `baseEl`, `priority`, `autofocus`, `forwardedRef`, and `menuRef`. The root is a span, refs use native React 19 `ref`, and `EvoMenuItems` accepts its own ref.
- `reverse`, `fixed`, `fixWidth`, `classPrefix`, and native HTML attributes remain supported.
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Generated changelogs are managed by Changesets
packages/*/CHANGELOG.md
2 changes: 2 additions & 0 deletions packages/ebayui-core-react/.prettierignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Autogenerated files
CHANGELOG.md
src/ebay-icon/icons/*
src/ebay-icon/__tests__/constants.ts
src/ebay-icon/__tests__/inline.stories.tsx
src/ebay-icon/types.ts
src/ebay-flag/__tests__/constants.ts
src/ebay-flag/types.ts
Expand Down
11 changes: 10 additions & 1 deletion packages/ebayui-core-react/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,16 @@ import tseslint from "typescript-eslint";

export default tseslint.config(
{
ignores: ["dist", "config", "scripts", ".storybook", "vitest.config.ts", "node_modules", "_site"],
ignores: [
"dist",
"config",
"scripts",
".storybook",
"vitest.config.ts",
"node_modules",
"_site",
"src/ebay-icon/__tests__/inline.stories.tsx",
],
},
eslint.configs.recommended,
...tseslint.configs.recommended,
Expand Down
3 changes: 2 additions & 1 deletion packages/evo-marko/src/tags/evo-menu-button/index.marko
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import Button, { type Input as ButtonInput } from "<evo-button>";
import { type Input as MenuInput } from "<evo-menu>";
export interface Input<
Index extends number | string | (number | string)[],
> extends Omit<MenuInput<Index>, "variant"> {
> extends Omit<MenuInput<Index>, "a11yLabelId" | "a11yText" | "variant">,
Pick<Marko.HTML.Span, "aria-label" | "aria-labelledby"> {
open?: boolean;
openChange?: (open: boolean) => void;
collapseOnSelect?: boolean;
Expand Down
14 changes: 9 additions & 5 deletions packages/evo-marko/src/tags/evo-menu/index.marko
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ export interface Item extends Marko.HTML.Div {
}
export interface Input<
Index extends number | string | (number | string)[],
> extends Marko.HTML.Span {
> extends Omit<Marko.HTML.Span, "aria-label" | "aria-labelledby"> {
/** Localized accessible name for the menu. Cannot be used with `a11yLabelId`. */
a11yText?: string;
/** ID of the element that labels the menu. Cannot be used with `a11yText`. */
a11yLabelId?: string;
selected?: Index;
selectedChange?: (selected: Index) => void;
item?: Marko.AttrTag<Item>;
Expand All @@ -23,6 +27,8 @@ export interface Input<
<const/{
class: inputClass,
classPrefix,
a11yText,
a11yLabelId,
selected: inputIndex,
selectedChange,
reverse,
Expand All @@ -32,8 +38,6 @@ export interface Input<
typeaheadTimeoutLength,
item: items,
variant,
"aria-label": ariaLabel,
"aria-labelledby": ariaLabelledby,
...htmlInput
}=input>
<const/isRadio=typeof inputIndex === "number" || typeof inputIndex === "string">
Expand All @@ -58,8 +62,8 @@ export interface Input<
<div
role="menu"
class=`${baseClass}__items`
aria-label=ariaLabel
aria-labelledby=ariaLabelledby
aria-label=a11yText
aria-labelledby=a11yLabelId
id=menu>
<const/$items: Iterable<HTMLElement>=$item> // TODO: remove when Marko TS works
<evo-roving-tabindex/rovTabindex
Expand Down
12 changes: 12 additions & 0 deletions packages/evo-marko/src/tags/evo-menu/menu.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ export default {
},

argTypes: {
a11yText: {
type: "string",
control: "text",
description:
"Localized accessible name for the menu. Cannot be used with `a11yLabelId`.",
},
a11yLabelId: {
type: "string",
control: "text",
description:
"ID of the element that labels the menu. Cannot be used with `a11yText`.",
},
selected: {
controllable: true,
description:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,12 +242,12 @@ exports[`menu > renders basic version 1`] = `
</span>"
`;

exports[`menu > renders with aria-label 1`] = `
exports[`menu > renders with a11yLabelId 1`] = `
"<span
arialabel="test"
class="menu"
>
<div
aria-labelledby="test"
class="menu__items"
id="GENERATED-0"
role="menu"
Expand Down Expand Up @@ -321,12 +321,12 @@ exports[`menu > renders with aria-label 1`] = `
</span>"
`;

exports[`menu > renders with aria-labelledby 1`] = `
exports[`menu > renders with a11yText 1`] = `
"<span
arialabelledby="test"
class="menu"
>
<div
aria-label="test"
class="menu__items"
id="GENERATED-0"
role="menu"
Expand Down
8 changes: 4 additions & 4 deletions packages/evo-marko/src/tags/evo-menu/test/test.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ describe("menu", () => {
await snapshotHTML(Default);
});

it("renders with aria-label", async () => {
await snapshotHTML(Default, { ariaLabel: "test" });
it("renders with a11yText", async () => {
await snapshotHTML(Default, { a11yText: "test" });
});

it("renders with aria-labelledby", async () => {
await snapshotHTML(Default, { ariaLabelledBy: "test" });
it("renders with a11yLabelId", async () => {
await snapshotHTML(Default, { a11yLabelId: "test" });
});

it("renders with reverse=true", async () => {
Expand Down
5 changes: 5 additions & 0 deletions packages/evo-react/src/menu/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# EvoMenu

## Documentation

[Storybook](https://opensource.ebay.com/evo-web/react/?path=/docs/building-blocks-evo-menu--documentation)
Loading
Loading