Add tooltips (#9899)

- Dashboard side of #9828
- Add tooltips for elements rendered by the Dashboard

# Important Notes
- The UI for the tooltips can be changed at any time.
This commit is contained in:
somebody1234 2024-05-10 18:17:06 +10:00 committed by GitHub
parent 720d32cbe3
commit 35571f64ba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 214 additions and 165 deletions

View File

@ -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. */

View File

@ -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<aria.ButtonProps> {
/** 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<ButtonProps['variant'], string> = {
/** 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 = (
<aria.Button
{...aria.mergeProps<aria.ButtonProps>()(ariaButtonProps, focusChildProps, {
className: values =>
@ -84,4 +89,13 @@ export function Button(props: ButtonProps) {
{childrenFactory()}
</aria.Button>
)
return tooltipElement == null ? (
button
) : (
<ariaComponents.TooltipTrigger>
{button}
<ariaComponents.Tooltip>{tooltipElement}</ariaComponents.Tooltip>
</ariaComponents.TooltipTrigger>
)
}

View File

@ -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

View File

@ -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 (
<div
{...(onClick == null ? {} : { role: 'button' })}
title={title}
style={{
...(style ?? {}),
backgroundColor: color ?? 'currentcolor',
@ -50,10 +49,7 @@ export default function SvgMask(props: SvgMaskProps) {
...(invert ? { WebkitMaskComposite: 'exclude, exclude' } : {}),
/* eslint-enable @typescript-eslint/naming-convention */
}}
className={`inline-block ${onClick != null ? 'cursor-pointer' : ''} ${
className ?? 'h-max w-max'
}`}
onClick={onClick}
className={tailwindMerge.twMerge('inline-block h-max w-max', className)}
>
{/* This is required for this component to have the right size. */}
<img alt={alt} src={src} className="transparent" draggable={false} />

View File

