-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
98dccbf
commit cc1ee91
Showing
5 changed files
with
194 additions
and
0 deletions.
There are no files selected for viewing
Binary file added
BIN
+11.2 KB
...wright/visual.test.ts-snapshots/Menu-RadioMenuItem-Primary-1-chromium-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
/* | ||
* Copyright 2025 New Vector Ltd | ||
* | ||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial | ||
* Please see LICENSE files in the repository root for full details. | ||
*/ | ||
|
||
import React, { useState } from "react"; | ||
import { RadioMenuItem as RadioMenuItemComponent } from "./RadioMenuItem.tsx"; | ||
import { Meta, StoryObj } from "@storybook/react"; | ||
|
||
type Props = Omit< | ||
React.ComponentProps<typeof RadioMenuItemComponent>, | ||
"Icon" | "label" | "checked" | "onSelect" | "name" | ||
>; | ||
|
||
const Template: React.FC<Props> = (props: Props) => { | ||
const [selected, setSelected] = useState<"first" | "second">("first"); | ||
return ( | ||
<div style={{ width: 300 }}> | ||
<RadioMenuItemComponent | ||
{...props} | ||
label="First item" | ||
checked={selected === "first"} | ||
onSelect={(e) => { | ||
e.preventDefault(); | ||
setSelected("first"); | ||
}} | ||
/> | ||
<RadioMenuItemComponent | ||
{...props} | ||
label="Second item" | ||
checked={selected === "second"} | ||
onSelect={(e) => { | ||
e.preventDefault(); | ||
setSelected("second"); | ||
}} | ||
/> | ||
<RadioMenuItemComponent | ||
{...props} | ||
label="Third item with a name that's quite long" | ||
checked | ||
disabled | ||
onSelect={() => {}} | ||
/> | ||
</div> | ||
); | ||
}; | ||
|
||
const meta = { | ||
title: "Menu/RadioMenuItem", | ||
component: Template, | ||
tags: ["autodocs"], | ||
argTypes: {}, | ||
args: {}, | ||
} satisfies Meta<typeof Template>; | ||
export default meta; | ||
|
||
type Story = StoryObj<typeof meta>; | ||
|
||
export const Primary: Story = { args: {} }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
/* | ||
* Copyright 2025 New Vector Ltd | ||
* | ||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial | ||
* Please see LICENSE files in the repository root for full details. | ||
*/ | ||
|
||
import { describe, expect, it, vi } from "vitest"; | ||
import { render, screen } from "@testing-library/react"; | ||
import { RadioMenuItem } from "./RadioMenuItem.tsx"; | ||
import userEvent from "@testing-library/user-event"; | ||
import React from "react"; | ||
|
||
describe("RadioMenuItem", () => { | ||
it("renders", () => { | ||
const { asFragment } = render( | ||
<RadioMenuItem label="Always show" checked={false} onSelect={() => {}} />, | ||
); | ||
expect(asFragment()).toMatchSnapshot(); | ||
}); | ||
|
||
it("toggles", async () => { | ||
const user = userEvent.setup(); | ||
const toggle = vi.fn(); | ||
render( | ||
<RadioMenuItem label="Always show" checked={false} onSelect={toggle} />, | ||
); | ||
|
||
// Try toggling using keyboard controls | ||
await user.tab(); | ||
await user.keyboard("[Space]"); | ||
expect(toggle).toBeCalledTimes(1); | ||
toggle.mockClear(); | ||
|
||
// Try toggling by clicking | ||
await user.click(screen.getByRole("menuitemradio")); | ||
expect(toggle).toBeCalledTimes(1); | ||
toggle.mockClear(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
/* | ||
* Copyright 2025 New Vector Ltd | ||
* | ||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial | ||
* Please see LICENSE files in the repository root for full details. | ||
*/ | ||
|
||
import React, { ComponentProps, forwardRef, useCallback, useId } from "react"; | ||
import { MenuItem } from "./MenuItem"; | ||
import { RadioInput } from "../Form"; | ||
|
||
type Props = Pick< | ||
ComponentProps<typeof MenuItem>, | ||
"className" | "label" | "onSelect" | "disabled" | ||
> & { | ||
/** | ||
* Whether the radio is checked. | ||
*/ | ||
checked: boolean; | ||
}; | ||
|
||
/** | ||
* A menu item with a radio control. | ||
* Must be used within a compound Menu or other `menu` or `menubar` aria role subtree. | ||
*/ | ||
export const RadioMenuItem = forwardRef<HTMLInputElement, Props>( | ||
function RadioMenuItem( | ||
{ className, label, onSelect, checked, disabled }, | ||
ref, | ||
) { | ||
const toggleId = useId(); | ||
// The radio is controlled and we intend to ignore its events. We do need | ||
// to at least set onChange though to make React happy. | ||
const onChange = useCallback(() => {}, []); | ||
|
||
// <label> elements are not allowed to have a role like menuitemradio, so | ||
// we must instead use a plain <div> for the menu item and use aria-checked | ||
// etc. to communicate its state. | ||
return ( | ||
<MenuItem | ||
as="div" | ||
role="menuitemradio" | ||
aria-checked={checked} | ||
className={className} | ||
label={label} | ||
onSelect={onSelect} | ||
disabled={disabled} | ||
Icon={ | ||
<RadioInput | ||
id={toggleId} | ||
ref={ref} | ||
// This is purely cosmetic; really the whole MenuItem is the toggle. | ||
aria-hidden | ||
checked={checked} | ||
disabled={disabled} | ||
onChange={onChange} | ||
/> | ||
} | ||
></MenuItem> | ||
); | ||
}, | ||
); |
31 changes: 31 additions & 0 deletions
31
src/components/Menu/__snapshots__/RadioMenuItem.test.tsx.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html | ||
|
||
exports[`RadioMenuItem > renders 1`] = ` | ||
<DocumentFragment> | ||
<div | ||
aria-checked="false" | ||
class="_item_831119 _interactive_831119" | ||
data-kind="primary" | ||
role="menuitemradio" | ||
> | ||
<div | ||
class="_container_5881bc _icon_831119" | ||
> | ||
<input | ||
aria-hidden="true" | ||
class="_input_5881bc" | ||
id=":r0:" | ||
type="radio" | ||
/> | ||
<div | ||
class="_ui_5881bc" | ||
/> | ||
</div> | ||
<span | ||
class="_typography_489030 _font-body-md-medium_489030 _label_831119" | ||
> | ||
Always show | ||
</span> | ||
</div> | ||
</DocumentFragment> | ||
`; |