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
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions src/iotMapManager/css/map.css
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,11 @@
color: black;
}

.leaflet-control-zoom a:focus-visible {
outline: 3px solid black;
outline-offset: 3px;
box-shadow: 0 0 0 6px white;
position: relative;
z-index: 1;
}

13 changes: 12 additions & 1 deletion src/iotMapManager/css/markers.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@

/* markers.css */

.iotmap-divicon:focus-visible {
outline: 3px solid black;
outline-offset: 3px;
box-shadow: 0 0 0 6px white;
}

.iotmap-divicon:focus-within {
z-index: 10000 !important;
}

.iotmap-markericon{
position: relative;
width: 0px;
Expand Down Expand Up @@ -169,7 +179,8 @@
fill-opacity: 0.5;
}

.iotmap-clustericon:hover > .iotmap-clusteropacity{
.iotmap-clustericon:hover > .iotmap-clusteropacity,
.iotmap-divicon:focus-within > .iotmap-clustericon > .iotmap-clusteropacity {
fill-opacity: 1;
}

Expand Down
32 changes: 24 additions & 8 deletions src/iotMapManager/css/popup.css
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
height: 10px;
width: 10px;
font-size: 1px;
visibility: hidden;
}

.iotmap-marker-selected > .iotmap-marker-popup {
Expand Down Expand Up @@ -92,10 +93,13 @@
height: 10px;
width: 10px;
font-size: 1px;
visibility: hidden;
}


