Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/api-reference/widgets/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ new Deck({

Widgets with UI (e.g. a button or panel) can be positioned relative to the deck.gl view they are controlling, via the `viewId` and `placement` props. See [WidgetProps](../core/widget.md#widgetprops).

Custom widgets can opt into [themed text tooltips](./widget-tooltip.md) by calling `updateWidgetTooltip` from `@deck.gl/widgets` in `onAfterRenderHTML()`.
Bundled button widgets use [themed text tooltips](./widget-tooltip.md). Custom widgets can call `updateWidgetTooltip` from `onAfterRenderHTML()` to use the same renderer.

The `viewId` controls which HTML container will mount to, and the `placement` prop will position it relative to the container it is in, like so:

Expand Down
4 changes: 2 additions & 2 deletions docs/api-reference/widgets/widget-tooltip.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Widget Tooltip

`updateWidgetTooltip` adds delegated, theme-aware text tooltips to a custom widget.
`updateWidgetTooltip` adds delegated, theme-aware text tooltips to a widget. Bundled button widgets use it by default.

```ts
import {Widget} from '@deck.gl/core';
Expand All @@ -11,7 +11,7 @@ import '@deck.gl/widgets/stylesheet.css';

## Usage

Call `updateWidgetTooltip` from `onAfterRenderHTML()`. Descendants with a `data-deck-widget-tooltip` attribute become tooltip anchors.
Bundled button widgets expose tooltip metadata and call the helper after rendering. In custom widgets, call `updateWidgetTooltip` from `onAfterRenderHTML()`. Descendants with a `data-deck-widget-tooltip` attribute become tooltip anchors.

```tsx
class ResetWidget extends Widget {
Expand Down
2 changes: 1 addition & 1 deletion docs/whats-new.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Release date: TBD

A new experimental `ViewLayout` system with a helper function `buildViewsFromViewLayout()` allows advanced nested and relative view layouts to be specified using a declarative layout tree.

The new `updateWidgetTooltip` helper lets custom widgets opt into themed text tooltips.
Bundled button widgets now use themed text tooltips. Custom widgets can use the same renderer through the new `updateWidgetTooltip` helper.

## deck.gl v9.3

Expand Down
4 changes: 4 additions & 0 deletions modules/widgets/src/compass-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import {Widget, FlyToInterpolator, WebMercatorViewport, _GlobeViewport} from '@deck.gl/core';
import type {Viewport, WidgetPlacement, WidgetProps} from '@deck.gl/core';
import {render} from 'preact';
import {updateWidgetTooltip} from './lib/widget-tooltip';

export type CompassWidgetProps = WidgetProps & {
/** Widget positioning within the view. Default 'top-left'. */
Expand Down Expand Up @@ -42,6 +43,7 @@ export class CompassWidget extends Widget<CompassWidgetProps> {

className = 'deck-widget-compass';
placement: WidgetPlacement = 'top-left';
protected override onAfterRenderHTML = updateWidgetTooltip;
viewports: {[id: string]: Viewport} = {};

constructor(props: CompassWidgetProps = {}) {
Expand Down Expand Up @@ -70,6 +72,8 @@ export class CompassWidget extends Widget<CompassWidgetProps> {
}
}}
title={this.props.label}
aria-label={this.props.label}
data-deck-widget-tooltip={this.props.label}
style={{transform: `rotateX(${rx}deg)`}}
>
<svg fill="none" width="100%" height="100%" viewBox="0 0 26 26">
Expand Down
2 changes: 2 additions & 0 deletions modules/widgets/src/fullscreen-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import {log, Widget, type WidgetProps, type WidgetPlacement} from '@deck.gl/core';
import {render} from 'preact';
import {IconButton} from './lib/components/icon-button';
import {updateWidgetTooltip} from './lib/widget-tooltip';

/* eslint-enable max-len */

Expand Down Expand Up @@ -44,6 +45,7 @@ export class FullscreenWidget extends Widget<FullscreenWidgetProps> {

className = 'deck-widget-fullscreen';
placement: WidgetPlacement = 'top-left';
protected override onAfterRenderHTML = updateWidgetTooltip;
fullscreen: boolean = false;

constructor(props: FullscreenWidgetProps = {}) {
Expand Down
4 changes: 4 additions & 0 deletions modules/widgets/src/gimbal-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import {Widget, LinearInterpolator} from '@deck.gl/core';
import type {Viewport, WidgetPlacement, WidgetProps} from '@deck.gl/core';
import {render} from 'preact';
import {updateWidgetTooltip} from './lib/widget-tooltip';

export type GimbalWidgetProps = WidgetProps & {
placement?: WidgetPlacement;
Expand Down Expand Up @@ -44,6 +45,7 @@ export class GimbalWidget extends Widget<GimbalWidgetProps> {

className = 'deck-widget-gimbal';
placement: WidgetPlacement = 'top-left';
protected override onAfterRenderHTML = updateWidgetTooltip;
viewports: {[id: string]: Viewport} = {};

constructor(props: GimbalWidgetProps = {}) {
Expand Down Expand Up @@ -72,6 +74,8 @@ export class GimbalWidget extends Widget<GimbalWidgetProps> {
}
}}
title={this.props.label}
aria-label={this.props.label}
data-deck-widget-tooltip={this.props.label}
style={{position: 'relative', width: 26, height: 26}}
>
{/* Outer ring */}
Expand Down
2 changes: 2 additions & 0 deletions modules/widgets/src/icon-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {WidgetPlacement, WidgetProps} from '@deck.gl/core';
import {render, type JSX} from 'preact';
import {Widget} from '@deck.gl/core';
import {IconButton} from './lib/components/icon-button';
import {updateWidgetTooltip} from './lib/widget-tooltip';

export type IconWidgetProps = WidgetProps & {
/** Widget positioning within the view. Default 'bottom-left'. */
Expand Down Expand Up @@ -39,6 +40,7 @@ export class IconWidget extends Widget<IconWidgetProps> {

className = '';
placement: WidgetPlacement = 'top-left';
protected override onAfterRenderHTML = updateWidgetTooltip;

constructor(props: IconWidgetProps) {
super(props);
Expand Down
2 changes: 2 additions & 0 deletions modules/widgets/src/lib/components/icon-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export const IconButton = (props: IconButtonProps) => {
type="button"
onClick={onClick}
title={label}
aria-label={label}
data-deck-widget-tooltip={label}
>
{children ? children : <div className="deck-widget-icon" style={iconStyle} />}
</button>
Expand Down
2 changes: 2 additions & 0 deletions modules/widgets/src/reset-view-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {ViewStateMap, View} from '@deck.gl/core';
import {render} from 'preact';
import {Widget} from '@deck.gl/core';
import {IconButton} from './lib/components/icon-button';
import {updateWidgetTooltip} from './lib/widget-tooltip';

/** @note Mirrors an internal calss in deck.gl/core. We can easily redefine it here */
type ViewOrViews = View | View[] | null;
Expand Down Expand Up @@ -51,6 +52,7 @@ export class ResetViewWidget<ViewsT extends ViewOrViews = null> extends Widget<

className = 'deck-widget-reset-view';
placement: WidgetPlacement = 'top-left';
protected override onAfterRenderHTML = updateWidgetTooltip;

constructor(props: ResetViewWidgetProps<ViewsT> = {}) {
super(props);
Expand Down
2 changes: 2 additions & 0 deletions modules/widgets/src/screenshot-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {WidgetPlacement, WidgetProps} from '@deck.gl/core';
import {render} from 'preact';
import {Widget} from '@deck.gl/core';
import {IconButton} from './lib/components/icon-button';
import {updateWidgetTooltip} from './lib/widget-tooltip';

/** Properties for the ScreenshotWidget */
export type ScreenshotWidgetProps = WidgetProps & {
Expand Down Expand Up @@ -42,6 +43,7 @@ export class ScreenshotWidget extends Widget<ScreenshotWidgetProps> {

className = 'deck-widget-screenshot';
placement: WidgetPlacement = 'top-left';
protected override onAfterRenderHTML = updateWidgetTooltip;

constructor(props: ScreenshotWidgetProps = {}) {
super(props);
Expand Down
2 changes: 2 additions & 0 deletions modules/widgets/src/selector-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {Widget, type WidgetProps, type WidgetPlacement} from '@deck.gl/core';
import {SimpleMenu, type MenuItem} from './lib/components/dropdown-menu';
import {Popover, type PopoverProps} from './lib/components/popover';
import {IconButton} from './lib/components/icon-button';
import {updateWidgetTooltip} from './lib/widget-tooltip';

export type SelectorWidgetOption<ValueT = string> = {
value: ValueT;
Expand Down Expand Up @@ -48,6 +49,7 @@ export class SelectorWidget<ValueT = string> extends Widget<SelectorWidgetProps<

className = 'deck-widget-selector';
placement: WidgetPlacement = 'top-left';
protected override onAfterRenderHTML = updateWidgetTooltip;
value: ValueT;
isOpen: {x: number; y: number; placement: PopoverProps['placement']} | false = false;

Expand Down
2 changes: 2 additions & 0 deletions modules/widgets/src/theme-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {Widget, type WidgetProps, type WidgetPlacement} from '@deck.gl/core';
import {render} from 'preact';
// import {useCallback} from 'preact/hooks';
import {IconButton} from './lib/components/icon-button';
import {updateWidgetTooltip} from './lib/widget-tooltip';
import type {DeckWidgetTheme} from './themes';
import {LightGlassTheme, DarkGlassTheme} from './themes';

Expand Down Expand Up @@ -55,6 +56,7 @@ export class ThemeWidget extends Widget<ThemeWidgetProps> {

className = 'deck-widget-theme';
placement: WidgetPlacement = 'top-left';
protected override onAfterRenderHTML = updateWidgetTooltip;
themeMode: 'light' | 'dark' = 'dark';
appliedTheme: DeckWidgetTheme = {};

Expand Down
2 changes: 2 additions & 0 deletions modules/widgets/src/toggle-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {WidgetPlacement, WidgetProps} from '@deck.gl/core';
import {render, type JSX} from 'preact';
import {Widget} from '@deck.gl/core';
import {IconButton} from './lib/components/icon-button';
import {updateWidgetTooltip} from './lib/widget-tooltip';

export type ToggleWidgetProps = WidgetProps & {
/** Widget positioning within the view. Default 'bottom-left'. */
Expand Down Expand Up @@ -51,6 +52,7 @@ export class ToggleWidget extends Widget<ToggleWidgetProps> {

className = 'deck-widget-toggle';
placement: WidgetPlacement = 'top-left';
protected override onAfterRenderHTML = updateWidgetTooltip;
checked: boolean;

constructor(props: ToggleWidgetProps) {
Expand Down
2 changes: 2 additions & 0 deletions modules/widgets/src/zoom-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {WidgetProps, WidgetPlacement, OrthographicViewState} from '@deck.gl
import {render} from 'preact';
import {ButtonGroup} from './lib/components/button-group';
import {IconButton} from './lib/components/icon-button';
import {updateWidgetTooltip} from './lib/widget-tooltip';

export type ZoomWidgetProps = WidgetProps & {
/** Widget positioning within the view. Default 'top-left'. */
Expand Down Expand Up @@ -59,6 +60,7 @@ export class ZoomWidget extends Widget<ZoomWidgetProps> {

className = 'deck-widget-zoom';
placement: WidgetPlacement = 'top-left';
protected override onAfterRenderHTML = updateWidgetTooltip;

constructor(props: ZoomWidgetProps = {}) {
super(props);
Expand Down
8 changes: 6 additions & 2 deletions test/modules/widgets/selector-widget.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ test('SelectorWidget', async () => {

await testInstance.idle();
let button = testInstance.findElements('.deck-widget-icon-button')[0] as HTMLButtonElement;
expect(button.title).toBe('Grid');
expect(button.title).toBe('');
expect(button.getAttribute('aria-label')).toBe('Grid');
expect(button.getAttribute('data-deck-widget-tooltip')).toBe('Grid');

testInstance.click('.deck-widget-icon-button');
await testInstance.idle();
Expand All @@ -45,6 +47,8 @@ test('SelectorWidget', async () => {

expect(onChange).toHaveBeenCalledWith('list');
button = testInstance.findElements('.deck-widget-icon-button')[0] as HTMLButtonElement;
expect(button.title).toBe('List');
expect(button.title).toBe('');
expect(button.getAttribute('aria-label')).toBe('List');
expect(button.getAttribute('data-deck-widget-tooltip')).toBe('List');
expect(testInstance.findElements('.deck-widget-dropdown-item')).toHaveLength(0);
});
8 changes: 6 additions & 2 deletions test/modules/widgets/theme-widget.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ test('ThemeWidget - button label and icon reflect current mode', async () => {
let button = testInstance.findElements(
'.deck-widget-theme .deck-widget-icon-button'
)[0] as HTMLButtonElement;
expect(button.title).toBe('Dark Mode');
expect(button.title).toBe('');
expect(button.getAttribute('aria-label')).toBe('Dark Mode');
expect(button.getAttribute('data-deck-widget-tooltip')).toBe('Dark Mode');
expect(button.className).toContain('deck-widget-moon');

testInstance.setProps({
Expand All @@ -86,7 +88,9 @@ test('ThemeWidget - button label and icon reflect current mode', async () => {
button = testInstance.findElements(
'.deck-widget-theme .deck-widget-icon-button'
)[0] as HTMLButtonElement;
expect(button.title).toBe('Light Mode');
expect(button.title).toBe('');
expect(button.getAttribute('aria-label')).toBe('Light Mode');
expect(button.getAttribute('data-deck-widget-tooltip')).toBe('Light Mode');
expect(button.className).toContain('deck-widget-sun');
});

Expand Down
8 changes: 6 additions & 2 deletions test/modules/widgets/toggle-widget.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,17 @@ test('ToggleWidget', async () => {
const icon = testInstance.findElements('.deck-widget-icon')[0] as HTMLDivElement;

expect(root.dataset.checked).toBe('false');
expect(button.title).toBe('Toggle off');
expect(button.title).toBe('');
expect(button.getAttribute('aria-label')).toBe('Toggle off');
expect(button.getAttribute('data-deck-widget-tooltip')).toBe('Toggle off');
expect(icon.style.backgroundColor).toBe('rgb(255, 0, 0)');

testInstance.click('.deck-widget-icon-button');

expect(onChange).toHaveBeenCalledWith(true);
expect(root.dataset.checked).toBe('true');
expect(button.title).toBe('Toggle on');
expect(button.title).toBe('');
expect(button.getAttribute('aria-label')).toBe('Toggle on');
expect(button.getAttribute('data-deck-widget-tooltip')).toBe('Toggle on');
expect(icon.style.backgroundColor).toBe('rgb(0, 255, 0)');
});
21 changes: 20 additions & 1 deletion test/modules/widgets/widget-tooltip.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@
// Copyright (c) vis.gl contributors

import {afterEach, test, expect} from 'vitest';
import {updateWidgetTooltip} from '@deck.gl/widgets';
import {updateWidgetTooltip, ZoomWidget} from '@deck.gl/widgets';
import {WidgetTester} from './common';

let rootElement: HTMLDivElement | undefined;
let testInstance: WidgetTester<any> | undefined;

afterEach(() => {
rootElement?.remove();
rootElement = undefined;
testInstance?.destroy();
testInstance = undefined;
});

function createTooltipTarget(label: string) {
Expand Down Expand Up @@ -72,3 +76,18 @@ test('updateWidgetTooltip resolves SVG targets and updated labels', () => {
dispatchPointerOver(button);
expect(rootElement?.querySelector('.deck-widget-tooltip')?.textContent).toBe('Reset view');
});

test('button widgets use themed tooltips', async () => {
testInstance = new WidgetTester({
widgets: [new ZoomWidget()]
});
await testInstance.idle();

const button = testInstance.findElements('.deck-widget-zoom-in')[0] as HTMLButtonElement;
expect(button.title).toBe('');
expect(button.getAttribute('aria-label')).toBe('Zoom In');
expect(button.getAttribute('data-deck-widget-tooltip')).toBe('Zoom In');

dispatchPointerOver(button);
expect(testInstance.findElements('.deck-widget-tooltip')[0].textContent).toBe('Zoom In');
});
Loading