From 9bbdc79fd9d0baed335e8024c2980a91c186693d Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Fri, 25 Oct 2024 11:10:50 -0700 Subject: [PATCH 01/27] Export widget prop types Signed-off-by: Chris Gervang --- modules/widgets/src/compass-widget.tsx | 2 +- modules/widgets/src/fullscreen-widget.tsx | 2 +- modules/widgets/src/index.ts | 4 ++++ modules/widgets/src/zoom-widget.tsx | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/modules/widgets/src/compass-widget.tsx b/modules/widgets/src/compass-widget.tsx index 013863cbd84..4618dd3c680 100644 --- a/modules/widgets/src/compass-widget.tsx +++ b/modules/widgets/src/compass-widget.tsx @@ -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; /** diff --git a/modules/widgets/src/fullscreen-widget.tsx b/modules/widgets/src/fullscreen-widget.tsx index 2619795399d..a96f5e3709b 100644 --- a/modules/widgets/src/fullscreen-widget.tsx +++ b/modules/widgets/src/fullscreen-widget.tsx @@ -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; /** diff --git a/modules/widgets/src/index.ts b/modules/widgets/src/index.ts index f98113831ea..b2355887aed 100644 --- a/modules/widgets/src/index.ts +++ b/modules/widgets/src/index.ts @@ -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'; diff --git a/modules/widgets/src/zoom-widget.tsx b/modules/widgets/src/zoom-widget.tsx index babdb2740c0..76b4012db6f 100644 --- a/modules/widgets/src/zoom-widget.tsx +++ b/modules/widgets/src/zoom-widget.tsx @@ -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 { id?: string; placement?: WidgetPlacement; /** From e55ab6f462ad0cd160d51c8f096e6aac56e25f64 Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Fri, 25 Oct 2024 11:17:05 -0700 Subject: [PATCH 02/27] Construct new props instead of mutating them Signed-off-by: Chris Gervang --- modules/widgets/src/compass-widget.tsx | 11 +++++++---- modules/widgets/src/fullscreen-widget.tsx | 11 +++++++---- modules/widgets/src/zoom-widget.tsx | 13 ++++++++----- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/modules/widgets/src/compass-widget.tsx b/modules/widgets/src/compass-widget.tsx index 4618dd3c680..d6b03f6fcc0 100644 --- a/modules/widgets/src/compass-widget.tsx +++ b/modules/widgets/src/compass-widget.tsx @@ -45,10 +45,13 @@ export class CompassWidget implements Widget { 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 || {} + }; } setProps(props: Partial) { diff --git a/modules/widgets/src/fullscreen-widget.tsx b/modules/widgets/src/fullscreen-widget.tsx index a96f5e3709b..fb28265411d 100644 --- a/modules/widgets/src/fullscreen-widget.tsx +++ b/modules/widgets/src/fullscreen-widget.tsx @@ -48,10 +48,13 @@ export class FullscreenWidget implements Widget { 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}): HTMLDivElement { diff --git a/modules/widgets/src/zoom-widget.tsx b/modules/widgets/src/zoom-widget.tsx index 76b4012db6f..88fdb09ef94 100644 --- a/modules/widgets/src/zoom-widget.tsx +++ b/modules/widgets/src/zoom-widget.tsx @@ -56,11 +56,14 @@ export class ZoomWidget implements Widget { 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, + zoomInLabel: props.zoomInLabel || 'Zoom In', + zoomOutLabel: props.zoomOutLabel || 'Zoom Out', + style: props.style || {} + }; } onAdd({deck}: {deck: Deck}): HTMLDivElement { From 980353809171b3a06569a4f633da2b647227fea1 Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Fri, 25 Oct 2024 11:20:35 -0700 Subject: [PATCH 03/27] Add DeckGLContext Signed-off-by: Chris Gervang --- modules/react/src/deckgl.ts | 2 +- modules/react/src/index.ts | 2 +- modules/react/src/utils/deckgl-context.ts | 15 +++++++++++++++ .../src/utils/position-children-under-views.ts | 13 +++---------- 4 files changed, 20 insertions(+), 12 deletions(-) create mode 100644 modules/react/src/utils/deckgl-context.ts diff --git a/modules/react/src/deckgl.ts b/modules/react/src/deckgl.ts index 6ab52f58ea4..bdcca88fa6d 100644 --- a/modules/react/src/deckgl.ts +++ b/modules/react/src/deckgl.ts @@ -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; diff --git a/modules/react/src/index.ts b/modules/react/src/index.ts index b390f3c0231..7ca74636c14 100644 --- a/modules/react/src/index.ts +++ b/modules/react/src/index.ts @@ -6,5 +6,5 @@ export {default as DeckGL} from './deckgl'; export {default} from './deckgl'; // Types -export type {DeckGLContextValue} from './utils/position-children-under-views'; +export type {DeckGLContextValue} from './utils/deckgl-context'; export type {DeckGLRef, DeckGLProps} from './deckgl'; diff --git a/modules/react/src/utils/deckgl-context.ts b/modules/react/src/utils/deckgl-context.ts new file mode 100644 index 00000000000..d40d7d9d0d3 --- /dev/null +++ b/modules/react/src/utils/deckgl-context.ts @@ -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; + widgets?: Widget[]; +}; + +// @ts-ignore +export const DeckGlContext = createContext(); diff --git a/modules/react/src/utils/position-children-under-views.ts b/modules/react/src/utils/position-children-under-views.ts index 3b1228a7467..3bbb5a1af39 100644 --- a/modules/react/src/utils/position-children-under-views.ts +++ b/modules/react/src/utils/position-children-under-views.ts @@ -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({ children, deck, - ContextProvider + ContextProvider = DeckGlContext.Provider }: { children: React.ReactNode[]; deck?: Deck; From c526999767988d503053d35bf41817b4e30972d0 Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Fri, 25 Oct 2024 11:21:18 -0700 Subject: [PATCH 04/27] Add useWidget hook Signed-off-by: Chris Gervang --- .../utils/position-children-under-views.ts | 3 ++- modules/react/src/utils/use-widget.ts | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 modules/react/src/utils/use-widget.ts diff --git a/modules/react/src/utils/position-children-under-views.ts b/modules/react/src/utils/position-children-under-views.ts index 3bbb5a1af39..1ce9e63693d 100644 --- a/modules/react/src/utils/position-children-under-views.ts +++ b/modules/react/src/utils/position-children-under-views.ts @@ -105,7 +105,8 @@ export default function positionChildrenUnderViews({ params.viewId = viewId; // @ts-expect-error accessing protected method deck._onViewStateChange(params); - } + }, + widgets: [] }; return createElement(ContextProvider, {key, value: contextValue}, viewElement); } diff --git a/modules/react/src/utils/use-widget.ts b/modules/react/src/utils/use-widget.ts new file mode 100644 index 00000000000..3f2e89a3ba9 --- /dev/null +++ b/modules/react/src/utils/use-widget.ts @@ -0,0 +1,23 @@ +import {useContext, useMemo, useEffect} from 'react'; +import {DeckGlContext} from './deckgl-context'; +import type {Widget} from '@deck.gl/core'; + +function useWidget( + WidgetClass: {new (props: PropsT): T}, + props: PropsT +): T { + const context = useContext(DeckGlContext); + const {widgets, deck} = context; + const widget = useMemo(() => new WidgetClass(props), [WidgetClass]); + + widgets?.push(widget); + widget.setProps(props); + + useEffect(() => { + deck?.setProps({widgets}); + }, [widgets]); + + return widget; +} + +export default useWidget; From 51154e8e7baf1ca37084646a5656a890338c1b4d Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Fri, 25 Oct 2024 11:21:50 -0700 Subject: [PATCH 05/27] Create react components for each widget Signed-off-by: Chris Gervang --- modules/react/package.json | 1 + modules/react/src/index.ts | 5 +++++ modules/react/src/widgets/compass-widget.tsx | 8 ++++++++ modules/react/src/widgets/fullscreen-widget.tsx | 8 ++++++++ modules/react/src/widgets/zoom-widget.tsx | 8 ++++++++ 5 files changed, 30 insertions(+) create mode 100644 modules/react/src/widgets/compass-widget.tsx create mode 100644 modules/react/src/widgets/fullscreen-widget.tsx create mode 100644 modules/react/src/widgets/zoom-widget.tsx diff --git a/modules/react/package.json b/modules/react/package.json index 8acfb363f85..ea766b8258a 100644 --- a/modules/react/package.json +++ b/modules/react/package.json @@ -35,6 +35,7 @@ "scripts": {}, "peerDependencies": { "@deck.gl/core": "9.0.0-alpha.0", + "@deck.gl/widgets": "9.0.0-alpha.0", "react": ">=16.3.0", "react-dom": ">=16.3.0" }, diff --git a/modules/react/src/index.ts b/modules/react/src/index.ts index 7ca74636c14..bf555883560 100644 --- a/modules/react/src/index.ts +++ b/modules/react/src/index.ts @@ -5,6 +5,11 @@ export {default as DeckGL} from './deckgl'; export {default} from './deckgl'; +// Widgets +export {CompassWidget} from './widgets/compass-widget'; +export {FullscreenWidget} from './widgets/fullscreen-widget'; +export {ZoomWidget} from './widgets/zoom-widget'; + // Types export type {DeckGLContextValue} from './utils/deckgl-context'; export type {DeckGLRef, DeckGLProps} from './deckgl'; diff --git a/modules/react/src/widgets/compass-widget.tsx b/modules/react/src/widgets/compass-widget.tsx new file mode 100644 index 00000000000..0e44f6fc007 --- /dev/null +++ b/modules/react/src/widgets/compass-widget.tsx @@ -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; +}; diff --git a/modules/react/src/widgets/fullscreen-widget.tsx b/modules/react/src/widgets/fullscreen-widget.tsx new file mode 100644 index 00000000000..33974514c8e --- /dev/null +++ b/modules/react/src/widgets/fullscreen-widget.tsx @@ -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; +}; diff --git a/modules/react/src/widgets/zoom-widget.tsx b/modules/react/src/widgets/zoom-widget.tsx new file mode 100644 index 00000000000..0a42103832b --- /dev/null +++ b/modules/react/src/widgets/zoom-widget.tsx @@ -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; +}; From e6a7a5fa999272c2343a60a55471d0476ef53770 Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Fri, 25 Oct 2024 11:22:28 -0700 Subject: [PATCH 06/27] Add widget to basic example Signed-off-by: Chris Gervang --- examples/get-started/react/basic/app.jsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/get-started/react/basic/app.jsx b/examples/get-started/react/basic/app.jsx index da124512468..d7fec7c0ee5 100644 --- a/examples/get-started/react/basic/app.jsx +++ b/examples/get-started/react/basic/app.jsx @@ -5,6 +5,8 @@ import React from 'react'; import {createRoot} from 'react-dom/client'; import DeckGL, {GeoJsonLayer, ArcLayer} from 'deck.gl'; +import {CompassWidget} from '@deck.gl/react'; +import '@deck.gl/widgets/stylesheet.css'; // source: Natural Earth http://www.naturalearthdata.com/ via geojson.xyz const COUNTRIES = @@ -30,6 +32,7 @@ function Root() { return ( + Date: Fri, 25 Oct 2024 11:26:12 -0700 Subject: [PATCH 07/27] Fix bootstrap Signed-off-by: Chris Gervang --- modules/react/src/widgets/fullscreen-widget.tsx | 2 +- modules/react/src/widgets/zoom-widget.tsx | 2 +- modules/react/tsconfig.json | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/modules/react/src/widgets/fullscreen-widget.tsx b/modules/react/src/widgets/fullscreen-widget.tsx index 33974514c8e..42030d571db 100644 --- a/modules/react/src/widgets/fullscreen-widget.tsx +++ b/modules/react/src/widgets/fullscreen-widget.tsx @@ -1,6 +1,6 @@ import {FullscreenWidget as VanillaFullscreenWidget} from '@deck.gl/widgets'; import type {FullscreenWidgetProps} from '@deck.gl/widgets'; -import useWidget from './utils/use-widget'; +import useWidget from '../utils/use-widget'; export const FullscreenWidget = (props: FullscreenWidgetProps = {}) => { const widget = useWidget(VanillaFullscreenWidget, props); diff --git a/modules/react/src/widgets/zoom-widget.tsx b/modules/react/src/widgets/zoom-widget.tsx index 0a42103832b..7c3a7b4f994 100644 --- a/modules/react/src/widgets/zoom-widget.tsx +++ b/modules/react/src/widgets/zoom-widget.tsx @@ -1,6 +1,6 @@ import {ZoomWidget as VanillaZoomWidget} from '@deck.gl/widgets'; import type {ZoomWidgetProps} from '@deck.gl/widgets'; -import useWidget from './utils/use-widget'; +import useWidget from '../utils/use-widget'; export const ZoomWidget = (props: ZoomWidgetProps = {}) => { const widget = useWidget(VanillaZoomWidget, props); diff --git a/modules/react/tsconfig.json b/modules/react/tsconfig.json index 2a3fef31e6e..5839b6736c4 100644 --- a/modules/react/tsconfig.json +++ b/modules/react/tsconfig.json @@ -8,6 +8,7 @@ "outDir": "dist" }, "references": [ - {"path": "../core"} + {"path": "../core"}, + {"path": "../widgets"} ] } From 214f5e604d8c623d703dfd62e1dab285fcb13327 Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Thu, 12 Dec 2024 16:01:20 -0800 Subject: [PATCH 08/27] Deck should be defined in context for widgets --- modules/react/src/utils/deckgl-context.ts | 2 +- modules/react/src/utils/position-children-under-views.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/react/src/utils/deckgl-context.ts b/modules/react/src/utils/deckgl-context.ts index d40d7d9d0d3..ed7e2fe802a 100644 --- a/modules/react/src/utils/deckgl-context.ts +++ b/modules/react/src/utils/deckgl-context.ts @@ -7,7 +7,7 @@ export type DeckGLContextValue = { container: HTMLElement; eventManager: EventManager; onViewStateChange: DeckProps['onViewStateChange']; - deck?: Deck; + deck?: Deck; widgets?: Widget[]; }; diff --git a/modules/react/src/utils/position-children-under-views.ts b/modules/react/src/utils/position-children-under-views.ts index 1ce9e63693d..2331f274c6e 100644 --- a/modules/react/src/utils/position-children-under-views.ts +++ b/modules/react/src/utils/position-children-under-views.ts @@ -96,6 +96,7 @@ export default function positionChildrenUnderViews({ if (ContextProvider) { const contextValue: DeckGLContextValue = { + deck, viewport, // @ts-expect-error accessing protected property container: deck.canvas.offsetParent, From 74c241b20e0ac22d52897297f04e6fc8916ed410 Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Thu, 12 Dec 2024 16:02:23 -0800 Subject: [PATCH 09/27] WIP React widgets should warn when vanilla widgets are used --- examples/get-started/react/basic/app.jsx | 10 ++++++++-- modules/react/src/utils/use-widget.ts | 11 ++++++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/examples/get-started/react/basic/app.jsx b/examples/get-started/react/basic/app.jsx index d7fec7c0ee5..2711e552551 100644 --- a/examples/get-started/react/basic/app.jsx +++ b/examples/get-started/react/basic/app.jsx @@ -5,7 +5,8 @@ import React from 'react'; import {createRoot} from 'react-dom/client'; import DeckGL, {GeoJsonLayer, ArcLayer} from 'deck.gl'; -import {CompassWidget} from '@deck.gl/react'; +import {CompassWidget, ZoomWidget} from '@deck.gl/react'; +import {FullscreenWidget} from '@deck.gl/widgets'; import '@deck.gl/widgets/stylesheet.css'; // source: Natural Earth http://www.naturalearthdata.com/ via geojson.xyz @@ -31,8 +32,13 @@ function Root() { }; return ( - + + ( WidgetClass: {new (props: PropsT): T}, @@ -8,6 +8,15 @@ function useWidget( ): 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); From a0d0b6efa881432711f7b5fb61d9e5057dd04160 Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Thu, 12 Dec 2024 16:49:11 -0800 Subject: [PATCH 10/27] Add widget prop --- examples/get-started/react/basic/app.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/get-started/react/basic/app.jsx b/examples/get-started/react/basic/app.jsx index 2711e552551..72ed284281b 100644 --- a/examples/get-started/react/basic/app.jsx +++ b/examples/get-started/react/basic/app.jsx @@ -38,7 +38,7 @@ function Root() { // widgets={[new FullscreenWidget({})]} > - + Date: Thu, 12 Dec 2024 17:57:47 -0800 Subject: [PATCH 11/27] Export useWidget --- modules/react/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/react/src/index.ts b/modules/react/src/index.ts index bf555883560..f284124b046 100644 --- a/modules/react/src/index.ts +++ b/modules/react/src/index.ts @@ -9,6 +9,7 @@ export {default} from './deckgl'; export {CompassWidget} from './widgets/compass-widget'; export {FullscreenWidget} from './widgets/fullscreen-widget'; export {ZoomWidget} from './widgets/zoom-widget'; +export {default as useWidget} from './utils/use-widget'; // Types export type {DeckGLContextValue} from './utils/deckgl-context'; From 271561f6877e0d0b925a13833cc9e1fe3deb247c Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Thu, 12 Dec 2024 18:02:47 -0800 Subject: [PATCH 12/27] Demo react widget --- examples/get-started/react/basic/app.jsx | 73 +++++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/examples/get-started/react/basic/app.jsx b/examples/get-started/react/basic/app.jsx index 72ed284281b..3809731edbb 100644 --- a/examples/get-started/react/basic/app.jsx +++ b/examples/get-started/react/basic/app.jsx @@ -5,9 +5,79 @@ 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'; +import {CompassWidget, ZoomWidget, useWidget} from '@deck.gl/react'; import '@deck.gl/widgets/stylesheet.css'; +import {FlyToInterpolator} from '@deck.gl/core'; + +class CustomWidget { + id = 'custom'; + placement = 'top-right'; + props = { + // ref: React.RefObject; + }; + viewports = {}; + + constructor(props) { + this.id = props.id || 'custom'; + this.placement = props.placement || 'top-right'; + this.props = props; + } + + onAdd({deck}) { + this.deck = deck; + return this.props.ref.current; + } + onRemove() { + + } + setProps(props) { + Object.assign(this.props, props); + } + + onViewportChange(viewport) { + // debugger; + this.viewports[viewport.id] = viewport; + } + + handleZoom(viewport, nextZoom) { + const viewId = viewport?.id || 'default-view'; + const nextViewState = { + ...viewport, + zoom: nextZoom, + transitionDuration: this.props.transitionDuration, + transitionInterpolator: new FlyToInterpolator() + }; + // @ts-ignore Using private method temporary until there's a public one + this.deck._onViewStateChange({viewId, viewState: nextViewState, interactionState: {}}); + } + + handleZoomIn() { + for (const viewport of Object.values(this.viewports)) { + this.handleZoom(viewport, viewport.zoom + 1); + } + } + + handleZoomOut() { + for (const viewport of Object.values(this.viewports)) { + this.handleZoom(viewport, viewport.zoom - 1); + } + } +} + +export const CustomReactWidget = (props) => { + const ref = React.useRef(); + const widget = useWidget(CustomWidget, {ref}); + return ( +
+ React Widget! + +
+ ); +}; + // source: Natural Earth http://www.naturalearthdata.com/ via geojson.xyz const COUNTRIES = @@ -38,6 +108,7 @@ function Root() { // widgets={[new FullscreenWidget({})]} > + Date: Thu, 12 Dec 2024 18:35:32 -0800 Subject: [PATCH 13/27] Widgets should be removed when JSX unmounts --- modules/react/src/utils/use-widget.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/modules/react/src/utils/use-widget.ts b/modules/react/src/utils/use-widget.ts index 7ff28282061..9f9336736d4 100644 --- a/modules/react/src/utils/use-widget.ts +++ b/modules/react/src/utils/use-widget.ts @@ -16,6 +16,15 @@ function useWidget( if (deck?.props.widgets.length !== 0) { log.warn('Detected deck "widgets" prop used simultaneously with React widgets. Vanilla widgets will be ignored.')(); } + + return () => { + // Remove widget from context when it is unmounted + const index = widgets?.indexOf(widget); + if (index !== -1) { + widgets?.splice(index, 1); + deck?.setProps({widgets}); + } + }; }, []); const widget = useMemo(() => new WidgetClass(props), [WidgetClass]); From ec8d18fec03407cfeb071becf51b6c1ab2e194ab Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Thu, 12 Dec 2024 18:39:41 -0800 Subject: [PATCH 14/27] Fix widget warning --- modules/react/src/utils/use-widget.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/modules/react/src/utils/use-widget.ts b/modules/react/src/utils/use-widget.ts index 9f9336736d4..aa6cbcfc862 100644 --- a/modules/react/src/utils/use-widget.ts +++ b/modules/react/src/utils/use-widget.ts @@ -9,11 +9,10 @@ function useWidget( 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) { + // warn if the user supplied a vanilla widget, since it will be ignored + // NOTE: This effect runs once per widget. Context widgets and deck widget props are synced after first effect runs. + const internalWidgets = deck?.props.widgets; + if (widgets?.length && internalWidgets && !deepEqual(deck?.props.widgets, widgets, 1)) { log.warn('Detected deck "widgets" prop used simultaneously with React widgets. Vanilla widgets will be ignored.')(); } From 7cd5d58f3664bf5bb0f6ed12036f36ca69a1326f Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Thu, 12 Dec 2024 18:41:41 -0800 Subject: [PATCH 15/27] Demo toggleable zoom widget in react --- examples/get-started/react/basic/app.jsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/get-started/react/basic/app.jsx b/examples/get-started/react/basic/app.jsx index 3809731edbb..b209494f0f6 100644 --- a/examples/get-started/react/basic/app.jsx +++ b/examples/get-started/react/basic/app.jsx @@ -59,6 +59,7 @@ class CustomWidget { } handleZoomOut() { + this.props.onClick(); for (const viewport of Object.values(this.viewports)) { this.handleZoom(viewport, viewport.zoom - 1); } @@ -67,7 +68,7 @@ class CustomWidget { export const CustomReactWidget = (props) => { const ref = React.useRef(); - const widget = useWidget(CustomWidget, {ref}); + const widget = useWidget(CustomWidget, {ref, ...props}); return (
React Widget! @@ -94,6 +95,7 @@ const INITIAL_VIEW_STATE = { }; function Root() { + const [zoomToggle, setZoomToggle] = React.useState(true); const onClick = info => { if (info.object) { // eslint-disable-next-line @@ -105,11 +107,11 @@ function Root() { - - + setZoomToggle(!zoomToggle)} /> + {zoomToggle && } Date: Thu, 12 Dec 2024 18:44:57 -0800 Subject: [PATCH 16/27] lint --- examples/get-started/react/basic/app.jsx | 30 ++++++++++++------------ modules/react/src/utils/use-widget.ts | 4 +++- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/examples/get-started/react/basic/app.jsx b/examples/get-started/react/basic/app.jsx index b209494f0f6..b809dd15dbf 100644 --- a/examples/get-started/react/basic/app.jsx +++ b/examples/get-started/react/basic/app.jsx @@ -11,12 +11,12 @@ import '@deck.gl/widgets/stylesheet.css'; import {FlyToInterpolator} from '@deck.gl/core'; class CustomWidget { - id = 'custom'; - placement = 'top-right'; - props = { - // ref: React.RefObject; - }; - viewports = {}; + // id = 'custom'; + // placement = 'top-right'; + // props = { + // ref: React.RefObject; + // }; + // viewports = {}; constructor(props) { this.id = props.id || 'custom'; @@ -28,9 +28,7 @@ class CustomWidget { this.deck = deck; return this.props.ref.current; } - onRemove() { - - } + onRemove() {} setProps(props) { Object.assign(this.props, props); } @@ -66,20 +64,22 @@ class CustomWidget { } } -export const CustomReactWidget = (props) => { +export const CustomReactWidget = props => { const ref = React.useRef(); const widget = useWidget(CustomWidget, {ref, ...props}); return (
React Widget! -
); }; - // source: Natural Earth http://www.naturalearthdata.com/ via geojson.xyz const COUNTRIES = 'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_50m_admin_0_scale_rank.geojson'; //eslint-disable-line @@ -104,9 +104,9 @@ function Root() { }; return ( - diff --git a/modules/react/src/utils/use-widget.ts b/modules/react/src/utils/use-widget.ts index aa6cbcfc862..84c9d4e92d9 100644 --- a/modules/react/src/utils/use-widget.ts +++ b/modules/react/src/utils/use-widget.ts @@ -13,7 +13,9 @@ function useWidget( // NOTE: This effect runs once per widget. Context widgets and deck widget props are synced after first effect runs. const internalWidgets = deck?.props.widgets; if (widgets?.length && internalWidgets && !deepEqual(deck?.props.widgets, widgets, 1)) { - log.warn('Detected deck "widgets" prop used simultaneously with React widgets. Vanilla widgets will be ignored.')(); + log.warn( + 'Detected deck "widgets" prop used simultaneously with React widgets. Vanilla widgets will be ignored.' + )(); } return () => { From 82287da5cfd1c5726134ae4f80bf4b8499df0ca6 Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Tue, 17 Dec 2024 11:49:30 -0800 Subject: [PATCH 17/27] clearer warning message --- modules/react/src/utils/use-widget.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/react/src/utils/use-widget.ts b/modules/react/src/utils/use-widget.ts index 84c9d4e92d9..82d324e7137 100644 --- a/modules/react/src/utils/use-widget.ts +++ b/modules/react/src/utils/use-widget.ts @@ -14,7 +14,7 @@ function useWidget( const internalWidgets = deck?.props.widgets; if (widgets?.length && internalWidgets && !deepEqual(deck?.props.widgets, widgets, 1)) { log.warn( - 'Detected deck "widgets" prop used simultaneously with React widgets. Vanilla widgets will be ignored.' + '"widgets" prop will be ignored because React widgets are in use.' )(); } From 09b577a8c56b829821fd0b34e2fe587a5e04ea4c Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Tue, 17 Dec 2024 13:45:55 -0800 Subject: [PATCH 18/27] Fix build --- modules/react/src/utils/use-widget.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/react/src/utils/use-widget.ts b/modules/react/src/utils/use-widget.ts index 82d324e7137..ff2965222aa 100644 --- a/modules/react/src/utils/use-widget.ts +++ b/modules/react/src/utils/use-widget.ts @@ -21,7 +21,7 @@ function useWidget( return () => { // Remove widget from context when it is unmounted const index = widgets?.indexOf(widget); - if (index !== -1) { + if (index && index !== -1) { widgets?.splice(index, 1); deck?.setProps({widgets}); } From 7e4c4fbaabe3d98a57f325151cc252135a2654f9 Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Tue, 17 Dec 2024 15:49:14 -0800 Subject: [PATCH 19/27] viewports possibly undefined --- examples/get-started/react/basic/app.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/get-started/react/basic/app.jsx b/examples/get-started/react/basic/app.jsx index b809dd15dbf..f13de950fc2 100644 --- a/examples/get-started/react/basic/app.jsx +++ b/examples/get-started/react/basic/app.jsx @@ -22,6 +22,7 @@ class CustomWidget { this.id = props.id || 'custom'; this.placement = props.placement || 'top-right'; this.props = props; + this.viewports = {}; } onAdd({deck}) { From ffe8362f32efa0a155d12e8928077d4cc6894683 Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Mon, 23 Dec 2024 16:17:19 -0800 Subject: [PATCH 20/27] basic jsx example --- examples/get-started/react/basic/app.jsx | 87 +----------------------- 1 file changed, 3 insertions(+), 84 deletions(-) diff --git a/examples/get-started/react/basic/app.jsx b/examples/get-started/react/basic/app.jsx index f13de950fc2..aef13c3af06 100644 --- a/examples/get-started/react/basic/app.jsx +++ b/examples/get-started/react/basic/app.jsx @@ -4,82 +4,8 @@ import React from 'react'; import {createRoot} from 'react-dom/client'; -import DeckGL, {GeoJsonLayer, ArcLayer} from 'deck.gl'; -import {FullscreenWidget} from '@deck.gl/widgets'; -import {CompassWidget, ZoomWidget, useWidget} from '@deck.gl/react'; +import DeckGL, {GeoJsonLayer, ArcLayer, CompassWidget} from 'deck.gl'; import '@deck.gl/widgets/stylesheet.css'; -import {FlyToInterpolator} from '@deck.gl/core'; - -class CustomWidget { - // id = 'custom'; - // placement = 'top-right'; - // props = { - // ref: React.RefObject; - // }; - // viewports = {}; - - constructor(props) { - this.id = props.id || 'custom'; - this.placement = props.placement || 'top-right'; - this.props = props; - this.viewports = {}; - } - - onAdd({deck}) { - this.deck = deck; - return this.props.ref.current; - } - onRemove() {} - setProps(props) { - Object.assign(this.props, props); - } - - onViewportChange(viewport) { - // debugger; - this.viewports[viewport.id] = viewport; - } - - handleZoom(viewport, nextZoom) { - const viewId = viewport?.id || 'default-view'; - const nextViewState = { - ...viewport, - zoom: nextZoom, - transitionDuration: this.props.transitionDuration, - transitionInterpolator: new FlyToInterpolator() - }; - // @ts-ignore Using private method temporary until there's a public one - this.deck._onViewStateChange({viewId, viewState: nextViewState, interactionState: {}}); - } - - handleZoomIn() { - for (const viewport of Object.values(this.viewports)) { - this.handleZoom(viewport, viewport.zoom + 1); - } - } - - handleZoomOut() { - this.props.onClick(); - for (const viewport of Object.values(this.viewports)) { - this.handleZoom(viewport, viewport.zoom - 1); - } - } -} - -export const CustomReactWidget = props => { - const ref = React.useRef(); - const widget = useWidget(CustomWidget, {ref, ...props}); - return ( -
- React Widget! - -
- ); -}; // source: Natural Earth http://www.naturalearthdata.com/ via geojson.xyz const COUNTRIES = @@ -96,7 +22,6 @@ const INITIAL_VIEW_STATE = { }; function Root() { - const [zoomToggle, setZoomToggle] = React.useState(true); const onClick = info => { if (info.object) { // eslint-disable-next-line @@ -105,14 +30,7 @@ function Root() { }; return ( - - - setZoomToggle(!zoomToggle)} /> - {zoomToggle && } + + ); } From 23a3210bff36e0866e05be0e0dfa84c7a1523a5d Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Mon, 23 Dec 2024 16:17:50 -0800 Subject: [PATCH 21/27] lint --- modules/react/src/utils/use-widget.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/modules/react/src/utils/use-widget.ts b/modules/react/src/utils/use-widget.ts index ff2965222aa..003663d3131 100644 --- a/modules/react/src/utils/use-widget.ts +++ b/modules/react/src/utils/use-widget.ts @@ -13,9 +13,7 @@ function useWidget( // NOTE: This effect runs once per widget. Context widgets and deck widget props are synced after first effect runs. const internalWidgets = deck?.props.widgets; if (widgets?.length && internalWidgets && !deepEqual(deck?.props.widgets, widgets, 1)) { - log.warn( - '"widgets" prop will be ignored because React widgets are in use.' - )(); + log.warn('"widgets" prop will be ignored because React widgets are in use.')(); } return () => { From 5b995d6e42afb06b12802821be84940af32681b0 Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Mon, 30 Dec 2024 18:48:07 -1000 Subject: [PATCH 22/27] chore(react) all children are provided a DeckContext by default --- .../utils/position-children-under-views.ts | 35 +++++++++---------- .../position-children-under-views.spec.ts | 32 ++++++++++++----- 2 files changed, 40 insertions(+), 27 deletions(-) diff --git a/modules/react/src/utils/position-children-under-views.ts b/modules/react/src/utils/position-children-under-views.ts index 2331f274c6e..818a3524301 100644 --- a/modules/react/src/utils/position-children-under-views.ts +++ b/modules/react/src/utils/position-children-under-views.ts @@ -94,24 +94,21 @@ export default function positionChildrenUnderViews({ // a key" warning. Sending each child as separate arguments removes this requirement. const viewElement = createElement('div', {key, id: key, style}, ...viewChildren); - if (ContextProvider) { - const contextValue: DeckGLContextValue = { - deck, - viewport, - // @ts-expect-error accessing protected property - container: deck.canvas.offsetParent, - // @ts-expect-error accessing protected property - eventManager: deck.eventManager, - onViewStateChange: params => { - params.viewId = viewId; - // @ts-expect-error accessing protected method - deck._onViewStateChange(params); - }, - widgets: [] - }; - return createElement(ContextProvider, {key, value: contextValue}, viewElement); - } - - return viewElement; + const contextValue: DeckGLContextValue = { + deck, + viewport, + // @ts-expect-error accessing protected property + container: deck.canvas.offsetParent, + // @ts-expect-error accessing protected property + eventManager: deck.eventManager, + onViewStateChange: params => { + params.viewId = viewId; + // @ts-expect-error accessing protected method + deck._onViewStateChange(params); + }, + widgets: [] + }; + const providerKey = `view-${viewId}-context`; + return createElement(ContextProvider, {key: providerKey, value: contextValue}, viewElement); }); } diff --git a/test/modules/react/utils/position-children-under-views.spec.ts b/test/modules/react/utils/position-children-under-views.spec.ts index 6bedfc6ec6f..fc1cca1eea8 100644 --- a/test/modules/react/utils/position-children-under-views.spec.ts +++ b/test/modules/react/utils/position-children-under-views.spec.ts @@ -5,6 +5,7 @@ import test from 'tape-promise/tape'; import React, {createElement} from 'react'; import positionChildrenUnderViews from '@deck.gl/react/utils/position-children-under-views'; +import {DeckGlContext} from '@deck.gl/react/utils/deckgl-context'; import {MapView, OrthographicView, View} from '@deck.gl/core'; const TEST_VIEW_STATES = { @@ -64,27 +65,40 @@ test('positionChildrenUnderViews#before initialization', t => { test('positionChildrenUnderViews', t => { const children = positionChildrenUnderViews({ children: TEST_CHILDREN, - deck: {viewManager: dummyViewManager} + deck: {viewManager: dummyViewManager, canvas: document.createElement('canvas')} }); t.is(children.length, 2, 'Returns wrapped children'); - t.is(children[0].key, 'view-map', 'Has map view'); - t.is(children[1].key, 'view-ortho', 'Has orthographic view'); - t.is(children[0].props.style.left, 0, 'Wrapper component has x position'); - t.is(children[1].props.style.left, 400, 'Wrapper component has x position'); + t.is(children[0].key, 'view-map-context', 'Child has deck context'); + t.is(children[0].type, DeckGlContext.Provider, 'view is wrapped in DeckGlContext.Provider'); + t.is(children[1].key, 'view-ortho-context', 'Child has deck context'); + t.is(children[1].type, DeckGlContext.Provider, 'view is wrapped in DeckGlContext.Provider'); - let wrappedChild = children[0].props.children; + // check first view + let wrappedView = children[0].props.children; + t.is(wrappedView.key, 'view-map', 'Has map view'); + t.is(wrappedView.props.style.left, 0, 'Wrapper component has x position'); + + // check first view's children + let wrappedChild = wrappedView.props.children; + t.is(wrappedChild.length, 2, 'Returns wrapped children'); t.is(wrappedChild[0].props.id, 'function-under-view', 'function child preserves id'); t.is(wrappedChild[0].props.width, 400, 'function child has width'); t.is(wrappedChild[0].props.viewState, TEST_VIEW_STATES.map, 'function child has viewState'); t.is(wrappedChild[1].props.id, 'element-without-view', 'element child preserves id'); - wrappedChild = children[1].props.children; + // check second view + wrappedView = children[1].props.children; + t.is(wrappedView.key, 'view-ortho', 'Has ortho view'); + t.is(wrappedView.props.style.left, 400, 'Wrapper component has x position'); + + // check second view's child + wrappedChild = wrappedView.props.children; t.is(wrappedChild.props.id, 'element-under-view', 'element child preserves id'); t.end(); }); -test('positionChildrenUnderViews#ContextProvider', t => { +test('positionChildrenUnderViews#override ContextProvider', t => { const context = React.createContext(); const children = positionChildrenUnderViews({ @@ -98,9 +112,11 @@ test('positionChildrenUnderViews#ContextProvider', t => { t.is(children.length, 2, 'Returns wrapped children'); + t.is(children[0].key, 'view-map-context', 'Child has deck context'); t.is(children[0].type, context.Provider, 'child is wrapped in ContextProvider'); t.is(children[0].props.value.viewport, TEST_VIEWPORTS.map, 'Context has viewport'); + t.is(children[1].key, 'view-ortho-context', 'Child has deck context'); t.is(children[1].type, context.Provider, 'child is wrapped in ContextProvider'); t.is(children[1].props.value.viewport, TEST_VIEWPORTS.ortho, 'Context has viewport'); t.end(); From 3cfd7984c30812d0a62a5dadd90a78beb996d83a Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Mon, 30 Dec 2024 19:16:09 -1000 Subject: [PATCH 23/27] chore(react) widgets should be reset when omitted in react --- modules/react/src/deckgl.ts | 1 + modules/react/src/utils/use-widget.ts | 1 + test/modules/core/lib/deck.spec.ts | 45 +++++++++++ test/modules/core/lib/widget-manager.spec.ts | 16 +++- test/modules/react/deckgl.spec.ts | 76 ++++++++++++++++++- .../react/utils/extract-jsx-layers.spec.ts | 13 ++++ 6 files changed, 150 insertions(+), 2 deletions(-) diff --git a/modules/react/src/deckgl.ts b/modules/react/src/deckgl.ts index bdcca88fa6d..acae94a8f79 100644 --- a/modules/react/src/deckgl.ts +++ b/modules/react/src/deckgl.ts @@ -157,6 +157,7 @@ function DeckGLWithRef( // Needs to be called both from initial mount, and when new props are received const deckProps = useMemo(() => { const forwardProps: DeckProps = { + widgets: [], ...props, // Override user styling props. We will set the canvas style in render() style: null, diff --git a/modules/react/src/utils/use-widget.ts b/modules/react/src/utils/use-widget.ts index 003663d3131..6c127fb5d22 100644 --- a/modules/react/src/utils/use-widget.ts +++ b/modules/react/src/utils/use-widget.ts @@ -27,6 +27,7 @@ function useWidget( }, []); const widget = useMemo(() => new WidgetClass(props), [WidgetClass]); + // Hook rebuilds widgets on every render: [] then [FirstWidget] then [FirstWidget, SecondWidget] widgets?.push(widget); widget.setProps(props); diff --git a/test/modules/core/lib/deck.spec.ts b/test/modules/core/lib/deck.spec.ts index b46156496f2..9d494965b0d 100644 --- a/test/modules/core/lib/deck.spec.ts +++ b/test/modules/core/lib/deck.spec.ts @@ -5,6 +5,7 @@ import test from 'tape-promise/tape'; import {Deck, log, MapView} from '@deck.gl/core'; import {ScatterplotLayer} from '@deck.gl/layers'; +import {FullscreenWidget} from '@deck.gl/widgets'; import {device} from '@deck.gl/test-utils'; import {sleep} from './async-iterator-test-utils'; @@ -280,3 +281,47 @@ test('Deck#resourceManager', async t => { deck.finalize(); t.end(); }); + +test('Deck#props omitted are unchanged', async t => { + const layer = new ScatterplotLayer({ + id: 'scatterplot-global-data', + data: 'deck://pins', + getPosition: d => d.position + }); + + const widget = new FullscreenWidget({}); + + // Initialize with widgets and layers. + const deck = new Deck({ + device, + width: 1, + height: 1, + + viewState: { + longitude: 0, + latitude: 0, + zoom: 0 + }, + + layers: [layer], + widgets: [widget], + + onLoad: () => { + const {widgets, layers} = deck.props; + t.is(widgets && Array.isArray(widgets) && widgets.length, 1, 'Widgets is set'); + t.is(layers && Array.isArray(layers) && layers.length, 1, 'Layers is set'); + + // Render deck a second time without changing widget or layer props. + deck.setProps({ + onAfterRender: () => { + const {widgets, layers} = deck.props; + t.is(widgets && Array.isArray(widgets) && widgets.length, 1, 'Widgets remain set'); + t.is(layers && Array.isArray(layers) && layers.length, 1, 'Layers remain set'); + + deck.finalize(); + t.end(); + } + }); + } + }); +}); diff --git a/test/modules/core/lib/widget-manager.spec.ts b/test/modules/core/lib/widget-manager.spec.ts index 55d6d05d657..f74a5eff8dc 100644 --- a/test/modules/core/lib/widget-manager.spec.ts +++ b/test/modules/core/lib/widget-manager.spec.ts @@ -96,10 +96,24 @@ test('WidgetManager#setProps', t => { t.notOk(widgetB.isVisible, 'widget.onRemove is called'); t.ok(widgetB2.isVisible, 'widget.onAdd is called'); + widgetManager.setProps({widgets: []}); + t.is(widgetManager.getWidgets().length, 0, 'all widgets are removed'); + t.notOk(widgetB2.isVisible, 'widget.onRemove is called'); + + t.end(); +}); + +test('WidgetManager#finalize', t => { + const container = document.createElement('div'); + const widgetManager = new WidgetManager({deck: mockDeckInstance, parentElement: container}); + + const widgetA = new TestWidget({id: 'A'}); + widgetManager.setProps({widgets: [widgetA]}); + widgetManager.finalize(); t.is(widgetManager.getWidgets().length, 0, 'all widgets are removed'); t.is(container.childElementCount, 0, 'all widget containers are removed'); - t.notOk(widgetB2.isVisible, 'widget.onRemove is called'); + t.notOk(widgetA.isVisible, 'widget.onRemove is called'); t.end(); }); diff --git a/test/modules/react/deckgl.spec.ts b/test/modules/react/deckgl.spec.ts index f11195a2394..f3c2f7f847d 100644 --- a/test/modules/react/deckgl.spec.ts +++ b/test/modules/react/deckgl.spec.ts @@ -8,7 +8,7 @@ import {createElement, createRef} from 'react'; import {createRoot} from 'react-dom/client'; import {act} from 'react-dom/test-utils'; -import DeckGL from 'deck.gl'; +import DeckGL, {Layer} from 'deck.gl'; import {gl} from '@deck.gl/test-utils'; @@ -84,3 +84,77 @@ test('DeckGL#render', t => { ) ); }); + +class TestLayer extends Layer { + initializeState() {} +} + +TestLayer.layerName = 'TestLayer'; + +const LAYERS = [new TestLayer({id: 'primitive'})]; + +class TestWidget { + constructor(props) { + this.id = props.id; + } + onAdd() {} +} + +const WIDGETS = [new TestWidget({id: 'A'})]; + +test('DeckGL#props omitted are reset', t => { + const ref = createRef(); + const container = document.createElement('div'); + document.body.append(container); + const root = createRoot(container); + + // Initialize widgets and layers on first render. + act(() => { + root.render( + createElement(DeckGL, { + initialViewState: TEST_VIEW_STATE, + ref, + width: 100, + height: 100, + gl: getMockContext(), + layers: LAYERS, + widgets: WIDGETS, + onLoad: () => { + const {deck} = ref.current; + t.ok(deck, 'DeckGL is initialized'); + const {widgets, layers} = deck.props; + t.is(widgets && Array.isArray(widgets) && widgets.length, 1, 'Widgets is set'); + t.is(layers && Array.isArray(layers) && layers.length, 1, 'Layers is set'); + + act(() => { + // Render deck a second time without setting widget or layer props. + root.render( + createElement(DeckGL, { + ref, + onAfterRender: () => { + const {deck} = ref.current; + const {widgets, layers} = deck.props; + t.is( + widgets && Array.isArray(widgets) && widgets.length, + 0, + 'Widgets is reset to an empty array' + ); + t.is( + layers && Array.isArray(layers) && layers.length, + 0, + 'Layers is reset to an empty array' + ); + + root.render(null); + container.remove(); + t.end(); + } + }) + ); + }); + } + }) + ); + }); + t.ok(ref.current, 'DeckGL overlay is rendered.'); +}); diff --git a/test/modules/react/utils/extract-jsx-layers.spec.ts b/test/modules/react/utils/extract-jsx-layers.spec.ts index 2fe06a87e37..c6b7472db09 100644 --- a/test/modules/react/utils/extract-jsx-layers.spec.ts +++ b/test/modules/react/utils/extract-jsx-layers.spec.ts @@ -32,6 +32,19 @@ const TEST_CASES = [ }, title: 'empty children' }, + { + input: { + children: null, + views: null, + layers: null + }, + output: { + children: [], + views: null, + layers: null + }, + title: 'empty layers' + }, { input: { children: noop, From 7dbf2cd39eb7597ae05234680581a73df6b11299 Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Fri, 3 Jan 2025 17:45:17 -0800 Subject: [PATCH 24/27] chore(react) widget prop warning shouldn't warn for empty array --- modules/react/src/utils/use-widget.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/react/src/utils/use-widget.ts b/modules/react/src/utils/use-widget.ts index 6c127fb5d22..f9b6cf56f71 100644 --- a/modules/react/src/utils/use-widget.ts +++ b/modules/react/src/utils/use-widget.ts @@ -12,7 +12,7 @@ function useWidget( // warn if the user supplied a vanilla widget, since it will be ignored // NOTE: This effect runs once per widget. Context widgets and deck widget props are synced after first effect runs. const internalWidgets = deck?.props.widgets; - if (widgets?.length && internalWidgets && !deepEqual(deck?.props.widgets, widgets, 1)) { + if (widgets?.length && internalWidgets?.length && !deepEqual(internalWidgets, widgets, 1)) { log.warn('"widgets" prop will be ignored because React widgets are in use.')(); } From 8c822b40672c64065bd8b5bd42a3aee2fdc2dad3 Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Fri, 3 Jan 2025 17:46:10 -0800 Subject: [PATCH 25/27] chore(react) rename react widgets --- examples/get-started/react/basic/app.jsx | 4 ++-- modules/react/src/index.ts | 6 +++--- modules/react/src/widgets/compass-widget.tsx | 8 -------- modules/react/src/widgets/compass.tsx | 8 ++++++++ modules/react/src/widgets/fullscreen-widget.tsx | 8 -------- modules/react/src/widgets/fullscreen.tsx | 8 ++++++++ modules/react/src/widgets/zoom-widget.tsx | 8 -------- modules/react/src/widgets/zoom.tsx | 8 ++++++++ 8 files changed, 29 insertions(+), 29 deletions(-) delete mode 100644 modules/react/src/widgets/compass-widget.tsx create mode 100644 modules/react/src/widgets/compass.tsx delete mode 100644 modules/react/src/widgets/fullscreen-widget.tsx create mode 100644 modules/react/src/widgets/fullscreen.tsx delete mode 100644 modules/react/src/widgets/zoom-widget.tsx create mode 100644 modules/react/src/widgets/zoom.tsx diff --git a/examples/get-started/react/basic/app.jsx b/examples/get-started/react/basic/app.jsx index aef13c3af06..93e1544be35 100644 --- a/examples/get-started/react/basic/app.jsx +++ b/examples/get-started/react/basic/app.jsx @@ -4,7 +4,7 @@ import React from 'react'; import {createRoot} from 'react-dom/client'; -import DeckGL, {GeoJsonLayer, ArcLayer, CompassWidget} from 'deck.gl'; +import {Compass} from '@deck.gl/react'; import '@deck.gl/widgets/stylesheet.css'; // source: Natural Earth http://www.naturalearthdata.com/ via geojson.xyz @@ -63,7 +63,7 @@ function Root() { getTargetColor={[200, 0, 80]} getWidth={1} /> - + ); } diff --git a/modules/react/src/index.ts b/modules/react/src/index.ts index f284124b046..508880503b5 100644 --- a/modules/react/src/index.ts +++ b/modules/react/src/index.ts @@ -6,9 +6,9 @@ export {default as DeckGL} from './deckgl'; export {default} from './deckgl'; // Widgets -export {CompassWidget} from './widgets/compass-widget'; -export {FullscreenWidget} from './widgets/fullscreen-widget'; -export {ZoomWidget} from './widgets/zoom-widget'; +export {Compass} from './widgets/compass'; +export {Fullscreen} from './widgets/fullscreen'; +export {Zoom} from './widgets/zoom'; export {default as useWidget} from './utils/use-widget'; // Types diff --git a/modules/react/src/widgets/compass-widget.tsx b/modules/react/src/widgets/compass-widget.tsx deleted file mode 100644 index 0e44f6fc007..00000000000 --- a/modules/react/src/widgets/compass-widget.tsx +++ /dev/null @@ -1,8 +0,0 @@ -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; -}; diff --git a/modules/react/src/widgets/compass.tsx b/modules/react/src/widgets/compass.tsx new file mode 100644 index 00000000000..461cafb88d8 --- /dev/null +++ b/modules/react/src/widgets/compass.tsx @@ -0,0 +1,8 @@ +import {CompassWidget} from '@deck.gl/widgets'; +import type {CompassWidgetProps} from '@deck.gl/widgets'; +import useWidget from '../utils/use-widget'; + +export const Compass = (props: CompassWidgetProps = {}) => { + const widget = useWidget(CompassWidget, props); + return null; +}; diff --git a/modules/react/src/widgets/fullscreen-widget.tsx b/modules/react/src/widgets/fullscreen-widget.tsx deleted file mode 100644 index 42030d571db..00000000000 --- a/modules/react/src/widgets/fullscreen-widget.tsx +++ /dev/null @@ -1,8 +0,0 @@ -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; -}; diff --git a/modules/react/src/widgets/fullscreen.tsx b/modules/react/src/widgets/fullscreen.tsx new file mode 100644 index 00000000000..7ce49fa2d51 --- /dev/null +++ b/modules/react/src/widgets/fullscreen.tsx @@ -0,0 +1,8 @@ +import {FullscreenWidget} from '@deck.gl/widgets'; +import type {FullscreenWidgetProps} from '@deck.gl/widgets'; +import useWidget from '../utils/use-widget'; + +export const Fullscreen = (props: FullscreenWidgetProps = {}) => { + const widget = useWidget(FullscreenWidget, props); + return null; +}; diff --git a/modules/react/src/widgets/zoom-widget.tsx b/modules/react/src/widgets/zoom-widget.tsx deleted file mode 100644 index 7c3a7b4f994..00000000000 --- a/modules/react/src/widgets/zoom-widget.tsx +++ /dev/null @@ -1,8 +0,0 @@ -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; -}; diff --git a/modules/react/src/widgets/zoom.tsx b/modules/react/src/widgets/zoom.tsx new file mode 100644 index 00000000000..b7fec26e46c --- /dev/null +++ b/modules/react/src/widgets/zoom.tsx @@ -0,0 +1,8 @@ +import {ZoomWidget} from '@deck.gl/widgets'; +import type {ZoomWidgetProps} from '@deck.gl/widgets'; +import useWidget from '../utils/use-widget'; + +export const Zoom = (props: ZoomWidgetProps = {}) => { + const widget = useWidget(ZoomWidget, props); + return null; +}; From d17b0123850e2ace1b36685ab011dcc369be78a8 Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Fri, 3 Jan 2025 17:48:49 -0800 Subject: [PATCH 26/27] chore(main) export react components --- examples/get-started/react/basic/app.jsx | 1 + modules/main/src/index.ts | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/examples/get-started/react/basic/app.jsx b/examples/get-started/react/basic/app.jsx index 93e1544be35..e63d4a1bd68 100644 --- a/examples/get-started/react/basic/app.jsx +++ b/examples/get-started/react/basic/app.jsx @@ -4,6 +4,7 @@ import React from 'react'; import {createRoot} from 'react-dom/client'; +import DeckGL, {GeoJsonLayer, ArcLayer} from 'deck.gl'; import {Compass} from '@deck.gl/react'; import '@deck.gl/widgets/stylesheet.css'; diff --git a/modules/main/src/index.ts b/modules/main/src/index.ts index 42145fa9d20..59f39914d97 100644 --- a/modules/main/src/index.ts +++ b/modules/main/src/index.ts @@ -194,4 +194,12 @@ export type { export type {MVTLayerProps, QuadkeyLayerProps, TileLayerProps} from '@deck.gl/geo-layers'; -export type {DeckGLProps, DeckGLRef, DeckGLContextValue} from '@deck.gl/react'; +export type { + DeckGLProps, + DeckGLRef, + DeckGLContextValue, + Compass, + Fullscreen, + Zoom, + useWidget +} from '@deck.gl/react'; From aa9ae7b4208c3a6f7ffc36b18004b5f0dddab576 Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Fri, 3 Jan 2025 18:49:08 -0800 Subject: [PATCH 27/27] chore(react) use consistent naming between react and purejs widgets --- examples/get-started/react/basic/app.jsx | 4 ++-- modules/main/src/index.ts | 12 ++---------- modules/react/src/index.ts | 6 +++--- modules/react/src/widgets/compass-widget.tsx | 8 ++++++++ modules/react/src/widgets/compass.tsx | 8 -------- modules/react/src/widgets/fullscreen-widget.tsx | 8 ++++++++ modules/react/src/widgets/fullscreen.tsx | 8 -------- modules/react/src/widgets/zoom-widget.tsx | 8 ++++++++ modules/react/src/widgets/zoom.tsx | 8 -------- 9 files changed, 31 insertions(+), 39 deletions(-) create mode 100644 modules/react/src/widgets/compass-widget.tsx delete mode 100644 modules/react/src/widgets/compass.tsx create mode 100644 modules/react/src/widgets/fullscreen-widget.tsx delete mode 100644 modules/react/src/widgets/fullscreen.tsx create mode 100644 modules/react/src/widgets/zoom-widget.tsx delete mode 100644 modules/react/src/widgets/zoom.tsx diff --git a/examples/get-started/react/basic/app.jsx b/examples/get-started/react/basic/app.jsx index e63d4a1bd68..f7f8026b709 100644 --- a/examples/get-started/react/basic/app.jsx +++ b/examples/get-started/react/basic/app.jsx @@ -5,7 +5,7 @@ import React from 'react'; import {createRoot} from 'react-dom/client'; import DeckGL, {GeoJsonLayer, ArcLayer} from 'deck.gl'; -import {Compass} from '@deck.gl/react'; +import {CompassWidget} from '@deck.gl/react'; import '@deck.gl/widgets/stylesheet.css'; // source: Natural Earth http://www.naturalearthdata.com/ via geojson.xyz @@ -64,7 +64,7 @@ function Root() { getTargetColor={[200, 0, 80]} getWidth={1} /> - +
); } diff --git a/modules/main/src/index.ts b/modules/main/src/index.ts index 59f39914d97..0988dd61bff 100644 --- a/modules/main/src/index.ts +++ b/modules/main/src/index.ts @@ -128,7 +128,7 @@ export {ScenegraphLayer, SimpleMeshLayer} from '@deck.gl/mesh-layers'; // REACT BINDINGS PACKAGE // -export {default, DeckGL} from '@deck.gl/react'; +export {default, DeckGL, useWidget} from '@deck.gl/react'; // // WIDGETS PACKAGE @@ -194,12 +194,4 @@ export type { export type {MVTLayerProps, QuadkeyLayerProps, TileLayerProps} from '@deck.gl/geo-layers'; -export type { - DeckGLProps, - DeckGLRef, - DeckGLContextValue, - Compass, - Fullscreen, - Zoom, - useWidget -} from '@deck.gl/react'; +export type {DeckGLProps, DeckGLRef, DeckGLContextValue} from '@deck.gl/react'; diff --git a/modules/react/src/index.ts b/modules/react/src/index.ts index 508880503b5..f284124b046 100644 --- a/modules/react/src/index.ts +++ b/modules/react/src/index.ts @@ -6,9 +6,9 @@ export {default as DeckGL} from './deckgl'; export {default} from './deckgl'; // Widgets -export {Compass} from './widgets/compass'; -export {Fullscreen} from './widgets/fullscreen'; -export {Zoom} from './widgets/zoom'; +export {CompassWidget} from './widgets/compass-widget'; +export {FullscreenWidget} from './widgets/fullscreen-widget'; +export {ZoomWidget} from './widgets/zoom-widget'; export {default as useWidget} from './utils/use-widget'; // Types diff --git a/modules/react/src/widgets/compass-widget.tsx b/modules/react/src/widgets/compass-widget.tsx new file mode 100644 index 00000000000..682d149beea --- /dev/null +++ b/modules/react/src/widgets/compass-widget.tsx @@ -0,0 +1,8 @@ +import {CompassWidget as _CompassWidget} 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(_CompassWidget, props); + return null; +}; diff --git a/modules/react/src/widgets/compass.tsx b/modules/react/src/widgets/compass.tsx deleted file mode 100644 index 461cafb88d8..00000000000 --- a/modules/react/src/widgets/compass.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import {CompassWidget} from '@deck.gl/widgets'; -import type {CompassWidgetProps} from '@deck.gl/widgets'; -import useWidget from '../utils/use-widget'; - -export const Compass = (props: CompassWidgetProps = {}) => { - const widget = useWidget(CompassWidget, props); - return null; -}; diff --git a/modules/react/src/widgets/fullscreen-widget.tsx b/modules/react/src/widgets/fullscreen-widget.tsx new file mode 100644 index 00000000000..3338e5f4f32 --- /dev/null +++ b/modules/react/src/widgets/fullscreen-widget.tsx @@ -0,0 +1,8 @@ +import {FullscreenWidget as _FullscreenWidget} 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(_FullscreenWidget, props); + return null; +}; diff --git a/modules/react/src/widgets/fullscreen.tsx b/modules/react/src/widgets/fullscreen.tsx deleted file mode 100644 index 7ce49fa2d51..00000000000 --- a/modules/react/src/widgets/fullscreen.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import {FullscreenWidget} from '@deck.gl/widgets'; -import type {FullscreenWidgetProps} from '@deck.gl/widgets'; -import useWidget from '../utils/use-widget'; - -export const Fullscreen = (props: FullscreenWidgetProps = {}) => { - const widget = useWidget(FullscreenWidget, props); - return null; -}; diff --git a/modules/react/src/widgets/zoom-widget.tsx b/modules/react/src/widgets/zoom-widget.tsx new file mode 100644 index 00000000000..2cc545912c4 --- /dev/null +++ b/modules/react/src/widgets/zoom-widget.tsx @@ -0,0 +1,8 @@ +import {ZoomWidget as _ZoomWidget} 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(_ZoomWidget, props); + return null; +}; diff --git a/modules/react/src/widgets/zoom.tsx b/modules/react/src/widgets/zoom.tsx deleted file mode 100644 index b7fec26e46c..00000000000 --- a/modules/react/src/widgets/zoom.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import {ZoomWidget} from '@deck.gl/widgets'; -import type {ZoomWidgetProps} from '@deck.gl/widgets'; -import useWidget from '../utils/use-widget'; - -export const Zoom = (props: ZoomWidgetProps = {}) => { - const widget = useWidget(ZoomWidget, props); - return null; -};