Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Breadcrumbs component #12115

Open
wants to merge 8 commits into
base: develop
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion app/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,6 @@
"@types/node": "^20.11.21",
"lib0": "^0.2.99",
"react": "^18.3.1",
"vitest": "3.0.0-beta.3"
"vitest": "3.0.3"
}
}
1 change: 1 addition & 0 deletions app/common/src/text/english.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"submit": "Submit",
"retry": "Retry",
"hide": "Hide",
"more": "More",

"arbitraryFetchError": "An error occurred while fetching data",
"arbitraryFetchImageError": "An error occurred while fetching an image",
Expand Down
26 changes: 13 additions & 13 deletions app/gui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@
"test-dev:unit": "vitest",
"test-dev:integration": "cross-env NODE_ENV=production playwright test --ui",
"test-dev-dashboard:integration": "cross-env NODE_ENV=production playwright test ./integration-test/dashboard/ --ui",
"storybook:react": "cross-env FRAMEWORK=react storybook dev",
"storybook:vue": "cross-env FRAMEWORK=vue storybook dev",
"storybook:react": "cross-env FRAMEWORK=react storybook dev --port 6006 --no-open",
"storybook:vue": "cross-env FRAMEWORK=vue storybook dev --port 7007 --no-open",
"build-storybook:react": "cross-env FRAMEWORK=react storybook build",
"build-storybook:vue": "cross-env FRAMEWORK=vue storybook build",
"chromatic:react": "cross-env FRAMEWORK=react chromatic deploy",
Expand Down Expand Up @@ -147,15 +147,15 @@
"@open-rpc/server-js": "^1.9.5",
"@playwright/test": "^1.49.1",
"@react-types/shared": "3.27.0",
"@storybook/addon-essentials": "^8.4.7",
"@storybook/addon-interactions": "^8.4.7",
"@storybook/addon-onboarding": "^8.4.7",
"@storybook/blocks": "^8.4.7",
"@storybook/react": "^8.4.7",
"@storybook/react-vite": "^8.4.7",
"@storybook/test": "^8.4.7",
"@storybook/vue3": "^8.4.7",
"@storybook/vue3-vite": "^8.4.7",
"@storybook/addon-essentials": "8.5.0",
"@storybook/addon-interactions": "8.5.0",
"@storybook/addon-onboarding": "8.5.0",
"@storybook/blocks": "8.5.0",
"@storybook/react": "8.5.0",
"@storybook/react-vite": "8.5.0",
"@storybook/test": "8.5.0",
"@storybook/vue3": "8.5.0",
"@storybook/vue3-vite": "8.5.0",
"@tanstack/react-query-devtools": "5.59.20",
"@testing-library/jest-dom": "6.6.3",
"@testing-library/react": "16.0.1",
Expand Down Expand Up @@ -200,7 +200,7 @@
"resize-observer-polyfill": "1.5.1",
"shuffle-seed": "^1.1.6",
"sql-formatter": "^13.1.0",
"storybook": "^8.4.7",
"storybook": "8.5.0",
"tailwindcss": "^3.4.17",
"tailwindcss-animate": "1.0.7",
"tailwindcss-react-aria-components": "1.2.0",
Expand All @@ -209,7 +209,7 @@
"vite": "^6.0.7",
"vite-plugin-vue-devtools": "7.6.8",
"vite-plugin-wasm": "^3.4.1",
"vitest": "3.0.0-beta.3",
"vitest": "3.0.3",
"vue-react-wrapper": "^0.3.1",
"vue-tsc": "^2.2.0",
"yaml": "^2.7.0",
Expand Down
3 changes: 3 additions & 0 deletions app/gui/src/dashboard/assets/expand_arrow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const STYLES = tv({
width: { auto: 'w-auto', full: 'w-full', min: 'w-min', max: 'w-max' },
gap: {
custom: '',
none: 'gap-0',
joined: 'gap-0',
large: 'gap-3.5',
medium: 'gap-2',
Expand Down
28 changes: 6 additions & 22 deletions app/gui/src/dashboard/components/AriaComponents/Button/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import type * as aria from '#/components/aria'
import type { ExtractFunction } from '#/utilities/tailwindVariants'
import type { ReactElement, ReactNode } from 'react'
import type { Addon, IconProp, TestIdProps } from '../types'
import type { BUTTON_STYLES, ButtonVariants } from './variants'

/**
Expand Down Expand Up @@ -61,19 +62,15 @@ interface PropsWithoutHref {

/** Base props for a button. */
export interface BaseButtonProps<Render>
extends Omit<ButtonVariants, 'iconOnly' | 'isJoined' | 'position'> {
extends Omit<ButtonVariants, 'iconOnly' | 'isJoined' | 'position'>,
TestIdProps {
/** If `true`, the loader will not be shown. */
readonly hideLoader?: boolean
/** Falls back to `aria-label`. Pass `false` to explicitly disable the tooltip. */
readonly tooltip?: ReactElement | string | false | null
readonly tooltipPlacement?: aria.Placement
/** The icon to display in the button */
readonly icon?:
| ReactElement
| string
| ((render: Render) => ReactElement | string | null)
| null
| undefined
readonly icon?: IconProp<Render>
/** When `true`, icon will be shown only when hovered. */
readonly showIconOnHover?: boolean
/**
Expand All @@ -82,7 +79,6 @@ export interface BaseButtonProps<Render>
*/
readonly onPress?: ((event: aria.PressEvent) => Promise<void> | void) | null | undefined
readonly contentClassName?: string
readonly testId?: string
readonly isDisabled?: boolean
readonly formnovalidate?: boolean
/**
Expand All @@ -94,20 +90,8 @@ export interface BaseButtonProps<Render>

readonly children?: ReactNode | ((render: Render) => ReactNode)

readonly addonStart?:
| ReactElement
| string
| false
| ((render: Render) => ReactElement | string | null)
| null
| undefined
readonly addonEnd?:
| ReactElement
| string
| false
| ((render: Render) => ReactElement | string | null)
| null
| undefined
readonly addonStart?: Addon<Render>
readonly addonEnd?: Addon<Render>
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export const BUTTON_STYLES = tv({
},
iconOnly: {
// Specified in the compoundVariants
true: '',
true: 'aspect-square',
},
rounded: {
full: 'rounded-full',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import * as twv from '#/utilities/tailwindVariants'

import { useEventCallback } from '#/hooks/eventCallbackHooks'
import { ResetButtonGroupContext } from '../Button'
import { Close } from './Close'
import * as dialogProvider from './DialogProvider'
import * as dialogStackProvider from './DialogStackProvider'
import { DialogTrigger } from './DialogTrigger'
Expand Down Expand Up @@ -86,7 +87,6 @@ export function Popover(props: PopoverProps) {
size,
rounded,
variant,
placement = 'bottom start',
isDismissable = true,
...ariaPopoverProps
} = props
Expand All @@ -110,7 +110,6 @@ export function Popover(props: PopoverProps) {
})
}
UNSTABLE_portalContainer={root}
placement={placement}
style={popoverStyle}
shouldCloseOnInteractOutside={() => false}
{...ariaPopoverProps}
Expand Down Expand Up @@ -209,3 +208,4 @@ function PopoverContent(props: PopoverContentProps) {
}

Popover.Trigger = DialogTrigger
Popover.Close = Close
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,19 @@ import EyeClosed from '#/assets/eye_crossed.svg'
import Folder from '#/assets/folder.svg'
import type { Meta, StoryObj } from '@storybook/react'

import { useText } from '#/providers/TextProvider'
import { expect, userEvent, within } from '@storybook/test'
import type { MenuProps } from '.'
import { Menu } from '.'
import { passwordSchema } from '../../../pages/authentication/schemas'
import { Button } from '../Button'
import { Popover } from '../Dialog'
import { Form } from '../Form'
import { Input } from '../Inputs'

const meta = {
title: 'Components/Menu',
component: Menu,
parameters: {
layout: 'centered',
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement)
const button = canvas.getByRole('button', { name: 'Open Menu' })
Expand Down Expand Up @@ -189,7 +191,7 @@ function MenuContentWithDescription() {
<Menu.Item icon={Folder} description="This is a description" shortcut="⌘O">
Open Submenu
</Menu.Item>
<Menu selectionMode="multiple" placement="right">
<Menu selectionMode="multiple">
<Menu.Item description="This is a description" icon={Eye}>
Submenu item
</Menu.Item>
Expand Down Expand Up @@ -268,3 +270,68 @@ export const DynamicContent: Story = {
await expect(canvas.getByRole('menu')).toBeInTheDocument()
},
}

export const WithPopover: Story = {
render: () => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const { getText } = useText()
return (
<Menu.Trigger>
<Button>Open Menu</Button>

<Menu>
<Menu.Item>New File</Menu.Item>

<Menu.Separator />

<Menu.Item>Save</Menu.Item>
<Menu.Item>Cut</Menu.Item>
<Menu.Item>Copy</Menu.Item>
<Menu.Item>Paste</Menu.Item>
<Menu.Item>Delete</Menu.Item>
<Menu.Item>Rename</Menu.Item>
<Menu.Item>Move</Menu.Item>

<Menu.SubmenuTrigger>
<Menu.Item>Edit Secret</Menu.Item>

<Popover isDismissable={false}>
<Form
method="dialog"
schema={(z) => z.object({ name: z.string(), password: passwordSchema(getText) })}
onSubmit={() => new Promise((resolve) => setTimeout(resolve, 1000))}
>
<Input name="name" label="Name" />
<Input name="password" type="password" label="Password" testId="password" />

<Button.Group>
<Form.Submit>Save</Form.Submit>
<Popover.Close>Cancel</Popover.Close>
</Button.Group>
<Form.FormError />
</Form>
</Popover>
</Menu.SubmenuTrigger>
</Menu>
</Menu.Trigger>
)
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement)

const button = canvas.getByRole('button', { name: 'Open Menu' })
await userEvent.click(button)

await userEvent.hover(canvas.getByRole('menuitem', { name: 'Edit Secret' }))

const nameInput = await canvas.findByRole('textbox', { name: 'Name' })
await userEvent.type(nameInput, 'John')

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const passwordInput = canvas.getByTestId('password').querySelector('input')!
await userEvent.type(passwordInput, 'abc123sadflmsdkf')

const saveButton = await canvas.findByRole('button', { name: 'Save' })
await userEvent.click(saveButton)
},
}
12 changes: 2 additions & 10 deletions app/gui/src/dashboard/components/AriaComponents/Menu/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { AnimatedBackground } from '../../AnimatedBackground'
import { Popover } from '../Dialog'
import { Separator, SEPARATOR_STYLES, type SeparatorProps } from '../Separator'
import { Text } from '../Text'
import type { Placement, TestIdProps } from '../types'
import type { TestIdProps } from '../types'
import { MenuItem } from './MenuItem'
import { MenuTrigger } from './MenuTrigger'

Expand Down Expand Up @@ -45,7 +45,6 @@ export interface MenuProps<T extends object>
TestIdProps {
readonly variant?: 'dark' | 'light'
readonly className?: string
readonly placement?: Placement
}

/** Props for {@link MenuSection} */
Expand Down Expand Up @@ -91,7 +90,6 @@ export const Menu = createHideableComponent(function Menu<T extends object>(prop
variant,
className,
children,
placement = 'bottom start',
variants = MENU_STYLES,
testId = 'menu',
...menuProps
Expand All @@ -100,13 +98,7 @@ export const Menu = createHideableComponent(function Menu<T extends object>(prop
const styles = variants()

return (
<Popover
variant={variant}
placement={placement}
className={styles.popover()}
size="xxsmall"
rounded="xxxlarge"
>
<Popover variant={variant} className={styles.popover()} size="xxsmall" rounded="xxxlarge">
{() => (
<AnimatedBackground>
<aria.Menu<T> data-testid={testId} className={styles.base({ className })} {...menuProps}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ import { memo, type ReactElement, type ReactNode } from 'react'
import type { MenuItemProps as AriaMenuItemProps, MenuItemRenderProps } from 'react-aria-components'
import { MenuItem as AriaMenuItem, Keyboard } from 'react-aria-components'
import { AnimatedBackground } from '../../AnimatedBackground'
import { Icon } from '../../Icon'
import SvgMask from '../../SvgMask'
import { Check } from '../Check'
import { Text, TEXT_STYLE } from '../Text'
import type { TestIdProps } from '../types'
import type { IconProp, TestIdProps } from '../types'

export const MENU_ITEM_STYLES = tv({
base: 'group flex w-full cursor-default gap-3 rounded-3xl px-[14px] py-1 outline-none transition-colors duration-75 text-left',
Expand Down Expand Up @@ -57,10 +58,7 @@ export type MenuItemProps<T extends object> = MenuItemBaseProps &
*/
export interface MenuItemBaseProps {
/** Icon to display before the menu item text. Can be a string (path to SVG), ReactElement, or a render function */
readonly icon?:
| ReactElement
| string
| ((renderProps: MenuItemRenderProps) => ReactElement | string)
readonly icon?: IconProp<MenuItemRenderProps>
/** Keyboard shortcut text to display */
readonly shortcut?: string
/** Additional class name */
Expand Down Expand Up @@ -166,15 +164,11 @@ interface MenuItemIconProps extends MenuItemRenderProps {
const MenuItemIcon = memo(function MenuItemIcon(props: MenuItemIconProps) {
const { icon, className, ...renderProps } = props

if (icon == null) return null

const iconContent = typeof icon === 'function' ? icon(renderProps) : icon

if (typeof iconContent === 'string') {
return <SvgMask src={iconContent} className={className} />
}

return iconContent
return (
<Icon color="current" renderProps={renderProps} className={className}>
{icon}
</Icon>
)
})

/** Renders the selection indicator for the menu item */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,13 +254,14 @@ const Heading = memo(
}),
)

Text.Heading = Heading

/** Text group component. It's used to visually group text elements together */
Text.Group = function TextGroup(props: React.PropsWithChildren) {
function TextGroup(props: React.PropsWithChildren) {
return (
<textProvider.TextProvider value={{ isInsideTextComponent: true }}>
{props.children}
</textProvider.TextProvider>
)
}

Text.Heading = Heading
Text.Group = TextGroup
Loading
Loading