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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions modules/widgets/src/button-group-widget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// deck.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

import {Widget} from '@deck.gl/core';
import {Viewport, WidgetProps, WidgetPlacement} from '@deck.gl/core';
import {render} from 'preact';
import {ButtonGroup} from './lib/components/button-group';
import {GroupedIconButton} from './lib/components/grouped-icon-button';

/** Defined one button in the menu */
export type Button =
| {
id: string;
label: string;
icon?: () => JSX.Element;
}
| {
id: string;
label: string;
className: string;
};

export type ButtonGroupWidgetProps = WidgetProps & {
/** Widget positioning within the view. Default 'top-left'. */
placement?: WidgetPlacement;
/** View to attach to and interact with. Required when using multiple views. */
viewId?: string | null;
/** Button orientation. */
orientation?: 'vertical' | 'horizontal';
/** List of buttons to show */
buttons: Button[];
/** Tooltip message on zoom out button. */
onButtonClick?: (id: string, widget: ButtonGroupWidget) => void;
};

/**
* A widget that lets the user add custom icon buttons to deck
* The buttons participate in widget positioning and theming,
* however the functionality is defined by the props.onButtonClick callback
*/
export class ButtonGroupWidget extends Widget<ButtonGroupWidgetProps> {
static defaultProps: Required<ButtonGroupWidgetProps> = {
...Widget.defaultProps,
id: 'button-group',
placement: 'top-left',
orientation: 'vertical',
viewId: undefined!,
buttons: [],
// eslint-disable-next-line no-console
onButtonClick: (id, widget) => console.log(`Button ${id} clicked`, widget)
};

className = 'deck-widget-zoom';
placement: WidgetPlacement = 'top-left';
viewId?: string | null = null;
viewports: {[id: string]: Viewport} = {};

constructor(props: ButtonGroupWidgetProps) {
super(props, ButtonGroupWidget.defaultProps);
this.setProps(props);
}

setProps(props: Partial<ButtonGroupWidgetProps>) {
this.placement = props.placement ?? this.placement;
this.viewId = props.viewId ?? this.viewId;
super.setProps(props);
}

onRenderHTML(rootElement: HTMLElement): void {
const ui = (
<ButtonGroup orientation={this.props.orientation}>
{this.props.buttons.map(button => (
<GroupedIconButton
key={button.id}
label={button.label}
className="deck-widget-button"
icon={'icon' in button && button.icon}
onClick={() => this.props.onButtonClick(button.id, this)}
/>
))}
</ButtonGroup>
);
render(ui, rootElement);
}
}
4 changes: 4 additions & 0 deletions modules/widgets/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ export {ContextMenuWidget as _ContextMenuWidget} from './context-menu-widget';
export {SplitterWidget as _SplitterWidget} from './splitter-widget';
export {TimelineWidget as _TimelineWidget} from './timeline-widget';
export {ViewSelectorWidget as _ViewSelectorWidget} from './view-selector-widget';
export {
ButtonGroupWidget as _ButtonGroupWidget,
type ButtonGroupWidgetProps
} from './button-group-widget';

export type {FullscreenWidgetProps} from './fullscreen-widget';
export type {CompassWidgetProps} from './compass-widget';
Expand Down
10 changes: 7 additions & 3 deletions modules/widgets/src/lib/components/grouped-icon-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,26 @@
// Copyright (c) vis.gl contributors

export type GroupedIconButtonProps = {
className?: string;
label: string;
/** Icons can be loaded from style sheet using class name */
className: string;
/** Alterhnatively an SVG icon element can be provided */
icon?: () => JSX.Element;
Copy link
Collaborator

@chrisgervang chrisgervang Sep 26, 2025

Choose a reason for hiding this comment

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

I learned today that this should should just be a JSX.Element.

Suggested change
icon?: () => JSX.Element;
icon?: JSX.Element;

The reason the function seemed to be needed was that defining JSX on the module level gets immediately executed by node tests as React code instead of Preact code due to the way our tests are configured. We could fix the configuration.. but in the meantime we should just type our preact code correctly and wrap all JSX in a function.

See 62fe293

/** Action to take when button was clicked */
onClick: () => void;
};

/** Renders an icon button as part of a ButtonGroup */
export const GroupedIconButton = props => {
const {className, label, onClick} = props;
const {className, label, icon, onClick} = props;
return (
<button
className={`deck-widget-icon-button ${className || ''}`}
type="button"
onClick={onClick}
title={label}
>
<div className="deck-widget-icon" />
<div className="deck-widget-icon">{icon && icon()}</div>
</button>
);
};
Loading