.iotmap-clustericon:hover > .iotmap-cluster-popup {
.iotmap-clustericon:hover > .iotmap-cluster-popup,
.iotmap-divicon:focus-within > .iotmap-clustericon > .iotmap-cluster-popup {
visibility: visible;
position: absolute;
bottom:-25px;
left:-75px;
Expand All @@ -116,7 +120,9 @@
z-index: 0;
}

.iotmap-clustericon:hover > .iotmap-cluster-big-popup {
.iotmap-clustericon:hover > .iotmap-cluster-big-popup,
.iotmap-divicon:focus-within > .iotmap-clustericon > .iotmap-cluster-big-popup {
visibility: visible;
position: absolute;
bottom:-25px;
left:-115px;
Expand All @@ -137,25 +143,35 @@
z-index: 0;
}

.iotmap-clustericon:hover > .iotmap-cluster-popup .iotmap-pop-up-title, .iotmap-cluster-big-popup .iotmap-pop-up-title {
.iotmap-clustericon:hover > .iotmap-cluster-popup .iotmap-pop-up-title,
.iotmap-divicon:focus-within > .iotmap-clustericon > .iotmap-cluster-popup .iotmap-pop-up-title,
.iotmap-cluster-big-popup .iotmap-pop-up-title {
font-size: 20px;
}

.iotmap-clustericon:hover > .iotmap-cluster-popup .iotmap-pop-up-title-icon, .iotmap-cluster-big-popup .iotmap-pop-up-title-icon {
.iotmap-clustericon:hover > .iotmap-cluster-popup .iotmap-pop-up-title-icon,
.iotmap-divicon:focus-within > .iotmap-clustericon > .iotmap-cluster-popup .iotmap-pop-up-title-icon,
.iotmap-cluster-big-popup .iotmap-pop-up-title-icon {
font-size: 20px;
margin-right: 15px;
}

.iotmap-clustericon:hover > .iotmap-cluster-popup .iotmap-pop-up-bullet, .iotmap-cluster-big-popup .iotmap-pop-up-bullet {
.iotmap-clustericon:hover > .iotmap-cluster-popup .iotmap-pop-up-bullet,
.iotmap-divicon:focus-within > .iotmap-clustericon > .iotmap-cluster-popup .iotmap-pop-up-bullet,
.iotmap-cluster-big-popup .iotmap-pop-up-bullet {
font-size: 12px;
color: transparent;
}

.iotmap-clustericon:hover > .iotmap-cluster-popup .iotmap-pop-up-body, .iotmap-cluster-big-popup .iotmap-pop-up-body {
.iotmap-clustericon:hover > .iotmap-cluster-popup .iotmap-pop-up-body,
.iotmap-divicon:focus-within > .iotmap-clustericon > .iotmap-cluster-popup .iotmap-pop-up-body,
.iotmap-cluster-big-popup .iotmap-pop-up-body {
font-size: 12px;
}



.iotmap-divicon.iotmap-popup-dismissed > .iotmap-clustericon > .iotmap-cluster-popup,
.iotmap-divicon.iotmap-popup-dismissed > .iotmap-clustericon > .iotmap-cluster-big-popup {
visibility: hidden;
}


6 changes: 5 additions & 1 deletion src/iotMapManager/css/tabs.css
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,11 @@
.iotmap-clustericon:hover > .iotmap-tab-top,
.iotmap-clustericon:hover > .iotmap-tab-top-big,
.iotmap-clustericon:hover > .iotmap-tab-top-big-left,
.iotmap-clustericon:hover > .iotmap-tab-top-big-right {
.iotmap-clustericon:hover > .iotmap-tab-top-big-right,
.iotmap-divicon:focus-within > .iotmap-clustericon > .iotmap-tab-top,
.iotmap-divicon:focus-within > .iotmap-clustericon > .iotmap-tab-top-big,
.iotmap-divicon:focus-within > .iotmap-clustericon > .iotmap-tab-top-big-left,
.iotmap-divicon:focus-within > .iotmap-clustericon > .iotmap-tab-top-big-right {
visibility: hidden;
font-size: 1px;
}
4 changes: 2 additions & 2 deletions src/iotMapManager/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/iotMapManager/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "iotmapmanager",
"version": "2.6.10-alpha.1",
"version": "2.7.0-alpha.1",
"description": "Manage markers, clusters, user marker or paths on IotMaps",
"main": "dist/index.js",
"types": "lib/index.d.ts",
Expand Down
65 changes: 63 additions & 2 deletions src/iotMapManager/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,69 @@ In your CSS file:

If you use TypeScript, all types are defined in lib/index.d.ts

## Keyboard accessibility

Keyboard interaction is enabled by default:

- **Tab / Shift+Tab**: move between visible markers, clusters and controls. Focus has a visible outline.
- **Enter / Space** on the **+ / - zoom buttons**: zoom in or out while keeping focus on the button. Holding Shift preserves Leaflet's accelerated zoom. Disabled controls and zoom limits remain respected.
- **Enter / Space** on a marker: toggle its information popup, using the same selection and `onEltClick` behavior as a mouse click.
- **Focus a cluster**: show its information popup without zooming. The popup stays visible while navigating its links.
- **Enter / Space** on a manual or automatic cluster: perform the usual click action (zoom or spiderfy). Focus returns to the map so navigation can continue when the cluster disappears.
- **Escape**: close the focused marker or cluster popup and return focus to its icon. A dismissed cluster popup reappears after leaving and refocusing the cluster.

Links and controls in visible popups retain their native keyboard and mouse actions without activating the enclosing marker or cluster. Hidden popups are excluded from keyboard navigation. Mouse selection, cluster hover and cluster expansion remain available.

Markers with a popup and clusters expose `aria-expanded` on their icon container. It follows marker selection and cluster popup visibility (hover, focus and Escape dismissal), including icon regeneration. Markers without a popup do not receive this attribute. Opening a popup does not move focus into its content: use Tab to reach its links and controls.

Include `iot-map-manager.css` for popup visibility and focus styling. These interactions do not make the entire map accessible; applications remain responsible for meaningful content and an accessible alternative representation.

To validate these interactions manually, run `npm run storybook` from the repository root and open a marker story and the **Clusters** story. Use Tab / Shift+Tab to check the focus indicator, Enter / Space to activate markers and clusters, and Escape to dismiss their popups. Check that links in visible popups remain usable, hidden popup content is skipped, and mouse clicks and hover still work.
Also check the + / - zoom buttons with Tab, Enter and Space, including Shift activation and the minimum/maximum zoom limits.
The **AccessibleNames** story includes a marker popup with a link and a cluster popup with an aggregation link. Use it to inspect `aria-expanded`, navigate into the popup and dismiss it with Escape.

## Accessible names

Every element can be given an optional accessible name announced by assistive technologies. Nothing changes in the generated HTML when it is not provided.

| Element | Property |
| --- | --- |
| Marker | `ariaLabel` |
| Manual cluster | `ariaLabel` |
| User marker | `ariaLabel` |
| Area | `ariaLabel` |
| Path | `ariaLabel` (main line), `additional[].ariaLabel` (additional lines), `iconsAriaLabel.start` / `.end` / `.position` (icons) |

```typescript
const marker: IotMarker = {
id: 'sensor-1',
location: { lat: 44.88, lng: 4.88 },
ariaLabel: 'Sensor 1, temperature alert'
}
```

Automatic clusters have no user data: provide a function in the configuration. It receives the cluster (`childCount`, `layer`, `aggregation`...) and is also used for manual clusters without their own `ariaLabel`.

```typescript
const conf = new IotMapConfig()
conf.setConfig({
accessibility: {
clusterAriaLabel: cluster => `Group of ${cluster.childCount} elements`
}
})
```

Labels are set as DOM attributes, not HTML, and are refreshed whenever an icon is regenerated. Markers, clusters and icons keep their focusable button role; paths and areas get `role="img"`. Set the configuration function before adding elements: existing icons are not refreshed.

Use `updateMarker`, `updateCluster` or `updateUserMarker` to change an existing element's `ariaLabel`; changing only this property also redraws the icon. The same behavior applies to `updateAllMarkers` and `updateAllClusters`.

- Omit `ariaLabel` (or pass `undefined`) to keep the current name.
- Pass `ariaLabel: ''` to remove the name. For manual clusters, this also suppresses the configured `clusterAriaLabel` fallback.

```typescript
markerManager.updateMarker('sensor-1', { ariaLabel: 'Sensor 1, normal temperature' })
markerManager.updateMarker('sensor-1', { ariaLabel: '' })
```
## marker definition
A marker is defined as follows:

Expand Down Expand Up @@ -397,5 +460,3 @@ IotMapPathManager requires a configuration object and an IotMapManager

### IotMapAreaManager
This class manages areas:


3 changes: 3 additions & 0 deletions src/iotMapManager/src/iot-map-area.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { IotMapConfig } from './iot-map-config'
import { IotArea } from './iot-map-types'
import { IotMapManager } from './iot-map-manager'
import { Polygon } from 'leaflet'
import { applyAriaLabel } from './iot-map-icons'

/**
* Class IotMapArea displaying a Area
Expand Down Expand Up @@ -43,5 +44,7 @@ export class IotMapArea extends Polygon {
this.data = area
this.config = config
this.map = map
// The SVG element only exists once the area is added to the map.
this.on('add', () => applyAriaLabel(this.getElement(), this.data.ariaLabel, 'img'))
}
}
5 changes: 5 additions & 0 deletions src/iotMapManager/src/iot-map-cluster-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@ export class IotMapClusterManager {
let htmlModificationNeeded = false
let oldLayerName: string = null

if (params.ariaLabel !== undefined) {
currentClusterInfos.ariaLabel = params.ariaLabel
htmlModificationNeeded = true
}

// location modified
if (params.location !== undefined) {
currentClusterInfos.location = params.location
Expand Down
17 changes: 16 additions & 1 deletion src/iotMapManager/src/iot-map-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,17 @@
* Software description: web library for interactive maps providing Orange branded reusable markers, tabs, clusters and paths
*/

import { MarkerStatus, MarkerTemplate, ShapeType, LayerTemplate, TabType } from './iot-map-types'
import { IotCluster, MarkerStatus, MarkerTemplate, ShapeType, LayerTemplate, TabType } from './iot-map-types'

export class IotMapConfig {
/*
*** Accessibility: optional, nothing is added to the generated HTML when undefined
*/
accessibility: {
// Accessible name of a cluster without its own ariaLabel (mainly automatic clusters)
clusterAriaLabel?: (cluster: IotCluster) => string
} = {}

/*
*** Default configuration definition :
*/
Expand Down Expand Up @@ -361,6 +369,13 @@ export class IotMapConfig {
*
*/
public setConfig (newConfig: Partial<IotMapConfig>): void {
/*
*** ACCESSIBILITY ***
*/
if (newConfig.accessibility?.clusterAriaLabel !== undefined) {
this.accessibility.clusterAriaLabel = newConfig.accessibility.clusterAriaLabel
}

/*
*** MAP ***
*/
Expand Down
Loading