Skip to content

Commit 754d85c

Browse files
fix(widgets): widget bug bash (#9797)
* Add deck.gl widgets 9.2 test apps Renamed and added new example apps for deck.gl widgets 9.2, including single and multi-view setups. Upgraded dependencies to @deck.gl/core, @deck.gl/layers, and @deck.gl/widgets v9.2.0-beta.1. Refactored LayerListWidget to extend Widget from @deck.gl/core. * fix: zoom transitions in non-geo views * fix: reset in multi-view apps * add: button text to theme * fix: animate and design fps widget * fix: gimbal in non-default views and multi-views * fix: timeline themed icon * fix: stats update on redraw * fix: loading status was flipped * stylesheet * Add --button-text-color CSS variable to docs Documents the new --button-text-color variable in the widget styling reference, including its default value and link to CSS color documentation. * types on IconButton * Refactor widget types for view state support Updated Widget and ResetViewWidget to support generic view state types using ViewOrViews and ViewStateMap. Matches `Deck`'s initialViewState type * Update test/apps/widgets-multi-view-9.2/package.json Co-authored-by: felixpalmer <[email protected]> * Update test/apps/widgets-9.2/package.json Co-authored-by: felixpalmer <[email protected]> * Add documentation for widgets with multiple views Introduces a new section explaining how to position widgets relative to deck.gl views using `viewId` and `placement` props. Provides example configuration and HTML structure, along with remarks and references to related guides. * Add data filtering to GeoJsonLayer with timeline control Introduces DataFilterExtension to the GeoJsonLayer, enabling dynamic filtering based on the 'scalerank' property. The timeline widget now updates the filter range interactively, allowing users to control visible features by time. * Refactor button components for improved type safety and fix lints * Add customizable --menu-gap CSS variable for menus * Refactor IconMenu to use elements instead of icon functions Updated IconMenu and related usage to accept JSX elements for icons instead of icon functions. Adjusted menu item definitions and rendering logic accordingly, and replaced inline menu styles with class-based styling and ButtonGroup/GroupedIconButton components for improved structure and maintainability. --------- Co-authored-by: felixpalmer <[email protected]>
1 parent e4fc4a2 commit 754d85c

35 files changed

+533
-175
lines changed

docs/api-reference/widgets/overview.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,59 @@ new Deck({
125125
});
126126
```
127127

128+
### Using with Multiple Views
129+
130+
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).
131+
132+
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:
133+
134+
```ts
135+
new Deck({
136+
views:[
137+
new MapView({id: 'left-map'}),
138+
new MapView({id: 'right-map'})
139+
],
140+
widgets: [
141+
new FullscreenWidget({placement: 'top-right'}),
142+
new ZoomWidget({viewId: 'left-map'}),
143+
new GimbalWidget({viewId: 'right-map'}),
144+
]
145+
})
146+
```
147+
148+
This configuration will result in the following HTML structure:
149+
150+
```html
151+
<!-- map container -->
152+
<div class="deck-widget-container">
153+
<canvas id="deckgl-overlay">
154+
<!-- size of full map container -->
155+
<div>
156+
<div class="top-right">
157+
</FullscreenWidget>
158+
</div>
159+
</div>
160+
<!-- size and position of the "left-map" view -->
161+
<div>
162+
<div class="top-left">
163+
</ZoomWidget>
164+
</div>
165+
</div>
166+
<!-- size and position of the "right-map" view -->
167+
<div>
168+
<div class="top-left">
169+
</GimbalWidget>
170+
</div>
171+
</div>
172+
</div>
173+
```
174+
175+
Remarks:
176+
177+
* Widgets in the default container will be overlapped by view-specific widgets.
178+
* Widget UI with dynamic positioning, such as an `InfoWidget`, may not expose the `placement` prop as they control positioning internally.
179+
* For more information about using multiple deck.gl views, see the [Using Multiple Views](../../developer-guide/views.md#using-multiple-views) guide.
180+
128181
## Writing new Widgets
129182

130183
A widget should inherit the `Widget` class.

docs/api-reference/widgets/styling.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,13 @@ Additionally, refer to each widget's API reference for variables specific to tha
131131
| `--button-backdrop-filter` | [Backdrop Filter](https://developer.mozilla.org/en-US/docs/Web/CSS/backdrop-filter) | `unset` |
132132
| `--button-icon-idle` | [Color](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value) | `rgba(97, 97, 102, 1)` |
133133
| `--button-icon-hover` | [Color](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value) | `rgba(24, 24, 26, 1)` |
134+
| `--button-text-color` | [Color](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value) | `rgba(24, 24, 26, 1)` |
135+
136+
### Menu
137+
138+
| Name | Type | Default |
139+
| ---- | ---- | ------- |
140+
| `--menu-gap` | [Dimension](https://developer.mozilla.org/en-US/docs/Web/CSS/dimension) | `4px` |
134141

135142
### Icon
136143

modules/core/src/lib/deck.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -476,7 +476,7 @@ export default class Deck<ViewsT extends ViewOrViews = null> {
476476
this.canvas = null;
477477
}
478478

479-
log.log(`recreating animation loop for new device! id=${props.device.id}`);
479+
log.log(`recreating animation loop for new device! id=${props.device.id}`)();
480480

481481
this.animationLoop = this._createAnimationLoop(props.device, props);
482482
this.animationLoop.start();

modules/core/src/lib/widget.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {PickingInfo} from './picking/pick-info';
88
import type {MjolnirPointerEvent, MjolnirGestureEvent} from 'mjolnir.js';
99
import type Layer from './layer';
1010
import type {WidgetManager, WidgetPlacement} from './widget-manager';
11+
import type {ViewOrViews} from './view-manager';
1112
import {deepEqual} from '../utils/deep-equal';
1213
import {applyStyles, removeStyles} from '../utils/apply-styles';
1314

@@ -19,7 +20,10 @@ export type WidgetProps = {
1920
className?: string;
2021
};
2122

22-
export abstract class Widget<PropsT extends WidgetProps = WidgetProps> {
23+
export abstract class Widget<
24+
PropsT extends WidgetProps = WidgetProps,
25+
ViewsT extends ViewOrViews = null
26+
> {
2327
static defaultProps: Required<WidgetProps> = {
2428
id: 'widget',
2529
style: {},
@@ -43,7 +47,7 @@ export abstract class Widget<PropsT extends WidgetProps = WidgetProps> {
4347

4448
// Populated by core when mounted
4549
widgetManager?: WidgetManager;
46-
deck?: Deck<any>;
50+
deck?: Deck<ViewsT>;
4751
rootElement?: HTMLDivElement | null;
4852

4953
constructor(props: PropsT, defaultProps: Required<PropsT>) {

modules/widgets/src/fps-widget.tsx

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
// Copyright (c) vis.gl contributors
44

55
import {Widget} from '@deck.gl/core';
6+
import {render} from 'preact';
67
import type {WidgetPlacement, Deck, WidgetProps} from '@deck.gl/core';
8+
import {IconButton} from './lib/components/icon-button';
79

810
/** Properties for the FpsWidget. */
911
export type FpsWidgetProps = WidgetProps & {
@@ -42,25 +44,30 @@ export class FpsWidget extends Widget<FpsWidgetProps> {
4244

4345
onAdd({}: {deck: Deck<any>; viewId: string | null}): void {
4446
this._lastFps = this._getFps();
45-
this.updateHTML();
47+
requestAnimationFrame(() => this._animate());
4648
}
4749

4850
onRenderHTML(rootElement: HTMLElement): void {
4951
const fps = this._getFps();
50-
// TODO - avoid changing the root element, instead create a child for better styling.
51-
rootElement.innerText = `FPS:\n${fps}`;
52-
rootElement.style.backgroundColor = 'white';
53-
rootElement.style.fontFamily = 'monospace';
54-
rootElement.style.fontSize = '8px';
55-
rootElement.style.fontWeight = '700'; // Make font bolder on click
52+
render(
53+
<IconButton>
54+
<div className="text">
55+
FPS
56+
<br />
57+
{fps}
58+
</div>
59+
</IconButton>,
60+
rootElement
61+
);
5662
}
5763

58-
onRedraw({}: {viewports: any[]; layers: any[]}): void {
64+
_animate(): void {
5965
const fps = this._getFps();
60-
if (fps !== this._lastFps) {
66+
if (this._lastFps !== fps) {
6167
this._lastFps = fps;
6268
this.updateHTML();
6369
}
70+
requestAnimationFrame(() => this._animate());
6471
}
6572

6673
_getFps(): number {

modules/widgets/src/fullscreen-widget.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// Copyright (c) vis.gl contributors
44

55
/* global document */
6-
import {Widget, type WidgetProps, type WidgetPlacement} from '@deck.gl/core';
6+
import {log, Widget, type WidgetProps, type WidgetPlacement} from '@deck.gl/core';
77
import {render} from 'preact';
88
import {IconButton} from './lib/components/icon-button';
99

@@ -57,7 +57,9 @@ export class FullscreenWidget extends Widget<FullscreenWidgetProps> {
5757
onRenderHTML(rootElement: HTMLElement): void {
5858
render(
5959
<IconButton
60-
onClick={this.handleClick.bind(this)}
60+
onClick={() => {
61+
this.handleClick().catch(err => log.error(err)());
62+
}}
6163
label={this.fullscreen ? this.props.exitLabel : this.props.enterLabel}
6264
className={this.fullscreen ? 'deck-widget-fullscreen-exit' : 'deck-widget-fullscreen-enter'}
6365
/>,

modules/widgets/src/gimbal-widget.tsx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export class GimbalWidget extends Widget<GimbalWidgetProps> {
3131

3232
className = 'deck-widget-gimbal';
3333
placement: WidgetPlacement = 'top-left';
34+
viewports: {[id: string]: Viewport} = {};
3435

3536
constructor(props: GimbalWidgetProps = {}) {
3637
super(props, GimbalWidget.defaultProps);
@@ -44,14 +45,18 @@ export class GimbalWidget extends Widget<GimbalWidgetProps> {
4445
}
4546

4647
onRenderHTML(rootElement: HTMLElement): void {
47-
const {rotationOrbit, rotationX} = this.getNormalizedRotation();
48+
const viewId = this.viewId || Object.values(this.viewports)[0]?.id || 'default-view';
49+
const widgetViewport = this.viewports[viewId];
50+
const {rotationOrbit, rotationX} = this.getNormalizedRotation(widgetViewport);
4851
// Note - we use CSS 3D transforms instead of SVG 2D transforms
4952
const ui = (
5053
<div className="deck-widget-button" style={{perspective: 100, pointerEvents: 'auto'}}>
5154
<button
5255
type="button"
5356
onClick={() => {
54-
this.resetOrbitView();
57+
for (const viewport of Object.values(this.viewports)) {
58+
this.resetOrbitView(viewport);
59+
}
5560
}}
5661
title={this.props.label}
5762
style={{position: 'relative', width: 26, height: 26}}
@@ -109,11 +114,12 @@ export class GimbalWidget extends Widget<GimbalWidgetProps> {
109114
}
110115

111116
onViewportChange(viewport: Viewport) {
117+
this.viewports[viewport.id] = viewport;
112118
this.updateHTML();
113119
}
114120

115-
resetOrbitView() {
116-
const viewId = this.getViewId();
121+
resetOrbitView(viewport?: Viewport) {
122+
const viewId = this.getViewId(viewport);
117123
const viewState = this.getViewState(viewId);
118124
if ('rotationOrbit' in viewState || 'rotationX' in viewState) {
119125
const nextViewState = {
@@ -130,8 +136,8 @@ export class GimbalWidget extends Widget<GimbalWidgetProps> {
130136
}
131137
}
132138

133-
getNormalizedRotation() {
134-
const viewState = this.getViewState(this.getViewId());
139+
getNormalizedRotation(viewport?: Viewport): {rotationOrbit: number; rotationX: number} {
140+
const viewState = this.getViewState(this.getViewId(viewport));
135141
const [rz, rx] = this.getRotation(viewState);
136142
const rotationOrbit = normalizeAndClampAngle(rz);
137143
const rotationX = normalizeAndClampAngle(rx);
@@ -147,8 +153,8 @@ export class GimbalWidget extends Widget<GimbalWidgetProps> {
147153

148154
// Move to Widget/WidgetManager?
149155

150-
getViewId() {
151-
const viewId = this.viewId || 'OrbitView';
156+
getViewId(viewport?: Viewport) {
157+
const viewId = this.viewId || viewport?.id || 'OrbitView';
152158
return viewId;
153159
}
154160

modules/widgets/src/info-widget.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// SPDX-License-Identifier: MIT
33
// Copyright (c) vis.gl contributors
44

5-
/* global document */
65
import {Widget} from '@deck.gl/core';
76
import type {Deck, PickingInfo, Viewport, WidgetProps} from '@deck.gl/core';
87
import {render, JSX} from 'preact';

modules/widgets/src/lib/components/button-group.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import type {ComponentChildren} from 'preact';
66

77
export type ButtonGroupProps = {
88
children: ComponentChildren;
9-
orientation;
9+
orientation: 'vertical' | 'horizontal';
1010
};
1111

1212
/** Renders a group of buttons with Widget CSS */
1313
export const ButtonGroup = (props: ButtonGroupProps) => {
14-
const {children, orientation} = props;
14+
const {children, orientation = 'horizontal'} = props;
1515
return <div className={`deck-widget-button-group ${orientation}`}>{children}</div>;
1616
};

modules/widgets/src/lib/components/grouped-icon-button.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,26 @@
22
// SPDX-License-Identifier: MIT
33
// Copyright (c) vis.gl contributors
44

5+
import type {ComponentChildren, JSX} from 'preact';
6+
57
export type GroupedIconButtonProps = {
68
className?: string;
7-
label: string;
8-
onClick: () => void;
9+
label?: string;
10+
onClick?: JSX.MouseEventHandler<HTMLButtonElement>;
11+
children?: ComponentChildren;
912
};
1013

1114
/** Renders an icon button as part of a ButtonGroup */
12-
export const GroupedIconButton = props => {
13-
const {className, label, onClick} = props;
15+
export const GroupedIconButton = (props: GroupedIconButtonProps) => {
16+
const {className = '', label, onClick, children} = props;
1417
return (
1518
<button
16-
className={`deck-widget-icon-button ${className || ''}`}
19+
className={`deck-widget-icon-button ${className}`}
1720
type="button"
1821
onClick={onClick}
1922
title={label}
2023
>
21-
<div className="deck-widget-icon" />
24+
{children ? children : <div className="deck-widget-icon" />}
2225
</button>
2326
);
2427
};

0 commit comments

Comments
 (0)