Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(react) Support for JSX Widgets in React #9278

Open
wants to merge 27 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
9bbdc79
Export widget prop types
chrisgervang Oct 25, 2024
e55ab6f
Construct new props instead of mutating them
chrisgervang Oct 25, 2024
9803538
Add DeckGLContext
chrisgervang Oct 25, 2024
c526999
Add useWidget hook
chrisgervang Oct 25, 2024
51154e8
Create react components for each widget
chrisgervang Oct 25, 2024
e6a7a5f
Add widget to basic example
chrisgervang Oct 25, 2024
5549372
Fix bootstrap
chrisgervang Oct 25, 2024
b8aaef2
Merge branch 'master' into chr/jsx-widgets
chrisgervang Nov 22, 2024
5508ccd
Merge branch 'master' into chr/jsx-widgets
chrisgervang Dec 5, 2024
3ed7e27
Merge branch 'master' into chr/jsx-widgets
chrisgervang Dec 12, 2024
214f5e6
Deck should be defined in context for widgets
chrisgervang Dec 13, 2024
74c241b
WIP React widgets should warn when vanilla widgets are used
chrisgervang Dec 13, 2024
a0d0b6e
Add widget prop
chrisgervang Dec 13, 2024
26a95a1
Export useWidget
chrisgervang Dec 13, 2024
271561f
Demo react widget
chrisgervang Dec 13, 2024
1e62c65
Widgets should be removed when JSX unmounts
chrisgervang Dec 13, 2024
ec8d18f
Fix widget warning
chrisgervang Dec 13, 2024
7cd5d58
Demo toggleable zoom widget in react
chrisgervang Dec 13, 2024
b7a79ec
lint
chrisgervang Dec 13, 2024
82287da
clearer warning message
chrisgervang Dec 17, 2024
971da0f
Merge branch 'master' into chr/jsx-widgets
chrisgervang Dec 17, 2024
09b577a
Fix build
chrisgervang Dec 17, 2024
7e4c4fb
viewports possibly undefined
chrisgervang Dec 17, 2024
1e5545f
Merge branch 'master' into chr/jsx-widgets
chrisgervang Dec 19, 2024
91c0930
Merge branch 'master' into chr/jsx-widgets
chrisgervang Dec 23, 2024
ffe8362
basic jsx example
chrisgervang Dec 24, 2024
23a3210
lint
chrisgervang Dec 24, 2024
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
11 changes: 10 additions & 1 deletion examples/get-started/react/basic/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
import React from 'react';
import {createRoot} from 'react-dom/client';
import DeckGL, {GeoJsonLayer, ArcLayer} from 'deck.gl';
import {CompassWidget, ZoomWidget} from '@deck.gl/react';
import {FullscreenWidget} from '@deck.gl/widgets';

Check failure on line 9 in examples/get-started/react/basic/app.jsx

View workflow job for this annotation

GitHub Actions / test-node

'FullscreenWidget' is defined but never used
import '@deck.gl/widgets/stylesheet.css';

// source: Natural Earth http://www.naturalearthdata.com/ via geojson.xyz
const COUNTRIES =
Expand All @@ -29,7 +32,13 @@
};

