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

Merged
merged 10 commits into from
Jan 27, 2025
Merged
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
32 changes: 17 additions & 15 deletions .github/workflows/gui-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,10 @@ jobs:
with:
path: |
**/.eslintcache
key: ${{ runner.os }}-gui-${{ github.run_id }}
**/node_modules/.vite
key: ${{ runner.os }}-gui-lint-${{ github.run_id }}
restore-keys: |
${{ runner.os }}-gui
${{ runner.os }}-gui-lint

# Next Tasks are depend on Typecheck, because we build libraries at this stage
- name: ⚙️ Compile
Expand All @@ -68,11 +69,6 @@ jobs:
continue-on-error: true
run: pnpm run ci:lint

- name: 🧪 Unit Tests
id: unit-tests
continue-on-error: true
run: pnpm run ci:unit-test

- name: 📝 Annotate Code Linting Results
if: always()
continue-on-error: true
Expand All @@ -86,14 +82,10 @@ jobs:
fail-on-error: false
fail-on-warning: false

- name: ❌ Fail if any check failed
if: always() && (steps.lint.outcome == 'failure' || steps.compile.outcome == 'failure' || steps.typecheck.outcome == 'failure' || steps.unit-tests.outcome == 'failure')
run: |
echo "Lint outcome: ${{ steps.lint.outcome }}"
echo "Compile outcome: ${{ steps.compile.outcome }}"
echo "Typecheck outcome: ${{ steps.typecheck.outcome }}"
echo "Unit tests outcome: ${{ steps.unit-tests.outcome }}"
exit 1
- name: 🧪 Unit Tests
id: unit-tests
continue-on-error: true
run: pnpm run ci:unit-test

- name: 💾 Save cache
uses: actions/cache/save@v4
Expand All @@ -103,6 +95,16 @@ jobs:
key: ${{ steps.cache.outputs.cache-primary-key }}
path: |
**/.eslintcache
**/node_modules/.vite

- name: ❌ Fail if any check failed
if: always() && (steps.lint.outcome == 'failure' || steps.compile.outcome == 'failure' || steps.typecheck.outcome == 'failure' || steps.unit-tests.outcome == 'failure')
run: |
echo "Lint outcome: ${{ steps.lint.outcome }}"
echo "Compile outcome: ${{ steps.compile.outcome }}"
echo "Typecheck outcome: ${{ steps.typecheck.outcome }}"
echo "Unit tests outcome: ${{ steps.unit-tests.outcome }}"
exit 1

playwright:
name: 🎭 Playwright Tests
Expand Down
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.9",
"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
Loading
Loading