geoview

Accessibility

Accessibility is a priority in GeoView’s development. The application follows WCAG 2.1 Level AA guidelines whenever possible, and this section highlights the key features implemented for visual, keyboard, and screen reader users.

For viewer users, start with the features overview, interaction patterns, and known limitations. For developers and reviewers, see the best practices, ARIA guidance, and review checklist.

001. Accessibility Features Overview

1. For Keyboard Users

Keyboard Navigation Mode

Dynamic maps support keyboard focus and crosshair-based map interaction. Static maps do not offer these interactions or the layer, legend, details, table, and navigation controls, but retain about/notification controls and attribution/scale.

With focus inside a dynamic viewer, Ctrl+M (or Cmd+M on macOS) focuses the map element and activates, not toggles, the crosshair. Ctrl+Q exits keyboard navigation mode and deactivates the crosshair. Tabbing to another control within the viewer also deactivates it. Escape is not a general crosshair-exit shortcut; drawer editing can use it to cancel a grabbed transform. Arrow keys pan by 128px by default. Shift+ArrowUp increases the step by 10px; Shift+ArrowDown decreases it by 10px (minimum 10px).

On first tab into the viewer, users are prompted to enable keyboard navigation mode or skip the viewer. This mode enables a focus trap within the viewer; focusing the map activates crosshair-based interaction.

Keyboard Focus Indicators

Keyboard focus is shown with a contrasting outline, usually with an additional halo. Placement varies by control so the indicator fits its shape and available space. Form inputs use an outline without the halo in keyboard navigation mode.

Some app-bar/nav-bar controls still suppress the standard ring; this remains an accessibility/design review item. See the contrast and visual-indicator release tests for verification, and styling best practices for developer guidance.

Focus Traps for Enhanced Navigation

In keyboard navigation mode, activating a panel (e.g., Layers, Legend, Details) enables a focus trap that confines keyboard navigation within that panel. Auto-opened panels may remain outside the active trap.

Sub-panels provide similar functionality.

Context-Aware Esc Key

The Esc key intelligently handles different scenarios:

Nested content, time-slider deferred closing, and browser fullscreen can affect which action happens first. See intentional interaction patterns and fullscreen constraints.

2. For Screen Reader Users

Semantic Landmarks

Semantic landmarks and named regions, including the map region, support quick navigation with a screen reader.

ARIA Live Regions

Real-time announcements for events such as:

Descriptive Labels

Interactive elements use context-specific accessible names through visible text, aria-labelledby, or aria-label:


002. Intentional Interaction Patterns

1. Time Slider

Esc key behaviour in normal mode:

When the time slider panel is open and either animation is playing OR layers are loading, pressing Esc initiates a deferred close:

  1. Animation stops immediately (if playing)
  2. The same Esc press does not also close an enclosing panel
  3. The panel remains open until both conditions clear (animation stopped AND layers finished loading)
  4. During this period, tabbing through the right panel remains possible
  5. Once conditions stabilize, the panel auto-closes and focus returns to the layer button in the left panel

Esc key behaviour in fullscreen mode:

When the time slider is in a panel fullscreen view, Esc stops any active animation and closes the fullscreen dialog. Focus returns to the surrounding panel after closing. This differs from browser fullscreen, which can consume Escape first.

Rationale:

Waiting for playback and loading to finish keeps keyboard focus stable while the panel closes.

2. Multi-Panel Auto-Open and Focus Priority

Focus trap mutual exclusion when multiple panels open:

When a map interaction (such as clicking a feature) triggers multiple panels to auto-open simultaneously (e.g., Details panel in the app-bar and Chart panel in the footer-bar), only one panel can receive the focus trap at a time.

Close buttons depend on the layout and content, not just the active focus trap. App-bar toggle layouts and mobile layouts can show a close button even when the panel is not focus-trapped.

