@deck.gl/widgets 9.2.11 → 9.3.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/dist/compass-widget.js +5 -5
  2. package/dist/compass-widget.js.map +1 -1
  3. package/dist/context-menu-widget.d.ts +27 -20
  4. package/dist/context-menu-widget.d.ts.map +1 -1
  5. package/dist/context-menu-widget.js +45 -48
  6. package/dist/context-menu-widget.js.map +1 -1
  7. package/dist/dist.dev.js +3114 -869
  8. package/dist/fullscreen-widget.js +1 -1
  9. package/dist/fullscreen-widget.js.map +1 -1
  10. package/dist/geocoder-widget.d.ts +2 -4
  11. package/dist/geocoder-widget.d.ts.map +1 -1
  12. package/dist/geocoder-widget.js +16 -20
  13. package/dist/geocoder-widget.js.map +1 -1
  14. package/dist/gimbal-widget.d.ts +0 -3
  15. package/dist/gimbal-widget.d.ts.map +1 -1
  16. package/dist/gimbal-widget.js +4 -22
  17. package/dist/gimbal-widget.js.map +1 -1
  18. package/dist/icon-widget.d.ts +28 -0
  19. package/dist/icon-widget.d.ts.map +1 -0
  20. package/dist/icon-widget.js +35 -0
  21. package/dist/icon-widget.js.map +1 -0
  22. package/dist/index.cjs +1271 -561
  23. package/dist/index.cjs.map +4 -4
  24. package/dist/index.d.ts +16 -12
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +11 -10
  27. package/dist/index.js.map +1 -1
  28. package/dist/info-widget.d.ts +39 -17
  29. package/dist/info-widget.d.ts.map +1 -1
  30. package/dist/info-widget.js +46 -130
  31. package/dist/info-widget.js.map +1 -1
  32. package/dist/lib/components/dropdown-menu.d.ts +12 -5
  33. package/dist/lib/components/dropdown-menu.d.ts.map +1 -1
  34. package/dist/lib/components/dropdown-menu.js +20 -11
  35. package/dist/lib/components/dropdown-menu.js.map +1 -1
  36. package/dist/lib/components/icon-button.d.ts +3 -0
  37. package/dist/lib/components/icon-button.d.ts.map +1 -1
  38. package/dist/lib/components/icon-button.js +10 -2
  39. package/dist/lib/components/icon-button.js.map +1 -1
  40. package/dist/lib/components/popover.d.ts +30 -0
  41. package/dist/lib/components/popover.d.ts.map +1 -0
  42. package/dist/lib/components/popover.js +93 -0
  43. package/dist/lib/components/popover.js.map +1 -0
  44. package/dist/lib/components/range-input.d.ts +26 -0
  45. package/dist/lib/components/range-input.d.ts.map +1 -0
  46. package/dist/lib/components/range-input.js +261 -0
  47. package/dist/lib/components/range-input.js.map +1 -0
  48. package/dist/lib/components/user-content.d.ts +8 -0
  49. package/dist/lib/components/user-content.d.ts.map +1 -0
  50. package/dist/lib/components/user-content.js +15 -0
  51. package/dist/lib/components/user-content.js.map +1 -0
  52. package/dist/lib/data-url.d.ts +5 -0
  53. package/dist/lib/data-url.d.ts.map +1 -0
  54. package/dist/lib/data-url.js +7 -0
  55. package/dist/lib/data-url.js.map +1 -0
  56. package/dist/popup-widget.d.ts +64 -0
  57. package/dist/popup-widget.d.ts.map +1 -0
  58. package/dist/popup-widget.js +76 -0
  59. package/dist/popup-widget.js.map +1 -0
  60. package/dist/reset-view-widget.d.ts +1 -1
  61. package/dist/reset-view-widget.d.ts.map +1 -1
  62. package/dist/reset-view-widget.js +12 -11
  63. package/dist/reset-view-widget.js.map +1 -1
  64. package/dist/scrollbar-widget.d.ts +68 -0
  65. package/dist/scrollbar-widget.d.ts.map +1 -0
  66. package/dist/scrollbar-widget.js +181 -0
  67. package/dist/scrollbar-widget.js.map +1 -0
  68. package/dist/selector-widget.d.ts +44 -0
  69. package/dist/selector-widget.d.ts.map +1 -0
  70. package/dist/selector-widget.js +64 -0
  71. package/dist/selector-widget.js.map +1 -0
  72. package/dist/splitter-widget.d.ts +52 -11
  73. package/dist/splitter-widget.d.ts.map +1 -1
  74. package/dist/splitter-widget.js +177 -76
  75. package/dist/splitter-widget.js.map +1 -1
  76. package/dist/stats-widget.d.ts +6 -1
  77. package/dist/stats-widget.d.ts.map +1 -1
  78. package/dist/stats-widget.js +39 -10
  79. package/dist/stats-widget.js.map +1 -1
  80. package/dist/stylesheet.css +291 -39
  81. package/dist/theme-widget.d.ts.map +1 -1
  82. package/dist/theme-widget.js +7 -5
  83. package/dist/theme-widget.js.map +1 -1
  84. package/dist/themes.d.ts +30 -0
  85. package/dist/themes.d.ts.map +1 -1
  86. package/dist/themes.js +28 -4
  87. package/dist/themes.js.map +1 -1
  88. package/dist/timeline-widget.d.ts +25 -6
  89. package/dist/timeline-widget.d.ts.map +1 -1
  90. package/dist/timeline-widget.js +51 -18
  91. package/dist/timeline-widget.js.map +1 -1
  92. package/dist/toggle-widget.d.ts +38 -0
  93. package/dist/toggle-widget.d.ts.map +1 -0
  94. package/dist/toggle-widget.js +47 -0
  95. package/dist/toggle-widget.js.map +1 -0
  96. package/dist/zoom-widget.d.ts +2 -8
  97. package/dist/zoom-widget.d.ts.map +1 -1
  98. package/dist/zoom-widget.js +23 -18
  99. package/dist/zoom-widget.js.map +1 -1
  100. package/dist.min.js +2 -2
  101. package/package.json +4 -3
  102. package/src/compass-widget.tsx +5 -5
  103. package/src/context-menu-widget.tsx +88 -78
  104. package/src/fullscreen-widget.tsx +1 -1
  105. package/src/geocoder-widget.tsx +17 -24
  106. package/src/gimbal-widget.tsx +4 -26
  107. package/src/icon-widget.tsx +69 -0
  108. package/src/index.ts +22 -15
  109. package/src/info-widget.tsx +100 -157
  110. package/src/lib/components/dropdown-menu.tsx +58 -29
  111. package/src/lib/components/icon-button.tsx +15 -3
  112. package/src/lib/components/popover.tsx +167 -0
  113. package/src/lib/components/range-input.tsx +432 -0
  114. package/src/lib/components/user-content.tsx +30 -0
  115. package/src/lib/data-url.ts +5 -0
  116. package/src/popup-widget.tsx +170 -0
  117. package/src/reset-view-widget.tsx +12 -11
  118. package/src/scrollbar-widget.tsx +288 -0
  119. package/src/selector-widget.tsx +111 -0
  120. package/src/splitter-widget.tsx +281 -96
  121. package/src/stats-widget.tsx +59 -12
  122. package/src/stylesheet.css +291 -39
  123. package/src/theme-widget.tsx +7 -5
  124. package/src/themes.ts +35 -4
  125. package/src/timeline-widget.tsx +103 -33
  126. package/src/toggle-widget.tsx +101 -0
  127. package/src/zoom-widget.tsx +25 -21
  128. package/dist/fps-widget.d.ts +0 -28
  129. package/dist/fps-widget.d.ts.map +0 -1
  130. package/dist/fps-widget.js +0 -51
  131. package/dist/fps-widget.js.map +0 -1
  132. package/dist/lib/components/grouped-icon-button.d.ts +0 -10
  133. package/dist/lib/components/grouped-icon-button.d.ts.map +0 -1
  134. package/dist/lib/components/grouped-icon-button.js +0 -7
  135. package/dist/lib/components/grouped-icon-button.js.map +0 -1
  136. package/dist/lib/components/icon-menu.d.ts +0 -16
  137. package/dist/lib/components/icon-menu.d.ts.map +0 -1
  138. package/dist/lib/components/icon-menu.js +0 -34
  139. package/dist/lib/components/icon-menu.js.map +0 -1
  140. package/dist/lib/components/simple-menu.d.ts +0 -16
  141. package/dist/lib/components/simple-menu.d.ts.map +0 -1
  142. package/dist/lib/components/simple-menu.js +0 -30
  143. package/dist/lib/components/simple-menu.js.map +0 -1
  144. package/dist/view-selector-widget.d.ts +0 -35
  145. package/dist/view-selector-widget.d.ts.map +0 -1
  146. package/dist/view-selector-widget.js +0 -63
  147. package/dist/view-selector-widget.js.map +0 -1
  148. package/src/fps-widget.tsx +0 -77
  149. package/src/lib/components/grouped-icon-button.tsx +0 -27
  150. package/src/lib/components/icon-menu.tsx +0 -71
  151. package/src/lib/components/simple-menu.tsx +0 -52
  152. package/src/view-selector-widget.tsx +0 -151
