diff --git a/.changeset/poor-schools-guess.md b/.changeset/poor-schools-guess.md new file mode 100644 index 0000000000..410796aabf --- /dev/null +++ b/.changeset/poor-schools-guess.md @@ -0,0 +1,5 @@ +--- +'@backstage/canon': minor +--- + +Added a new Menu component to Canon. diff --git a/package.json b/package.json index 38aef08583..920c67831a 100644 --- a/package.json +++ b/package.json @@ -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:^", diff --git a/packages/canon/report.api.md b/packages/canon/report.api.md index 1881d84e3c..e993df054b 100644 --- a/packages/canon/report.api.md +++ b/packages/canon/report.api.md @@ -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'; @@ -758,6 +759,30 @@ export const marginPropDefs: (spacingValues: string[]) => { // @public (undocumented) export type MarginProps = GetPropDefTypes; +// @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; diff --git a/packages/canon/src/components/Menu/Menu.stories.tsx b/packages/canon/src/components/Menu/Menu.stories.tsx new file mode 100644 index 0000000000..c971483b16 --- /dev/null +++ b/packages/canon/src/components/Menu/Menu.stories.tsx @@ -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; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + children: ( + <> + Menu + + + + Item 1 + Item 2 + Item 3 + + + + + ), + }, +}; diff --git a/packages/canon/src/components/Menu/Menu.styles.css b/packages/canon/src/components/Menu/Menu.styles.css new file mode 100644 index 0000000000..4f76e5552f --- /dev/null +++ b/packages/canon/src/components/Menu/Menu.styles.css @@ -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); +} diff --git a/packages/canon/src/components/Menu/Menu.tsx b/packages/canon/src/components/Menu/Menu.tsx new file mode 100644 index 0000000000..b1f5224bf0 --- /dev/null +++ b/packages/canon/src/components/Menu/Menu.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +MenuTrigger.displayName = MenuPrimitive.Trigger.displayName; + +const MenuBackdrop = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +MenuBackdrop.displayName = MenuPrimitive.Backdrop.displayName; + +const MenuPositioner = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +MenuPositioner.displayName = MenuPrimitive.Positioner.displayName; + +const MenuPopup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +MenuPopup.displayName = MenuPrimitive.Popup.displayName; + +const MenuArrow = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +MenuArrow.displayName = MenuPrimitive.Arrow.displayName; + +const MenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +MenuItem.displayName = MenuPrimitive.Item.displayName; + +const MenuGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +MenuGroup.displayName = MenuPrimitive.Group.displayName; + +const MenuGroupLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +MenuGroupLabel.displayName = MenuPrimitive.GroupLabel.displayName; + +const MenuRadioGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +MenuRadioGroup.displayName = MenuPrimitive.RadioGroup.displayName; + +const MenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +MenuRadioItem.displayName = MenuPrimitive.RadioItem.displayName; + +const MenuRadioItemIndicator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +MenuRadioItemIndicator.displayName = + MenuPrimitive.RadioItemIndicator.displayName; + +const MenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +MenuCheckboxItem.displayName = MenuPrimitive.CheckboxItem.displayName; + +const MenuCheckboxItemIndicator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +MenuCheckboxItemIndicator.displayName = + MenuPrimitive.CheckboxItemIndicator.displayName; + +const MenuSubmenuTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +MenuSubmenuTrigger.displayName = MenuPrimitive.SubmenuTrigger.displayName; + +const MenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +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, +}; diff --git a/packages/canon/src/components/Menu/index.ts b/packages/canon/src/components/Menu/index.ts new file mode 100644 index 0000000000..2f404d927a --- /dev/null +++ b/packages/canon/src/components/Menu/index.ts @@ -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'; diff --git a/packages/canon/src/components/Menu/types.ts b/packages/canon/src/components/Menu/types.ts new file mode 100644 index 0000000000..63f8069f61 --- /dev/null +++ b/packages/canon/src/components/Menu/types.ts @@ -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; +}; diff --git a/packages/canon/src/css/components.css b/packages/canon/src/css/components.css index b4a466abeb..8c77a0f58c 100644 --- a/packages/canon/src/css/components.css +++ b/packages/canon/src/css/components.css @@ -27,6 +27,7 @@ @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/Tooltip/Tooltip.styles.css'; @import '../components/ScrollArea/ScrollArea.styles.css'; diff --git a/packages/canon/src/index.ts b/packages/canon/src/index.ts index ff60e9b22c..3c9a28b44d 100644 --- a/packages/canon/src/index.ts +++ b/packages/canon/src/index.ts @@ -40,6 +40,7 @@ export * from './components/Table'; export * from './components/Input'; export * from './components/Field'; export * from './components/Tooltip'; +export * from './components/Menu'; export * from './components/ScrollArea'; // Types