Behavior:

  1. Focus priority — One panel receives the focus trap, while other auto-opened panels remain open without it
  2. Other panels — An active trap restricts Tab navigation to its own content. Other open panels are not necessarily reachable by Tab until that trap is exited
  3. Changing panels — Use the current panel’s Close or Exit control to return to its associated control, then navigate to another panel. Activating a layer entry can move focus into that panel’s content

Rationale:

This mutual exclusion pattern supports multi-panel workflows:

3. Mouse Movement Can Disrupt WCAG Mode

Behavior:

Moving the mouse over the viewer after entering the keyboard-navigation flow exits keyboard mode and dismisses the entry prompt if it is still open. Pointer placement alone does not trigger this transition.

To stay in keyboard navigation mode:

Rationale:

Mouse movement is treated as a return to mouse interaction, so the viewer releases its keyboard focus trap. This explains the mode change; unresponsive buttons or broken focus traps are not intended behavior.


003. Best Practices

These sections explain shared developer conventions. Source-specific maintenance notes cover keyboard/panel focus, focus indicators, and wrapper diagnostics.

Guidance for code review:

These guidelines help catch common accessibility issues during branch review. They are advisory, not blocking — the goal is to assist developers in writing accessible code, not to prevent work-in-progress from merging.

Priority levels:


1. Use unique and valid element IDs (🔴 HIGH PRIORITY)

Every element id in the DOM must be unique. Duplicate IDs break label-input associations (htmlFor), invalidate ARIA relationships (aria-labelledby, aria-describedby), and cause screen readers to behave unpredictably.

ID naming patterns (from least specific to most specific):

// Pattern 1: Element in a container
`${mapId}-${containerType}-${elementName}`
// Example: "mapA-footer-zoom-button"

// Pattern 2: Element in a panel within a container
`${mapId}-${containerType}-${panelId}-${elementName}`
// Example: "mapA-footer-layers-panel-close-button"

// Pattern 3: When a truly unique ID is required
// Prefer React's useId() for stable label/control associations
`${mapId}-${containerType}-${elementName}-${uniqueId}`;
// Example: "mapA-footer-layer-item-k8j3n2m9"

Layer paths and generated IDs:

Replacing punctuation can create duplicate IDs: group/a.b and group/a/b both become group-a-b. Use map and component-instance identifiers to keep IDs unique, and use the same ID in the associated label and ARIA references. If an ID contains special characters, escape it before using it in a CSS selector.

2. Use Semantic Elements for Interactions (🔴 HIGH PRIORITY)

Clickable <div> and <span> elements do not provide button semantics, keyboard focus, or Enter/Space activation by default. Native <button> and <a href> elements come with built-in keyboard support, appropriate semantics, and browser-managed focus behaviour. Use Button, IconButton, and Link from @/ui over attaching onClick handlers to arbitrary elements.

Pick the primitive by what the element does:

Need Use Notes
Action with a text label Button Default MUI button styling
Icon-only action IconButton (from @/ui) aria-label is required and is the fallback tooltip text when no tooltip prop is given
Card, tile or row that triggers an action ButtonBase No padding/typography; keeps the existing card layout. Render children as component="span" (button content must be phrasing content)
Clickable row in a List ListItemButton Put other controls in ListItem secondaryAction so they are not nested in the button
Navigation to a URL Link / <a href> Prefer a real link over a button that calls window.open (some existing download buttons still do; migrate when touched)

Do not hand-roll role="button" + tabIndex={0} + onKeyDown on a Box/div/Card — ButtonBase provides focus, Enter/Space activation, disabled semantics and the theme focus-visible indicator.

<ButtonBase onClick={handleClick} disableRipple sx={sxClasses.card}>
  <Typography component="span">{title}</Typography>
</ButtonBase>

Preserve the keyboard focus indicator when adapting the control’s appearance to an existing layout.

Delegated listeners — An onClick on a container that reacts to clicks bubbling from real child controls (e.g. scrolling the viewer into view) does not make the container a control. Clicking its empty space must remain harmless.

