Replace text-only tooltips with custom richly styled tooltips (#3436)

* Replace the title attribute with custom FloatingMenu tooltips

* Separate tooltip labels and descriptions into two styled blocks

* Move keyboard shortcut tooltips to a separate section at the bottom

* Update shortcut key styling in tooltips and hints bar

* Fix .to_string()
This commit is contained in:
Keavon Chambers
2025-11-30 13:32:58 -08:00
committed by GitHub
parent 94e5c8fc05
commit e8ebcc2c21
94 changed files with 1323 additions and 580 deletions
@@ -26,10 +26,10 @@
<LayoutCol class={`widget-section ${className}`.trim()} {classes}>
<button class="header" class:expanded on:click|stopPropagation={() => (expanded = !expanded)} tabindex="0">
<div class="expand-arrow" />
<TextLabel tooltip={widgetData.description} bold={true}>{widgetData.name}</TextLabel>
<TextLabel tooltipLabel={widgetData.name} tooltipDescription={widgetData.description} bold={true}>{widgetData.name}</TextLabel>
<IconButton
icon={widgetData.pinned ? "PinActive" : "PinInactive"}
tooltip={widgetData.pinned ? "Unpin this node so it's no longer shown here when nothing is selected" : "Pin this node so it's shown here when nothing is selected"}
tooltipDescription={widgetData.pinned ? "Unpin this node so it's no longer shown here when nothing is selected." : "Pin this node so it's shown here when nothing is selected."}
size={24}
action={(e) => {
editor.handle.setNodePinned(widgetData.id, !widgetData.pinned);
@@ -39,7 +39,7 @@
/>
<IconButton
icon="Trash"
tooltip="Delete this node from the layer chain"
tooltipDescription="Delete this node from the layer chain."
size={24}
action={(e) => {
editor.handle.deleteNode(widgetData.id);
@@ -50,7 +50,7 @@
<IconButton
icon={widgetData.visible ? "EyeVisible" : "EyeHidden"}
hoverIcon={widgetData.visible ? "EyeHide" : "EyeShow"}
tooltip={widgetData.visible ? "Hide this node" : "Show this node"}
tooltipDescription={widgetData.visible ? "Hide this node." : "Show this node."}
size={24}
action={(e) => {
editor.handle.toggleNodeVisibilityLayerPanel(widgetData.id);
@@ -4,12 +4,14 @@
export let labels: string[];
export let disabled = false;
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
// Callbacks
export let action: (index: number) => void;
</script>
<LayoutRow class="breadcrumb-trail-buttons" {tooltip}>
<LayoutRow class="breadcrumb-trail-buttons" {tooltipLabel} {tooltipDescription} {tooltipShortcut}>
{#each labels as label, index}
<TextButton {label} emphasized={index === labels.length - 1} {disabled} action={() => !disabled && index !== labels.length - 1 && action(index)} />
{/each}
@@ -8,7 +8,9 @@
export let size: IconSize;
export let disabled = false;
export let active = false;
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
// Callbacks
export let action: (e?: MouseEvent) => void;
@@ -28,7 +30,9 @@
class:active
on:click={action}
{disabled}
title={tooltip}
data-tooltip-label={tooltipLabel}
data-tooltip-description={tooltipDescription}
data-tooltip-shortcut={tooltipShortcut}
tabindex={active ? -1 : 0}
{...$$restProps}
>
@@ -8,7 +8,9 @@
export let image: string;
export let width: string | undefined;
export let height: string | undefined;
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
// Callbacks
export let action: (e?: MouseEvent) => void;
@@ -17,7 +19,17 @@
.join(" ");
</script>
<img src={IMAGE_BASE64_STRINGS[image]} style:width style:height class={`image-button ${className} ${extraClasses}`.trim()} title={tooltip} alt="" on:click={action} />
<img
src={IMAGE_BASE64_STRINGS[image]}
style:width
style:height
class={`image-button ${className} ${extraClasses}`.trim()}
data-tooltip-label={tooltipLabel}
data-tooltip-description={tooltipDescription}
data-tooltip-shortcut={tooltipShortcut}
alt=""
on:click={action}
/>
<style lang="scss" global>
.image-button {
@@ -5,7 +5,9 @@
export let exposed: boolean;
export let dataType: FrontendGraphDataType;
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
// Callbacks
export let action: (e?: MouseEvent) => void;
</script>
@@ -16,7 +18,9 @@
style:--data-type-color={`var(--color-data-${dataType.toLowerCase()})`}
style:--data-type-color-dim={`var(--color-data-${dataType.toLowerCase()}-dim)`}
on:click={action}
title={tooltip}
data-tooltip-label={tooltipLabel}
data-tooltip-description={tooltipDescription}
data-tooltip-shortcut={tooltipShortcut}
tabindex="-1"
>
{#if !exposed}
@@ -11,7 +11,9 @@
export let style: PopoverButtonStyle = "DropdownArrow";
export let menuDirection: MenuDirection = "Bottom";
export let icon: IconName | undefined = undefined;
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
export let disabled = false;
export let popoverMinWidth = 1;
@@ -27,9 +29,20 @@
</script>
<LayoutRow class="popover-button" classes={{ "has-icon": icon !== undefined, "direction-top": menuDirection === "Top" }}>
<IconButton class="dropdown-icon" classes={{ open }} {disabled} action={() => onClick()} icon={style || "DropdownArrow"} size={16} {tooltip} data-floating-menu-spawner />
<IconButton
class="dropdown-icon"
classes={{ open }}
{disabled}
action={() => onClick()}
icon={style || "DropdownArrow"}
size={16}
{tooltipLabel}
{tooltipDescription}
{tooltipShortcut}
data-floating-menu-spawner
/>
{#if icon !== undefined}
<IconLabel class="descriptive-icon" classes={{ open }} {disabled} {icon} {tooltip} />
<IconLabel class="descriptive-icon" classes={{ open }} {disabled} {icon} {tooltipLabel} {tooltipDescription} {tooltipShortcut} />
{/if}
<FloatingMenu {open} on:open={({ detail }) => (open = detail)} minWidth={popoverMinWidth} type="Popover" direction={menuDirection || "Bottom"}>
@@ -20,7 +20,9 @@
export let minWidth = 0;
export let disabled = false;
export let narrow = false;
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
export let menuListChildren: MenuListEntry[][] | undefined = undefined;
// Callbacks
@@ -59,7 +61,9 @@
class:narrow
class:flush
style:min-width={minWidth > 0 ? `${minWidth}px` : undefined}
title={tooltip}
data-tooltip-label={tooltipLabel}
data-tooltip-description={tooltipDescription}
data-tooltip-shortcut={tooltipShortcut}
data-emphasized={emphasized || undefined}
data-disabled={disabled || undefined}
data-text-button
@@ -11,7 +11,9 @@
export let checked = false;
export let disabled = false;
export let icon: IconName = "Checkmark";
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
export let forLabel: bigint | undefined = undefined;
let inputElement: HTMLInputElement | undefined;
@@ -46,7 +48,15 @@
tabindex={disabled ? -1 : 0}
bind:this={inputElement}
/>
<label class:disabled class:checked for={`checkbox-input-${id}`} on:keydown={(e) => e.key === "Enter" && toggleCheckboxFromLabel(e)} title={tooltip}>
<label
class:disabled
class:checked
for={`checkbox-input-${id}`}
on:keydown={(e) => e.key === "Enter" && toggleCheckboxFromLabel(e)}
data-tooltip-label={tooltipLabel}
data-tooltip-description={tooltipDescription}
data-tooltip-shortcut={tooltipShortcut}
>
<LayoutRow class="checkbox-box">
<IconLabel icon={displayIcon} />
</LayoutRow>
@@ -17,7 +17,9 @@
export let allowNone = false;
export let menuDirection: MenuDirection = "Bottom";
// export let allowTransparency = false; // TODO: Implement
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
$: outlineFactor = contrastingOutlineFactor(value, ["--color-1-nearblack", "--color-3-darkgray"], 0.01);
$: outlined = outlineFactor > 0.0001;
@@ -26,7 +28,7 @@
$: transparency = value instanceof Gradient ? value.stops.some((stop) => stop.color.alpha < 1) : value.alpha < 1;
</script>
<LayoutCol class="color-button" classes={{ open, disabled, narrow, none, transparency, outlined, "direction-top": menuDirection === "Top" }} {tooltip}>
<LayoutCol class="color-button" classes={{ open, disabled, narrow, none, transparency, outlined, "direction-top": menuDirection === "Top" }} {tooltipLabel} {tooltipDescription} {tooltipShortcut}>
<button style:--chosen-gradient={chosenGradient} style:--outline-amount={outlineFactor} on:click={() => (open = true)} tabindex="0" data-floating-menu-spawner>
<!-- {#if disabled && value instanceof Color && !value.none}
<TextLabel>sRGB</TextLabel>
@@ -16,7 +16,9 @@
export let styles: Record<string, string | number | undefined> = {};
export let value: Curve;
export let disabled = false;
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
const GRID_SIZE = 4;
@@ -77,7 +79,7 @@
}
function handleManipulatorPointerDown(e: PointerEvent, i: number) {
// Delete an anchor with RMB or MMB
// Delete an anchor with right click or middle click
if (e.button > 0 && i > 0 && i < manipulatorsList.length - 1) {
draggedNodeIndex = undefined;
selectedNodeIndex = undefined;
@@ -188,7 +190,7 @@
}
</script>
<LayoutRow class="curve-input" classes={{ disabled, ...classes }} style={styleName} {styles} {tooltip}>
<LayoutRow class="curve-input" classes={{ disabled, ...classes }} style={styleName} {styles} {tooltipLabel} {tooltipDescription} {tooltipShortcut}>
<svg viewBox="0 0 1 1" on:pointermove={handlePointerMove} on:pointerup={handlePointerUp}>
{#each { length: GRID_SIZE - 1 } as _, i}
<path class="grid" d={`M 0 ${(i + 1) / GRID_SIZE} L 1 ${(i + 1) / GRID_SIZE}`} />
@@ -21,7 +21,9 @@
export let interactive = true;
export let disabled = false;
export let narrow = false;
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
export let minWidth = 0;
export let maxWidth = 0;
@@ -98,7 +100,9 @@
<LayoutRow
class="dropdown-box"
classes={{ disabled, open }}
{tooltip}
{tooltipLabel}
{tooltipDescription}
{tooltipShortcut}
on:click={() => !disabled && (open = true)}
on:blur={unFocusDropdownBox}
tabindex={disabled ? -1 : 0}
@@ -25,7 +25,9 @@
export let disabled = false;
export let narrow = false;
export let textarea = false;
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
export let placeholder: string | undefined = undefined;
export let hideContextMenu = false;
@@ -74,7 +76,7 @@
</script>
<!-- This is a base component, extended by others like NumberInput and TextInput. It should not be used directly. -->
<LayoutRow class={`field-input ${className}`} classes={{ disabled, narrow, ...classes }} style={styleName} {styles} {tooltip}>
<LayoutRow class={`field-input ${className}`} classes={{ disabled, narrow, ...classes }} style={styleName} {styles} {tooltipLabel} {tooltipDescription} {tooltipShortcut}>
{#if !textarea}
<input
type="text"
@@ -23,7 +23,9 @@
export let fontStyle: string;
export let isStyle = false;
export let disabled = false;
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
let open = false;
let entries: MenuListEntry[] = [];
@@ -108,7 +110,9 @@
class="dropdown-box"
classes={{ disabled }}
styles={{ ...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}) }}
{tooltip}
{tooltipLabel}
{tooltipDescription}
{tooltipShortcut}
tabindex={disabled ? -1 : 0}
on:click={toggleOpen}
data-floating-menu-spawner
@@ -18,7 +18,9 @@
// Label
export let label: string | undefined = undefined;
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
// Disabled
export let disabled = false;
@@ -688,7 +690,9 @@
{label}
{disabled}
{narrow}
{tooltip}
{tooltipLabel}
{tooltipDescription}
{tooltipShortcut}
{styles}
hideContextMenu={true}
spellcheck={false}
@@ -27,7 +27,15 @@
<LayoutRow class="radio-input" classes={{ disabled, narrow, mixed }} styles={{ ...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}) }}>
{#each entries as entry, index}
<button class:active={!mixed ? index === selectedIndex : undefined} on:click={() => handleEntryClick(entry)} title={entry.tooltip} tabindex={index === selectedIndex ? -1 : 0} {disabled}>
<button
class:active={!mixed ? index === selectedIndex : undefined}
on:click={() => handleEntryClick(entry)}
data-tooltip-label={entry.tooltipLabel}
data-tooltip-description={entry.tooltipDescription}
data-tooltip-shortcut={entry.tooltipShortcut}
tabindex={index === selectedIndex ? -1 : 0}
{disabled}
>
{#if entry.icon}
<IconLabel icon={entry.icon} />
{/if}
@@ -7,14 +7,16 @@
export let value: string;
export let disabled = false;
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
function setValue(newValue: ReferencePoint) {
dispatch("value", newValue);
}
</script>
<div class="reference-point-input" class:disabled title={tooltip}>
<div class="reference-point-input" class:disabled data-tooltip-label={tooltipLabel} data-tooltip-description={tooltipDescription} data-tooltip-shortcut={tooltipShortcut}>
<button on:click={() => setValue("TopLeft")} class="row-1 col-1" class:active={value === "TopLeft"} tabindex="-1" {disabled}><div /></button>
<button on:click={() => setValue("TopCenter")} class="row-1 col-2" class:active={value === "TopCenter"} tabindex="-1" {disabled}><div /></button>
<button on:click={() => setValue("TopRight")} class="row-1 col-3" class:active={value === "TopRight"} tabindex="-1" {disabled}><div /></button>
@@ -16,7 +16,9 @@
export let disabled = false;
export let activeMarkerIndex = 0 as number | undefined;
// export let disabled = false;
// export let tooltip: string | undefined = undefined;
// export let tooltipLabel: string | undefined = undefined;
// export let tooltipDescription: string | undefined = undefined;
// export let tooltipShortcut: string | undefined = undefined;
let markerTrack: LayoutRow | undefined = undefined;
let positionRestore: number | undefined = undefined;
@@ -7,7 +7,9 @@
export let value: string;
export let label: string | undefined = undefined;
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
export let disabled = false;
let self: FieldInput | undefined;
@@ -55,7 +57,9 @@
spellcheck={true}
{label}
{disabled}
{tooltip}
{tooltipLabel}
{tooltipDescription}
{tooltipShortcut}
bind:this={self}
/>
@@ -7,7 +7,9 @@
// Label
export let label: string | undefined = undefined;
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
export let placeholder: string | undefined = undefined;
// Disabled
export let disabled = false;
@@ -79,7 +81,9 @@
{label}
{disabled}
{narrow}
{tooltip}
{tooltipLabel}
{tooltipDescription}
{tooltipShortcut}
{placeholder}
bind:this={self}
/>
@@ -9,7 +9,9 @@
export let icon: IconName;
export let iconSizeOverride: number | undefined = undefined;
export let disabled = false;
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
$: iconSizeClass = ((icon: IconName) => {
const iconData = ICONS[icon];
@@ -26,7 +28,7 @@
.join(" ");
</script>
<LayoutRow class={`icon-label ${iconSizeClass} ${className} ${extraClasses}`.trim()} classes={{ disabled }} {tooltip}>
<LayoutRow class={`icon-label ${iconSizeClass} ${className} ${extraClasses}`.trim()} classes={{ disabled }} {tooltipLabel} {tooltipDescription} {tooltipShortcut}>
{@html ICON_SVG_STRINGS[icon] || ""}
</LayoutRow>
@@ -6,14 +6,25 @@
export let url: string;
export let width: string | undefined;
export let height: string | undefined;
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
$: extraClasses = Object.entries(classes)
.flatMap(([className, stateName]) => (stateName ? [className] : []))
.join(" ");
</script>
<img src={url} style:width style:height class={`image-label ${className} ${extraClasses}`.trim()} title={tooltip} alt="" />
<img
src={url}
style:width
style:height
class={`image-label ${className} ${extraClasses}`.trim()}
data-tooltip-label={tooltipLabel}
data-tooltip-description={tooltipDescription}
data-tooltip-shortcut={tooltipShortcut}
alt=""
/>
<style lang="scss" global>
.image-label {
@@ -14,7 +14,9 @@
export let tableAlign = false;
export let minWidth = "";
export let multiline = false;
export let tooltip: string | undefined = undefined;
export let tooltipLabel: string | undefined = undefined;
export let tooltipDescription: string | undefined = undefined;
export let tooltipShortcut: string | undefined = undefined;
export let forCheckbox: bigint | undefined = undefined;
$: extraClasses = Object.entries(classes)
@@ -37,7 +39,9 @@
class:table-align={tableAlign}
style:min-width={minWidth || undefined}
style={`${styleName} ${extraStyles}`.trim() || undefined}
title={tooltip}
data-tooltip-label={tooltipLabel}
data-tooltip-description={tooltipDescription}
data-tooltip-shortcut={tooltipShortcut}
for={forCheckbox !== undefined ? `checkbox-input-${forCheckbox}` : undefined}
>
<slot />
@@ -45,7 +45,7 @@
$: displayKeyboardLockNotice = requiresLock && !$fullscreen.keyboardLocked;
function watchKeyboardLockInfoMessage(keyboardLockApiSupported: boolean): string {
const RESERVED = "This hotkey is reserved by the browser. ";
const RESERVED = "This keyboard shortcut is reserved by the browser.";
const USE_FULLSCREEN = "It is made available in fullscreen mode.";
const USE_SECURE_CTX = "It is made available in fullscreen mode when this website is served from a secure context (https or localhost).";
const SWITCH_BROWSER = "Use a Chromium-based browser (like Chrome or Edge) in fullscreen mode to directly use the shortcut.";
@@ -118,7 +118,7 @@
</script>
{#if displayKeyboardLockNotice}
<IconLabel class="user-input-label keyboard-lock-notice" icon="Info" tooltip={keyboardLockInfoMessage} />
<IconLabel class="user-input-label keyboard-lock-notice" icon="Info" tooltipDescription={keyboardLockInfoMessage} />
{:else}
<LayoutRow class="user-input-label" classes={{ "text-only": textOnly }}>
{#each keysWithLabelsGroups as keysWithLabels, groupIndex}
@@ -189,11 +189,13 @@
font-weight: 400;
text-align: center;
height: 16px;
box-sizing: border-box;
border: 1px solid;
border-radius: 4px;
border-color: var(--color-5-dullgray);
color: var(--color-e-nearwhite);
background: var(--color-3-darkgray);
color: var(--color-b-lightgray);
.icon-label {
fill: var(--color-b-lightgray);
}
.text-label {
// Firefox renders the text 1px lower than Chrome (tested on Windows) with 16px line-height,