Merge pull request #29151 from backstage/canon-menu

Canon - Add new Menu component
This commit is contained in:
Charles de Dreuille
2025-03-16 19:20:04 +00:00
committed by GitHub
10 changed files with 469 additions and 9 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/canon': minor
---
Added a new Menu component to Canon.
+9 -9
View File
@@ -1,11 +1,6 @@
{
"name": "root",
"version": "1.37.0-next.2",
"private": true,
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage"
},
"backstage": {
"cli": {
"new": {
@@ -16,6 +11,11 @@
}
}
},
"private": true,
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage"
},
"workspaces": {
"packages": [
"packages/*",
@@ -51,11 +51,11 @@
"snyk:test": "npx snyk test --yarn-workspaces --strict-out-of-sync=false",
"snyk:test:package": "yarn snyk:test --include",
"start": "yarn workspace example-app start",
"start-backend": "yarn workspace example-backend start",
"start-backend:legacy": "yarn workspace example-backend-legacy start",
"start:lighthouse": "yarn workspaces foreach -A --include example-backend --include example-app --parallel --jobs unlimited -v -i run start",
"start:microsite": "cd microsite/ && yarn start",
"start:next": "yarn workspace example-app-next start",
"start-backend": "yarn workspace example-backend start",
"start-backend:legacy": "yarn workspace example-backend-legacy start",
"storybook": "yarn ./storybook run storybook",
"techdocs-cli": "node scripts/techdocs-cli.js",
"techdocs-cli:dev": "cross-env TECHDOCS_CLI_DEV_MODE=true node scripts/techdocs-cli.js",
@@ -103,9 +103,9 @@
"@material-ui/pickers@^3.3.10": "patch:@material-ui/pickers@npm%3A3.3.11#./.yarn/patches/@material-ui-pickers-npm-3.3.11-1c8f68ea20.patch",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"jest-haste-map@^29.7.0": "patch:jest-haste-map@npm%3A29.7.0#./.yarn/patches/jest-haste-map-npm-29.7.0-e3be419eff.patch",
"ast-types@0.14.2": "patch:ast-types@npm%3A0.14.2#./.yarn/patches/ast-types-npm-0.14.2-43c4ac4b0d.patch",
"ast-types@^0.14.1": "patch:ast-types@npm%3A0.14.2#./.yarn/patches/ast-types-npm-0.14.2-43c4ac4b0d.patch",
"ast-types@0.14.2": "patch:ast-types@npm%3A0.14.2#./.yarn/patches/ast-types-npm-0.14.2-43c4ac4b0d.patch"
"jest-haste-map@^29.7.0": "patch:jest-haste-map@npm%3A29.7.0#./.yarn/patches/jest-haste-map-npm-29.7.0-e3be419eff.patch"
},
"dependencies": {
"@backstage/errors": "workspace:^",
+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';
@@ -757,6 +758,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,5 +27,6 @@
@import '../components/IconButton/styles.css';
@import '../components/Input/Input.styles.css';
@import '../components/Field/Field.styles.css';
@import '../components/Menu/Menu.styles.css';
@import '../components/Link/styles.css';
@import '../components/ScrollArea/ScrollArea.styles.css';
+1
View File
@@ -39,6 +39,7 @@ export * from './components/Checkbox';
export * from './components/Table';
export * from './components/Input';
export * from './components/Field';
export * from './components/Menu';
export * from './components/ScrollArea';
// Types