3. Use IconButton for buttons without labels (🔴 HIGH PRIORITY)

The IconButton component in GeoView has built-in accessibility support. Use IconButton instead of Button when a button does not have a text label.

Its aria-label prop is required by TypeScript. The compiler catches a missing prop, but reviewers should still check that the value is non-empty, translated, and describes the action and target.

<IconButton aria-label={t('general.close')} onClick={handleClose}>
  <CloseIcon />
</IconButton>

4. Ensure WCAG 2.1 Level AA Compliance (🟡 MEDIUM PRIORITY)

Critical checks during code review:

Measure the clickable area, not just the icon. Small targets are difficult for users with motor impairments or touch screen users.

Testing tools (post-implementation):

5. Focus management: Use aria-disabled instead of disabled on UI elements that toggle between enabled/disabled states (🟡 MEDIUM PRIORITY)

When a focused button becomes natively disabled, focus behavior can vary by browser and the button is removed from sequential keyboard navigation. For transient states where users need to retain their position, consider aria-disabled instead.

const handleClick = (e) => {
  if (isDisabled) return;
  // real logic
};

<button
  aria-disabled={isDisabled}
  onClick={handleClick}
>
  Submit
</button>

6. Focus management: Avoid removing buttons from the DOM (🟡 MEDIUM PRIORITY)

7. Handle Esc key for dismissible UI elements (🔴 HIGH PRIORITY)

Dismissible modal dialogs, panels, and popups should respond to Esc. MUI Dialog provides Escape handling through onClose; custom components may need their own handler. Nested content handles Escape first, and the time slider can defer closing as described above.

Pattern 1: GeoView Popper component (simplest)

The GeoView Popper component from @/ui accepts handleEscapeKey directly:

import { handleEscapeKey } from '@/core/utils/utilities';

<Popper
  open={isOpen}
  anchorEl={anchorEl}
  onClose={handleClose}
  handleKeyDown={handleEscapeKey}  // ← Pass directly
>
  {/* content */}
</Popper>

Pattern 2: MUI Dialog dismissal

MUI Dialog handles Escape and calls onClose with the escapeKeyDown reason unless Escape dismissal is disabled. GeoView’s fullscreen dialog uses this behavior; an explicit onKeyDown is not required:

<Dialog open={isOpen} onClose={handleClose}>
  {/* content */}
</Dialog>

Pattern 3: Custom keyboard dismissal

For custom content that owns dismissal and must keep a parent panel open, handle propagation at the event boundary:

import { handleEscapeKey } from '@/core/utils/utilities';

<Box
  onKeyDown={(event) => {
    if (event.key !== 'Escape') return;
    event.stopPropagation();
    handleEscapeKey(event.key, handleClose);
  }}
>
  {/* content */}
</Box>

Critical detail: handleEscapeKey expects a key string (event.key), not the event object itself. It does not stop propagation. If closing nested content must leave its parent open, call event.stopPropagation() in the nested handler.

What to check in code review:

8. Announce loading states and progress updates using ARIA live regions (🟡 MEDIUM PRIORITY)

Avoid “loader fatigue” by not announcing every incremental progress update. Keep the named ProgressBar available to assistive technology and use a separate status message for meaningful milestones, completion, or errors when needed.

See aria-live for announcement rules, atomic messages, and the shared implementation example.

9. Avoid Tooltips on Non-Interactive Elements (🟡 MEDIUM PRIORITY)

Tooltips on non-focusable elements like ListItem are not available through keyboard focus. For an action, attach the tooltip to an interactive control such as ListItemButton. For static content, provide essential information as visible text rather than a hover-only tooltip.

// Bad
<Tooltip title="Select layer xyz on the map">
  <ListItem>Layer xyz</ListItem>
</Tooltip>