@@ -5,43 +5,67 @@
5
5
  import {Widget} from '@deck.gl/core';
6
6
  import type {Deck, PickingInfo, Viewport, WidgetProps} from '@deck.gl/core';
7
7
  import {render, JSX} from 'preact';
8
+ import {Popover, type PopoverProps} from './lib/components/popover';
9
+ import {UserContent} from './lib/components/user-content';
10
+
11
+ export type TooltipContent = {
12
+ /** Anchor of the popup in world coordinates, e.g. [longitude, latitude].
13
+ * If not supplied, default to the mouse position where the popup was triggered.
14
+ */
15
+ position?: number[];
16
+ /** Text content to display in the popup */
17
+ text?: string;
18
+ /** HTML content to display in the popup. If supplied, `text` is ignored. */
19
+ html?: string;
20
+ /** HTML element to attach inside the popup. */
21
+ element?: HTMLElement | null;
22
+ /** Custom class name to add to the popup */
23
+ className?: string;
24
+ /** CSS style overrides of the popup */
25
+ style?: Partial<CSSStyleDeclaration>;
26
+ };
8
27
 
9
28
  export type InfoWidgetProps = WidgetProps & {
10
29
  /** View to attach to and interact with. Required when using multiple views */
11
30
  viewId?: string | null;
12
31
  /** Determines the interaction mode of the widget */
13
- mode: 'click' | 'hover' | 'static';
32
+ mode: 'click' | 'hover';
14
33
  /** Function to generate the popup contents from the selected element */
15
- getTooltip?: (info: PickingInfo, widget: InfoWidget) => InfoWidgetProps | null;
16
- /** Position at which to place popup (clicked point: [longitude, latitude]) */
17
- position: [number, number];
18
- /** Text content of popup */
19
- text?: string;
20
- /** Visibility of info widget */
21
- visible?: boolean;
22
- /** Minimum offset (in pixels) to keep the popup away from the canvas edges. */
23
- minOffset?: number;
24
- /** Callback triggered when the widget is clicked. */
25
- onClick?: (widget: InfoWidget, info: PickingInfo) => boolean;
34
+ getTooltip?: (
35
+ info: PickingInfo,
36
+ widget: InfoWidget
37
+ ) => TooltipContent | string | null | undefined;
38
+ /** Position popup relative to the anchor.
39
+ * @default 'right'
40
+ */
41
+ placement?: PopoverProps['placement'];
42
+ /** Pixel offset
43
+ * @default 10
44
+ */
45
+ offset?: PopoverProps['offset'];
46
+ /**
47
+ * Show an arrow pointing at the anchor. Optionally accepts a pixel size.
48
+ * @default 10
49
+ */
50
+ arrow?: PopoverProps['arrow'];
26
51
  };
