mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 09:22:41 +03:00
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:
parent
720d32cbe3
commit
35571f64ba
@ -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. */
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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} />
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}}
|
||||
/>
|
||||
|
@ -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)
|
||||
}}
|
||||
/>
|
||||
|
@ -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)
|
||||
}}
|
||||
/>
|
||||
|
@ -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)
|
||||
}}
|
||||
/>
|
||||
|
@ -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)
|
||||
}}
|
||||
/>
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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)
|
||||
}}
|
||||
/>
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}}
|
||||
/>
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
}}
|
||||
|
@ -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 : (
|
||||
|
@ -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)
|
||||
}}
|
||||
/>
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user