From 52e21d9e1712ca1f3ded18c250a30b82dc98c9bb Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Tue, 11 Mar 2025 11:19:22 +0000 Subject: [PATCH 1/2] First Menu implementation Signed-off-by: Charles de Dreuille --- packages/canon/report.api.md | 25 ++ .../src/components/Menu/Menu.stories.tsx | 46 ++++ .../canon/src/components/Menu/Menu.styles.css | 104 ++++++++ packages/canon/src/components/Menu/Menu.tsx | 223 ++++++++++++++++++ packages/canon/src/components/Menu/index.ts | 18 ++ packages/canon/src/components/Menu/types.ts | 37 +++ packages/canon/src/css/components.css | 1 + packages/canon/src/index.ts | 1 + 8 files changed, 455 insertions(+) create mode 100644 packages/canon/src/components/Menu/Menu.stories.tsx create mode 100644 packages/canon/src/components/Menu/Menu.styles.css create mode 100644 packages/canon/src/components/Menu/Menu.tsx create mode 100644 packages/canon/src/components/Menu/index.ts create mode 100644 packages/canon/src/components/Menu/types.ts diff --git a/packages/canon/report.api.md b/packages/canon/report.api.md index 5ad27f537a..6ff02d7437 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'; @@ -732,6 +733,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 1abee6eb5d..40b791a83f 100644 --- a/packages/canon/src/css/components.css +++ b/packages/canon/src/css/components.css @@ -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'; diff --git a/packages/canon/src/index.ts b/packages/canon/src/index.ts index 18dad55e89..0248ca092e 100644 --- a/packages/canon/src/index.ts +++ b/packages/canon/src/index.ts @@ -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'; From bf319b703d995ff226fc50cca1bdc45a429a8046 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Sun, 16 Mar 2025 09:54:11 +0000 Subject: [PATCH 2/2] Create poor-schools-guess.md Signed-off-by: Charles de Dreuille --- .changeset/poor-schools-guess.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/poor-schools-guess.md 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.