return (
<DeckGL controller={true} initialViewState={INITIAL_VIEW_STATE}>
<DeckGL
controller={true}
initialViewState={INITIAL_VIEW_STATE}
// widgets={[new FullscreenWidget({})]}
>
<CompassWidget />
<ZoomWidget orientation="horizontal" />
<GeoJsonLayer
id="base-map"
data={COUNTRIES}
Expand Down
1 change: 1 addition & 0 deletions modules/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"scripts": {},
"peerDependencies": {
"@deck.gl/core": "^9.1.0-beta",
"@deck.gl/widgets": "^9.1.0-beta",
"react": ">=16.3.0",
"react-dom": ">=16.3.0"
},
Expand Down
2 changes: 1 addition & 1 deletion modules/react/src/deckgl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import extractJSXLayers, {DeckGLRenderCallback} from './utils/extract-jsx-layers
import positionChildrenUnderViews from './utils/position-children-under-views';
import extractStyles from './utils/extract-styles';

import type {DeckGLContextValue} from './utils/position-children-under-views';
import type {DeckGLContextValue} from './utils/deckgl-context';
import type {DeckProps, View, Viewport} from '@deck.gl/core';

export type ViewOrViews = View | View[] | null;
Expand Down
7 changes: 6 additions & 1 deletion modules/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
export {default as DeckGL} from './deckgl';
export {default} from './deckgl';

// Widgets
export {CompassWidget} from './widgets/compass-widget';
felixpalmer marked this conversation as resolved.
Show resolved Hide resolved
export {FullscreenWidget} from './widgets/fullscreen-widget';
export {ZoomWidget} from './widgets/zoom-widget';
chrisgervang marked this conversation as resolved.
Show resolved Hide resolved

// Types
export type {DeckGLContextValue} from './utils/position-children-under-views';
export type {DeckGLContextValue} from './utils/deckgl-context';
export type {DeckGLRef, DeckGLProps} from './deckgl';
15 changes: 15 additions & 0 deletions modules/react/src/utils/deckgl-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {createContext} from 'react';
import type {EventManager} from 'mjolnir.js';
import type {Deck, DeckProps, Viewport, Widget} from '@deck.gl/core';

export type DeckGLContextValue = {
viewport: Viewport;
container: HTMLElement;
eventManager: EventManager;
onViewStateChange: DeckProps['onViewStateChange'];
deck?: Deck<any>;
widgets?: Widget[];
};

// @ts-ignore
export const DeckGlContext = createContext<DeckGLContextValue>();
17 changes: 6 additions & 11 deletions modules/react/src/utils/position-children-under-views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,15 @@ import {inheritsFrom} from './inherits-from';
import evaluateChildren, {isComponent} from './evaluate-children';

import type {ViewOrViews} from '../deckgl';
import type {Deck, DeckProps, Viewport} from '@deck.gl/core';
import type {EventManager} from 'mjolnir.js';

export type DeckGLContextValue = {
viewport: Viewport;
container: HTMLElement;
eventManager: EventManager;
onViewStateChange: DeckProps['onViewStateChange'];
};
import type {Deck, Viewport} from '@deck.gl/core';
import {DeckGlContext, type DeckGLContextValue} from './deckgl-context';

// Iterate over views and reposition children associated with views
// TODO - Can we supply a similar function for the non-React case?
export default function positionChildrenUnderViews<ViewsT extends ViewOrViews>({
children,
deck,
ContextProvider
ContextProvider = DeckGlContext.Provider
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we remove the if (ContextProvider) { check on L97 if this is always defined now?

}: {
children: React.ReactNode[];
deck?: Deck<ViewsT>;
Expand Down Expand Up @@ -103,6 +96,7 @@ export default function positionChildrenUnderViews<ViewsT extends ViewOrViews>({

if (ContextProvider) {
const contextValue: DeckGLContextValue = {
deck,
viewport,
// @ts-expect-error accessing protected property
container: deck.canvas.offsetParent,
Expand All @@ -112,7 +106,8 @@ export default function positionChildrenUnderViews<ViewsT extends ViewOrViews>({
params.viewId = viewId;
// @ts-expect-error accessing protected method
deck._onViewStateChange(params);
}
},
widgets: []
};
return createElement(ContextProvider, {key, value: contextValue}, viewElement);
}
Expand Down
32 changes: 32 additions & 0 deletions modules/react/src/utils/use-widget.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {useContext, useMemo, useEffect} from 'react';
import {DeckGlContext} from './deckgl-context';
import {log, type Widget, _deepEqual as deepEqual} from '@deck.gl/core';

function useWidget<T extends Widget, PropsT extends {}>(
WidgetClass: {new (props: PropsT): T},
props: PropsT
): T {
const context = useContext(DeckGlContext);
const {widgets, deck} = context;
useEffect(() => {
// warn if the user supplied a vanilla prop, since it will be ignored
// NOTE: effect runs once per widget after the first render
// TODO: this doesn't work since the widgets override the deck prop. Can't tell which are user set.
// if (!deepEqual(deck?.props.widgets, widgets, 1)) {
if (deck?.props.widgets.length !== 0) {
log.warn('Detected deck "widgets" prop used simultaneously with React widgets. Vanilla widgets will be ignored.')();
}
}, []);
const widget = useMemo(() => new WidgetClass(props), [WidgetClass]);

widgets?.push(widget);
widget.setProps(props);

useEffect(() => {
deck?.setProps({widgets});
felixpalmer marked this conversation as resolved.
Show resolved Hide resolved
}, [widgets]);

return widget;
}

export default useWidget;
8 changes: 8 additions & 0 deletions modules/react/src/widgets/compass-widget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {CompassWidget as VanillaCompassWidget} from '@deck.gl/widgets';
import type {CompassWidgetProps} from '@deck.gl/widgets';
import useWidget from '../utils/use-widget';

export const CompassWidget = (props: CompassWidgetProps = {}) => {
const widget = useWidget(VanillaCompassWidget, props);
return null;
};
8 changes: 8 additions & 0 deletions modules/react/src/widgets/fullscreen-widget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {FullscreenWidget as VanillaFullscreenWidget} from '@deck.gl/widgets';
import type {FullscreenWidgetProps} from '@deck.gl/widgets';
import useWidget from '../utils/use-widget';

export const FullscreenWidget = (props: FullscreenWidgetProps = {}) => {
const widget = useWidget(VanillaFullscreenWidget, props);
return null;
};
8 changes: 8 additions & 0 deletions modules/react/src/widgets/zoom-widget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {ZoomWidget as VanillaZoomWidget} from '@deck.gl/widgets';
import type {ZoomWidgetProps} from '@deck.gl/widgets';
import useWidget from '../utils/use-widget';

export const ZoomWidget = (props: ZoomWidgetProps = {}) => {
const widget = useWidget(VanillaZoomWidget, props);
return null;
};
3 changes: 2 additions & 1 deletion modules/react/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"outDir": "dist"
},
"references": [
{"path": "../core"}
{"path": "../core"},
{"path": "../widgets"}
]
}
13 changes: 8 additions & 5 deletions modules/widgets/src/compass-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {FlyToInterpolator, WebMercatorViewport, _GlobeViewport} from '@deck.gl/c
import type {Deck, Viewport, Widget, WidgetPlacement} from '@deck.gl/core';
import {render} from 'preact';

interface CompassWidgetProps {
export interface CompassWidgetProps {
id?: string;
placement?: WidgetPlacement;
/**
Expand Down Expand Up @@ -45,10 +45,13 @@ export class CompassWidget implements Widget<CompassWidgetProps> {
this.id = props.id || 'compass';
this.viewId = props.viewId || null;
this.placement = props.placement || 'top-left';
props.transitionDuration = props.transitionDuration || 200;
props.label = props.label || 'Compass';
props.style = props.style || {};
this.props = props;

this.props = {
...props,
transitionDuration: props.transitionDuration || 200,
label: props.label || 'Compass',
style: props.style || {}
};
chrisgervang marked this conversation as resolved.
Show resolved Hide resolved
}

setProps(props: Partial<CompassWidgetProps>) {
Expand Down
13 changes: 8 additions & 5 deletions modules/widgets/src/fullscreen-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type {Deck, Widget, WidgetPlacement} from '@deck.gl/core';
import {render} from 'preact';
import {IconButton} from './components';

interface FullscreenWidgetProps {
export interface FullscreenWidgetProps {
id?: string;
placement?: WidgetPlacement;
/**
Expand Down Expand Up @@ -48,10 +48,13 @@ export class FullscreenWidget implements Widget<FullscreenWidgetProps> {
constructor(props: FullscreenWidgetProps) {
this.id = props.id || 'fullscreen';
this.placement = props.placement || 'top-left';
props.enterLabel = props.enterLabel || 'Enter Fullscreen';
props.exitLabel = props.exitLabel || 'Exit Fullscreen';
props.style = props.style || {};
this.props = props;

this.props = {
...props,
enterLabel: props.enterLabel || 'Enter Fullscreen',
exitLabel: props.exitLabel || 'Exit Fullscreen',
style: props.style || {}
};
}

onAdd({deck}: {deck: Deck<any>}): HTMLDivElement {
Expand Down
4 changes: 4 additions & 0 deletions modules/widgets/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,8 @@ export {FullscreenWidget} from './fullscreen-widget';
export {CompassWidget} from './compass-widget';
export {ZoomWidget} from './zoom-widget';

export type {FullscreenWidgetProps} from './fullscreen-widget';
export type {CompassWidgetProps} from './compass-widget';
export type {ZoomWidgetProps} from './zoom-widget';

export * from './themes';
15 changes: 9 additions & 6 deletions modules/widgets/src/zoom-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type {Deck, Viewport, Widget, WidgetPlacement} from '@deck.gl/core';
import {render} from 'preact';
import {ButtonGroup, GroupedIconButton} from './components';

interface ZoomWidgetProps {
export interface ZoomWidgetProps {
Copy link
Collaborator Author

@chrisgervang chrisgervang Dec 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Pessimistress I'm noticing that the widget's onViewportChange isn't called on initialization. The user needs to interact with deck before a viewport is set.

Vanilla widgets work. Any idea what's different?

Copy link
Collaborator Author

@chrisgervang chrisgervang Dec 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cause is this guard is hit before the viewports have been assigned to the widget. lastViewports was set on the first time props are set (when widgets: [] and it initialized the tooltip).
Screenshot 2024-12-12 at 5 18 07 PM

The goal of this code is to cache the current viewport so that the widget can use it as the starting point of its view modification.

The hook adds widgets in one-by-one: [] then [FirstWidget] then [FirstWidget, SecondWidget]

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we could push this check down into each widget to implement since the manager isn't aware of how widgets store viewports, or we could add viewports: {} to class Widget and have the manager check that instead? Thoughts?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See proposed fix in #9303

id?: string;
placement?: WidgetPlacement;
/**
Expand Down Expand Up @@ -56,11 +56,14 @@ export class ZoomWidget implements Widget<ZoomWidgetProps> {
this.viewId = props.viewId || null;
this.placement = props.placement || 'top-left';
this.orientation = props.orientation || 'vertical';
props.transitionDuration = props.transitionDuration || 200;
props.zoomInLabel = props.zoomInLabel || 'Zoom In';
props.zoomOutLabel = props.zoomOutLabel || 'Zoom Out';
props.style = props.style || {};
this.props = props;

this.props = {
...props,
transitionDuration: props.transitionDuration || 200,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wonder if it would be nice to support defaultProps for widgets to avoid this sort of code

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Widget is only an interface, so I think the best we can do is lead by example of least annoying code to maintain. I just changed the dev guide to use this.id = props.id ?? this.id to avoid needing to repeat the hard-coded values, which is a marked improvement over this.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could it be an abstract class instead?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's tempting but I'm more in favor of flexibility at this stage. I'm hesitant to abstract too early across widgets with there are so many ways to implement UI.

It's a bit of extra work to fill out the interface, but not much.

Maybe when we have a fuller library of examples exploring a breadth of use cases.. until then, there's nothing preventing an author from making their own abstract class.

zoomInLabel: props.zoomInLabel || 'Zoom In',
zoomOutLabel: props.zoomOutLabel || 'Zoom Out',
style: props.style || {}
};
}

onAdd({deck}: {deck: Deck<any>}): HTMLDivElement {
Expand Down
Loading