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
3 changes: 1 addition & 2 deletions packages/propel/src/components/menu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export { MenuCheckboxItem, type MenuCheckboxItemProps } from "./menu-checkbox-it
export { MenuContent, type MenuContentProps } from "./menu-content";
export { MenuFooter, type MenuFooterProps } from "./menu-footer";
export { MenuItem, type MenuItemProps } from "./menu-item";
export { MenuLabel, type MenuLabelProps } from "./menu-label";
export { MenuLinkItem, type MenuLinkItemProps } from "./menu-link-item";
export { MenuSearch, type MenuSearchProps } from "./menu-search";
export { MenuSubContent, type MenuSubContentProps } from "./menu-sub-content";
Expand All @@ -20,8 +21,6 @@ export {
type MenuGroupProps,
MenuGroupLabel,
type MenuGroupLabelProps,
MenuLabel,
type MenuLabelProps,
MenuPopup,
type MenuPopupProps,
MenuPortal,
Expand Down
23 changes: 15 additions & 8 deletions packages/propel/src/components/menu/menu-checkbox-item.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import type * as React from "react";

import { useControllableState } from "../../hooks/use-controllable-state/index";
import { NodeSlot } from "../../internal/node-slot";
import { CheckboxVisual } from "../../ui/checkbox/index";
import {
MenuCheckboxItem as MenuCheckboxItemRoot,
type MenuCheckboxItemProps as MenuCheckboxItemRootProps,
MenuItemContent,
MenuItemControl,
MenuItemIcon,
MenuItemMeta,
MenuItemTitle,
MenuItemTitleRow,
} from "../../ui/menu";

export type MenuCheckboxItemProps = MenuCheckboxItemRootProps & {
Expand Down Expand Up @@ -46,14 +51,16 @@ export function MenuCheckboxItem({
}}
{...props}
>
<span className="flex shrink-0 items-center">
<MenuItemControl>
<CheckboxVisual tone="neutral" checked={isChecked} disabled={props.disabled} />
</span>
{inlineStartNode != null ? <NodeSlot>{inlineStartNode}</NodeSlot> : null}
<span className="min-w-0 flex-1 truncate">{label ?? children}</span>
{inlineEndNode != null ? (
<span className="shrink-0 text-12 text-tertiary">{inlineEndNode}</span>
) : null}
</MenuItemControl>
{inlineStartNode != null ? <MenuItemIcon>{inlineStartNode}</MenuItemIcon> : null}
<MenuItemContent>
<MenuItemTitleRow>
<MenuItemTitle>{label ?? children}</MenuItemTitle>
</MenuItemTitleRow>
</MenuItemContent>
{inlineEndNode != null ? <MenuItemMeta>{inlineEndNode}</MenuItemMeta> : null}
</MenuCheckboxItemRoot>
);
}
16 changes: 6 additions & 10 deletions packages/propel/src/components/menu/menu-content.shared.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Menu as BaseMenu } from "@base-ui/react/menu";
import type * as React from "react";

import { OverlayPanel, type OverlayPanelWidth } from "../../internal/overlay-panel";
import { MenuPopup, MenuPortal, MenuPositioner } from "../../ui/menu";

export type MenuContentWidth = OverlayPanelWidth;

Expand Down Expand Up @@ -34,17 +35,12 @@ export function MenuContentSurface({
align: BaseMenu.Positioner.Props["align"];
}) {
return (
<BaseMenu.Portal>
<BaseMenu.Positioner
side={side}
sideOffset={sideOffset}
align={align}
className="outline-none"
>
<MenuPortal>
<MenuPositioner side={side} sideOffset={sideOffset} align={align}>
<OverlayPanel elevation="overlay" radius="lg" width={width} header={search} footer={footer}>
<BaseMenu.Popup className="p-1 outline-none" {...props} />
<MenuPopup surface="inset" {...props} />
</OverlayPanel>
</BaseMenu.Positioner>
</BaseMenu.Portal>
</MenuPositioner>
</MenuPortal>
);
}
14 changes: 1 addition & 13 deletions packages/propel/src/components/menu/menu-footer.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1 @@
import type * as React from "react";

export type MenuFooterProps = Omit<React.ComponentProps<"div">, "className" | "style">;

/** A non-interactive footer pinned below a `MenuContent` menu popup. */
export function MenuFooter(props: MenuFooterProps) {
return (
<div
className="shrink-0 border-t border-subtle bg-layer-2 px-3 py-2 text-12 text-tertiary"
{...props}
/>
);
}
export { MenuFooter, type MenuFooterProps } from "../../ui/menu";
44 changes: 24 additions & 20 deletions packages/propel/src/components/menu/menu-item.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import { Check } from "lucide-react";
import type * as React from "react";