@ -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<React.PropsWithChildren> {
// 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<React.PropsWithChildren> {
/** An unstyled button with a focus ring and focus movement behavior. */
function UnstyledButton(props: UnstyledButtonProps, ref: React.ForwardedRef<HTMLButtonElement>) {
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 = (
<FocusRing {...(focusRingPlacement == null ? {} : { placement: focusRingPlacement })}>
<aria.Button
{...aria.mergeProps<aria.ButtonProps & React.RefAttributes<HTMLButtonElement>>()(
@ -42,6 +47,15 @@ function UnstyledButton(props: UnstyledButtonProps, ref: React.ForwardedRef<HTML
</aria.Button>
</FocusRing>
)
return tooltipElement == null ? (
button
) : (
<ariaComponents.TooltipTrigger>
{button}
<ariaComponents.Tooltip>{tooltipElement}</ariaComponents.Tooltip>
</ariaComponents.TooltipTrigger>
)
}
export default React.forwardRef(UnstyledButton)

View File

@ -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) {
}
}}
>
<SvgMask
src={FolderArrowIcon}
<Button
image={FolderArrowIcon}
alt={item.children == null ? getText('expand') : getText('collapse')}
className={`m-name-column-icon hidden size-icon cursor-pointer transition-transform duration-arrow group-hover:inline-block ${
item.children != null ? 'rotate-90' : ''
}`}
onClick={event => {
event.stopPropagation()
onPress={() => {
doToggleDirectoryExpansion(asset.id, item.key, asset.title)
}}
/>

View File

@ -8,7 +8,7 @@ import * as textProvider from '#/providers/TextProvider'
import * as aria from '#/components/aria'
import type * as column from '#/components/dashboard/column'
import * as columnUtils from '#/components/dashboard/column/columnUtils'
import SvgMask from '#/components/SvgMask'
import Button from '#/components/styled/Button'
/** A heading for the "Accessed by projects" column. */
export default function AccessedByProjectsColumnHeading(props: column.AssetColumnHeadingProps) {
@ -18,12 +18,12 @@ export default function AccessedByProjectsColumnHeading(props: column.AssetColum
return (
<div className="flex h-drive-table-heading w-full items-center gap-icon-with-text">
<SvgMask
src={AccessedByProjectsIcon}
<Button
active
image={AccessedByProjectsIcon}
className="size-icon"
title={getText('accessedByProjectsColumnHide')}
onClick={event => {
event.stopPropagation()
alt={getText('accessedByProjectsColumnHide')}
onPress={() => {
hideColumn(columnUtils.Column.accessedByProjects)
}}
/>

View File

@ -8,7 +8,7 @@ import * as textProvider from '#/providers/TextProvider'
import * as aria from '#/components/aria'
import type * as column from '#/components/dashboard/column'
import * as columnUtils from '#/components/dashboard/column/columnUtils'
import SvgMask from '#/components/SvgMask'
import Button from '#/components/styled/Button'
/** A heading for the "Accessed data" column. */
export default function AccessedDataColumnHeading(props: column.AssetColumnHeadingProps) {
@ -18,12 +18,12 @@ export default function AccessedDataColumnHeading(props: column.AssetColumnHeadi
return (
<div className="flex h-drive-table-heading w-full items-center gap-icon-with-text">
<SvgMask
src={AccessedDataIcon}
<Button
active
image={AccessedDataIcon}
className="size-icon"
title={getText('accessedDataColumnHide')}
onClick={event => {
event.stopPropagation()
alt={getText('accessedDataColumnHide')}
onPress={() => {
hideColumn(columnUtils.Column.accessedData)
}}
/>

View File

@ -8,7 +8,7 @@ import * as textProvider from '#/providers/TextProvider'
import * as aria from '#/components/aria'
import type * as column from '#/components/dashboard/column'
import * as columnUtils from '#/components/dashboard/column/columnUtils'
import SvgMask from '#/components/SvgMask'
import Button from '#/components/styled/Button'
/** A heading for the "Docs" column. */
export default function DocsColumnHeading(props: column.AssetColumnHeadingProps) {
@ -18,12 +18,12 @@ export default function DocsColumnHeading(props: column.AssetColumnHeadingProps)
return (
<div className="flex h-drive-table-heading w-full items-center gap-icon-with-text">
<SvgMask
src={DocsIcon}
<Button
active
image={DocsIcon}
className="size-icon"
title={getText('docsColumnHide')}
onClick={event => {
event.stopPropagation()
alt={getText('docsColumnHide')}
onPress={() => {
hideColumn(columnUtils.Column.docs)
}}
/>

View File

@ -8,7 +8,7 @@ import * as textProvider from '#/providers/TextProvider'
import * as aria from '#/components/aria'
import type * as column from '#/components/dashboard/column'
import * as columnUtils from '#/components/dashboard/column/columnUtils'
import SvgMask from '#/components/SvgMask'
import Button from '#/components/styled/Button'
/** A heading for the "Labels" column. */
export default function LabelsColumnHeading(props: column.AssetColumnHeadingProps) {
@ -18,12 +18,12 @@ export default function LabelsColumnHeading(props: column.AssetColumnHeadingProp
return (
<div className="flex h-drive-table-heading w-full items-center gap-icon-with-text">
<SvgMask
src={TagIcon}
<Button
active
image={TagIcon}
className="size-icon"
title={getText('labelsColumnHide')}
onClick={event => {
event.stopPropagation()
alt={getText('labelsColumnHide')}
onPress={() => {
hideColumn(columnUtils.Column.labels)
}}
/>

View File

@ -9,7 +9,7 @@ import * as textProvider from '#/providers/TextProvider'
import * as aria from '#/components/aria'
import type * as column from '#/components/dashboard/column'
import * as columnUtils from '#/components/dashboard/column/columnUtils'
import SvgMask from '#/components/SvgMask'
import Button from '#/components/styled/Button'
import UnstyledButton from '#/components/UnstyledButton'
import * as sorting from '#/utilities/sorting'
@ -23,7 +23,7 @@ export default function ModifiedColumnHeading(props: column.AssetColumnHeadingPr
const isDescending = sortInfo?.direction === sorting.SortDirection.descending
return (
<UnstyledButton
<div
aria-label={
!isSortActive
? getText('sortByModificationDate')
@ -32,34 +32,38 @@ export default function ModifiedColumnHeading(props: column.AssetColumnHeadingPr
: getText('sortByModificationDateDescending')
}
className="group flex h-drive-table-heading w-full cursor-pointer items-center gap-icon-with-text"
onPress={() => {
const nextDirection = isSortActive
? sorting.nextSortDirection(sortInfo.direction)
: sorting.SortDirection.ascending
if (nextDirection == null) {
setSortInfo(null)
} else {
setSortInfo({ field: columnUtils.Column.modified, direction: nextDirection })
}
}}
>
<SvgMask
src={TimeIcon}
<Button
active
image={TimeIcon}
className="size-icon"
title={getText('modifiedColumnHide')}
onClick={event => {
event.stopPropagation()
alt={getText('modifiedColumnHide')}
onPress={() => {
hideColumn(columnUtils.Column.modified)
}}
/>
<aria.Text className="text-header">{getText('modifiedColumnName')}</aria.Text>
<img
alt={isDescending ? getText('sortDescending') : getText('sortAscending')}
src={SortAscendingIcon}
className={`transition-all duration-arrow ${
isSortActive ? 'selectable active' : 'transparent group-hover:selectable'
} ${isDescending ? 'rotate-180' : ''}`}
/>
</UnstyledButton>
<UnstyledButton
className="flex grow gap-icon-with-text"
onPress={() => {
const nextDirection = isSortActive
? sorting.nextSortDirection(sortInfo.direction)
: sorting.SortDirection.ascending
if (nextDirection == null) {
setSortInfo(null)
} else {
setSortInfo({ field: columnUtils.Column.modified, direction: nextDirection })
}
}}
>
<aria.Text className="text-header">{getText('modifiedColumnName')}</aria.Text>
<img
alt={isDescending ? getText('sortDescending') : getText('sortAscending')}
src={SortAscendingIcon}
className={`transition-all duration-arrow ${
isSortActive ? 'selectable active' : 'transparent group-hover:selectable'
} ${isDescending ? 'rotate-180' : ''}`}
/>
</UnstyledButton>
</div>
)
}

View File

@ -8,7 +8,7 @@ import * as textProvider from '#/providers/TextProvider'
import * as aria from '#/components/aria'
import type * as column from '#/components/dashboard/column'
import * as columnUtils from '#/components/dashboard/column/columnUtils'
import SvgMask from '#/components/SvgMask'
import Button from '#/components/styled/Button'
/** A heading for the "Shared with" column. */
export default function SharedWithColumnHeading(props: column.AssetColumnHeadingProps) {
@ -18,12 +18,12 @@ export default function SharedWithColumnHeading(props: column.AssetColumnHeading
return (
<div className="flex h-drive-table-heading w-full items-center gap-icon-with-text">
<SvgMask
src={PeopleIcon}
<Button
active
image={PeopleIcon}
className="size-icon"
title={getText('sharedWithColumnHide')}
onClick={event => {
event.stopPropagation()
alt={getText('sharedWithColumnHide')}
onPress={() => {
hideColumn(columnUtils.Column.sharedWith)
}}
/>

View File

@ -1,9 +1,12 @@
/** @file A styled button. */
import * as React from 'react'
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 FocusRing from '#/components/styled/FocusRing'
import SvgMask from '#/components/SvgMask'
@ -13,6 +16,8 @@ import SvgMask from '#/components/SvgMask'
/** Props for a {@link Button}. */
export interface ButtonProps {
/** Falls back to `aria-label`. Pass `false` to explicitly disable the tooltip. */
readonly tooltip?: React.ReactNode
readonly autoFocus?: boolean
/** When `true`, the button is not faded out even when not hovered. */
readonly active?: boolean
@ -25,7 +30,10 @@ export interface ButtonProps {
readonly alt?: string
/** A title that is only shown when `disabled` is `true`. */
readonly error?: string | null
/** Class names for the icon itself. */
readonly className?: string
/** Extra class names for the `button` element wrapping the icon.
* This is useful for things like positioning the entire button (e.g. `absolute`). */
readonly buttonClassName?: string
readonly onPress: (event: aria.PressEvent) => void
}
@ -33,31 +41,31 @@ export interface ButtonProps {
/** A styled button. */
function Button(props: ButtonProps, ref: React.ForwardedRef<HTMLButtonElement>) {
const {
tooltip,
active = false,
softDisabled = false,
image,
error,
alt,
className,
buttonClassName = '',
buttonClassName,
...buttonProps
} = props
const { isDisabled = false } = buttonProps
const focusChildProps = focusHooks.useFocusChild()
return (
const tooltipElement = tooltip === false ? null : tooltip ?? alt
const button = (
<FocusRing placement="after">
<aria.Button
{...aria.mergeProps<aria.ButtonProps>()(
buttonProps,
focusChildProps,
{
ref,
className:
'relative after:pointer-events-none after:absolute after:inset-button-focus-ring-inset after:rounded-button-focus-ring',
},
{ className: buttonClassName }
)}
{...aria.mergeProps<aria.ButtonProps>()(buttonProps, focusChildProps, {
ref,
className: tailwindMerge.twMerge(
'relative after:pointer-events-none after:absolute after:inset-button-focus-ring-inset after:rounded-button-focus-ring',
buttonClassName
),
})}
>
<div
className={`group flex selectable ${isDisabled || softDisabled ? 'disabled' : ''} ${active ? 'active' : ''}`}
@ -72,6 +80,15 @@ function Button(props: ButtonProps, ref: React.ForwardedRef<HTMLButtonElement>)
</aria.Button>
</FocusRing>
)
return tooltipElement == null ? (
button
) : (
<ariaComponents.TooltipTrigger>
{button}
<ariaComponents.Tooltip>{tooltipElement}</ariaComponents.Tooltip>
</ariaComponents.TooltipTrigger>
)
}
export default React.forwardRef(Button)

View File

@ -6,9 +6,11 @@ import EyeIcon from 'enso-assets/eye.svg'
import * as focusHooks from '#/hooks/focusHooks'
import * as textProvider from '#/providers/TextProvider'
import * as aria from '#/components/aria'
import Button from '#/components/styled/Button'
import FocusRing from '#/components/styled/FocusRing'
import SvgMask from '#/components/SvgMask'
// =====================
// === SettingsInput ===
@ -27,6 +29,7 @@ export interface SettingsInputProps {
function SettingsInput(props: SettingsInputProps, ref: React.ForwardedRef<HTMLInputElement>) {
const { type, placeholder, autoComplete, onChange, onSubmit } = props
const focusChildProps = focusHooks.useFocusChild()
const { getText } = textProvider.useText()
// This is SAFE. The value of this context is never a `SlottedContext`.
// eslint-disable-next-line no-restricted-syntax
const inputProps = (React.useContext(aria.InputContext) ?? null) as aria.InputProps | null
@ -86,10 +89,12 @@ function SettingsInput(props: SettingsInputProps, ref: React.ForwardedRef<HTMLIn
)}
/>
{type === 'password' && (
<SvgMask
src={isShowingPassword ? EyeIcon : EyeCrossedIcon}
className="absolute right-2 top-1 cursor-pointer rounded-full"
onClick={() => {
<Button
active
image={isShowingPassword ? EyeIcon : EyeCrossedIcon}
alt={isShowingPassword ? getText('hidePassword') : getText('showPassword')}
buttonClassName="absolute right-2 top-1 cursor-pointer rounded-full size-icon"
onPress={() => {
setIsShowingPassword(show => !show)
}}
/>

View File

@ -96,8 +96,9 @@ function CategorySwitcherItem(props: InternalCategorySwitcherItemProps) {
onDrop={onDrop}
>
<UnstyledButton
aria-label={getText(buttonTextId)}
tooltip={false}
className={`rounded-inherit ${isCurrent ? 'focus-default' : ''}`}
aria-label={getText(buttonTextId)}
onPress={onPress}
>
<div

View File

@ -10,9 +10,9 @@ import * as textProvider from '#/providers/TextProvider'
import * as aria from '#/components/aria'
import Label from '#/components/dashboard/Label'
import * as labelUtils from '#/components/dashboard/Label/labelUtils'
import Button from '#/components/styled/Button'
import FocusArea from '#/components/styled/FocusArea'
import FocusRing from '#/components/styled/FocusRing'
import SvgMask from '#/components/SvgMask'
import ConfirmDeleteModal from '#/modals/ConfirmDeleteModal'
import DragModal from '#/modals/DragModal'
@ -127,8 +127,11 @@ export default function Labels(props: LabelsProps) {
</Label>
{!newLabelNames.has(label.value) && (
<FocusRing placement="after">
<aria.Button
className="relative flex after:absolute after:inset-button-focus-ring-inset after:rounded-button-focus-ring"
<Button
active
image={Trash2Icon}
alt={getText('delete')}
className="relative flex size-icon text-delete transition-all transparent after:absolute after:inset-button-focus-ring-inset after:rounded-button-focus-ring group-has-[[data-focus-visible]]:active group-hover:active"
onPress={() => {
setModal(
<ConfirmDeleteModal
@ -139,13 +142,7 @@ export default function Labels(props: LabelsProps) {
/>
)
}}
>
<SvgMask
src={Trash2Icon}
alt={getText('delete')}
className="size-icon text-delete transition-all transparent group-has-[[data-focus-visible]]:active group-hover:active"
/>
</aria.Button>
/>
</FocusRing>
)}
</div>

View File

@ -37,17 +37,15 @@ interface PageUIData {
readonly page: Page
readonly icon: string
readonly altId: Extract<text.TextId, `${Page}PageAltText`>
readonly tooltipId: Extract<text.TextId, `${Page}PageTooltip`>
}
const PAGE_DATA: PageUIData[] = [
{ page: Page.home, icon: HomeIcon, altId: 'homePageAltText', tooltipId: 'homePageTooltip' },
{ page: Page.drive, icon: DriveIcon, altId: 'drivePageAltText', tooltipId: 'drivePageTooltip' },
{ page: Page.home, icon: HomeIcon, altId: 'homePageAltText' },
{ page: Page.drive, icon: DriveIcon, altId: 'drivePageAltText' },
{
page: Page.editor,
icon: NetworkIcon,
altId: 'editorPageAltText',
tooltipId: 'editorPageTooltip',
},
]
@ -87,16 +85,16 @@ export default function PageSwitcher(props: PageSwitcherProps) {
{...innerProps}
>
{PAGE_DATA.map(pageData => {
const error = ERRORS[pageData.page]
return (
<Button
key={pageData.page}
aria-label={getText(pageData.tooltipId)}
alt={getText(pageData.altId)}
image={pageData.icon}
active={page === pageData.page}
softDisabled={page === pageData.page}
isDisabled={pageData.page === Page.editor && isEditorDisabled}
error={ERRORS[pageData.page]}
error={error == null ? null : getText(error)}
onPress={() => {
setPage(pageData.page)
}}

View File

@ -75,6 +75,7 @@ export default function UserBar(props: UserBarProps) {
<Button
active={isHelpChatOpen}
image={ChatIcon}
alt={getText('chatButtonAltText')}
onPress={() => {
setIsHelpChatOpen(!isHelpChatOpen)
}}
@ -82,6 +83,7 @@ export default function UserBar(props: UserBarProps) {
{shouldShowInviteButton && (
<UnstyledButton
className="text my-auto rounded-full bg-share px-button-x text-inversed"
aria-label={getText('inviteButtonAltText')}
onPress={() => {
setModal(<InviteUsersModal />)
}}
@ -92,6 +94,7 @@ export default function UserBar(props: UserBarProps) {
{shouldShowShareButton && (
<UnstyledButton
className="text my-auto rounded-full bg-share px-button-x text-inversed"
aria-label={getText('shareButtonAltText')}
onPress={() => {
setModal(
<ManagePermissionsModal
@ -109,6 +112,7 @@ export default function UserBar(props: UserBarProps) {
)}
<UnstyledButton
className="flex size-profile-picture select-none items-center overflow-clip rounded-full"
aria-label={getText('userMenuAltText')}
onPress={() => {
updateModal(oldModal =>
oldModal?.type === UserMenu ? null : (

View File

@ -11,10 +11,10 @@ import * as textProvider from '#/providers/TextProvider'
import * as aria from '#/components/aria'
import Modal from '#/components/Modal'
import Button from '#/components/styled/Button'
import ButtonRow from '#/components/styled/ButtonRow'
import FocusArea from '#/components/styled/FocusArea'
import FocusRing from '#/components/styled/FocusRing'
import SvgMask from '#/components/SvgMask'
import UnstyledButton from '#/components/UnstyledButton'
import type * as backend from '#/services/Backend'
@ -110,10 +110,10 @@ export default function UpsertSecretModal(props: UpsertSecretModalProps) {
}}
/>
</FocusRing>
<SvgMask
src={isShowingValue ? EyeIcon : EyeCrossedIcon}
<Button
image={isShowingValue ? EyeIcon : EyeCrossedIcon}
className="absolute right-2 top-1 cursor-pointer rounded-full"
onClick={() => {
onPress={() => {
setIsShowingValue(show => !show)
}}
/>

View File

@ -222,11 +222,11 @@
"homeCategory": "Home",
"rootCategory": "Root",
"trashCategory": "Trash",
"recentCategoryButtonLabel": "Go to Recent category",
"draftsCategoryButtonLabel": "Go to Drafts category",
"homeCategoryButtonLabel": "Go to Home category",
"rootCategoryButtonLabel": "Go to Root category",
"trashCategoryButtonLabel": "Go to Trash category",
"recentCategoryButtonLabel": "Recent",
"draftsCategoryButtonLabel": "Drafts",
"homeCategoryButtonLabel": "Home",
"rootCategoryButtonLabel": "Root",
"trashCategoryButtonLabel": "Trash",
"recentCategoryDropZoneLabel": "Move to Recent category",
"draftsCategoryDropZoneLabel": "Move to Drafts category",
"homeCategoryDropZoneLabel": "Move to Home category",
@ -235,8 +235,8 @@
"newFolder": "New Folder",
"newProject": "New Project",
"uploadFiles": "Upload Files",
"downloadFiles": "Download Files",
"uploadFiles": "Import",
"downloadFiles": "Export",
"newDataLink": "New Data Link",
"newSecret": "New Secret",
"newLabel": "New Label",
@ -260,8 +260,8 @@
"sortByTimestamp": "Sort by timestamp",
"sortByTimestampDescending": "Sort by timestamp descending",
"stopSortingByTimestamp": "Stop sorting by timestamp",
"closeAssetPanel": "Close Asset Panel",
"openAssetPanel": "Open Asset Panel",
"closeAssetPanel": "Asset Panel",
"openAssetPanel": "Asset Panel",
"confirmEdit": "Confirm Edit",
"cancelEdit": "Cancel Edit",
"loadingAppMessage": "Logging in to Enso...",
@ -320,6 +320,8 @@
"noVersionsFound": "No versions found",
"latestIndicator": "(Latest)",
"noDateSelected": "No date selected",
"hidePassword": "Hide password",
"showPassword": "Show password",
"deleteLabelActionText": "delete the label '$0'",
"deleteSelectedAssetActionText": "delete '$0'",
@ -392,14 +394,14 @@
"newsItemCommunityServer": "Join our community server",
"newsItemCommunityServerDescription": "Chat with our team and other Enso users.",
"homePageAltText": "Home tab",
"drivePageAltText": "Drive tab",
"editorPageAltText": "Project tab",
"settingsPageAltText": "Settings tab",
"homePageTooltip": "Go to homepage",
"drivePageTooltip": "Go to drive",
"editorPageTooltip": "Go to project",
"settingsPageTooltip": "Go to settings",
"chatButtonAltText": "Chat",
"inviteButtonAltText": "Invite others to try Enso",
"shareButtonAltText": "Share",
"userMenuAltText": "User Settings",
"homePageAltText": "Home",
"drivePageAltText": "Catalog",
"editorPageAltText": "Graph Editor",
"settingsPageAltText": "Settings",
"soloPlanName": "Solo",
"teamPlanName": "Team",
@ -476,20 +478,20 @@
"pasteAllShortcut": "Paste All",
"deleteLabelShortcut": "Delete Label",
"nameColumnShow": "Show Name column",
"nameColumnHide": "Hide Name column",
"modifiedColumnShow": "Show Modified date column",
"modifiedColumnHide": "Hide Modified date column",
"sharedWithColumnShow": "Show Shared with column",
"sharedWithColumnHide": "Hide Shared with column",
"labelsColumnShow": "Show Labels column",
"labelsColumnHide": "Hide Labels column",
"accessedByProjectsColumnShow": "Show Accessed by projects column",
"accessedByProjectsColumnHide": "Hide Accessed by projects column",
"accessedDataColumnShow": "Show Accessed data column",
"accessedDataColumnHide": "Hide Accessed data column",
"docsColumnShow": "Show Docs column",
"docsColumnHide": "Hide Doc columns",
"nameColumnShow": "Name",
"nameColumnHide": "Name",
"modifiedColumnShow": "Modified",
"modifiedColumnHide": "Modified",
"sharedWithColumnShow": "Shared With",
"sharedWithColumnHide": "Shared With",
"labelsColumnShow": "Labels",
"labelsColumnHide": "Labels",
"accessedByProjectsColumnShow": "Accessed By Projects",
"accessedByProjectsColumnHide": "Accessed By Projects",
"accessedDataColumnShow": "Accessed Data",
"accessedDataColumnHide": "Accessed Data",
"docsColumnShow": "Docs",
"docsColumnHide": "Docs",
"activityLog": "Activity Log",
"startDate": "Start Date",