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

New icon component #12091

Merged
merged 15 commits into from
Feb 3, 2025
11 changes: 11 additions & 0 deletions app/gui/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,3 +199,14 @@ declare global {
(message: string, projectId?: string | null, metadata?: object | null): void
}
}

// Add additional types for svg imports from `#/assets/*.svg`
declare module 'vite/client' {
declare module '#/assets/*.svg' {
/**
* @deprecated Prefer defined keys over importing from `#/assets/*.svg
*/
const src: string
export default src
}
}
2 changes: 1 addition & 1 deletion app/gui/scripts/generateIconMetadata.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ await fs.writeFile(
// Please run \`bazel run //:write_all\` to regenerate this file whenever \`icons.svg\` is changed.

/** All icon names present in icons.svg. */
const iconNames = [
export const iconNames = [
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added export here to have a canvas with all icons in Storybook

${iconNames?.map((name) => ` '${name}',`).join('\n')}
] as const

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { expect, userEvent, within } from '@storybook/test'
import { Button, type BaseButtonProps } from '.'
import { Badge } from '../../Badge'

type Story = StoryObj<BaseButtonProps<aria.ButtonRenderProps>>
type Story = StoryObj<BaseButtonProps<string, aria.ButtonRenderProps>>

const variants = [
'primary',
Expand Down Expand Up @@ -40,7 +40,7 @@ export default {
addonStart: { control: false },
addonEnd: { control: false },
},
} as Meta<BaseButtonProps<aria.ButtonRenderProps>>
} satisfies Meta<BaseButtonProps<string, aria.ButtonRenderProps>>

export const Variants: Story = {
render: () => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ const ICON_LOADER_DELAY = 150
// Manually casting types to make TS infer the final type correctly (e.g. RenderProps in icon)
// eslint-disable-next-line no-restricted-syntax
export const Button = memo(
forwardRef(function Button(props: ButtonProps, ref: ForwardedRef<HTMLButtonElement>) {
forwardRef(function Button<IconType extends string>(
props: ButtonProps<IconType>,
ref: ForwardedRef<HTMLButtonElement>,
) {
props = useMergedButtonStyles(props)
const {
className,
Expand Down Expand Up @@ -252,7 +255,9 @@ export const Button = memo(
</TooltipTrigger>
)
}),
) as unknown as ((props: ButtonProps & { ref?: ForwardedRef<HTMLButtonElement> }) => ReactNode) & {
) as unknown as (<IconType extends string>(
props: ButtonProps<IconType> & { ref?: ForwardedRef<HTMLButtonElement> },
) => ReactNode) & {
// eslint-disable-next-line @typescript-eslint/naming-convention
Group: typeof ButtonGroup
// eslint-disable-next-line @typescript-eslint/naming-convention
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,15 @@ import { Button } from './Button'
import type { ButtonProps } from './types'

/** Props for a {@link CloseButton}. */
export type CloseButtonProps = Omit<ButtonProps, 'children' | 'rounding' | 'size' | 'variant'>
export type CloseButtonProps<IconType extends string> = Omit<
ButtonProps<IconType>,
'children' | 'rounding' | 'size' | 'variant'
>

/** A styled button with a close icon that appears on hover. */
export const CloseButton = memo(function CloseButton(props: CloseButtonProps) {
export const CloseButton = memo(function CloseButton<IconType extends string>(
props: CloseButtonProps<IconType>,
) {
const { getText } = useText()

const {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ import type { ButtonProps } from './types'
// ==================

/** Props for a {@link CopyButton}. */
export interface CopyButtonProps extends Omit<ButtonProps, 'icon' | 'loading' | 'onPress'> {
export interface CopyButtonProps<IconType extends string>
extends Omit<ButtonProps<IconType>, 'icon' | 'loading' | 'onPress'> {
/** The text to copy to the clipboard. */
readonly copyText: string
/**
Expand All @@ -38,7 +39,7 @@ export interface CopyButtonProps extends Omit<ButtonProps, 'icon' | 'loading' |
}

/** A button that copies text to the clipboard. */
export function CopyButton(props: CopyButtonProps) {
export function CopyButton<IconType extends string>(props: CopyButtonProps<IconType>) {
const {
variant = 'icon',
copyIcon = CopyIcon,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ export interface LinkRenderProps extends aria.LinkRenderProps {
}

/** Props for a Button. */
export type ButtonProps =
| (BaseButtonProps<ButtonRenderProps> &
export type ButtonProps<IconType extends string = string> =
| (BaseButtonProps<IconType, ButtonRenderProps> &
Omit<aria.ButtonProps, 'children' | 'isPending' | 'onPress'> &
PropsWithoutHref)
| (BaseButtonProps<LinkRenderProps> &
| (BaseButtonProps<IconType, LinkRenderProps> &
Omit<aria.LinkProps, 'children' | 'onPress'> &
PropsWithHref)

Expand All @@ -61,7 +61,7 @@ interface PropsWithoutHref {
}

/** Base props for a button. */
export interface BaseButtonProps<Render>
export interface BaseButtonProps<IconType extends string, Render>
extends Omit<ButtonVariants, 'iconOnly' | 'isJoined' | 'position'>,
TestIdProps {
/** If `true`, the loader will not be shown. */
Expand All @@ -70,7 +70,7 @@ export interface BaseButtonProps<Render>
readonly tooltip?: ReactElement | string | false | null
readonly tooltipPlacement?: aria.Placement
/** The icon to display in the button */
readonly icon?: IconProp<Render>
readonly icon?: IconProp<IconType, Render>
/** When `true`, icon will be shown only when hovered. */
readonly showIconOnHover?: boolean
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ import { type ButtonProps, Button } from '../Button'
import * as dialogProvider from './DialogProvider'

/** Props for {@link Close} component. */
export type CloseProps = ButtonProps
export type CloseProps<IconType extends string> = ButtonProps<IconType>

/** Close button for a dialog. */
export function Close(props: CloseProps) {
export function Close<IconType extends string>(props: CloseProps<IconType>) {
const dialogContext = dialogProvider.useDialogContext()

invariant(dialogContext, 'Close must be used inside a DialogProvider')

const onPressCallback = useEventCallback<NonNullable<ButtonProps['onPress']>>((event) => {
const onPressCallback = useEventCallback<NonNullable<ButtonProps<IconType>['onPress']>>((event) => {
dialogContext.close()
return props.onPress?.(event)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -573,5 +573,6 @@ const DialogHeader = React.memo(function DialogHeader(props: DialogHeaderProps)
})

Dialog.Close = Close
/** @deprecated Use {@link Dialog.Close} instead. */
Dialog.Dismiss = DialogDismiss
Dialog.Trigger = DialogTrigger
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,21 @@ import { Button, type ButtonProps } from '../Button'
import { useDialogContext } from './DialogProvider'

/** Additional props for the Cancel component. */
interface DialogDismissBaseProps {
readonly variant?: ButtonProps['variant']
interface DialogDismissBaseProps<IconType extends string> {
readonly variant?: ButtonProps<IconType>['variant']
}

/** Props for a {@link DialogDismiss}. */
export type DialogDismissProps = DialogDismissBaseProps &
Omit<ButtonProps, 'formnovalidate' | 'href' | 'variant'>
export type DialogDismissProps<IconType extends string> = DialogDismissBaseProps<IconType> &
Omit<ButtonProps<IconType>, 'formnovalidate' | 'href' | 'variant'>

/** Dismiss button for dialogs. */
export function DialogDismiss(props: DialogDismissProps): JSX.Element {
/**
* Dismiss button for dialogs.
* @deprecated Use {@link Close} instead.
*/
export function DialogDismiss<IconType extends string>(
props: DialogDismissProps<IconType>,
): JSX.Element {
const { getText } = useText()

const { size = 'medium', ...buttonProps } = props
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import * as formContext from './FormProvider'
import type * as types from './types'

/** Props for the Reset component. */
export interface ResetProps extends Omit<ButtonProps, 'href' | 'loading'> {
export interface ResetProps<IconType extends string>
extends Omit<ButtonProps<IconType>, 'href' | 'loading'> {
/**
* Connects the reset button to a form.
* If not provided, the button will use the nearest form context.
Expand All @@ -20,7 +21,7 @@ export interface ResetProps extends Omit<ButtonProps, 'href' | 'loading'> {
}

/** Reset button for forms. */
export function Reset(props: ResetProps): React.JSX.Element {
export function Reset<IconType extends string>(props: ResetProps<IconType>): React.JSX.Element {
const { getText } = useText()
const {
variant = 'outline',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import { useFormContext } from './FormProvider'
import type { FormInstance } from './types'

/** Additional props for the Submit component. */
interface SubmitButtonBaseProps {
readonly variant?: ButtonProps['variant']
interface SubmitButtonBaseProps<IconType extends string> {
readonly variant?: ButtonProps<IconType>['variant']
/**
* Connects the submit button to a form.
* If not provided, the button will use the nearest form context.
Expand All @@ -27,15 +27,18 @@ interface SubmitButtonBaseProps {
}

/** Props for the Submit component. */
export type SubmitProps = Omit<ButtonProps, 'formnovalidate' | 'href' | 'variant'> &
SubmitButtonBaseProps
export type SubmitProps<IconType extends string> = Omit<
ButtonProps<IconType>,
'formnovalidate' | 'href' | 'variant'
> &
SubmitButtonBaseProps<IconType>

/**
* Submit button for forms.
*
* Manages the form state and displays a loading spinner when the form is submitting.
*/
export function Submit(props: SubmitProps): JSX.Element {
export function Submit<IconType extends string>(props: SubmitProps<IconType>): JSX.Element {
const { getText } = useText()

const {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const MENU_ITEM_STYLES = tv({
})

/** Props for {@link MenuItem} */
export type MenuItemProps<T extends object> = MenuItemBaseProps &
export type MenuItemProps<T extends object, IconType extends string> = MenuItemBaseProps<IconType> &
Omit<AriaMenuItemProps<T>, 'children'> &
TestIdProps &
VariantProps<typeof MENU_ITEM_STYLES> &
Expand All @@ -56,9 +56,9 @@ export type MenuItemProps<T extends object> = MenuItemBaseProps &
/**
* Base props for the menu item.
*/
export interface MenuItemBaseProps {
export interface MenuItemBaseProps<IconType extends string> {
/** Icon to display before the menu item text. Can be a string (path to SVG), ReactElement, or a render function */
readonly icon?: IconProp<MenuItemRenderProps>
readonly icon?: IconProp<IconType, MenuItemRenderProps>
/** Keyboard shortcut text to display */
readonly shortcut?: string
/** Additional class name */
Expand Down Expand Up @@ -87,7 +87,9 @@ export interface MenuItemCustomContentProps {
/**
* An item within a menu that represents a single action or option.
*/
export const MenuItem = memo(function MenuItem<T extends object>(props: MenuItemProps<T>) {
export const MenuItem = memo(function MenuItem<T extends object, IconType extends string>(
props: MenuItemProps<T, IconType>,
) {
const {
icon,
shortcut,
Expand Down Expand Up @@ -154,14 +156,16 @@ export const MenuItem = memo(function MenuItem<T extends object>(props: MenuItem
})

/** Props for {@link MenuItemIcon} */
interface MenuItemIconProps extends MenuItemRenderProps {
readonly icon: MenuItemProps<object>['icon']
interface MenuItemIconProps<IconType extends string> extends MenuItemRenderProps {
readonly icon: MenuItemProps<object, IconType>['icon']
readonly className?: string
}

/** Renders the icon for the menu item */
// eslint-disable-next-line no-restricted-syntax
const MenuItemIcon = memo(function MenuItemIcon(props: MenuItemIconProps) {
const MenuItemIcon = memo(function MenuItemIcon<IconType extends string>(
props: MenuItemIconProps<IconType>,
) {
const { icon, className, ...renderProps } = props

return (
Expand Down
43 changes: 39 additions & 4 deletions app/gui/src/dashboard/components/AriaComponents/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/** @file Common types for ARIA components. */
import type { Icon as PossibleIcon } from '@/util/iconMetadata/iconName'
import type { ReactElement } from 'react'

import type * as reactAria from 'react-aria'

/** Props for adding a test id to a component */
Expand All @@ -16,16 +18,49 @@ export interface TestIdProps {
export type Placement = reactAria.Placement

/**
* Generic type for any icon
* Type for any icon
*/
export type IconProp<Icon extends string = string, Render = never> =
| IconPropSvgUse<Render>
| LegacyIconProp<Icon, Render>

/**
* A type that represents the possible return values for a legacy icon.
*/
export type IconProp<Render> =
export type LegacyAvialableIconReturn<Icon extends string> =
| LegacyIcon<Icon>
| ReactElement
| string
| false
| ((render: Render) => ReactElement | string | false | null | undefined)
| null
| undefined

/**
* A type that represents the possible return values for a legacy icon.
*/
export type AvailableIconReturn = ReactElement | SvgUseIcon | false | null | undefined

/**
* Generic type for any icon
* @deprecated Prefer defined keys over importing from `#/assets/*.svg
*/
export type LegacyIconProp<Icon extends string, Render> =
| LegacyAvialableIconReturn<Icon>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be Available

| ((render: Render) => LegacyAvialableIconReturn<Icon>)

/**
* Generic type for imported from figma icons
*/
export type IconPropSvgUse<Render> = AvailableIconReturn | ((render: Render) => AvailableIconReturn)

/**
* @deprecated
*/
export type LegacyIcon<T extends string> = Exclude<T, PossibleIcon> & {}

/**
* Type for any icon imported from figma
*/
export type SvgUseIcon = PossibleIcon
/**
* Generic type for any addon
*/
Expand Down
Loading
Loading