First Menu implementation

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2025-03-11 11:19:22 +00:00
parent 37416e06d6
commit 52e21d9e17
8 changed files with 455 additions and 0 deletions
+25
View File
@@ -8,6 +8,7 @@ import type { CSSProperties } from 'react';
import { Field as Field_2 } from '@base-ui-components/react/field';
import { ForwardRefExoticComponent } from 'react';
import { Input as Input_2 } from '@base-ui-components/react/input';
import { Menu as Menu_2 } from '@base-ui-components/react/menu';
import { default as React_2 } from 'react';
import * as React_3 from 'react';
import { ReactNode } from 'react';
@@ -732,6 +733,30 @@ export const marginPropDefs: (spacingValues: string[]) => {
// @public (undocumented)
export type MarginProps = GetPropDefTypes<typeof marginPropDefs>;
// @public (undocumented)
export const Menu: MenuComponent;
// @public (undocumented)
export type MenuComponent = {
Root: typeof Menu_2.Root;
Trigger: typeof Menu_2.Trigger;
Portal: typeof Menu_2.Portal;
Backdrop: typeof Menu_2.Backdrop;
Positioner: typeof Menu_2.Positioner;
Popup: typeof Menu_2.Popup;
Arrow: typeof Menu_2.Arrow;
Item: typeof Menu_2.Item;
Group: typeof Menu_2.Group;
GroupLabel: typeof Menu_2.GroupLabel;
RadioGroup: typeof Menu_2.RadioGroup;
RadioItem: typeof Menu_2.RadioItem;
RadioItemIndicator: typeof Menu_2.RadioItemIndicator;
CheckboxItem: typeof Menu_2.CheckboxItem;
CheckboxItemIndicator: typeof Menu_2.CheckboxItemIndicator;
SubmenuTrigger: typeof Menu_2.SubmenuTrigger;
Separator: typeof Menu_2.Separator;
};
// @public (undocumented)
export type NonStylingPropDef = {
className?: never;
@@ -0,0 +1,46 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { Menu } from './Menu';
const meta = {
title: 'Components/Menu',
component: Menu.Root,
} satisfies Meta<typeof Menu.Root>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
children: (
<>
<Menu.Trigger>Menu</Menu.Trigger>
<Menu.Portal>
<Menu.Positioner>
<Menu.Popup>
<Menu.Item>Item 1</Menu.Item>
<Menu.Item>Item 2</Menu.Item>
<Menu.Item>Item 3</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</>
),
},
};
@@ -0,0 +1,104 @@
.canon-MenuTrigger {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;
@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}
&:active {
background-color: var(--color-gray-100);
}
&[data-popup-open] {
background-color: var(--color-gray-100);
}
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
.canon-MenuPositioner {
outline: 0;
}
.canon-MenuPopup {
box-sizing: border-box;
padding-block: 0.25rem;
border-radius: 0.375rem;
background-color: var(--canon-bg-elevated);
color: var(--color-fg-primary);
transform-origin: var(--transform-origin);
transition: transform 150ms, opacity 150ms;
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0.9);
}
@media (prefers-color-scheme: light) {
outline: 1px solid var(--color-gray-200);
box-shadow: 0px 10px 15px -3px var(--color-gray-200),
0px 4px 6px -4px var(--color-gray-200);
}
@media (prefers-color-scheme: dark) {
outline: 1px solid var(--color-gray-300);
outline-offset: -1px;
}
}
.canon-MenuItem {
outline: 0;
cursor: default;
user-select: none;
padding-block: 0.5rem;
padding-left: 1rem;
padding-right: 2rem;
display: flex;
font-size: 0.875rem;
line-height: 1rem;
&[data-highlighted] {
z-index: 0;
position: relative;
color: var(--color-gray-50);
}
&[data-highlighted]::before {
content: '';
z-index: -1;
position: absolute;
inset-block: 0;
inset-inline: 0.25rem;
border-radius: 0.25rem;
background-color: var(--color-gray-900);
}
}
.canon-MenuSeparator {
margin: 0.375rem 1rem;
height: 1px;
background-color: var(--color-gray-200);
}
+223
View File
@@ -0,0 +1,223 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { Menu as MenuPrimitive } from '@base-ui-components/react/menu';
import clsx from 'clsx';
import { MenuComponent } from './types';
const MenuTrigger = React.forwardRef<
React.ElementRef<typeof MenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof MenuPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<MenuPrimitive.Trigger
ref={ref}
className={clsx('canon-MenuTrigger', className)}
{...props}
/>
));
MenuTrigger.displayName = MenuPrimitive.Trigger.displayName;
const MenuBackdrop = React.forwardRef<
React.ElementRef<typeof MenuPrimitive.Backdrop>,
React.ComponentPropsWithoutRef<typeof MenuPrimitive.Backdrop>
>(({ className, ...props }, ref) => (
<MenuPrimitive.Backdrop
ref={ref}
className={clsx('canon-MenuBackdrop', className)}
{...props}
/>
));
MenuBackdrop.displayName = MenuPrimitive.Backdrop.displayName;
const MenuPositioner = React.forwardRef<
React.ElementRef<typeof MenuPrimitive.Positioner>,
React.ComponentPropsWithoutRef<typeof MenuPrimitive.Positioner>
>(({ className, ...props }, ref) => (
<MenuPrimitive.Positioner
ref={ref}
className={clsx('canon-MenuPositioner', className)}
{...props}
/>
));
MenuPositioner.displayName = MenuPrimitive.Positioner.displayName;
const MenuPopup = React.forwardRef<
React.ElementRef<typeof MenuPrimitive.Popup>,
React.ComponentPropsWithoutRef<typeof MenuPrimitive.Popup>
>(({ className, ...props }, ref) => (
<MenuPrimitive.Popup
ref={ref}
className={clsx('canon-MenuPopup', className)}
{...props}
/>
));
MenuPopup.displayName = MenuPrimitive.Popup.displayName;
const MenuArrow = React.forwardRef<
React.ElementRef<typeof MenuPrimitive.Arrow>,
React.ComponentPropsWithoutRef<typeof MenuPrimitive.Arrow>
>(({ className, ...props }, ref) => (
<MenuPrimitive.Arrow
ref={ref}
className={clsx('canon-MenuArrow', className)}
{...props}
/>
));
MenuArrow.displayName = MenuPrimitive.Arrow.displayName;
const MenuItem = React.forwardRef<
React.ElementRef<typeof MenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof MenuPrimitive.Item>
>(({ className, ...props }, ref) => (
<MenuPrimitive.Item
ref={ref}
className={clsx('canon-MenuItem', className)}
{...props}
/>
));
MenuItem.displayName = MenuPrimitive.Item.displayName;
const MenuGroup = React.forwardRef<
React.ElementRef<typeof MenuPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof MenuPrimitive.Group>
>(({ className, ...props }, ref) => (
<MenuPrimitive.Group
ref={ref}
className={clsx('canon-MenuGroup', className)}
{...props}
/>
));
MenuGroup.displayName = MenuPrimitive.Group.displayName;
const MenuGroupLabel = React.forwardRef<
React.ElementRef<typeof MenuPrimitive.GroupLabel>,
React.ComponentPropsWithoutRef<typeof MenuPrimitive.GroupLabel>
>(({ className, ...props }, ref) => (
<MenuPrimitive.GroupLabel
ref={ref}
className={clsx('canon-MenuGroupLabel', className)}
{...props}
/>
));
MenuGroupLabel.displayName = MenuPrimitive.GroupLabel.displayName;
const MenuRadioGroup = React.forwardRef<
React.ElementRef<typeof MenuPrimitive.RadioGroup>,
React.ComponentPropsWithoutRef<typeof MenuPrimitive.RadioGroup>
>(({ className, ...props }, ref) => (
<MenuPrimitive.RadioGroup
ref={ref}
className={clsx('canon-MenuRadioGroup', className)}
{...props}
/>
));
MenuRadioGroup.displayName = MenuPrimitive.RadioGroup.displayName;
const MenuRadioItem = React.forwardRef<
React.ElementRef<typeof MenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof MenuPrimitive.RadioItem>
>(({ className, ...props }, ref) => (
<MenuPrimitive.RadioItem
ref={ref}
className={clsx('canon-MenuRadioItem', className)}
{...props}
/>
));
MenuRadioItem.displayName = MenuPrimitive.RadioItem.displayName;
const MenuRadioItemIndicator = React.forwardRef<
React.ElementRef<typeof MenuPrimitive.RadioItemIndicator>,
React.ComponentPropsWithoutRef<typeof MenuPrimitive.RadioItemIndicator>
>(({ className, ...props }, ref) => (
<MenuPrimitive.RadioItemIndicator
ref={ref}
className={clsx('canon-MenuRadioItemIndicator', className)}
{...props}
/>
));
MenuRadioItemIndicator.displayName =
MenuPrimitive.RadioItemIndicator.displayName;
const MenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof MenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof MenuPrimitive.CheckboxItem>
>(({ className, ...props }, ref) => (
<MenuPrimitive.CheckboxItem
ref={ref}
className={clsx('canon-MenuCheckboxItem', className)}
{...props}
/>
));
MenuCheckboxItem.displayName = MenuPrimitive.CheckboxItem.displayName;
const MenuCheckboxItemIndicator = React.forwardRef<
React.ElementRef<typeof MenuPrimitive.CheckboxItemIndicator>,
React.ComponentPropsWithoutRef<typeof MenuPrimitive.CheckboxItemIndicator>
>(({ className, ...props }, ref) => (
<MenuPrimitive.CheckboxItemIndicator
ref={ref}
className={clsx('canon-MenuCheckboxItemIndicator', className)}
{...props}
/>
));
MenuCheckboxItemIndicator.displayName =
MenuPrimitive.CheckboxItemIndicator.displayName;
const MenuSubmenuTrigger = React.forwardRef<
React.ElementRef<typeof MenuPrimitive.SubmenuTrigger>,
React.ComponentPropsWithoutRef<typeof MenuPrimitive.SubmenuTrigger>
>(({ className, ...props }, ref) => (
<MenuPrimitive.SubmenuTrigger
ref={ref}
className={clsx('canon-MenuSubmenuTrigger', className)}
{...props}
/>
));
MenuSubmenuTrigger.displayName = MenuPrimitive.SubmenuTrigger.displayName;
const MenuSeparator = React.forwardRef<
React.ElementRef<typeof MenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof MenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<MenuPrimitive.Separator
ref={ref}
className={clsx('canon-MenuSeparator', className)}
{...props}
/>
));
MenuSeparator.displayName = MenuPrimitive.Separator.displayName;
/** @public */
export const Menu: MenuComponent = {
Root: MenuPrimitive.Root,
Trigger: MenuTrigger,
Portal: MenuPrimitive.Portal,
Backdrop: MenuBackdrop,
Positioner: MenuPositioner,
Popup: MenuPopup,
Arrow: MenuArrow,
Item: MenuItem,
Group: MenuGroup,
GroupLabel: MenuGroupLabel,
RadioGroup: MenuRadioGroup,
RadioItem: MenuRadioItem,
RadioItemIndicator: MenuRadioItemIndicator,
CheckboxItem: MenuCheckboxItem,
CheckboxItemIndicator: MenuCheckboxItemIndicator,
SubmenuTrigger: MenuSubmenuTrigger,
Separator: MenuSeparator,
};
@@ -0,0 +1,18 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './Menu';
export * from './types';
@@ -0,0 +1,37 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Menu as MenuPrimitive } from '@base-ui-components/react/menu';
/** @public */
export type MenuComponent = {
Root: typeof MenuPrimitive.Root;
Trigger: typeof MenuPrimitive.Trigger;
Portal: typeof MenuPrimitive.Portal;
Backdrop: typeof MenuPrimitive.Backdrop;
Positioner: typeof MenuPrimitive.Positioner;
Popup: typeof MenuPrimitive.Popup;
Arrow: typeof MenuPrimitive.Arrow;
Item: typeof MenuPrimitive.Item;
Group: typeof MenuPrimitive.Group;
GroupLabel: typeof MenuPrimitive.GroupLabel;
RadioGroup: typeof MenuPrimitive.RadioGroup;
RadioItem: typeof MenuPrimitive.RadioItem;
RadioItemIndicator: typeof MenuPrimitive.RadioItemIndicator;
CheckboxItem: typeof MenuPrimitive.CheckboxItem;
CheckboxItemIndicator: typeof MenuPrimitive.CheckboxItemIndicator;
SubmenuTrigger: typeof MenuPrimitive.SubmenuTrigger;
Separator: typeof MenuPrimitive.Separator;
};
+1
View File
@@ -27,3 +27,4 @@
@import '../components/Heading/styles.css';
@import '../components/Input/Input.styles.css';
@import '../components/Field/Field.styles.css';
@import '../components/Menu/Menu.styles.css';
+1
View File
@@ -38,6 +38,7 @@ export * from './components/Checkbox';
export * from './components/Table';
export * from './components/Input';
export * from './components/Field';
export * from './components/Menu';
// Types
export * from './types';