diff --git a/app/ide-desktop/lib/dashboard/e2e/actions.ts b/app/ide-desktop/lib/dashboard/e2e/actions.ts index 30bfed7a848..06db58650b1 100644 --- a/app/ide-desktop/lib/dashboard/e2e/actions.ts +++ b/app/ide-desktop/lib/dashboard/e2e/actions.ts @@ -194,52 +194,52 @@ export function locateAssetLabels(page: test.Locator | test.Page) { /** Find a toggle for the "Name" column (if any) on the current page. */ export function locateNameColumnToggle(page: test.Locator | test.Page) { - return page.getByAltText(/^(?:Show|Hide) Name$/) + return page.getByAltText('Name') } /** Find a toggle for the "Modified" column (if any) on the current page. */ export function locateModifiedColumnToggle(page: test.Locator | test.Page) { - return page.getByAltText(/^(?:Show|Hide) Modified date column$/) + return page.getByAltText('Modified') } /** Find a toggle for the "Shared with" column (if any) on the current page. */ export function locateSharedWithColumnToggle(page: test.Locator | test.Page) { - return page.getByAltText(/^(?:Show|Hide) Shared with column$/) + return page.getByAltText('Shared With') } /** Find a toggle for the "Labels" column (if any) on the current page. */ export function locateLabelsColumnToggle(page: test.Locator | test.Page) { - return page.getByAltText(/^(?:Show|Hide) Labels column$/) + return page.getByAltText('Labels') } /** Find a toggle for the "Accessed by projects" column (if any) on the current page. */ export function locateAccessedByProjectsColumnToggle(page: test.Locator | test.Page) { - return page.getByAltText(/^(?:Show|Hide) Accessed by projects column$/) + return page.getByAltText('Accessed By Projects') } /** Find a toggle for the "Accessed data" column (if any) on the current page. */ export function locateAccessedDataColumnToggle(page: test.Locator | test.Page) { - return page.getByAltText(/^(?:Show|Hide) Accessed data column$/) + return page.getByAltText('Accessed Data') } /** Find a toggle for the "Docs" column (if any) on the current page. */ export function locateDocsColumnToggle(page: test.Locator | test.Page) { - return page.getByAltText(/^(?:Show|Hide) Docs column$/) + return page.getByAltText('Docs') } /** Find a button for the "Recent" category (if any) on the current page. */ export function locateRecentCategory(page: test.Locator | test.Page) { - return page.getByLabel('Go To Recent category') + return page.getByLabel('Recent').locator('visible=true') } /** Find a button for the "Home" category (if any) on the current page. */ export function locateHomeCategory(page: test.Locator | test.Page) { - return page.getByLabel('Go To Home category') + return page.getByLabel('Home').locator('visible=true') } /** Find a button for the "Trash" category (if any) on the current page. */ export function locateTrashCategory(page: test.Locator | test.Page) { - return page.getByLabel('Go To Trash category') + return page.getByLabel('Trash').locator('visible=true') } // === Context menu buttons === @@ -363,30 +363,27 @@ export function locateUpgradeButton(page: test.Locator | test.Page) { /** Find a "new folder" icon (if any) on the current page. */ export function locateNewFolderIcon(page: test.Locator | test.Page) { - return page.getByAltText('New Folder') + return page.getByRole('button').filter({ has: page.getByAltText('New Folder') }) } /** Find a "new secret" icon (if any) on the current page. */ export function locateNewSecretIcon(page: test.Locator | test.Page) { - return page.getByAltText('New Secret') + return page.getByRole('button').filter({ has: page.getByAltText('New Secret') }) } -/** Find a "upload files" icon (if any) on the current page. */ +/** Find an "upload files" icon (if any) on the current page. */ export function locateUploadFilesIcon(page: test.Locator | test.Page) { - return page.getByAltText('Upload Files') + return page.getByRole('button').filter({ has: page.getByAltText('Import') }) } /** Find a "download files" icon (if any) on the current page. */ export function locateDownloadFilesIcon(page: test.Locator | test.Page) { - return page.getByAltText('Download Files') + return page.getByRole('button').filter({ has: page.getByAltText('Export') }) } /** Find an icon to open or close the asset panel (if any) on the current page. */ export function locateAssetPanelIcon(page: test.Locator | test.Page) { - return page - .getByAltText('Open Asset Panel') - .or(page.getByAltText('Close Asset Panel')) - .locator('visible=true') + return page.getByAltText('Asset Panel').locator('visible=true') } /** Find a list of tags in the search bar (if any) on the current page. */ @@ -423,22 +420,22 @@ export function locateSortDescendingIcon(page: test.Locator | test.Page) { /** Find a "home page" icon (if any) on the current page. */ export function locateHomePageIcon(page: test.Locator | test.Page) { - return page.getByAltText('Home tab') + return page.getByRole('button').filter({ has: page.getByAltText('Home') }) } /** Find a "drive page" icon (if any) on the current page. */ export function locateDrivePageIcon(page: test.Locator | test.Page) { - return page.getByAltText('Drive tab') + return page.getByRole('button').filter({ has: page.getByAltText('Catalog') }) } /** Find an "editor page" icon (if any) on the current page. */ export function locateEditorPageIcon(page: test.Locator | test.Page) { - return page.getByAltText('Project tab') + return page.getByRole('button').filter({ has: page.getByAltText('Graph Editor') }) } /** Find a "settings page" icon (if any) on the current page. */ export function locateSettingsPageIcon(page: test.Locator | test.Page) { - return page.getByAltText('Settings tab') + return page.getByRole('button').filter({ has: page.getByAltText('Settings') }) } /** Find a "name" column heading (if any) on the current page. */ diff --git a/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Button/Button.tsx b/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Button/Button.tsx index a3598e72860..f52c2b33105 100644 --- a/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Button/Button.tsx +++ b/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Button/Button.tsx @@ -7,6 +7,7 @@ import * as tailwindMerge from 'tailwind-merge' import * as focusHooks from '#/hooks/focusHooks' import * as aria from '#/components/aria' +import * as ariaComponents from '#/components/AriaComponents' import Spinner, * as spinnerModule from '#/components/Spinner' import SvgMask from '#/components/SvgMask' @@ -16,6 +17,8 @@ import SvgMask from '#/components/SvgMask' /** Props for a {@link Button}. */ export interface ButtonProps extends Readonly { + /** Falls back to `aria-label`. Pass `false` to explicitly disable the tooltip. */ + readonly tooltip?: React.ReactNode readonly loading?: boolean readonly variant: 'cancel' | 'delete' | 'icon' | 'submit' readonly icon?: string @@ -46,9 +49,11 @@ const CLASSES_FOR_VARIANT: Record = { /** A button allows a user to perform an action, with mouse, touch, and keyboard interactions. */ export function Button(props: ButtonProps) { - const { className, children, variant, icon, loading = false, ...ariaButtonProps } = props + const { tooltip, className, children, variant, icon, loading = false, ...ariaButtonProps } = props const focusChildProps = focusHooks.useFocusChild() + const tooltipElement = tooltip === false ? null : tooltip ?? ariaButtonProps['aria-label'] + const classes = clsx( DEFAULT_CLASSES, DISABLED_CLASSES, @@ -71,7 +76,7 @@ export function Button(props: ButtonProps) { } } - return ( + const button = ( ()(ariaButtonProps, focusChildProps, { className: values => @@ -84,4 +89,13 @@ export function Button(props: ButtonProps) { {childrenFactory()} ) + + return tooltipElement == null ? ( + button + ) : ( + + {button} + {tooltipElement} + + ) } diff --git a/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Tooltip/Tooltip.tsx b/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Tooltip/Tooltip.tsx index 5252b2d2f25..c8081da6b73 100644 --- a/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Tooltip/Tooltip.tsx +++ b/app/ide-desktop/lib/dashboard/src/components/AriaComponents/Tooltip/Tooltip.tsx @@ -9,7 +9,7 @@ import * as portal from '#/components/Portal' // ================= const DEFAULT_CLASSES = - 'flex bg-frame backdrop-blur-default text-primary p-2 rounded-default shadow-soft text-xs' + 'flex bg-frame outline outline-2 outline-primary backdrop-blur-default text-primary px-2 leading-cozy min-h-6 rounded-default shadow-soft text-xs' const DEFAULT_CONTAINER_PADDING = 4 const DEFAULT_OFFSET = 4 diff --git a/app/ide-desktop/lib/dashboard/src/components/SvgMask.tsx b/app/ide-desktop/lib/dashboard/src/components/SvgMask.tsx index 74739bb2977..a62d274b4d3 100644 --- a/app/ide-desktop/lib/dashboard/src/components/SvgMask.tsx +++ b/app/ide-desktop/lib/dashboard/src/components/SvgMask.tsx @@ -1,6 +1,8 @@ /** @file File containing SVG icon definitions. */ import * as React from 'react' +import * as tailwindMerge from 'tailwind-merge' + // =============== // === SvgMask === // =============== @@ -20,19 +22,16 @@ export interface SvgMaskProps { // underlying `div`. // eslint-disable-next-line no-restricted-syntax readonly className?: string | undefined - readonly onClick?: (event: React.MouseEvent) => void } /** Use an SVG as a mask. This lets the SVG use the text color (`currentColor`). */ export default function SvgMask(props: SvgMaskProps) { - const { invert = false, alt, src, title, style, color, className, onClick } = props + const { invert = false, alt, src, style, color, className } = props const urlSrc = `url(${JSON.stringify(src)})` const mask = invert ? `${urlSrc}, linear-gradient(white 0 0)` : urlSrc return (
{/* This is required for this component to have the right size. */} {alt} diff --git a/app/ide-desktop/lib/dashboard/src/components/UnstyledButton.tsx b/app/ide-desktop/lib/dashboard/src/components/UnstyledButton.tsx index 98cd6ac855c..fb912d01595 100644 --- a/app/ide-desktop/lib/dashboard/src/components/UnstyledButton.tsx +++ b/app/ide-desktop/lib/dashboard/src/components/UnstyledButton.tsx @@ -4,6 +4,7 @@ import * as React from 'react' import * as focusHooks from '#/hooks/focusHooks' import * as aria from '#/components/aria' +import * as ariaComponents from '#/components/AriaComponents' import type * as focusRing from '#/components/styled/FocusRing' import FocusRing from '#/components/styled/FocusRing' @@ -15,6 +16,8 @@ import FocusRing from '#/components/styled/FocusRing' export interface UnstyledButtonProps extends Readonly { // eslint-disable-next-line @typescript-eslint/naming-convention readonly 'aria-label'?: string + /** Falls back to `aria-label`. Pass `false` to explicitly disable the tooltip. */ + readonly tooltip?: React.ReactNode readonly focusRingPlacement?: focusRing.FocusRingPlacement readonly autoFocus?: boolean /** When `true`, the button is not clickable. */ @@ -26,10 +29,12 @@ export interface UnstyledButtonProps extends Readonly { /** An unstyled button with a focus ring and focus movement behavior. */ function UnstyledButton(props: UnstyledButtonProps, ref: React.ForwardedRef) { - const { focusRingPlacement, children, ...buttonProps } = props + const { tooltip, focusRingPlacement, children, ...buttonProps } = props const focusChildProps = focusHooks.useFocusChild() - return ( + const tooltipElement = tooltip === false ? null : tooltip ?? buttonProps['aria-label'] + + const button = ( >()( @@ -42,6 +47,15 @@ function UnstyledButton(props: UnstyledButtonProps, ref: React.ForwardedRef ) + + return tooltipElement == null ? ( + button + ) : ( + + {button} + {tooltipElement} + + ) } export default React.forwardRef(UnstyledButton) diff --git a/app/ide-desktop/lib/dashboard/src/components/dashboard/DirectoryNameColumn.tsx b/app/ide-desktop/lib/dashboard/src/components/dashboard/DirectoryNameColumn.tsx index 64b73cec52f..a4ec1eec916 100644 --- a/app/ide-desktop/lib/dashboard/src/components/dashboard/DirectoryNameColumn.tsx +++ b/app/ide-desktop/lib/dashboard/src/components/dashboard/DirectoryNameColumn.tsx @@ -17,6 +17,7 @@ import AssetListEventType from '#/events/AssetListEventType' import type * as column from '#/components/dashboard/column' import EditableSpan from '#/components/EditableSpan' +import Button from '#/components/styled/Button' import SvgMask from '#/components/SvgMask' import * as backendModule from '#/services/Backend' @@ -158,14 +159,13 @@ export default function DirectoryNameColumn(props: DirectoryNameColumnProps) { } }} > -