// Good
<Tooltip title="Select layer xyz on the map">
  <ListItemButton>layer xyz</ListItemButton>
</Tooltip>

10. Provide Text Alternatives for Images (🟡 MEDIUM PRIORITY)

Informative images need a meaningful text alternative. Decorative images, or images whose information is already conveyed by adjacent text, use alt="" so screen readers can ignore them. Use translated descriptions for images authored by the application.

// Decorative or redundant with adjacent text
<img src={imageUrl} alt="" />

// Informative
<img src={imageUrl} alt={imageDescription} />

Some legend and map imagery lacks descriptive source metadata; see the non-text content limitations.


004. ARIA Best Practices

aria-current

Indicates that this element represents the current item within a container or set of related elements. This is a good choice for buttons that select layers.

aria-label

ARIA labels should clearly describe an element’s purpose, not just its type or location.

// Bad — role is repeated in the label, and label lacks context
<Button aria-label="Toggle visibility button" />

// Good — role is omitted, action and target are clear
<Button aria-label="Toggle visibility - Confederation to 1914" />
// Bad — identical labels across multiple elements
<Button aria-label="Learn more" />
<Button aria-label="Learn more" />
<Button aria-label="Learn more" />

// Good — each label is distinguishable
<Button aria-label="Learn more about the Confederation era" />
<Button aria-label="Learn more about WWI" />
<Button aria-label="Learn more about Industrialisation" />

Select Naming

GeoView’s Select wrapper requires exactly one naming prop: a non-empty visible label or aria-label. Use translated text, or the configured attribute display label for filter controls. The wrapper connects visible labels automatically; provide labelId only when an integration needs a specific label ID.

<Select label={fieldLabel} value={value} onChange={handleChange} menuItems={menuItems} />

<Select aria-label={fieldLabel} value={value} onChange={handleChange} menuItems={menuItems} />

aria-live

Announce loading states and progress updates using ARIA live regions

When to use aria-live

Use aria-live to announce state changes that a screen reader user might otherwise miss — such as a background process completing, an error appearing off-screen, or an asynchronous result arriving. Avoid duplicate or excessively frequent announcements:

aria-atomic

Pattern

The aria-live region and progress bar serve separate concerns — the live region handles screen reader announcements, while the progress bar provides visual feedback. Keep them decoupled:

// Visually hidden live region — announces final outcome to screen readers.
// Not updated during incremental progress, only on completion or error.
<Box sx={sxClasses.visuallyHidden} role="status" aria-live="polite" aria-atomic="true">
  {statusMessage}
</Box>

// Progress bar provides visual feedback during loading.
// Not announced via aria-live — user can discover via navigation.
{isLoading && (
  <Box sx={sxClasses.progressBar}>
    <ProgressBar aria-label={t('geolocator.loadingResults')} />
  </Box>
)}

aria-pressed

See the Legend panel for an implementation example.

The aria-pressed attribute is only relevant for toggle buttons. Use aria-pressed to communicate toggle state rather than changing the button label dynamically.

GeoView uses stable labels with aria-pressed for button toggles. A changing action label without aria-pressed is a different valid pattern, but is not the convention used here. Checkbox controls use checked state instead. See the WAI-ARIA button pattern.

// Bad — label changes on toggle, causing verbose and repetitive announcements
<IconButton
  aria-label={isPressed ? 'Hide Layer Name' : 'Show Layer Name'}
/>

// Good — label stays stable, aria-pressed communicates state change
<IconButton
  aria-label="Toggle visibility, Layer Name"
  aria-pressed={isPressed}
/>

Note: The comma in “Toggle visibility, Layer Name” creates a natural spoken pause between the action and the target, improving clarity for screen reader users.


005. Quick Testing Tips and Notes

These are quick checks for human testers. The accessibility release tests cover keyboard navigation, focus traps, screen-reader behavior, and visual indicators.

SC 1.4.4 - Resize text

SC 1.4.5 - Images of Text