27
52
 
28
53
  export class InfoWidget extends Widget<InfoWidgetProps> {
29
54
  static defaultProps: Required<InfoWidgetProps> = {
30
55
  ...Widget.defaultProps,
31
56
  id: 'info',
32
- position: [0, 0],
33
- text: '',
34
- visible: false,
35
- minOffset: 0,
36
57
  viewId: null,
37
58
  mode: 'hover',
38
59
  getTooltip: undefined!,
39
- onClick: undefined!
60
+ placement: 'right',
61
+ offset: 10,
62
+ arrow: 10
40
63
  };
41
64
 
42
65
  className = 'deck-widget-info';
43
66
  placement = 'fill' as const;
44
67
  viewport?: Viewport;
68
+ tooltip: Required<TooltipContent> | null = null;
45
69
 
46
70
  constructor(props: InfoWidgetProps) {
47
71
  super(props);
@@ -53,11 +77,15 @@ export class InfoWidget extends Widget<InfoWidgetProps> {
53
77
  super.setProps(props);
54
78
  }
55
79
 
56
- onCreateRootElement(): HTMLDivElement {
57
- const element = super.onCreateRootElement();
58
- const style = {margin: '0px', top: '0px', left: '0px', position: 'absolute'};
59
- Object.entries(style).forEach(([key, value]) => element.style.setProperty(key, value));
60
- return element;
80
+ onAdd({deck}: {deck: Deck<any>; viewId: string | null}) {
81
+ this.deck = deck;
82
+ }
83
+
84
+ onRemove() {
85
+ // Invoke clean up of preact hooks
86
+ if (this.rootElement) {
87
+ render(null, this.rootElement);
88
+ }
61
89
  }
62
90
 
63
91
  onViewportChange(viewport) {
@@ -66,156 +94,71 @@ export class InfoWidget extends Widget<InfoWidgetProps> {
66
94
  }
67
95
 
68
96
  onHover(info: PickingInfo): void {
69
- if (this.props.mode === 'hover' && this.props.getTooltip) {
70
- const tooltip = this.props.getTooltip(info, this);
71
- // hover tooltips should show over static and click infos
72
- this.setProps({
73
- visible: tooltip !== null,
74
- ...tooltip,
75
- style: {zIndex: '1', ...tooltip?.style}
76
- });
97
+ if (this.props.mode === 'hover') {
98
+ this.tooltip = this._getTooltip(info);
77
99
  }
78
100
  }
79
101
 
80
- onClick(info: PickingInfo): boolean {
81
- if (this.props.mode === 'click' && this.props.getTooltip) {
82
- const tooltip = this.props.getTooltip(info, this);
83
- this.setProps({
84
- visible: tooltip !== null,
85
- ...tooltip
86
- });
87
- return tooltip !== null;
102
+ onClick(info: PickingInfo): void {
103
+ if (this.props.mode === 'click') {
104
+ this.tooltip = this._getTooltip(info);
88
105
  }
89
-
90
- // Original behavior
91
- return this.props.onClick?.(this, info) || false;
92
106
  }
93
107
 
94
- onAdd({deck, viewId}: {deck: Deck<any>; viewId: string | null}) {
95
- this.deck = deck;
96
- if (!viewId) {
97
- this.viewport = deck.getViewports()[0];
98
- } else {
99
- this.viewport = deck.getViewports().find(viewport => viewport.id === viewId);
100
- }
108
+ protected _getTooltip(info: PickingInfo): Required<TooltipContent> | null {
109
+ if (!this.props.getTooltip) return null;
110
+
111
+ const content = this.props.getTooltip(info, this) ?? null;
112
+ if (content === null) return null;
113
+ const normalizedTooltip: TooltipContent =
114
+ typeof content === 'string' ? {text: content} : content;
115
+
116
+ const position = normalizedTooltip.position || info.coordinate;
117
+ if (!position) return null;
118
+
119
+ return {
120
+ position,
121
+ text: '',
122
+ html: '',
123
+ element: null,
124
+ className: '',
125
+ style: {},
126
+ ...normalizedTooltip
127
+ };
101
128
  }
102
129
 
103
130
  onRenderHTML(rootElement: HTMLElement): void {
104
- if (!this.viewport) {
131
+ if (!this.viewport || this.tooltip === null) {
132
+ render(null, rootElement);
105
133
  return;
106
134
  }
107
- const [longitude, latitude] = this.props.position;
135
+ const style: Partial<CSSStyleDeclaration> = {
136
+ ...this.props.style,
137
+ ...this.tooltip.style
138
+ };
108
139
  // Project the clicked geographic coordinate to canvas (x, y)
109
- const [x, y] = this.viewport.project([longitude, latitude]);
110
-
111
- const minOffset = this.props.minOffset || 0;
112
- const gap = 10; // gap between clicked point and popup box
113
- const arrowHeight = 8; // height of the triangle arrow
114
- const arrowWidth = 16; // full width of the arrow
115
-
116
- // Decide vertical orientation.
117
- // If the clicked point is in the bottom half, place the popup above the point.
118
- const isAbove = y > this.viewport.height / 2;
119
- const background = (this.props.style && this.props.style.background) || 'rgba(255,255,255,0.9)';
140
+ const [x, y] = this.viewport.project(this.tooltip.position);
120
141
 
121
142
  // Render the popup container with a content box and a placeholder for the arrow.
122
143
  // The container is positioned absolutely (initially at 0,0) and will be repositioned after measuring.
123
- const ui = this.props.visible ? (
124
- <div className="popup-container" style={{position: 'absolute', left: 0, top: 0}}>
125
- <div
126
- className="popup-content"
127
- style={{
128
- background,
129
- padding: '10px',
130
- position: 'relative',
131
- // Include any additional styles
132
- ...(this.props.style as JSX.CSSProperties)
133
- }}
134
- >
135
- {this.props.text}
136
- </div>
137
- <div className="popup-arrow" style={{position: 'absolute', width: '0px', height: '0px'}} />
138
- </div>
139
- ) : null;
144
+ const ui = (
145
+ <Popover
146
+ x={x}
147
+ y={y}
148
+ placement={this.props.placement}
149
+ arrow={this.props.arrow}
150
+ arrowColor="var(--menu-background, #fff)"
151
+ offset={this.props.offset}
152
+ >
153
+ <UserContent
154
+ className={`deck-widget-popup-content ${this.tooltip.className} ${this.props.className}`}
155
+ style={style as JSX.CSSProperties}
156
+ html={this.tooltip.html}
157
+ text={this.tooltip.text}
158
+ element={this.tooltip.element}
159
+ />
160
+ </Popover>
161
+ );
140
162
  render(ui, rootElement);
141
-
142
- // After rendering, measure the content and adjust positioning so that:
143
- // - The popup (with its arrow) does not cover the clicked point.
144
- // - The arrow's tip points to the clicked point.
145
- // - The popup remains within the canvas bounds, with minOffset.
146
- // eslint-disable-next-line max-statements, complexity
147
- requestAnimationFrame(() => {
148
- if (!this.props.visible || !rootElement.firstChild || !this.viewport) return;
149
-
150
- const container = rootElement.firstChild as HTMLElement;
151
- const contentEl = container.querySelector('.popup-content') as HTMLElement;
152
- const arrowEl = container.querySelector('.popup-arrow') as HTMLElement;
153
- if (!contentEl || !arrowEl) return;
154
- const contentRect = contentEl.getBoundingClientRect();
155
- const popupWidth = contentRect.width;
156
- const popupHeight = contentRect.height;
157
-
158
- // Compute ideal horizontal position: center the popup on the clicked x
159
- let computedLeft = x - popupWidth / 2;
160
- // Compute vertical position based on orientation:
161
- // If the popup is above, position its bottom edge (minus arrow & gap) at the clicked y.
162
- // Otherwise, position its top edge (plus arrow & gap) at the clicked y.
163
- let computedTop;
164
- if (isAbove) {
165
- computedTop = y - gap - arrowHeight - popupHeight;
166
- } else {
167
- computedTop = y + gap + arrowHeight;
168
- }
169
-
170
- // Ensure the popup (content) stays within horizontal bounds of the canvas
171
- if (computedLeft < minOffset) {
172
- computedLeft = minOffset;
173
- }
174
- if (computedLeft + popupWidth > this.viewport.width - minOffset) {
175
- computedLeft = this.viewport.width - minOffset - popupWidth;
176
- }
177
-
178
- // Ensure the vertical position (including the arrow) stays within canvas bounds.
179
- if (isAbove) {
180
- if (computedTop < minOffset) {
181
- computedTop = minOffset;
182
- }
183
- } else if (computedTop + popupHeight + arrowHeight > this.viewport.height - minOffset) {
184
- computedTop = this.viewport.height - minOffset - popupHeight - arrowHeight;
185
- }
186
-
187
- // Update container position (remove any transform and set left/top explicitly)
188
- container.style.left = `${computedLeft}px`;
189
- container.style.top = `${computedTop}px`;
190
- container.style.transform = '';
191
-
192
- // Compute arrow's horizontal offset relative to the container.
193
- // We want the arrow's tip to align with the clicked point.
194
- let arrowLeft = x - computedLeft - arrowWidth / 2;
195
- // Optionally constrain arrowLeft so it stays within the popup's width.
196
- arrowLeft = Math.max(arrowLeft, 0);
197
- arrowLeft = Math.min(arrowLeft, popupWidth - arrowWidth);
198
-
199
- // Update arrow element's style based on orientation.
200
- if (isAbove) {
201
- // Popup is above the clicked point so arrow appears at the bottom of the popup.
202
- arrowEl.style.left = `${arrowLeft}px`;
203
- arrowEl.style.bottom = `-${arrowHeight}px`;
204
- arrowEl.style.top = '';
205
- arrowEl.style.borderLeft = `${arrowWidth / 2}px solid transparent`;
206
- arrowEl.style.borderRight = `${arrowWidth / 2}px solid transparent`;
207
- arrowEl.style.borderTop = `${arrowHeight}px solid ${background}`;
208
- arrowEl.style.borderBottom = '';
209
- } else {
210
- // Popup is below the clicked point so arrow appears at the top.
211
- arrowEl.style.left = `${arrowLeft}px`;
212
- arrowEl.style.top = `-${arrowHeight}px`;
213
- arrowEl.style.bottom = '';
214
- arrowEl.style.borderLeft = `${arrowWidth / 2}px solid transparent`;
215
- arrowEl.style.borderRight = `${arrowWidth / 2}px solid transparent`;
216
- arrowEl.style.borderBottom = `${arrowHeight}px solid ${background}`;
217
- arrowEl.style.borderTop = '';
218
- }
219
- });
220
163
  }
221
164
  }
@@ -2,18 +2,26 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
- import {type JSX} from 'preact';
5
+ import {type JSX, type ComponentChild} from 'preact';
6
6
  import {useState, useRef, useEffect} from 'preact/hooks';
7
+ import {getCSSMask} from '../data-url';
7
8
 
8
- export type MenuItem = string | {label: string; value: string; icon?: string};
9
+ export type MenuItem =
10
+ | string
11
+ | {
12
+ value?: string;
13
+ label: string;
14
+ icon?: string;
15
+ onSelect?: () => void;
16
+ };
9
17
 
10
18
  export type DropdownMenuProps = {
11
19
  menuItems: MenuItem[];
12
- onSelect: (value: string) => void;
13
- style?: JSX.CSSProperties;
20
+ onSelect?: (value: string) => void;
21
+ style?: Partial<CSSStyleDeclaration>;
14
22
  };
15
23
 
16
- function getMenuItemValue(item: MenuItem): string {
24
+ function getMenuItemValue(item: MenuItem): string | undefined {
17
25
  return typeof item === 'string' ? item : item.value;
18
26
  }
19
27
 
@@ -27,26 +35,51 @@ function getMenuItemIcon(item: MenuItem): string | undefined {
27
35
 
28
36
  export const DropdownMenu = (props: DropdownMenuProps) => {
29
37
  const [isOpen, setIsOpen] = useState(false);
30
- const dropdownRef = useRef<HTMLDivElement>(null);
31
38
 
32
- const toggleDropdown = () => setIsOpen(!isOpen);
39
+ return (
40
+ <SimpleMenu
41
+ {...props}
42
+ style={{...props.style, position: 'absolute'}}
43
+ isOpen={isOpen}
44
+ onClose={() => setIsOpen(false)}
45
+ trigger={
46
+ <button className="deck-widget-dropdown-button" onClick={() => setIsOpen(!isOpen)}>
47
+ <span className={`deck-widget-dropdown-icon ${isOpen ? 'open' : ''}`} />
48
+ </button>
49
+ }
50
+ />
51
+ );
52
+ };
33
53
 
34
- const handleClickOutside = (event: MouseEvent) => {
35
- if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
36
- setIsOpen(false);
37
- }
38
- };
54
+ export type SimpleMenuProps = DropdownMenuProps & {
55
+ trigger?: ComponentChild;
56
+ isOpen: boolean;
57
+ onClose: () => void;
58
+ };
59
+
60
+ export const SimpleMenu = (props: SimpleMenuProps) => {
61
+ const dropdownRef = useRef<HTMLDivElement>(null);
39
62
 
40
63
  useEffect(() => {
64
+ const handleClickOutside = (event: MouseEvent) => {
65
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
66
+ props.onClose();
67
+ }
68
+ };
41
69
  document.addEventListener('mousedown', handleClickOutside);
42
70
  return () => {
43
71
  document.removeEventListener('mousedown', handleClickOutside);
44
72
  };
45
73
  }, []);
46
74
 
47
- const handleSelect = (item: MenuItem) => {
48
- props.onSelect(getMenuItemValue(item));
49
- setIsOpen(false);
75
+ const handleSelect = (value: string | undefined, item: MenuItem) => {
76
+ if (value) {
77
+ if (typeof item === 'object') {
78
+ item.onSelect?.();
79
+ }
80
+ props.onSelect?.(value);
81
+ props.onClose();
82
+ }
50
83
  };
51
84
 
52
85
  // Don't render anything if there are no menu items
@@ -55,25 +88,21 @@ export const DropdownMenu = (props: DropdownMenuProps) => {
55
88
  }
56
89
 
57
90
  return (
58
- <div className="deck-widget-dropdown-container" ref={dropdownRef} style={props.style}>
59
- <button className="deck-widget-dropdown-button" onClick={toggleDropdown}>
60
- <span className={`deck-widget-dropdown-icon ${isOpen ? 'open' : ''}`} />
61
- </button>
62
- {isOpen && (
63
- <ul className="deck-widget-dropdown-menu">
64
- {props.menuItems.map(item => {
91
+ <div className="deck-widget-dropdown-container" ref={dropdownRef}>
92
+ {props.trigger}
93
+ {props.isOpen && (
94
+ <ul className="deck-widget-dropdown-menu" style={props.style as JSX.CSSProperties}>
95
+ {props.menuItems.map((item, i) => {
96
+ const value = getMenuItemValue(item);
65
97
  const icon = getMenuItemIcon(item);
66
98
  return (
67
99
  <li
68
- className="deck-widget-dropdown-item"
69
- key={getMenuItemValue(item)}
70
- onClick={() => handleSelect(item)}
100
+ className={`deck-widget-dropdown-item ${value ? '' : 'disabled'}`}
101
+ key={i}
102
+ onClick={() => handleSelect(value, item)}
71
103
  >
72
104
  {icon && (
73
- <span
74
- className="deck-widget-dropdown-item-icon"
75
- style={{maskImage: `url("${icon}")`, WebkitMaskImage: `url("${icon}")`}}
76
- />
105
+ <span className="deck-widget-dropdown-item-icon" style={getCSSMask(icon)} />
77
106
  )}
78
107
  {getMenuItemLabel(item)}
79
108
  </li>
@@ -3,26 +3,38 @@
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
5
  import type {ComponentChildren, JSX} from 'preact';
6
+ import {useMemo} from 'preact/hooks';
7
+ import {getCSSMask} from '../data-url';
6
8
 
7
9
  export type IconButtonProps = {
8
10
  className?: string;
11
+ icon?: string;
9
12
  label?: string;
13
+ color?: string;
14
+ style?: JSX.CSSProperties;
10
15
  onClick?: JSX.MouseEventHandler<HTMLButtonElement>;
11
16
  children?: ComponentChildren;
12
17
  };
13
18
 
14
19
  /** Renders a button component with widget CSS */
15
20
  export const IconButton = (props: IconButtonProps) => {
16
- const {className = '', label, onClick, children} = props;
21
+ const {className = '', style, color, icon, label, onClick, children} = props;
22
+
23
+ const iconStyle = useMemo(() => {
24
+ const css: JSX.CSSProperties | undefined = getCSSMask(icon);
25
+ if (!color) return css;
26
+ return {...css, backgroundColor: color};
27
+ }, [color, icon]);
28
+
17
29
  return (
18
- <div className="deck-widget-button">
30
+ <div className="deck-widget-button" style={style}>
19
31
  <button
20
32
  className={`deck-widget-icon-button ${className}`}
21
33
  type="button"
22
34
  onClick={onClick}
23
35
  title={label}
24
36
  >
25
- {children ? children : <div className="deck-widget-icon" />}
37
+ {children ? children : <div className="deck-widget-icon" style={iconStyle} />}
26
38
  </button>
27
39
  </div>
28
40
  );
@@ -0,0 +1,167 @@
1
+ // deck.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+ import type {JSX, ComponentChildren} from 'preact';
5
+ import {useRef, useEffect, useMemo} from 'preact/hooks';
6
+ import {
7
+ computePosition,
8
+ flip,
9
+ shift,
10
+ offset,
11
+ arrow,
12
+ autoUpdate,
13
+ type Placement,
14
+ type ComputePositionConfig
15
+ } from '@floating-ui/dom';
16
+
17
+ export type PopoverProps = {
18
+ /** Anchor x */
19
+ x: number;
20
+ /** Anchor y */
21
+ y: number;
22
+ /** Position content relative to the anchor.
23
+ * @default 'right'
24
+ */
25
+ placement?: Placement;
26
+ /** Pixel offset
27
+ * @default 0
28
+ */
29
+ offset?: number;
30
+ /**
31
+ * Show an arrow pointing at the anchor. Optionally accepts a pixel size.
32
+ * @default false
33
+ */
34
+ arrow?: false | number | [width: number, height: number];
35
+ /**
36
+ * CSS color of the arrow
37
+ * @default 'white'
38
+ */
39
+ arrowColor?: string;
40
+ /** Content of the popover */
41
+ children: ComponentChildren;
42
+ };
43
+
44
+ export const Popover = ({
45
+ x,
46
+ y,
47
+ placement = 'right',
48
+ offset: pixelOffset = 0,
49
+ arrow: arrowSize = false,
50
+ arrowColor = 'white',
51
+ children
52
+ }: PopoverProps) => {
53
+ const anchorRef = useRef<HTMLDivElement>(null);
54
+ const contentRef = useRef<HTMLDivElement>(null);
55
+ const arrowRef = useRef<HTMLDivElement>(null);
56
+ const updaterRef = useRef<() => void>();
57
+
58
+ updaterRef.current = () => {
59
+ if (!anchorRef.current || !contentRef.current) return;
60
+
61
+ const arrowWidth = Array.isArray(arrowSize) ? arrowSize[0] : arrowSize || 0;
62
+ const arrowHeight = Array.isArray(arrowSize) ? arrowSize[1] : arrowSize || 0;
63
+ const padding = pixelOffset + Math.max(arrowHeight, arrowWidth);
64
+
65
+ const middleware: ComputePositionConfig['middleware'] = placement.includes('-')
66
+ ? [offset(padding), flip(), shift()]
67
+ : [offset(padding), shift(), flip()];
68
+ if (arrowRef.current) middleware.push(arrow({element: arrowRef.current}));
69
+ computePosition(anchorRef.current, contentRef.current, {
70
+ placement,
71
+ strategy: 'fixed',
72
+ middleware
73
+ }).then(popoverPos => {
74
+ if (contentRef.current) {
75
+ Object.assign(contentRef.current.style, {
76
+ left: `${popoverPos.x}px`,
77
+ top: `${popoverPos.y}px`
78
+ });
79
+ }
80
+
81
+ const arrowData = popoverPos.middlewareData.arrow;
82
+ if (arrowData && arrowRef.current) {
83
+ const arrowStyle = createArrow(arrowWidth, arrowHeight, arrowColor, popoverPos.placement);
84
+ arrowStyle.transform = `translate(${arrowData.x || 0}px, ${arrowData.y || 0}px)`;
85
+ Object.assign(arrowRef.current.style, arrowStyle);
86
+ }
87
+ });
88
+ };
89
+
90
+ useMemo(() => {
91
+ updaterRef.current?.();
92
+ }, [x, y, placement, arrowSize, pixelOffset]);
93
+
94
+ useEffect(() => {
95
+ // initial mount
96
+ if (contentRef.current) {
97
+ contentRef.current.style.visibility = 'visible';
98
+ }
99
+ const cleanup = autoUpdate(anchorRef.current!, contentRef.current!, () =>
100
+ updaterRef.current?.()
101
+ );
102
+ return () => {
103
+ cleanup();
104
+ };
105
+ }, []);
106
+
107
+ return (
108
+ <div style={{position: 'absolute', left: x, top: y}} ref={anchorRef}>
109
+ <div
110
+ className="deck-widget deck-widget-popover"
111
+ style={{position: 'fixed', visibility: 'hidden', pointerEvents: 'none'}}
112
+ ref={contentRef}
113
+ >
114
+ {Boolean(arrowSize) && (
115
+ <div
116
+ className="deck-widget-popover-arrow"
117
+ style={{position: 'absolute'}}
118
+ ref={arrowRef}
119
+ />
120
+ )}
121
+ {children}
122
+ </div>
123
+ </div>
124
+ );
125
+ };
126
+
127
+ function createArrow(
128
+ width: number,
129
+ height: number,
130
+ color: string,
131
+ placement: Placement
132
+ ): JSX.CSSProperties {
133
+ const result: JSX.CSSProperties = {
134
+ width: 0,
135
+ height: 0,
136
+ top: '',
137
+ bottom: '',
138
+ left: '',
139
+ right: ''
140
+ };
141
+ if (placement.startsWith('bottom')) {
142
+ result.borderLeft = `${width / 2}px solid transparent`;
143
+ result.borderRight = `${width / 2}px solid transparent`;
144
+ result.borderBottom = `${height}px solid ${color}`;
145
+ result.borderTop = '';
146
+ result.top = `${-height}px`;
147
+ } else if (placement.startsWith('top')) {
148
+ result.borderLeft = `${width / 2}px solid transparent`;
149
+ result.borderRight = `${width / 2}px solid transparent`;
150
+ result.borderTop = `${height}px solid ${color}`;
151
+ result.borderBottom = '';
152
+ result.bottom = `${-height}px`;
153
+ } else if (placement.startsWith('right')) {
154
+ result.borderTop = `${width / 2}px solid transparent`;
155
+ result.borderBottom = `${width / 2}px solid transparent`;
156
+ result.borderRight = `${height}px solid ${color}`;
157
+ result.borderLeft = '';
158
+ result.left = `${-height}px`;
159
+ } else if (placement.startsWith('left')) {
160
+ result.borderTop = `${width / 2}px solid transparent`;
161
+ result.borderBottom = `${width / 2}px solid transparent`;
162
+ result.borderLeft = `${height}px solid ${color}`;
163
+ result.borderRight = '';
164
+ result.right = `${-height}px`;
165
+ }
166
+ return result;
167
+ }