import { NodeSlot } from "../../internal/node-slot";
import { MenuItem as MenuItemRoot, type MenuItemProps as MenuItemRootProps } from "../../ui/menu";
import {
MenuItem as MenuItemRoot,
MenuItemContent,
MenuItemDescription,
MenuItemIcon,
MenuItemSecondaryText,
MenuItemSelectedIndicator,
MenuItemTitle,
MenuItemTitleRow,
MenuItemTrailing,
type MenuItemProps as MenuItemRootProps,
} from "../../ui/menu";

export type MenuItemProps = MenuItemRootProps & {
/** Leading content before the label. */
Expand Down Expand Up @@ -35,27 +45,21 @@ export function MenuItem({
}: MenuItemProps) {
return (
<MenuItemRoot {...props}>
{inlineStartNode != null ? <NodeSlot>{inlineStartNode}</NodeSlot> : null}
<span className="flex min-w-0 flex-1 flex-col">
<span className="flex min-w-0 items-baseline gap-1.5">
<span className="truncate">{label ?? children}</span>
{inlineStartNode != null ? <MenuItemIcon>{inlineStartNode}</MenuItemIcon> : null}
<MenuItemContent>
<MenuItemTitleRow>
<MenuItemTitle>{label ?? children}</MenuItemTitle>
{secondaryText != null ? (
<span className="shrink-0 truncate text-12 text-tertiary group-data-disabled/item:text-disabled">
{secondaryText}
</span>
<MenuItemSecondaryText>{secondaryText}</MenuItemSecondaryText>
) : null}
</span>
{description != null ? (
<span className="truncate text-12 text-tertiary group-data-disabled/item:text-disabled">
{description}
</span>
) : null}
</span>
{inlineEndNode != null ? <NodeSlot>{inlineEndNode}</NodeSlot> : null}
</MenuItemTitleRow>
{description != null ? <MenuItemDescription>{description}</MenuItemDescription> : null}
</MenuItemContent>
{inlineEndNode != null ? <MenuItemTrailing>{inlineEndNode}</MenuItemTrailing> : null}
{selected ? (
<span className="flex h-5 w-4 shrink-0 items-center justify-center">
<Check className="size-4 text-icon-accent-primary" aria-hidden="true" />
</span>
<MenuItemSelectedIndicator>
<Check />
</MenuItemSelectedIndicator>
) : null}
</MenuItemRoot>
);
Expand Down
24 changes: 24 additions & 0 deletions packages/propel/src/components/menu/menu-label.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type * as React from "react";

import {
MenuLabel as MenuLabelRoot,
MenuLabelMeta,
type MenuLabelProps as MenuLabelRootProps,
MenuLabelTitle,
} from "../../ui/menu";

export type MenuLabelProps = MenuLabelRootProps & {
/** Optional inline-end content on the heading row. */
inlineEndNode?: React.ReactNode;
children?: React.ReactNode;
};

/** A non-interactive section heading for a group of menu items. */
export function MenuLabel({ inlineEndNode, children, ...props }: MenuLabelProps) {
return (
<MenuLabelRoot {...props}>
<MenuLabelTitle>{children}</MenuLabelTitle>
{inlineEndNode != null ? <MenuLabelMeta>{inlineEndNode}</MenuLabelMeta> : null}
</MenuLabelRoot>
);
}
16 changes: 12 additions & 4 deletions packages/propel/src/components/menu/menu-link-item.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import type * as React from "react";

import { NodeSlot } from "../../internal/node-slot";
import {
MenuItemContent,
MenuItemIcon,
MenuItemTitle,
MenuItemTitleRow,
MenuItemTrailing,
MenuLinkItem as MenuLinkItemRoot,
type MenuLinkItemProps as MenuLinkItemRootProps,
} from "../../ui/menu";
Expand All @@ -28,9 +32,13 @@ export function MenuLinkItem({
}: MenuLinkItemProps) {
return (
<MenuLinkItemRoot {...props}>
{inlineStartNode != null ? <NodeSlot>{inlineStartNode}</NodeSlot> : null}
<span className="min-w-0 flex-1 truncate">{label ?? children}</span>
{inlineEndNode != null ? <NodeSlot>{inlineEndNode}</NodeSlot> : null}
{inlineStartNode != null ? <MenuItemIcon>{inlineStartNode}</MenuItemIcon> : null}
<MenuItemContent>
<MenuItemTitleRow>
<MenuItemTitle>{label ?? children}</MenuItemTitle>
</MenuItemTitleRow>
</MenuItemContent>
{inlineEndNode != null ? <MenuItemTrailing>{inlineEndNode}</MenuItemTrailing> : null}
</MenuLinkItemRoot>
);
}
16 changes: 9 additions & 7 deletions packages/propel/src/components/menu/menu-search.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Search } from "lucide-react";
import type * as React from "react";

import { MenuSearch as MenuSearchRoot, MenuSearchIcon, MenuSearchInput } from "../../ui/menu";

export type MenuSearchProps = Omit<
React.ComponentProps<"input">,
React.ComponentPropsWithoutRef<"input">,
"className" | "style" | "onChange" | "value" | "type"
> & {
/** Current search text. */
Expand All @@ -21,17 +23,17 @@ export function MenuSearch({
...props
}: MenuSearchProps) {
return (
<div className="flex shrink-0 items-center gap-1.5 border-b border-subtle bg-surface-1 px-3 py-2">
<Search className="size-4 shrink-0 text-icon-tertiary" aria-hidden="true" />
<input
<MenuSearchRoot>
<MenuSearchIcon>
<Search />
</MenuSearchIcon>
<MenuSearchInput
type="text"
onKeyDown={(event) => event.stopPropagation()}
value={value}
onChange={(event) => onValueChange?.(event.target.value)}
placeholder={placeholder}
className="min-w-0 flex-1 bg-transparent text-13 text-secondary outline-none placeholder:text-placeholder"
{...props}
/>
</div>
</MenuSearchRoot>
);
}
26 changes: 17 additions & 9 deletions packages/propel/src/components/menu/menu-sub-trigger.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { ChevronRight } from "lucide-react";
import type * as React from "react";

import { NodeSlot } from "../../internal/node-slot";
import {
MenuItemContent,
MenuItemIcon,
MenuItemSubmenuIndicator,
MenuItemTitle,
MenuItemTitleRow,
MenuItemTrailing,
MenuSubTrigger as MenuSubTriggerRoot,
type MenuSubTriggerProps as MenuSubTriggerRootProps,
} from "../../ui/menu";
Expand All @@ -18,7 +23,7 @@ export type MenuSubTriggerProps = MenuSubTriggerRootProps & {

/**
* The ready-made submenu trigger row: composes the atomic `MenuSubTrigger` and lays out optional
* leading/trailing nodes, the label, and the chevron.
* leading/trailing nodes, the label, and the submenu chevron indicator.
*/
export function MenuSubTrigger({
inlineStartNode,
Expand All @@ -29,13 +34,16 @@ export function MenuSubTrigger({
}: MenuSubTriggerProps) {
return (
<MenuSubTriggerRoot {...props}>
{inlineStartNode != null ? <NodeSlot>{inlineStartNode}</NodeSlot> : null}
<span className="min-w-0 flex-1 truncate">{label ?? children}</span>
{inlineEndNode != null ? <NodeSlot>{inlineEndNode}</NodeSlot> : null}
<ChevronRight
className="size-4 shrink-0 text-icon-tertiary group-data-disabled/item:text-icon-disabled rtl:-scale-x-100"
aria-hidden="true"
/>
{inlineStartNode != null ? <MenuItemIcon>{inlineStartNode}</MenuItemIcon> : null}
<MenuItemContent>
<MenuItemTitleRow>
<MenuItemTitle>{label ?? children}</MenuItemTitle>
</MenuItemTitleRow>
</MenuItemContent>
{inlineEndNode != null ? <MenuItemTrailing>{inlineEndNode}</MenuItemTrailing> : null}
<MenuItemSubmenuIndicator>
<ChevronRight />
</MenuItemSubmenuIndicator>
</MenuSubTriggerRoot>
);
}
23 changes: 23 additions & 0 deletions packages/propel/src/ui/menu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,30 @@ export {
MenuCheckboxItemIndicator,
type MenuCheckboxItemIndicatorProps,
} from "./menu-checkbox-item-indicator";
export { MenuFooter, type MenuFooterProps } from "./menu-footer";
export { MenuGroup, type MenuGroupProps } from "./menu-group";
export { MenuGroupLabel, type MenuGroupLabelProps } from "./menu-group-label";
export { MenuItem, type MenuItemProps } from "./menu-item";
export { MenuItemContent, type MenuItemContentProps } from "./menu-item-content";
export { MenuItemControl, type MenuItemControlProps } from "./menu-item-control";
export { MenuItemDescription, type MenuItemDescriptionProps } from "./menu-item-description";
export { MenuItemIcon, type MenuItemIconProps } from "./menu-item-icon";
export { MenuItemMeta, type MenuItemMetaProps } from "./menu-item-meta";
export { MenuItemSecondaryText, type MenuItemSecondaryTextProps } from "./menu-item-secondary-text";
export {
MenuItemSelectedIndicator,
type MenuItemSelectedIndicatorProps,
} from "./menu-item-selected-indicator";
export {
MenuItemSubmenuIndicator,
type MenuItemSubmenuIndicatorProps,
} from "./menu-item-submenu-indicator";
export { MenuItemTitle, type MenuItemTitleProps } from "./menu-item-title";
export { MenuItemTrailing, type MenuItemTrailingProps } from "./menu-item-trailing";
export { MenuItemTitleRow, type MenuItemTitleRowProps } from "./menu-item-title-row";
export { MenuLabel, type MenuLabelProps } from "./menu-label";
export { MenuLabelMeta, type MenuLabelMetaProps } from "./menu-label-meta";
export { MenuLabelTitle, type MenuLabelTitleProps } from "./menu-label-title";
export { MenuLinkItem, type MenuLinkItemProps } from "./menu-link-item";
export { MenuPopup, type MenuPopupProps } from "./menu-popup";
export { MenuPortal } from "./menu-portal";
Expand All @@ -20,6 +40,9 @@ export {
MenuRadioItemIndicator,
type MenuRadioItemIndicatorProps,
} from "./menu-radio-item-indicator";
export { MenuSearch, type MenuSearchProps } from "./menu-search";
export { MenuSearchIcon, type MenuSearchIconProps } from "./menu-search-icon";
export { MenuSearchInput, type MenuSearchInputProps } from "./menu-search-input";
export { MenuSeparator, type MenuSeparatorProps } from "./menu-separator";
export { MenuSub, type MenuSubProps } from "./menu-sub";
export { MenuSubTrigger, type MenuSubTriggerProps } from "./menu-sub-trigger";
Expand Down
7 changes: 1 addition & 6 deletions packages/propel/src/ui/menu/menu-checkbox-item-indicator.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Menu as BaseMenu } from "@base-ui/react/menu";
import { Check } from "lucide-react";

import { menuItemIndicatorVariants } from "./variants";

Expand All @@ -10,9 +9,5 @@ export type MenuCheckboxItemIndicatorProps = Omit<

/** Shows whether the checkbox item is ticked. Wraps `Menu.CheckboxItemIndicator` 1:1. */
export function MenuCheckboxItemIndicator(props: MenuCheckboxItemIndicatorProps) {
return (
<BaseMenu.CheckboxItemIndicator className={menuItemIndicatorVariants()} {...props}>
{props.children ?? <Check className="size-4" aria-hidden="true" />}
</BaseMenu.CheckboxItemIndicator>
);
return <BaseMenu.CheckboxItemIndicator className={menuItemIndicatorVariants()} {...props} />;
}
15 changes: 3 additions & 12 deletions packages/propel/src/ui/menu/menu-checkbox-item.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Menu as BaseMenu } from "@base-ui/react/menu";
import { cx } from "class-variance-authority";

import { menuCheckboxItemVariants } from "./variants";

export type MenuCheckboxItemProps = Omit<
BaseMenu.CheckboxItem.Props,
Expand All @@ -8,15 +9,5 @@ export type MenuCheckboxItemProps = Omit<

/** A toggleable multi-select menu row with `role="menuitemcheckbox"`. Wraps `Menu.CheckboxItem` 1:1. */
export function MenuCheckboxItem(props: MenuCheckboxItemProps) {
return (
<BaseMenu.CheckboxItem
className={cx(
"group/item flex h-[34px] w-full cursor-default items-center gap-2 rounded-md px-2 text-13 outline-none select-none [--node-size:1rem]",
"text-secondary",
"data-highlighted:bg-layer-transparent-hover",
"data-disabled:pointer-events-none data-disabled:text-disabled",
)}
{...props}
/>
);
return <BaseMenu.CheckboxItem className={menuCheckboxItemVariants()} {...props} />;
}
10 changes: 10 additions & 0 deletions packages/propel/src/ui/menu/menu-footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type * as React from "react";

import { menuFooterVariants } from "./variants";

export type MenuFooterProps = Omit<React.ComponentPropsWithoutRef<"div">, "className" | "style">;

/** A non-interactive footer pinned below a menu popup list (sticky chrome outside `role="menu"`). */
export function MenuFooter(props: MenuFooterProps) {
return <div className={menuFooterVariants()} {...props} />;
}
Loading