SC 1.4.10 - Reflow

SC 1.4.12 - Text Spacing


006. Constraints and Limitations

GeoView is built to meet WCAG 2.1 Level AA. Some requirements are limited by the nature of map content, missing descriptions from external data providers, or browser behavior. This section explains those limitations, their impact, and how the relevant WCAG criteria have been interpreted. Where a limitation remains an accessibility gap rather than a permitted exception, it is identified below.

Fullscreen API and ESC Key Priority

When the Fullscreen API is active, browsers commonly use Esc to exit fullscreen before the application receives the key. GeoView cannot override the browser’s exit mechanism.

Implication for UI design: If browser fullscreen and a panel are both open, users may need two Esc presses: one to exit browser fullscreen and another to close the panel. The exact behavior depends on the browser and operating system.

Note: This constraint applies only to browser fullscreen via the Fullscreen API, not panel “fullscreen views”.

SC 1.1.1 Non-text Content

Providing meaningful alternative text is not always possible for all image types within GeoView:

Empty alt text does not describe informative imagery. Where information is unavailable in text, this remains an accessibility limitation rather than an H67 exception.

Examples:

References:

SC 1.3.1 Info and Relationships

Symbology Images

This limitation is related to the missing symbol descriptions discussed under SC 1.1.1 above. Where a legend relies on colour, size, or shape to connect a symbol to a map feature, users of assistive technology may not have an equivalent text description of that relationship.

Impact for users: Users can show and hide map elements by name, but may be unable to independently interpret the associated symbols. Named controls preserve access to visibility actions; they do not resolve the missing descriptions.

Examples:

Select Components

Non-native MUI Select controls use ARIA labelling rather than a native <label for> association. GeoView’s wrapper connects a visible label through labelId, or accepts an aria-label when no visible label is provided.

If an automated validator reports an orphaned label, check the rendered control’s accessible name and label references. A warning can be documented as a false positive when the association works correctly, including with a screen reader; do not suppress it solely because the control uses MUI.

References:

SC 3.1.2 Language of Parts

Some content within the GeoView app may not include a lang attribute to identify passages in a language other than the page default. This is a known limitation with the following rationale:

Examples:

References:


007. Tools and Resources

Resources

Tools and Browser Extensions:


008. Code Review Checklist — Common Accessibility Patterns

This section helps branch reviewers spot common accessibility issues. Some checks are supported by TypeScript or ESLint; others require human judgment and runtime testing. See the related best practices.

Purpose: Assist developers during branch review by flagging patterns that commonly lead to accessibility issues. These are not merge blockers — they help catch mistakes early, but WIP and incremental improvements are welcome.

The explanations and examples are maintained once in Best Practices and ARIA Best Practices. Use this checklist as a review aid:

Check What to verify Guidance
Icon buttons The required label is non-empty, translated, and meaningful Use IconButton
Semantic controls Actions use buttons and navigation uses links; no hand-rolled buttons Semantic controls
Toggle state Button toggles have stable labels and aria-pressed; checkboxes use checked state aria-pressed
Disabled states Temporary unavailability preserves focus where appropriate; activation handlers return early Disabled states
Removed controls Focus moves to a logical target when an element is removed Removed controls
Escape dismissal Built-in or custom handlers close the intended content and restore focus Escape dismissal
Image alternatives Informative images have descriptions; decorative images use empty alt text Non-text content
Tooltips Essential information is available to keyboard users Keyboard-accessible tooltips
Live announcements Necessary results are announced without duplicating focus or control feedback aria-live
IDs IDs and label references remain unique across loops and viewer instances Unique element IDs
Contextual names Labels distinguish the action and its target aria-label
Atomic messages Announcements include enough context; account for the implicit behavior of role="status" aria-live
Touch targets Measure the clickable area against the 44×44 CSS pixel design goal WCAG checks

For source-search patterns and enforcement details, see the accessibility review procedure.