diff --git a/.changeset/clean-label-slots.md b/.changeset/clean-label-slots.md new file mode 100644 index 00000000..47ac3748 --- /dev/null +++ b/.changeset/clean-label-slots.md @@ -0,0 +1,9 @@ +--- +"@makeplane/propel": minor +--- + +Tighten component text props, add a decorative `Shortcut` component slot with `aria-keyshortcuts` +examples on actionable controls, remove shortcut-specific menu element wrappers, and add an accent +menu row tone for emphasized menu actions. Remove unused/story-only element exports, including +group-label wrappers, popover anatomy wrappers, pass-through select scroll arrows, scroll-area +content, tooltip shortcut, and the unused `initialsToneClass` avatar style-map export. diff --git a/packages/propel/src/components/accordion/accordion-trigger.tsx b/packages/propel/src/components/accordion/accordion-trigger.tsx index b4964ddb..e5b90229 100644 --- a/packages/propel/src/components/accordion/accordion-trigger.tsx +++ b/packages/propel/src/components/accordion/accordion-trigger.tsx @@ -7,14 +7,18 @@ import { AccordionTriggerTitle, } from "../../elements/accordion"; import { DisclosureIndicator } from "../../internal/disclosure-indicator"; -import { Icon } from "../../internal/icon"; -export type AccordionTriggerProps = Omit & { +export type AccordionTriggerProps = Omit< + BaseAccordion.Trigger.Props, + "children" | "className" | "style" +> & { /** - * Node rendered before the label (inline-start), matching the Figma header icon. Sized to the - * trigger's `--node-size`. Decorative, kept out of the name. + * Element rendered before the label (inline-start), e.g. ``. */ icon?: React.ReactNode; + /** Visible trigger label. */ + label: string; }; /** @@ -22,11 +26,11 @@ export type AccordionTriggerProps = Omit}> - {icon ? {icon} : null} - {children} + {icon} + {label} diff --git a/packages/propel/src/components/accordion/accordion.stories.tsx b/packages/propel/src/components/accordion/accordion.stories.tsx index cf002fb8..fa686893 100644 --- a/packages/propel/src/components/accordion/accordion.stories.tsx +++ b/packages/propel/src/components/accordion/accordion.stories.tsx @@ -2,6 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import { CircleHelp } from "lucide-react"; import { expect } from "storybook/test"; +import { Icon } from "../icon"; import { Accordion, AccordionHeader, @@ -75,7 +76,7 @@ export const Default: Story = { {ITEMS.map((item) => ( - {item.label} + {item.body} @@ -91,7 +92,10 @@ export const WithIcon: Story = { {ITEMS.map((item) => ( - }>{item.label} + } + label={item.label} + /> {item.body} @@ -131,7 +135,7 @@ export const States: Story = { ).map(([label, id]) => ( - {label} — What is Plane? + Plane is an open-source project management tool. @@ -149,7 +153,7 @@ export const MultipleItems: Story = { {ITEMS.map((item) => ( - {item.label} + {item.body} @@ -168,7 +172,7 @@ export const Interaction: Story = { - Section A + Panel A content @@ -239,7 +243,7 @@ export const KeyboardToggle: Story = { - Section A + Panel A content diff --git a/packages/propel/src/components/alert-dialog/alert-dialog-close.tsx b/packages/propel/src/components/alert-dialog/alert-dialog-close.tsx index 712b9e0e..ced28284 100644 --- a/packages/propel/src/components/alert-dialog/alert-dialog-close.tsx +++ b/packages/propel/src/components/alert-dialog/alert-dialog-close.tsx @@ -7,9 +7,13 @@ export type AlertDialogCloseProps = Omit}> - * Cancel - * ; + * ; + * + label="Delete project" + /> @@ -89,18 +88,16 @@ export const Default: Story = { tone="neutral" magnitude="xl" render={} - > - Cancel - + label="Cancel" + /> + label="Delete" + /> @@ -152,13 +149,12 @@ export const OpenFromMenu: Story = { tone="neutral" magnitude="xl" render={} - > - Project options - + label="Project options" + /> - Rename - Duplicate - setConfirmOpen(true)}>Delete project… + + + setConfirmOpen(true)} label="Delete project…" /> @@ -182,18 +178,16 @@ export const OpenFromMenu: Story = { tone="neutral" magnitude="xl" render={} - > - Cancel - + label="Cancel" + /> + label="Delete" + /> @@ -259,9 +253,8 @@ export const CloseConfirmation: Story = { tone="neutral" magnitude="xl" render={} - > - Write a comment - + label="Write a comment" + /> @@ -289,9 +282,8 @@ export const CloseConfirmation: Story = { tone="neutral" magnitude="xl" render={} - > - Close - + label="Close" + /> @@ -313,9 +305,8 @@ export const CloseConfirmation: Story = { tone="neutral" magnitude="xl" render={} - > - Keep writing - + label="Keep writing" + /> + label="Discard" + /> @@ -411,9 +401,8 @@ export const MultipleTriggers: Story = { tone="neutral" magnitude="xl" render={} - > - Delete {project} - + label={`Delete ${project}`} + /> ))} @@ -440,18 +429,16 @@ export const MultipleTriggers: Story = { tone="neutral" magnitude="xl" render={} - > - Cancel - + label="Cancel" + /> + label="Delete" + /> )} diff --git a/packages/propel/src/components/anchor-button/anchor-button.stories.tsx b/packages/propel/src/components/anchor-button/anchor-button.stories.tsx index 0008282d..7dfa459d 100644 --- a/packages/propel/src/components/anchor-button/anchor-button.stories.tsx +++ b/packages/propel/src/components/anchor-button/anchor-button.stories.tsx @@ -2,6 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import { ArrowUpRight, Plus } from "lucide-react"; import { expect, fn } from "storybook/test"; +import { Icon } from "../icon"; import { AnchorButton, type AnchorButtonMagnitude, type AnchorButtonProminence } from "./index"; const PROMINENCES: AnchorButtonProminence[] = ["primary", "secondary"]; @@ -12,7 +13,7 @@ const MAGNITUDES: AnchorButtonMagnitude[] = ["sm", "md", "lg", "xl"]; const meta = { title: "Components/AnchorButton", component: AnchorButton, - args: { children: "Show more", prominence: "primary", magnitude: "md" }, + args: { label: "Show more", prominence: "primary", magnitude: "md" }, } satisfies Meta; export default meta; @@ -22,13 +23,16 @@ export const Default: Story = {}; /** `prominence`: `primary` is the blue link; `secondary` the muted gray inline link. */ export const Prominences: Story = { - argTypes: { prominence: { control: false }, children: { control: false } }, + argTypes: { prominence: { control: false }, label: { control: false } }, render: (args) => (
{PROMINENCES.map((prominence) => ( - - {prominence} action - + ))}
), @@ -36,13 +40,11 @@ export const Prominences: Story = { /** Text sizes (Figma S/Base/L/XL map to sm/md/lg/xl). */ export const Magnitudes: Story = { - argTypes: { magnitude: { control: false }, children: { control: false } }, + argTypes: { magnitude: { control: false }, label: { control: false } }, render: (args) => (
{MAGNITUDES.map((magnitude) => ( - - {magnitude} - + ))}
), @@ -67,9 +69,12 @@ export const ActsNotNavigates: Story = { export const AsLink: Story = { args: { prominence: "primary", magnitude: "md" }, render: (args) => ( - }> - View all work items - + } + label="View all work items" + /> ), }; @@ -94,12 +99,18 @@ export const WithIcons: Story = { parameters: { controls: { disable: true } }, render: () => (
- }> - Add condition - - }> - View docs - + } + label="Add condition" + /> + } + label="View docs" + />
), }; @@ -107,5 +118,5 @@ export const WithIcons: Story = { /** The `loading` spinner replaces the leading icon and dims the label. */ export const Loading: Story = { args: { prominence: "primary", magnitude: "md", loading: true }, - render: (args) => Saving…, + render: (args) => , }; diff --git a/packages/propel/src/components/anchor-button/anchor-button.tsx b/packages/propel/src/components/anchor-button/anchor-button.tsx index 28f5bfb2..931ad4b3 100644 --- a/packages/propel/src/components/anchor-button/anchor-button.tsx +++ b/packages/propel/src/components/anchor-button/anchor-button.tsx @@ -7,28 +7,23 @@ import { AnchorButtonLabel, type AnchorButtonProps as AnchorButtonElementProps, } from "../../elements/anchor-button"; -import { Icon } from "../../internal/icon"; import { Spinner } from "../../internal/spinner"; export type { AnchorButtonMagnitude, AnchorButtonProminence } from "../../elements/anchor-button"; -export type AnchorButtonProps = AnchorButtonElementProps & { +export type AnchorButtonProps = Omit & { /** * Set `false` when `render` swaps the underlying tag away from ` + label="Open command palette" + /> } + icon={} placeholder="Search commands" aria-label="Search commands" /> diff --git a/packages/propel/src/components/badge/badge.stories.tsx b/packages/propel/src/components/badge/badge.stories.tsx index 026015fa..aaf470aa 100644 --- a/packages/propel/src/components/badge/badge.stories.tsx +++ b/packages/propel/src/components/badge/badge.stories.tsx @@ -2,6 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import { Check, Sparkles } from "lucide-react"; import { iconControl } from "../../storybook/icon-control"; +import { Icon } from "../icon"; import { Badge, BadgeLabel, type BadgeMagnitude, type BadgeTone } from "./index"; const TONES: BadgeTone[] = [ @@ -31,7 +32,7 @@ const meta = { subcomponents: { BadgeLabel }, argTypes: { startIcon: iconControl, endIcon: iconControl }, args: { - children: "Badge", + label: "Badge", tone: "neutral", magnitude: "md", }, @@ -50,13 +51,11 @@ export const Default: Story = {}; /** Every color/intent the badge supports, side by side. */ export const Tones: Story = { - argTypes: { tone: { control: false }, children: { control: false } }, + argTypes: { tone: { control: false }, label: { control: false } }, render: (args) => (
{TONES.map((tone) => ( - - {tone} - + ))}
), @@ -64,13 +63,11 @@ export const Tones: Story = { /** The three sizes (Figma S / Base / Large). */ export const Magnitudes: Story = { - argTypes: { magnitude: { control: false }, children: { control: false } }, + argTypes: { magnitude: { control: false }, label: { control: false } }, render: (args) => (
{MAGNITUDES.map((magnitude) => ( - - {magnitude} - + ))}
), @@ -82,9 +79,14 @@ export const WithLeadingIcon: Story = { render: (args) => (
{MAGNITUDES.map((magnitude) => ( - }> - Done - + } + label="Done" + /> ))}
), @@ -96,9 +98,14 @@ export const WithTrailingIcon: Story = { render: (args) => (
{MAGNITUDES.map((magnitude) => ( - }> - Pro - + } + label="Pro" + /> ))}
), @@ -129,9 +136,13 @@ export const PlanBadges: Story = { {(["paid", "free"] as const).map((plan) => (
{MAGNITUDES.map((magnitude) => ( - - {plan === "paid" ? "Paid" : "Free"} - + ))}
))} diff --git a/packages/propel/src/components/badge/badge.tsx b/packages/propel/src/components/badge/badge.tsx index 319b052e..6a7a021a 100644 --- a/packages/propel/src/components/badge/badge.tsx +++ b/packages/propel/src/components/badge/badge.tsx @@ -5,20 +5,13 @@ import { type BadgeProps as BadgeElementProps, BadgeLabel, } from "../../elements/badge"; -import { Icon } from "../../internal/icon"; -export type BadgeProps = BadgeElementProps & { - /** The badge label — text, a count, or any inline content. */ - children?: React.ReactNode; - /** - * Node rendered before the label (inline-start). A bare icon or any node; sized to the badge's - * `--node-size` and tinted to the tone. Decorative, kept out of the name. - */ +export type BadgeProps = Omit & { + /** The badge label text. */ + label?: string; + /** Element rendered before the label (inline-start), e.g. ``. */ startIcon?: React.ReactNode; - /** - * Node rendered after the label (inline-end). A bare icon or any node; sized to the badge's - * `--node-size` and tinted to the tone. Decorative, kept out of the name. - */ + /** Element rendered after the label (inline-end), e.g. ``. */ endIcon?: React.ReactNode; }; @@ -26,12 +19,12 @@ export type BadgeProps = BadgeElementProps & { * The ready-made badge: composes the atomic `Badge` pill with the `BadgeLabel` and optional leading * (`startIcon`) and trailing (`endIcon`) icon slots. */ -export function Badge({ children, startIcon, endIcon, ...props }: BadgeProps) { +export function Badge({ label, startIcon, endIcon, ...props }: BadgeProps) { return ( - {startIcon ? {startIcon} : null} - {children} - {endIcon ? {endIcon} : null} + {startIcon} + {label} + {endIcon} ); } diff --git a/packages/propel/src/components/banner/banner.stories.tsx b/packages/propel/src/components/banner/banner.stories.tsx index d95324ba..9345b01c 100644 --- a/packages/propel/src/components/banner/banner.stories.tsx +++ b/packages/propel/src/components/banner/banner.stories.tsx @@ -4,6 +4,7 @@ import { expect, fn } from "storybook/test"; import { iconControl } from "../../storybook/icon-control"; import { Button } from "../button/index"; +import { Icon } from "../icon"; import { IconButton } from "../icon-button/index"; import { Banner, type BannerTone } from "./index"; @@ -67,24 +68,35 @@ export const WithActions: Story = { tone: "neutral", actions: ( <> - - - + + - - + + - - + - - + + /> ); }, }; @@ -155,12 +144,8 @@ export const Stretch: Story = { parameters: { controls: { disable: true } }, render: (args) => (
- - +
), }; @@ -318,9 +303,7 @@ export const CustomTag: Story = { sizing: "hug", }, render: (args) => ( - + + /> ); }, play: async ({ canvas, userEvent }) => { @@ -372,9 +354,7 @@ export const AsLink: Story = { sizing: "hug", }, render: (args) => ( - +
+ + label="Cancel" + /> + label="Create" + />
diff --git a/packages/propel/src/components/combobox/index.tsx b/packages/propel/src/components/combobox/index.tsx index 59713fe4..07338e29 100644 --- a/packages/propel/src/components/combobox/index.tsx +++ b/packages/propel/src/components/combobox/index.tsx @@ -12,10 +12,4 @@ export * from "./combobox-item-indicator"; export * from "./combobox-list"; export * from "./combobox-row"; export * from "./combobox-status"; -// The ready-mades above bundle every Base UI behavior part (`Clear`/`Trigger` as node props on -// `ComboboxInputGroup`; `Portal`/`Positioner`/`Popup` inside `ComboboxContent`; -// `Value`/`Chip`/`ChipRemove` inside `ComboboxChips`; `List` as a passthrough), so a full combobox -// composes from this entry without importing `@base-ui/react`. `ComboboxLabel` stays a styled -// `elements` part — graft it onto `BaseCombobox.Label` at the call site for the advanced anatomy. -export { ComboboxLabel, type ComboboxLabelProps } from "../../elements/combobox"; export * from "./use-filter"; diff --git a/packages/propel/src/components/context-menu/context-menu-checkbox-item.tsx b/packages/propel/src/components/context-menu/context-menu-checkbox-item.tsx index 2b00364a..02029551 100644 --- a/packages/propel/src/components/context-menu/context-menu-checkbox-item.tsx +++ b/packages/propel/src/components/context-menu/context-menu-checkbox-item.tsx @@ -6,21 +6,21 @@ import { ContextMenuCheckboxItem as ContextMenuCheckboxItemElement, ContextMenuCheckboxItemIndicator, ContextMenuItemLabel, - ContextMenuItemShortcut, type ContextMenuItemTone, } from "../../elements/context-menu"; -import { Icon } from "../../internal/icon"; export type ContextMenuCheckboxItemProps = Omit< BaseContextMenu.CheckboxItem.Props, - "className" | "style" + "children" | "className" | "label" | "style" > & { /** Neutral rows use the standard text hierarchy; `danger` rows use the error palette. */ tone: ContextMenuItemTone; - /** Leading icon after the tick. */ + /** Leading element after the tick, e.g. ``. */ icon?: React.ReactNode; - /** Trailing keyboard-shortcut hint after the label. */ - trailing?: React.ReactNode; + /** Primary row label. */ + label: string; + /** Element rendered at the inline end of the row, e.g. ``. */ + endContent?: React.ReactElement; }; /** @@ -31,8 +31,8 @@ export type ContextMenuCheckboxItemProps = Omit< export function ContextMenuCheckboxItem({ tone, icon, - trailing, - children, + endContent, + label, ...props }: ContextMenuCheckboxItemProps) { return ( @@ -46,9 +46,9 @@ export function ContextMenuCheckboxItem({ > - {icon != null ? {icon} : null} - {children} - {trailing != null ? {trailing} : null} + {icon} + {label} + {endContent} ); } diff --git a/packages/propel/src/components/context-menu/context-menu-group.tsx b/packages/propel/src/components/context-menu/context-menu-group.tsx index 1639d13c..74590964 100644 --- a/packages/propel/src/components/context-menu/context-menu-group.tsx +++ b/packages/propel/src/components/context-menu/context-menu-group.tsx @@ -2,7 +2,7 @@ import { ContextMenu as BaseContextMenu } from "@base-ui/react/context-menu"; export type ContextMenuGroupProps = Omit; -/** Groups related rows so a `ContextMenuGroupLabel` names them for assistive tech. */ +/** Groups related rows; provide `aria-label`/`aria-labelledby` when the group needs a name. */ export function ContextMenuGroup(props: ContextMenuGroupProps) { return ; } diff --git a/packages/propel/src/components/context-menu/context-menu-item.tsx b/packages/propel/src/components/context-menu/context-menu-item.tsx index 9cc3fae9..cfaba5c7 100644 --- a/packages/propel/src/components/context-menu/context-menu-item.tsx +++ b/packages/propel/src/components/context-menu/context-menu-item.tsx @@ -7,42 +7,42 @@ import { ContextMenuItemLabel, ContextMenuItem as ContextMenuItemElement, type ContextMenuItemTone, - ContextMenuItemShortcut, } from "../../elements/context-menu"; -import { Icon } from "../../internal/icon"; export type ContextMenuItemProps = Omit< BaseContextMenu.Item.Props, - "className" | "style" | "render" + "children" | "className" | "label" | "style" | "render" > & { /** Neutral rows use the standard text hierarchy; `danger` rows use the error palette. */ tone: ContextMenuItemTone; - /** Leading icon before the label. */ + /** Leading element before the label, e.g. ``. */ icon?: React.ReactNode; - /** Trailing keyboard-shortcut hint after the label. */ - trailing?: React.ReactNode; + /** Primary row label. */ + label: string; + /** Element rendered at the inline end of the row, e.g. ``. */ + endContent?: React.ReactElement; /** Single-select selected state. */ selected?: boolean; }; /** * The ready-made menu row: grafts Base UI's `Item` behavior onto the styled `ContextMenuItem` and - * composes its region parts — a leading icon, the label, an optional trailing shortcut hint, and a - * trailing check for single-select selected state. Pass `tone="danger"` for destructive actions. + * composes its region parts — a leading icon, the label, optional end content, and a trailing check + * for single-select selected state. Pass `tone="danger"` for destructive actions. */ export function ContextMenuItem({ tone, icon, - trailing, + endContent, selected, - children, + label, ...props }: ContextMenuItemProps) { return ( }> - {icon != null ? {icon} : null} - {children} - {trailing != null ? {trailing} : null} + {icon} + {label} + {endContent} {selected ? ( diff --git a/packages/propel/src/components/context-menu/context-menu-link-item.tsx b/packages/propel/src/components/context-menu/context-menu-link-item.tsx index 5cc6c56a..3576ec87 100644 --- a/packages/propel/src/components/context-menu/context-menu-link-item.tsx +++ b/packages/propel/src/components/context-menu/context-menu-link-item.tsx @@ -5,39 +5,39 @@ import { ContextMenuItemLabel, ContextMenuLinkItem as ContextMenuLinkItemElement, type ContextMenuItemTone, - ContextMenuItemShortcut, } from "../../elements/context-menu"; -import { Icon } from "../../internal/icon"; export type ContextMenuLinkItemProps = Omit< BaseContextMenu.LinkItem.Props, - "className" | "style" | "render" + "children" | "className" | "label" | "style" | "render" > & { /** Neutral rows use the standard text hierarchy; `danger` rows use the error palette. */ tone: ContextMenuItemTone; - /** Leading icon before the label. */ + /** Leading element before the label, e.g. ``. */ icon?: React.ReactNode; - /** Trailing hint after the label. */ - trailing?: React.ReactNode; + /** Primary row label. */ + label: string; + /** Element rendered at the inline end of the row, e.g. ``. */ + endContent?: React.ReactElement; }; /** * The ready-made navigational menu row: grafts Base UI's `LinkItem` behavior onto the styled - * `ContextMenuLinkItem` and composes its region parts — a leading icon, the label, and an optional - * trailing hint. + * `ContextMenuLinkItem` and composes its region parts — a leading icon, the label, and optional end + * content. */ export function ContextMenuLinkItem({ tone, icon, - trailing, - children, + endContent, + label, ...props }: ContextMenuLinkItemProps) { return ( }> - {icon != null ? {icon} : null} - {children} - {trailing != null ? {trailing} : null} + {icon} + {label} + {endContent} ); } diff --git a/packages/propel/src/components/context-menu/context-menu-radio-item.tsx b/packages/propel/src/components/context-menu/context-menu-radio-item.tsx index 91eda3bb..649b91aa 100644 --- a/packages/propel/src/components/context-menu/context-menu-radio-item.tsx +++ b/packages/propel/src/components/context-menu/context-menu-radio-item.tsx @@ -4,23 +4,23 @@ import type * as React from "react"; import { ContextMenuItemLabel, - ContextMenuItemShortcut, type ContextMenuItemTone, ContextMenuRadioItem as ContextMenuRadioItemElement, ContextMenuRadioItemIndicator, } from "../../elements/context-menu"; -import { Icon } from "../../internal/icon"; export type ContextMenuRadioItemProps = Omit< BaseContextMenu.RadioItem.Props, - "className" | "style" + "children" | "className" | "label" | "style" > & { /** Neutral rows use the standard text hierarchy; `danger` rows use the error palette. */ tone: ContextMenuItemTone; - /** Leading icon after the radio dot. */ + /** Leading element after the radio dot, e.g. ``. */ icon?: React.ReactNode; - /** Trailing keyboard-shortcut hint after the label. */ - trailing?: React.ReactNode; + /** Primary row label. */ + label: string; + /** Element rendered at the inline end of the row, e.g. ``. */ + endContent?: React.ReactElement; }; /** @@ -31,8 +31,8 @@ export type ContextMenuRadioItemProps = Omit< export function ContextMenuRadioItem({ tone, icon, - trailing, - children, + endContent, + label, ...props }: ContextMenuRadioItemProps) { return ( @@ -40,9 +40,9 @@ export function ContextMenuRadioItem({ }> - {icon != null ? {icon} : null} - {children} - {trailing != null ? {trailing} : null} + {icon} + {label} + {endContent} ); } diff --git a/packages/propel/src/components/context-menu/context-menu-submenu-trigger.tsx b/packages/propel/src/components/context-menu/context-menu-submenu-trigger.tsx index 4b6a1bac..b845e431 100644 --- a/packages/propel/src/components/context-menu/context-menu-submenu-trigger.tsx +++ b/packages/propel/src/components/context-menu/context-menu-submenu-trigger.tsx @@ -4,35 +4,35 @@ import type * as React from "react"; import { ContextMenuItemLabel, - ContextMenuItemShortcut, ContextMenuSubmenuTrigger as ContextMenuSubmenuTriggerElement, type ContextMenuSubmenuTriggerTone, } from "../../elements/context-menu"; import { DisclosureIndicator } from "../../internal/disclosure-indicator"; -import { Icon } from "../../internal/icon"; export type ContextMenuSubmenuTriggerProps = Omit< BaseContextMenu.SubmenuTrigger.Props, - "className" | "style" | "render" + "children" | "className" | "label" | "style" | "render" > & { /** Neutral rows use the standard text hierarchy; `danger` rows use the error palette. */ tone: ContextMenuSubmenuTriggerTone; - /** Leading icon before the label. */ + /** Leading element before the label, e.g. ``. */ icon?: React.ReactNode; - /** Trailing hint before the submenu caret. */ - trailing?: React.ReactNode; + /** Primary row label. */ + label: string; + /** Element rendered before the submenu caret at the inline end of the row. */ + endContent?: React.ReactElement; }; /** * The ready-made submenu trigger: grafts Base UI's `SubmenuTrigger` behavior onto the styled * `ContextMenuSubmenuTrigger` and composes its region parts — a leading icon, the label, an - * optional trailing hint, and the caret that points toward the submenu. + * optional end content, and the caret that points toward the submenu. */ export function ContextMenuSubmenuTrigger({ tone, icon, - trailing, - children, + endContent, + label, ...props }: ContextMenuSubmenuTriggerProps) { return ( @@ -40,9 +40,9 @@ export function ContextMenuSubmenuTrigger({ {...props} render={} > - {icon != null ? {icon} : null} - {children} - {trailing != null ? {trailing} : null} + {icon} + {label} + {endContent} diff --git a/packages/propel/src/components/context-menu/context-menu.stories.tsx b/packages/propel/src/components/context-menu/context-menu.stories.tsx index 31d4ebe8..8b9494e2 100644 --- a/packages/propel/src/components/context-menu/context-menu.stories.tsx +++ b/packages/propel/src/components/context-menu/context-menu.stories.tsx @@ -12,6 +12,8 @@ import { import * as React from "react"; import { expect, fireEvent, userEvent, waitFor, within } from "storybook/test"; +import { Icon } from "../icon"; +import { Shortcut } from "../shortcut"; import { ContextMenu, ContextMenuContent, @@ -27,8 +29,8 @@ import { } from "./index"; // Components-tier story: the ready-made `ContextMenu` root and `ContextMenuContent` surface graft -// Base UI's portal/positioner/popup, and the rich `ContextMenuItem` lays out an icon + label + a -// trailing shortcut. `ContextMenuTrigger` is the behavior surface that opens the menu, and +// Base UI's portal/positioner/popup, and the rich `ContextMenuItem` lays out an icon + label + +// `endContent`. `ContextMenuTrigger` is the behavior surface that opens the menu, and // `ContextMenuSeparator` is the ready-made divider. The elements-tier story composes the raw // `ContextMenuItem` rows by hand. const meta = { @@ -59,19 +61,29 @@ export const Default: Story = { Right-click here - } trailing="⌘X"> - Cut - - } trailing="⌘C"> - Copy - - } trailing="⌘V"> - Paste - + } + aria-keyshortcuts="Meta+X" + endContent={} + label="Cut" + /> + } + aria-keyshortcuts="Meta+C" + endContent={} + label="Copy" + /> + } + aria-keyshortcuts="Meta+V" + endContent={} + label="Paste" + /> - }> - Delete - + } label="Delete" /> ), @@ -85,6 +97,10 @@ export const DefaultInteraction: Story = { await waitFor(() => expect(document.body.querySelector('[role="menu"]')).toBeInTheDocument()); await expect(document.body).toHaveTextContent("Copy"); await expect(document.body).toHaveTextContent("Delete"); + const copy = within(document.body).getByRole("menuitem", { name: "Copy" }); + await expect(copy).toHaveAttribute("aria-keyshortcuts", "Meta+C"); + await expect(copy).toHaveAccessibleName("Copy"); + await expect(within(copy).getByText("⌘C")).toHaveAttribute("aria-hidden", "true"); }, }; @@ -101,16 +117,10 @@ export const Tones: Story = { Right-click here - }> - Rename - - }> - Duplicate - + } label="Rename" /> + } label="Duplicate" /> - }> - Delete - + } label="Delete" /> ), @@ -156,26 +166,22 @@ export const Submenu: Story = { Right-click here - }> - Rename - - }> - Duplicate - + } label="Rename" /> + } label="Duplicate" /> - }> - Move to - + } + label="Move to" + /> - Mobile app - Web app - Design system + + + - }> - Delete - + } label="Delete" /> ), @@ -219,20 +225,26 @@ export const LinkItems: Story = { } + icon={} href="#work-item" target="_blank" rel="noopener noreferrer" - > - Open in new tab - - } href="#activity"> - Go to activity - + label="Open in new tab" + /> + } + href="#activity" + label="Go to activity" + /> - } trailing="⌘C"> - Copy link - + } + aria-keyshortcuts="Meta+C" + endContent={} + label="Copy link" + /> ), @@ -288,9 +300,8 @@ export const SingleSelect: Story = { selected={sortBy === option.key} closeOnClick={false} onClick={() => setSortBy(option.key)} - > - {option.label} - + label={option.label} + /> ))} @@ -343,17 +354,22 @@ export const SelectionRows: Story = { checked={wrap} onCheckedChange={setWrap} closeOnClick={false} - > - Wrap titles - + label="Wrap titles" + /> - - Manual order - - - Last updated - + + diff --git a/packages/propel/src/components/context-menu/index.tsx b/packages/propel/src/components/context-menu/index.tsx index c34c5488..3c4d70a1 100644 --- a/packages/propel/src/components/context-menu/index.tsx +++ b/packages/propel/src/components/context-menu/index.tsx @@ -19,14 +19,10 @@ export * from "./context-menu-radio-item"; export { ContextMenuCheckboxItemIndicator, type ContextMenuCheckboxItemIndicatorProps, - ContextMenuGroupLabel, - type ContextMenuGroupLabelProps, ContextMenuItemIndicator, type ContextMenuItemIndicatorProps, ContextMenuItemLabel, type ContextMenuItemLabelProps, - ContextMenuItemShortcut, - type ContextMenuItemShortcutProps, type ContextMenuItemTone, ContextMenuRadioItemIndicator, type ContextMenuRadioItemIndicatorProps, diff --git a/packages/propel/src/components/dialog/dialog-close.tsx b/packages/propel/src/components/dialog/dialog-close.tsx index 8b916ea7..25da6a9b 100644 --- a/packages/propel/src/components/dialog/dialog-close.tsx +++ b/packages/propel/src/components/dialog/dialog-close.tsx @@ -7,11 +7,14 @@ export type DialogCloseProps = Omit}> - * - * - * - * ; + * } + * render={} + * />; * ``` * * Maps 1:1 to `Dialog.Close`. diff --git a/packages/propel/src/components/dialog/dialog-trigger.tsx b/packages/propel/src/components/dialog/dialog-trigger.tsx index 356d0b62..eb4d1397 100644 --- a/packages/propel/src/components/dialog/dialog-trigger.tsx +++ b/packages/propel/src/components/dialog/dialog-trigger.tsx @@ -7,9 +7,13 @@ export type DialogTriggerProps = Omit}> - * Open dialog - * ; + * + label="Delete project" + /> @@ -81,9 +81,8 @@ export const Default: Story = { magnitude="lg" aria-label="Close" render={} - > - - + icon={} + /> @@ -98,18 +97,16 @@ export const Default: Story = { tone="neutral" magnitude="xl" render={} - > - Cancel - + label="Cancel" + /> + label="Delete" + /> @@ -154,9 +151,8 @@ export const EscapeCloses: Story = { tone="neutral" magnitude="xl" render={} - > - Open settings - + label="Open settings" + /> @@ -194,9 +190,8 @@ export const NonDismissable: Story = { tone="neutral" magnitude="xl" render={} - > - Open locked dialog - + label="Open locked dialog" + /> @@ -215,9 +210,8 @@ export const NonDismissable: Story = { tone="neutral" magnitude="xl" render={} - > - Discard - + label="Discard" + /> @@ -274,9 +268,8 @@ export const ScrollableBody: Story = { tone="neutral" magnitude="xl" render={} - > - What's new - + label="What's new" + /> @@ -288,9 +281,8 @@ export const ScrollableBody: Story = { magnitude="lg" aria-label="Close" render={} - > - - + icon={} + /> {/* A scrollable region must be keyboard-reachable (axe scrollable-region-focusable). */} @@ -313,9 +305,8 @@ export const ScrollableBody: Story = { tone="neutral" magnitude="xl" render={} - > - Got it - + label="Got it" + /> @@ -368,16 +359,18 @@ export const OpenFromMenu: Story = { tone="neutral" magnitude="xl" render={} - > - Project options - + label="Project options" + /> - }>Edit - }>Copy link + } label="Edit" /> + } label="Copy link" /> - } onClick={() => setOpen(true)}> - Delete project… - + } + onClick={() => setOpen(true)} + label="Delete project…" + /> @@ -392,9 +385,8 @@ export const OpenFromMenu: Story = { magnitude="lg" aria-label="Close" render={} - > - - + icon={} + /> @@ -409,18 +401,16 @@ export const OpenFromMenu: Story = { tone="neutral" magnitude="xl" render={} - > - Cancel - + label="Cancel" + /> + label="Delete" + /> @@ -472,9 +462,8 @@ export const NestedDialogs: Story = { tone="neutral" magnitude="xl" render={} - > - Invite teammates - + label="Invite teammates" + /> @@ -486,9 +475,8 @@ export const NestedDialogs: Story = { magnitude="lg" aria-label="Close" render={} - > - - + icon={} + /> @@ -504,9 +492,8 @@ export const NestedDialogs: Story = { tone="neutral" magnitude="xl" render={} - > - Create custom role - + label="Create custom role" + /> @@ -525,9 +512,8 @@ export const NestedDialogs: Story = { tone="neutral" magnitude="xl" render={} - > - Back - + label="Back" + /> @@ -537,9 +523,8 @@ export const NestedDialogs: Story = { tone="neutral" magnitude="xl" render={} - > - Send invites - + label="Send invites" + /> @@ -610,9 +595,8 @@ export const CloseConfirmation: Story = { tone="neutral" magnitude="xl" render={} - > - Add comment - + label="Add comment" + /> @@ -624,9 +608,8 @@ export const CloseConfirmation: Story = { magnitude="lg" aria-label="Close" render={} - > - - + icon={} + /> - Post comment - + label="Post comment" + /> @@ -674,9 +656,8 @@ export const CloseConfirmation: Story = { tone="neutral" magnitude="xl" render={} - > - Go back - + label="Go back" + /> + label="Discard" + /> @@ -766,18 +746,16 @@ export const DetachedTriggers: Story = { tone="neutral" magnitude="xl" render={} - > - Keyboard shortcuts - + label="Keyboard shortcuts" + /> + label="Help" + /> @@ -790,9 +768,8 @@ export const DetachedTriggers: Story = { magnitude="lg" aria-label="Close" render={} - > - - + icon={} + /> @@ -807,9 +784,8 @@ export const DetachedTriggers: Story = { tone="neutral" magnitude="xl" render={} - > - Done - + label="Done" + /> diff --git a/packages/propel/src/components/drawer/drawer-close.tsx b/packages/propel/src/components/drawer/drawer-close.tsx index f463417e..f27b2e7d 100644 --- a/packages/propel/src/components/drawer/drawer-close.tsx +++ b/packages/propel/src/components/drawer/drawer-close.tsx @@ -10,9 +10,13 @@ export type DrawerCloseProps = Omit - * - * + * } + * /> * } * />; * ``` diff --git a/packages/propel/src/components/drawer/drawer-trigger.tsx b/packages/propel/src/components/drawer/drawer-trigger.tsx index 66ee3748..e6c5e3a8 100644 --- a/packages/propel/src/components/drawer/drawer-trigger.tsx +++ b/packages/propel/src/components/drawer/drawer-trigger.tsx @@ -10,9 +10,13 @@ export type DrawerTriggerProps = Omit - * Open details - * + * + + + + + + + + + + + label="Keep editing" + /> + label="Discard" + /> diff --git a/packages/propel/src/components/field/field-label-group.tsx b/packages/propel/src/components/field/field-label-group.tsx index daa10eb2..c1f00896 100644 --- a/packages/propel/src/components/field/field-label-group.tsx +++ b/packages/propel/src/components/field/field-label-group.tsx @@ -11,7 +11,7 @@ import { FieldLabel } from "./field-label"; export type FieldLabelGroupProps = FieldLabelGroupVariantProps & { magnitude: InputMagnitude; required?: boolean; - label?: React.ReactNode; + label?: string; description?: React.ReactNode; }; diff --git a/packages/propel/src/components/form/form.stories.tsx b/packages/propel/src/components/form/form.stories.tsx index e930d49a..e3b66cc4 100644 --- a/packages/propel/src/components/form/form.stories.tsx +++ b/packages/propel/src/components/form/form.stories.tsx @@ -168,9 +168,14 @@ export const Default: Story = { /> - + + + label="Create account" + /> ); @@ -338,9 +347,8 @@ export const ServerFunction: Story = { tone="neutral" magnitude="md" loading={pending} - > - Reserve username - + label="Reserve username" + /> ); @@ -406,9 +414,14 @@ export const SchemaValidation: Story = { /> - + + label="Options" + /> @@ -153,9 +154,8 @@ export const DisplayProperties: Story = { tone="neutral" magnitude="xl" render={} - > - Display - + label="Display" + /> Display Properties
@@ -165,9 +165,8 @@ export const DisplayProperties: Story = { magnitude="md" pressed={Boolean(pills[p])} onPressedChange={(next) => setPills((s) => ({ ...s, [p]: next }))} - > - {p} - + label={p} + /> ))}
@@ -233,9 +232,8 @@ export const DisplayAccordion: Story = { tone="neutral" magnitude="xl" render={} - > - Display options - + label="Display options" + /> {SECTIONS.map((title) => { const key = title.split(" ")[0].toLowerCase(); @@ -322,9 +320,8 @@ export const OpenOnHover: Story = { tone="neutral" magnitude="xl" render={} - > - Filters - + label="Filters" + /> Applied filters
@@ -379,9 +376,8 @@ export const DetachedTrigger: Story = { tone="neutral" magnitude="xl" render={} - > - Display - + label="Display" + />
{/* The popover: declared elsewhere, associated by `handle`. */} @@ -446,9 +442,8 @@ export const MultipleTriggers: Story = { tone="neutral" magnitude="xl" render={} - > - {member.name} - + label={member.name} + /> ))} @@ -522,18 +517,16 @@ export const Controlled: Story = { tone="neutral" magnitude="xl" render={} - > - Sort - + label="Sort" + /> + label="Filter" + /> {triggerId === "view-filter-trigger" ? ( <> @@ -562,9 +555,8 @@ export const Controlled: Story = { setTriggerId("view-filter-trigger"); setOpen(true); }} - > - Reopen filters - + label="Reopen filters" + /> ); }, @@ -645,9 +637,8 @@ export const RTL: Story = { tone="neutral" magnitude="xl" render={} - > - خيارات - + label="خيارات" + /> الترتيب حسب diff --git a/packages/propel/src/components/preview-card/preview-card.stories.tsx b/packages/propel/src/components/preview-card/preview-card.stories.tsx index a19a6f89..059f66d3 100644 --- a/packages/propel/src/components/preview-card/preview-card.stories.tsx +++ b/packages/propel/src/components/preview-card/preview-card.stories.tsx @@ -132,7 +132,7 @@ export const WithImageInteraction: Story = { }, }; -// Per-trigger payload content: the card resolves its body from the string payload the +// Per-trigger payload body: the card resolves its body from the string payload the // active trigger passed, so one card serves several launch points. const topics: Record = { cycles: { @@ -284,9 +284,8 @@ export const Controlled: Story = { setTriggerId("modules-trigger"); setOpen(true); }} - > - Preview modules - + label="Preview modules" + /> ); }, diff --git a/packages/propel/src/components/radio-group-field/radio-group-field-option.tsx b/packages/propel/src/components/radio-group-field/radio-group-field-option.tsx index 1884400c..88e8dd82 100644 --- a/packages/propel/src/components/radio-group-field/radio-group-field-option.tsx +++ b/packages/propel/src/components/radio-group-field/radio-group-field-option.tsx @@ -7,7 +7,7 @@ import { Radio, type RadioProps } from "../radio/radio"; export type RadioGroupFieldOptionProps = RadioProps & { /** Visible option label. */ - label: React.ReactNode; + label: string; /** Optional supporting text announced as the radio description. */ description?: React.ReactNode; /** Label and description size. Inherited from `RadioGroupField` when omitted. */ diff --git a/packages/propel/src/components/radio-group-field/radio-group-field.tsx b/packages/propel/src/components/radio-group-field/radio-group-field.tsx index 0d0177bb..9d6f1026 100644 --- a/packages/propel/src/components/radio-group-field/radio-group-field.tsx +++ b/packages/propel/src/components/radio-group-field/radio-group-field.tsx @@ -22,7 +22,7 @@ export type RadioGroupFieldProps = Omit< /** Helper text shown below the group. Replaced by `error` when an error is set. */ hint?: React.ReactNode; /** Visible fieldset legend. */ - label: React.ReactNode; + label: string; /** Legend and helper text size. */ magnitude: FieldMagnitude; /** Submitted field name. */ diff --git a/packages/propel/src/components/radio/radio.stories.tsx b/packages/propel/src/components/radio/radio.stories.tsx index ccd82b80..69ca6f5f 100644 --- a/packages/propel/src/components/radio/radio.stories.tsx +++ b/packages/propel/src/components/radio/radio.stories.tsx @@ -209,9 +209,14 @@ export const FormIntegration: Story = { - +