mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 22:16:26 +00:00
fix(maps): the hover label on the GL maps goes away again when the pointer leaves a pin or the map moves
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
*
|
||||
* Pinned: the layers go in once per map (and wait for `idle` when tiles are
|
||||
* still loading, the failure the hazard overlay had), the line is the dark
|
||||
* dashed content colour and never a grade colour, new data replaces the
|
||||
* dashed via ferrata violet and never a grade colour, new data replaces the
|
||||
* sources' contents instead of rebuilding the layers, a click on the hit line
|
||||
* hands the ferrata and the way to `onSelect`, hover and selection highlight
|
||||
* the whole ferrata (solid, on a wider casing) and name it above the pointer,
|
||||
@@ -156,10 +156,10 @@ describe('useFerrataLayerGL', () => {
|
||||
expect(addedIds(gl)).toEqual(LAYERS);
|
||||
});
|
||||
|
||||
it('FE-FERRATA-GL-002: draws a dark dashed line in the content colour on a light casing, never by grade', () => {
|
||||
it('FE-FERRATA-GL-002: draws a dashed violet line on a white casing, never by grade', () => {
|
||||
const root = document.documentElement;
|
||||
root.style.setProperty('--text-primary', 'black');
|
||||
root.style.setProperty('--bg-primary', 'white');
|
||||
root.style.setProperty('--map-ferrata', 'black');
|
||||
root.style.setProperty('--map-ferrata-casing', 'white');
|
||||
const gl = fakeGlMap();
|
||||
renderHook(() => useFerrataLayerGL(gl.map, GROUPS, vi.fn()));
|
||||
|
||||
@@ -167,8 +167,8 @@ describe('useFerrataLayerGL', () => {
|
||||
expect(line.paint['line-color']).toBe('black');
|
||||
expect(line.paint['line-dasharray']).toEqual([2, 1.5]);
|
||||
expect(spec(gl, CASING).paint['line-color']).toBe('white');
|
||||
root.style.removeProperty('--text-primary');
|
||||
root.style.removeProperty('--bg-primary');
|
||||
root.style.removeProperty('--map-ferrata');
|
||||
root.style.removeProperty('--map-ferrata-casing');
|
||||
});
|
||||
|
||||
it('FE-FERRATA-GL-003: waits for idle when the style is still loading tiles', () => {
|
||||
@@ -269,19 +269,19 @@ describe('useFerrataLayerGL', () => {
|
||||
|
||||
it('FE-FERRATA-GL-010: a theme change repaints the installed layers without rebuilding them', async () => {
|
||||
const root = document.documentElement;
|
||||
root.style.setProperty('--text-primary', 'black');
|
||||
root.style.setProperty('--map-ferrata', 'black');
|
||||
const gl = fakeGlMap();
|
||||
renderHook(() => useFerrataLayerGL(gl.map, GROUPS, vi.fn()));
|
||||
expect(gl.spies.setPaintProperty).not.toHaveBeenCalled();
|
||||
|
||||
await act(async () => {
|
||||
root.style.setProperty('--text-primary', 'ivory');
|
||||
root.style.setProperty('--map-ferrata', 'ivory');
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
await waitFor(() => expect(gl.spies.setPaintProperty).toHaveBeenCalledWith(LINE, 'line-color', 'ivory'));
|
||||
expect(addedIds(gl).filter((id) => LAYERS.includes(id))).toEqual(LAYERS);
|
||||
root.style.removeProperty('--text-primary');
|
||||
root.style.removeProperty('--map-ferrata');
|
||||
});
|
||||
it('FE-FERRATA-GL-011: names the ferrata under the pointer with its grade, above the pointer, and hides it on leave', () => {
|
||||
const gl = fakeGlMap();
|
||||
@@ -290,14 +290,14 @@ describe('useFerrataLayerGL', () => {
|
||||
act(() => gl.emit(`mousemove:${HIT}`, click));
|
||||
|
||||
const label = gl.container.querySelector<HTMLElement>('[data-testid="ferrata-hover-label"]');
|
||||
expect(label?.hidden).toBe(false);
|
||||
expect(label?.style.display).toBe('');
|
||||
expect(label?.textContent).toBe('Via AK2');
|
||||
expect(label?.querySelector('span')?.textContent).toBe('K2');
|
||||
expect(label?.style.transform).toContain('translate(4px, -8px)');
|
||||
expect(label).toHaveClass('bg-surface-card', 'text-content', 'border-edge-faint');
|
||||
|
||||
act(() => gl.emit(`mouseleave:${HIT}`));
|
||||
expect(label?.hidden).toBe(true);
|
||||
expect(label?.style.display).toBe('none');
|
||||
unmount();
|
||||
expect(gl.container.querySelector('[data-testid="ferrata-hover-label"]')).toBeNull();
|
||||
});
|
||||
|
||||
@@ -26,17 +26,26 @@ interface LabelHost {
|
||||
}
|
||||
|
||||
export interface MapHoverLabel {
|
||||
show(text: { name: string; detail: string | null }, point: { x: number; y: number }): void;
|
||||
/** `owner` is what the label is shown for, so a stale hide from another pin leaves it alone. */
|
||||
show(text: { name: string; detail: string | null }, point: { x: number; y: number }, owner?: object): void;
|
||||
/** Whether the label is up for this owner. */
|
||||
shownFor(owner: object): boolean;
|
||||
hide(): void;
|
||||
remove(): void;
|
||||
}
|
||||
|
||||
const NO_LABEL: MapHoverLabel = { show: () => undefined, hide: () => undefined, remove: () => undefined };
|
||||
const NO_LABEL: MapHoverLabel = {
|
||||
show: () => undefined,
|
||||
shownFor: () => false,
|
||||
hide: () => undefined,
|
||||
remove: () => undefined,
|
||||
};
|
||||
|
||||
export function mapHoverLabel(map: LabelHost, testId = 'map-hover-label'): MapHoverLabel {
|
||||
if (typeof map.getContainer !== 'function' || typeof document === 'undefined') return NO_LABEL;
|
||||
const container = map.getContainer();
|
||||
let element: HTMLDivElement | null = null;
|
||||
let shownOwner: object | null = null;
|
||||
|
||||
const ensure = () => {
|
||||
if (element) return element;
|
||||
@@ -53,8 +62,9 @@ export function mapHoverLabel(map: LabelHost, testId = 'map-hover-label'): MapHo
|
||||
};
|
||||
|
||||
return {
|
||||
show({ name, detail }, { x, y }) {
|
||||
show({ name, detail }, { x, y }, owner) {
|
||||
const label = ensure();
|
||||
shownOwner = owner ?? null;
|
||||
label.replaceChildren(document.createTextNode(name));
|
||||
if (detail) {
|
||||
const badge = document.createElement('span');
|
||||
@@ -63,10 +73,15 @@ export function mapHoverLabel(map: LabelHost, testId = 'map-hover-label'): MapHo
|
||||
label.appendChild(badge);
|
||||
}
|
||||
label.style.transform = `translate(${x}px, ${y - POINTER_GAP}px) translate(-50%, -100%)`;
|
||||
label.hidden = false;
|
||||
// Not the hidden attribute: the label's `flex` class would win over it and keep it on screen.
|
||||
label.style.display = '';
|
||||
},
|
||||
shownFor(owner) {
|
||||
return !!element && element.style.display !== 'none' && shownOwner === owner;
|
||||
},
|
||||
hide() {
|
||||
if (element) element.hidden = true;
|
||||
shownOwner = null;
|
||||
if (element) element.style.display = 'none';
|
||||
},
|
||||
remove() {
|
||||
element?.remove();
|
||||
@@ -83,7 +98,10 @@ export interface PinHoverText {
|
||||
|
||||
/**
|
||||
* Shows the label above a pin while the pointer rests on it, and hides it the
|
||||
* moment the pin is pressed, so it never trails a drag. Returns the unwiring.
|
||||
* moment the pin is pressed, so it never trails a drag. A map that moves its
|
||||
* markers every frame (the 3D terrain) can leave a pin without a leave event,
|
||||
* so a move anywhere else on the map, or off it, hides the label too, as long
|
||||
* as it is still this pin's. Returns the unwiring.
|
||||
*/
|
||||
export function bindPinHoverLabel(
|
||||
pin: HTMLElement,
|
||||
@@ -94,15 +112,23 @@ export function bindPinHoverLabel(
|
||||
const show = () => {
|
||||
const box = pin.getBoundingClientRect();
|
||||
const frame = container.getBoundingClientRect();
|
||||
label.show(text, { x: box.left + box.width / 2 - frame.left, y: box.top - frame.top });
|
||||
label.show(text, { x: box.left + box.width / 2 - frame.left, y: box.top - frame.top }, pin);
|
||||
};
|
||||
const hide = () => label.hide();
|
||||
const away = (event: Event) => {
|
||||
if (!label.shownFor(pin)) return;
|
||||
if (event.type === 'pointerleave' || !(event.target instanceof Node && pin.contains(event.target))) label.hide();
|
||||
};
|
||||
pin.addEventListener('pointerenter', show);
|
||||
pin.addEventListener('pointerleave', hide);
|
||||
pin.addEventListener('pointerdown', hide);
|
||||
container.addEventListener('pointermove', away);
|
||||
container.addEventListener('pointerleave', away);
|
||||
return () => {
|
||||
pin.removeEventListener('pointerenter', show);
|
||||
pin.removeEventListener('pointerleave', hide);
|
||||
pin.removeEventListener('pointerdown', hide);
|
||||
container.removeEventListener('pointermove', away);
|
||||
container.removeEventListener('pointerleave', away);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ import { wirePlannerWaypointDrag } from './plannerWaypointDrag';
|
||||
interface PlannerWaypointMap {
|
||||
getContainer: () => HTMLElement;
|
||||
unproject: (point: [number, number]) => { lat: number; lng: number };
|
||||
/** Optional so a stand-in map without events still takes the pins. */
|
||||
on?: (type: 'movestart', listener: () => void) => unknown;
|
||||
off?: (type: 'movestart', listener: () => void) => unknown;
|
||||
}
|
||||
|
||||
/** The slice of a hand-positioned pin the waypoint markers drive. */
|
||||
@@ -61,6 +64,10 @@ export function usePlannerWaypointMarkersGL<Pin extends PlannerWaypointPin>({
|
||||
pinsRef.current = [];
|
||||
const dragCleanups: (() => void)[] = [];
|
||||
const hoverLabel = mapHoverLabel(map, 'planner-waypoint-label');
|
||||
// A label left behind by a pan or a zoom would float over the wrong spot.
|
||||
const hideOnMove = () => hoverLabel.hide();
|
||||
map.on?.('movestart', hideOnMove);
|
||||
dragCleanups.push(() => map.off?.('movestart', hideOnMove));
|
||||
|
||||
for (const [index, point] of plannerWaypoints.entries()) {
|
||||
const selected = point.id === selectedPlannerWaypointId;
|
||||
|
||||
@@ -33,7 +33,10 @@ export function useTerrainPins(
|
||||
if (!gl || !ready) return;
|
||||
pinsRef.current.forEach((pin) => pin.remove());
|
||||
const hoverLabel = mapHoverLabel(gl.map, 'planner-waypoint-label');
|
||||
const unbind: (() => void)[] = [];
|
||||
// A label left behind by a turn or a tilt would float over the wrong spot.
|
||||
const hideOnMove = () => hoverLabel.hide();
|
||||
gl.map.on('movestart', hideOnMove);
|
||||
const unbind: (() => void)[] = [() => gl.map.off('movestart', hideOnMove)];
|
||||
pinsRef.current = points.map((point, index) => {
|
||||
const el = waypointPinElement(index, point.id === selectedId, editable);
|
||||
const name = point.name ?? String(index + 1);
|
||||
|
||||
Reference in New Issue
Block a user