Skip to content

Commit 15b42d1

Browse files
authored
fix(widgets): GeocoderWidget dark theme colors (#9899) (#9974)
1 parent 8484984 commit 15b42d1

File tree

7 files changed

+303
-114
lines changed

7 files changed

+303
-114
lines changed

docs/api-reference/widgets/styling.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,12 @@ Additionally, refer to each widget's API reference for variables specific to tha
138138
| Name | Type | Default |
139139
| ---- | ---- | ------- |
140140
| `--menu-gap` | [Dimension](https://developer.mozilla.org/en-US/docs/Web/CSS/dimension) | `4px` |
141+
| `--menu-background` | [Color](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value) | `#fff` |
142+
| `--menu-backdrop-filter` | [Backdrop Filter](https://developer.mozilla.org/en-US/docs/Web/CSS/backdrop-filter) | `unset` |
143+
| `--menu-border` | [Border](https://developer.mozilla.org/en-US/docs/Web/CSS/border) | `unset` |
144+
| `--menu-shadow` | [Box Shadow](https://developer.mozilla.org/en-US/docs/Web/CSS/box-shadow) | `0px 0px 8px 0px rgba(0, 0, 0, 0.25)` |
145+
| `--menu-text` | [Color](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value) | `rgb(24, 24, 26)` |
146+
| `--menu-item-hover` | [Color](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value) | `rgba(0, 0, 0, 0.08)` |
141147

142148
### Icon
143149

examples/get-started/pure-js/widgets/app.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
CompassWidget,
99
ZoomWidget,
1010
FullscreenWidget,
11+
_GeocoderWidget as GeocoderWidget,
1112
DarkGlassTheme,
1213
LightGlassTheme
1314
} from '@deck.gl/widgets';
@@ -77,6 +78,7 @@ new Deck({
7778
widgets: [
7879
new ZoomWidget({style: widgetTheme}),
7980
new CompassWidget({style: widgetTheme}),
80-
new FullscreenWidget({style: widgetTheme})
81+
new FullscreenWidget({style: widgetTheme}),
82+
new GeocoderWidget({style: widgetTheme, _geolocation: true})
8183
]
8284
});

modules/widgets/src/geocoder-widget.tsx

Lines changed: 58 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {Widget} from '@deck.gl/core';
66
import type {WidgetPlacement, Viewport, WidgetProps} from '@deck.gl/core';
77
import {FlyToInterpolator, LinearInterpolator} from '@deck.gl/core';
88
import {render} from 'preact';
9-
import {DropdownMenu} from './lib/components/dropdown-menu';
9+
import {DropdownMenu, type MenuItem} from './lib/components/dropdown-menu';
1010
import {type Geocoder} from './lib/geocode/geocoder';
1111
import {GeocoderHistory} from './lib/geocode/geocoder-history';
1212
import {
@@ -22,6 +22,15 @@ type ViewState = Record<string, unknown>;
2222

2323
const CURRENT_LOCATION = 'current';
2424

25+
// Location pin icon (from Google Material Symbols)
26+
const LOCATION_ICON = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 -960 960 960'%3E%3Cpath d='M480-480q33 0 56.5-23.5T560-560q0-33-23.5-56.5T480-640q-33 0-56.5 23.5T400-560q0 33 23.5 56.5T480-480Zm0 294q122-112 181-203.5T720-552q0-109-69.5-178.5T480-800q-101 0-170.5 69.5T240-552q0 71 59 162.5T480-186Zm0 106Q319-217 239.5-334.5T160-552q0-150 96.5-239T480-880q127 0 223.5 89T800-552q0 100-79.5 217.5T480-80Z'/%3E%3C/svg%3E`;
27+
28+
const CURRENT_LOCATION_ITEM: MenuItem = {
29+
label: 'Current location',
30+
value: CURRENT_LOCATION,
31+
icon: LOCATION_ICON
32+
};
33+
2534
/** Properties for the GeocoderWidget */
2635
export type GeocoderWidgetProps = WidgetProps & {
2736
viewId?: string | null;
@@ -66,6 +75,7 @@ export class GeocoderWidget extends Widget<GeocoderWidgetProps> {
6675
geocodeHistory = new GeocoderHistory({});
6776
addressText: string = '';
6877
geocoder: Geocoder = CoordinatesGeocoder;
78+
isGettingLocation: boolean = false;
6979

7080
constructor(props: GeocoderWidgetProps = {}) {
7181
super(props);
@@ -83,45 +93,27 @@ export class GeocoderWidget extends Widget<GeocoderWidgetProps> {
8393
}
8494

8595
onRenderHTML(rootElement: HTMLElement): void {
86-
const menuItems = this.props._geolocation
87-
? [CURRENT_LOCATION, ...this.geocodeHistory.addressHistory]
96+
const menuItems: MenuItem[] = this.props._geolocation
97+
? [CURRENT_LOCATION_ITEM, ...this.geocodeHistory.addressHistory]
8898
: [...this.geocodeHistory.addressHistory];
8999
render(
90-
<div
91-
className="deck-widget-geocoder"
92-
style={{
93-
pointerEvents: 'auto',
94-
display: 'flex',
95-
alignItems: 'center',
96-
flexWrap: 'wrap' // Allows wrapping on smaller screens
97-
}}
98-
>
100+
<div className="deck-widget-geocoder">
99101
<input
102+
className="deck-widget-geocoder-input"
100103
type="text"
101-
placeholder={this.geocoder.placeholderLocation ?? 'Enter address or location'}
104+
placeholder={
105+
this.isGettingLocation
106+
? 'Finding your location...'
107+
: (this.geocoder.placeholderLocation ?? 'Enter address or location')
108+
}
102109
value={this.geocodeHistory.addressText}
103110
// @ts-expect-error event type
104111
onInput={e => this.setInput(e.target?.value || '')}
105112
onKeyPress={this.handleKeyPress}
106-
style={{
107-
flex: '1 1 auto',
108-
minWidth: '200px',
109-
margin: 0,
110-
padding: '8px',
111-
boxSizing: 'border-box'
112-
}}
113-
/>
114-
<DropdownMenu
115-
menuItems={menuItems}
116-
onSelect={this.handleSelect}
117-
style={{
118-
margin: 2,
119-
padding: '4px 2px',
120-
boxSizing: 'border-box'
121-
}}
122113
/>
114+
<DropdownMenu menuItems={menuItems} onSelect={this.handleSelect} />
123115
{this.geocodeHistory.errorText && (
124-
<div className="error">{this.geocodeHistory.errorText}</div>
116+
<div className="deck-widget-geocoder-error">{this.geocodeHistory.errorText}</div>
125117
)}
126118
</div>,
127119
rootElement
@@ -138,9 +130,15 @@ export class GeocoderWidget extends Widget<GeocoderWidgetProps> {
138130
}
139131
};
140132

141-
handleSelect = (address: string) => {
142-
this.setInput(address);
143-
this.handleSubmit();
133+
handleSelect = (value: string) => {
134+
if (value === CURRENT_LOCATION) {
135+
// Don't put "current" in the text field, just trigger geolocation
136+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
137+
this.getCurrentLocation();
138+
} else {
139+
this.setInput(value);
140+
this.handleSubmit();
141+
}
144142
};
145143

146144
/** Sync wrapper for async geocode() */
@@ -149,15 +147,39 @@ export class GeocoderWidget extends Widget<GeocoderWidgetProps> {
149147
this.geocode(this.addressText);
150148
};
151149

150+
/** Get current location via browser geolocation API */
151+
getCurrentLocation = async () => {
152+
this.isGettingLocation = true;
153+
if (this.rootElement) {
154+
this.updateHTML();
155+
}
156+
157+
try {
158+
const coordinates = await CurrentLocationGeocoder.geocode();
159+
if (coordinates) {
160+
this.setViewState(coordinates);
161+
}
162+
} catch (error) {
163+
this.geocodeHistory.errorText = error instanceof Error ? error.message : 'Location error';
164+
} finally {
165+
this.isGettingLocation = false;
166+
if (this.rootElement) {
167+
this.updateHTML();
168+
}
169+
}
170+
};
171+
152172
/** Perform geocoding */
153173
geocode: (address: string) => Promise<void> = async address => {
154-
const useGeolocation = this.props._geolocation && address === CURRENT_LOCATION;
155-
const geocoder = useGeolocation ? CurrentLocationGeocoder : this.geocoder;
156174
const coordinates = await this.geocodeHistory.geocode(
157-
geocoder,
175+
this.geocoder,
158176
this.addressText,
159177
this.props.apiKey
160178
);
179+
// Re-render to show updated history or error (guard against torn-down widget)
180+
if (this.rootElement) {
181+
this.updateHTML();
182+
}
161183
if (coordinates) {
162184
this.setViewState(coordinates);
163185
}

modules/widgets/src/lib/components/dropdown-menu.tsx

Lines changed: 44 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,26 @@
55
import {type JSX} from 'preact';
66
import {useState, useRef, useEffect} from 'preact/hooks';
77

8+
export type MenuItem = string | {label: string; value: string; icon?: string};
9+
810
export type DropdownMenuProps = {
9-
menuItems: string[];
11+
menuItems: MenuItem[];
1012
onSelect: (value: string) => void;
1113
style?: JSX.CSSProperties;
1214
};
1315

16+
function getMenuItemValue(item: MenuItem): string {
17+
return typeof item === 'string' ? item : item.value;
18+
}
19+
20+
function getMenuItemLabel(item: MenuItem): string {
21+
return typeof item === 'string' ? item : item.label;
22+
}
23+
24+
function getMenuItemIcon(item: MenuItem): string | undefined {
25+
return typeof item === 'string' ? undefined : item.icon;
26+
}
27+
1428
export const DropdownMenu = (props: DropdownMenuProps) => {
1529
const [isOpen, setIsOpen] = useState(false);
1630
const dropdownRef = useRef<HTMLDivElement>(null);
@@ -30,67 +44,41 @@ export const DropdownMenu = (props: DropdownMenuProps) => {
3044
};
3145
}, []);
3246

33-
const handleSelect = (value: string) => {
34-
props.onSelect(value);
47+
const handleSelect = (item: MenuItem) => {
48+
props.onSelect(getMenuItemValue(item));
3549
setIsOpen(false);
3650
};
3751

52+
// Don't render anything if there are no menu items
53+
if (props.menuItems.length === 0) {
54+
return null;
55+
}
56+
3857
return (
39-
<div
40-
className="dropdown-container"
41-
ref={dropdownRef}
42-
style={{
43-
position: 'relative',
44-
display: 'inline-block',
45-
...props.style
46-
}}
47-
>
48-
<button
49-
onClick={toggleDropdown}
50-
style={{
51-
width: '30px',
52-
height: '30px',
53-
display: 'flex',
54-
alignItems: 'center',
55-
justifyContent: 'center',
56-
border: '1px solid #ccc',
57-
borderRadius: '4px',
58-
background: '#fff',
59-
cursor: 'pointer',
60-
padding: 0
61-
}}
62-
>
63-
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' : ''}`} />
6461
</button>
6562
{isOpen && (
66-
<ul
67-
style={{
68-
position: 'absolute',
69-
top: '100%',
70-
right: '100%',
71-
background: '#fff',
72-
border: '1px solid #ccc',
73-
borderRadius: '4px',
74-
listStyle: 'none',
75-
padding: '4px 0',
76-
margin: 0,
77-
zIndex: 1000,
78-
minWidth: '200px'
79-
}}
80-
>
81-
{props.menuItems.map(item => (
82-
<li
83-
key={item}
84-
onClick={() => handleSelect(item)}
85-
style={{
86-
padding: '4px 8px',
87-
cursor: 'pointer',
88-
whiteSpace: 'nowrap'
89-
}}
90-
>
91-
{item}
92-
</li>
93-
))}
63+
<ul className="deck-widget-dropdown-menu">
64+
{props.menuItems.map(item => {
65+
const icon = getMenuItemIcon(item);
66+
return (
67+
<li
68+
className="deck-widget-dropdown-item"
69+
key={getMenuItemValue(item)}
70+
onClick={() => handleSelect(item)}
71+
>
72+
{icon && (
73+
<span
74+
className="deck-widget-dropdown-item-icon"
75+
style={{maskImage: `url("${icon}")`, WebkitMaskImage: `url("${icon}")`}}
76+
/>
77+
)}
78+
{getMenuItemLabel(item)}
79+
</li>
80+
);
81+
})}
9482
</ul>
9583
)}
9684
</div>

0 commit comments

Comments
 (0)