Skip to content
Open
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
51 changes: 47 additions & 4 deletions src/components/layout/EditorShell.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,68 @@
"use client";

import type { ReactNode } from "react";
import { IconButton } from "@/components/ui/IconButton";
import { MenuIcon } from "@/icons/MenuIcon";

type EditorShellProps = {
header: ReactNode;
sidebar: ReactNode;
preview: ReactNode;
/** Whether the mobile controls drawer is open (<=900px only). */
controlsOpen: boolean;
onCloseControls: () => void;
};

export function EditorShell({ header, sidebar, preview }: EditorShellProps) {
export function EditorShell({
header,
sidebar,
preview,
controlsOpen,
onCloseControls,
}: EditorShellProps) {
return (
<div className="editor-shell flex h-dvh flex-col bg-editor-bg-base text-editor-fg-primary [--editor-sidebar-width:min(400px,36vw)]">
<header className="relative z-[100] flex-shrink-0 overflow-visible bg-editor-bg-base">
{header}
</header>
<div className="flex min-h-0 flex-1 max-[900px]:flex-col">
<div className="flex min-h-0 flex-1">
{/* Backdrop behind the mobile drawer. */}
<button
type="button"
aria-hidden="true"
tabIndex={-1}
onClick={onCloseControls}
className={`fixed inset-0 z-[105] bg-black/40 transition-opacity duration-300 min-[901px]:hidden ${
controlsOpen
? "opacity-100"
: "pointer-events-none opacity-0"
}`}
/>
<aside
className="flex w-[var(--editor-sidebar-width)] flex-shrink-0 flex-col gap-3 overflow-y-auto bg-editor-bg-base p-4 max-[900px]:max-h-[42vh] max-[900px]:w-full max-[900px]:border-b max-[900px]:border-editor-border"
className={`flex w-[var(--editor-sidebar-width)] flex-shrink-0 flex-col gap-3 overflow-y-auto bg-editor-bg-base p-4 max-[900px]:fixed max-[900px]:inset-0 max-[900px]:z-[110] max-[900px]:w-full max-[900px]:transition-transform max-[900px]:duration-300 max-[900px]:ease-[cubic-bezier(0.16,1,0.3,1)] ${
controlsOpen
? "max-[900px]:translate-x-0"
: "max-[900px]:-translate-x-full"
}`}
aria-label="Map controls"
aria-hidden={!controlsOpen}
>
Comment on lines 41 to 49

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

On desktop layouts (>900px), controlsOpen is initialized to false, which causes aria-hidden={!controlsOpen} to evaluate to true. This hides the entire sidebar from screen readers on desktop. Additionally, when the drawer is closed on mobile, interactive elements inside the sidebar can still be focused via keyboard tabbing.

Using Tailwind's max-[900px]:visible and max-[900px]:invisible utility classes solves both issues: it automatically handles screen reader visibility and prevents keyboard focus when closed on mobile, while keeping the sidebar fully accessible on desktop.

Suggested change
<aside
className="flex w-[var(--editor-sidebar-width)] flex-shrink-0 flex-col gap-3 overflow-y-auto bg-editor-bg-base p-4 max-[900px]:max-h-[42vh] max-[900px]:w-full max-[900px]:border-b max-[900px]:border-editor-border"
className={`flex w-[var(--editor-sidebar-width)] flex-shrink-0 flex-col gap-3 overflow-y-auto bg-editor-bg-base p-4 max-[900px]:fixed max-[900px]:inset-0 max-[900px]:z-[110] max-[900px]:w-full max-[900px]:transition-transform max-[900px]:duration-300 max-[900px]:ease-[cubic-bezier(0.16,1,0.3,1)] ${
controlsOpen
? "max-[900px]:translate-x-0"
: "max-[900px]:-translate-x-full"
}`}
aria-label="Map controls"
aria-hidden={!controlsOpen}
>
<aside
className={"flex w-[var(--editor-sidebar-width)] flex-shrink-0 flex-col gap-3 overflow-y-auto bg-editor-bg-base p-4 max-[900px]:fixed max-[900px]:inset-0 max-[900px]:z-[110] max-[900px]:w-full max-[900px]:transition-[transform,visibility] max-[900px]:duration-300 max-[900px]:ease-[cubic-bezier(0.16,1,0.3,1)] " + (
controlsOpen
? "max-[900px]:translate-x-0 max-[900px]:visible"
: "max-[900px]:-translate-x-full max-[900px]:invisible"
)}
aria-label="Map controls"
>

{/* Drawer header — mobile only. */}
<div className="hidden items-center justify-between max-[900px]:flex">
<h2 className="text-[15px] font-semibold tracking-[-0.02em] text-editor-fg-primary">
Controls
</h2>
<IconButton
variant="plain"
aria-label="Close controls"
onClick={onCloseControls}
>
<MenuIcon open />
</IconButton>
</div>
{sidebar}
</aside>
<div className="flex min-w-0 flex-1 bg-editor-bg-base pb-2 pr-2 max-[900px]:pl-2">
<div className="flex min-w-0 flex-1 bg-editor-bg-base pb-2 pr-2 max-[900px]:px-2 max-[900px]:pb-2">
<div className="flex min-h-0 flex-1 flex-col overflow-hidden rounded-editor-lg border border-editor-border bg-editor-bg-primary">
<div className="relative isolate min-h-0 flex-1 overflow-hidden rounded-[inherit]">
{preview}
Expand Down
74 changes: 49 additions & 25 deletions src/components/layout/Nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { EditorViewTabs } from "@/components/layout/EditorViewTabs";
import { NavActions } from "@/components/nav/NavActions";
import { IconButton } from "@/components/ui/IconButton";
import { CrosshairIcon } from "@/icons/CrosshairIcon";
import { MenuIcon } from "@/icons/MenuIcon";
import { PatchIcon } from "@/icons/PatchIcon";
import type { MapViewMode } from "@/types/map";

Expand All @@ -15,6 +16,9 @@ type NavProps = {
onZoomToSelection?: () => void;
showPatch?: boolean;
onTogglePatch?: () => void;
/** Toggles the mobile controls drawer (<=900px only). */
controlsOpen?: boolean;
onToggleControls?: () => void;
};

export function Nav({
Expand All @@ -24,41 +28,61 @@ export function Nav({
onZoomToSelection,
showPatch = false,
onTogglePatch,
controlsOpen = false,
onToggleControls,
}: NavProps) {
return (
<nav className="relative z-[100] flex w-full flex-col gap-0 overflow-visible">
<div className="relative z-[2] box-border grid h-12 w-full grid-cols-[var(--editor-sidebar-width)_1fr] items-center gap-0 overflow-visible rounded-none border-0 bg-transparent p-0">
<div className="box-border flex min-w-0 items-center px-4 py-2">
<div className="box-border flex min-w-0 items-center gap-2 px-4 py-2">
{onToggleControls ? (
<div className="hidden max-[900px]:flex">
<IconButton
variant="plain"
aria-label={controlsOpen ? "Close controls" : "Open controls"}
aria-expanded={controlsOpen}
onClick={onToggleControls}
>
<MenuIcon open={controlsOpen} />
</IconButton>
</div>
) : null}
<Brand />
</div>
<div className="box-border flex min-w-0 items-center bg-editor-bg-base py-2 pl-0 pr-2">
<div className="mr-auto flex flex-shrink-0 items-center gap-1.5">
{/* On mobile (<=900px) the view toggle and these controls move to
the map's floating side panels. */}
{onViewModeChange ? (
<EditorViewTabs value={viewMode} onChange={onViewModeChange} />
) : null}
{hasSelection && onZoomToSelection ? (
<IconButton
className="animate-[zoomToSelectionIn_0.18s_cubic-bezier(0.16,1,0.3,1)]"
aria-label="Zoom to selection"
onClick={onZoomToSelection}
>
<CrosshairIcon />
</IconButton>
) : null}
{hasSelection && onTogglePatch ? (
<IconButton
className="animate-[zoomToSelectionIn_0.18s_cubic-bezier(0.16,1,0.3,1)]"
aria-label={
showPatch
? "Hide downloaded patch extent"
: "Show downloaded patch extent"
}
aria-pressed={showPatch}
onClick={onTogglePatch}
>
<PatchIcon />
</IconButton>
<div className="max-[900px]:hidden">
<EditorViewTabs value={viewMode} onChange={onViewModeChange} />
</div>
) : null}
<div className="flex items-center gap-1.5 max-[900px]:hidden">
{hasSelection && onZoomToSelection ? (
<IconButton
className="animate-[zoomToSelectionIn_0.18s_cubic-bezier(0.16,1,0.3,1)]"
aria-label="Zoom to selection"
onClick={onZoomToSelection}
>
<CrosshairIcon />
</IconButton>
) : null}
{hasSelection && onTogglePatch ? (
<IconButton
className="animate-[zoomToSelectionIn_0.18s_cubic-bezier(0.16,1,0.3,1)]"
aria-label={
showPatch
? "Hide downloaded patch extent"
: "Show downloaded patch extent"
}
aria-pressed={showPatch}
onClick={onTogglePatch}
>
<PatchIcon />
</IconButton>
) : null}
</div>
</div>
<NavActions />
</div>
Expand Down
21 changes: 21 additions & 0 deletions src/components/map/EarthMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import type { MapSelection, MapViewMode, MapViewState } from "@/types/map";
import { useTheme } from "@/providers/ThemeProvider";
import { EditorShell } from "@/components/layout/EditorShell";
import { Nav } from "@/components/layout/Nav";
import { MapSideControls } from "@/components/map/MapSideControls";
import { MapSearch } from "@/components/map/MapSearch";
import { MapReadout } from "@/components/map/MapReadout";
import { GlobeSelectionOverlay } from "@/components/map/GlobeSelectionOverlay";

Expand Down Expand Up @@ -58,6 +60,7 @@ export function EarthMap() {
const [mapSize, setMapSize] = useState({ width: 0, height: 0 });
const [selection, setSelection] = useState<MapSelection | null>(null);
const [showPatch, setShowPatch] = useState(true);
const [controlsOpen, setControlsOpen] = useState(false);
const [historyYears, setHistoryYears] = useState(DEFAULT_HISTORY_YEARS);
const [loadingSeries, setLoadingSeries] = useState(false);
const [seriesProgress, setSeriesProgress] = useState<{
Expand Down Expand Up @@ -235,6 +238,8 @@ export function EarthMap() {

return (
<EditorShell
controlsOpen={controlsOpen}
onCloseControls={() => setControlsOpen(false)}
header={
<Nav
viewMode={viewMode}
Expand All @@ -243,6 +248,8 @@ export function EarthMap() {
onZoomToSelection={handleZoomToSelection}
showPatch={showPatch}
onTogglePatch={handleTogglePatch}
controlsOpen={controlsOpen}
onToggleControls={() => setControlsOpen((open) => !open)}
/>
}
sidebar={
Expand Down Expand Up @@ -298,6 +305,20 @@ export function EarthMap() {
/>
) : null}
</Map>
<div className="pointer-events-none absolute inset-x-0 top-2 z-30 flex justify-center px-2">
<MapSearch
onSelect={handlePick}
className="pointer-events-auto w-full min-[901px]:max-w-[400px]"
/>
</div>
<MapSideControls
viewMode={viewMode}
onViewModeChange={handleViewModeChange}
hasSelection={selection !== null}
onZoomToSelection={handleZoomToSelection}
showPatch={showPatch}
onTogglePatch={handleTogglePatch}
/>
</div>
}
/>
Expand Down
Loading
Loading