@aiaiai-pt/design-system 0.31.0 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,7 +15,7 @@
15
15
  <script>
16
16
  import { fromLonLat } from 'ol/proj.js';
17
17
  import { boundingExtent } from 'ol/extent.js';
18
- import { createTileLayer, createMapStyles, createOverlayLayers, watchTheme, renderMapError } from './map-utils.js';
18
+ import { createTileLayer, createMapStyles, createOverlayLayers, clusterTooltipLabel, watchTheme, renderMapError } from './map-utils.js';
19
19
 
20
20
  let {
21
21
  /** @type {{ id: string, lon: number, lat: number, label?: string, [key: string]: any }[]} */
@@ -286,21 +286,21 @@
286
286
  return;
287
287
  }
288
288
 
289
- const clustered = feature.get('features');
290
289
  if (container) container.style.cursor = 'pointer';
291
290
 
292
- if (clustered?.length === 1) {
293
- const data = clustered[0].get('markerData');
294
- if (data?.label) {
295
- tooltipEl.textContent = data.label;
296
- tooltipEl.style.display = 'block';
297
- tooltipOverlay.setPosition(feature.getGeometry()?.getCoordinates());
298
- }
299
- } else {
300
- tooltipEl.textContent = `${clustered?.length ?? 0} items`;
301
- tooltipEl.style.display = 'block';
302
- tooltipOverlay.setPosition(feature.getGeometry()?.getCoordinates());
291
+ const label = clusterTooltipLabel(feature);
292
+ if (label == null) {
293
+ tooltipEl.style.display = 'none';
294
+ return;
303
295
  }
296
+ tooltipEl.textContent = label;
297
+ tooltipEl.style.display = 'block';
298
+ // Cluster features carry a Point geometry; raw overlay (file/WFS)
299
+ // features can be polygons/lines, so anchor those at the pointer.
300
+ const coords = feature.get('features') === undefined
301
+ ? evt.coordinate
302
+ : feature.getGeometry()?.getCoordinates();
303
+ tooltipOverlay.setPosition(coords);
304
304
  });
305
305
 
306
306
  // `popup` opens the anchored ficha-resumo overlay; otherwise `onclick`
@@ -61,27 +61,32 @@
61
61
  data-testid="text-size-adjuster"
62
62
  {...rest}
63
63
  >
64
+ <!--
65
+ WCAG 2.5.3 (Label in Name): the accessible name MUST contain the visible
66
+ text. The visible label is the glyph ("A&minus;" / "A" / "A+"), so the
67
+ glyph is part of the name (NOT aria-hidden, NOT replaced by aria-label) and
68
+ the descriptive i18n label rides along as visually-hidden text. Result, e.g.
69
+ name = "A&minus; Decrease text size", whose visible substring "A&minus;"
70
+ satisfies the rule while screen readers still hear the full description.
71
+ -->
64
72
  <button
65
73
  type="button"
66
74
  class="ts-btn ts-decrease"
67
- aria-label={decreaseLabel}
68
75
  disabled={atMin}
69
76
  onclick={() => emit(decreaseTextSize(current))}
70
- >A<span aria-hidden="true">&minus;</span></button>
77
+ >A&minus;<span class="sr-only"> {decreaseLabel}</span></button>
71
78
  <button
72
79
  type="button"
73
80
  class="ts-btn ts-reset"
74
- aria-label={resetLabel}
75
81
  disabled={atDefault}
76
82
  onclick={() => emit(DEFAULT_TEXT_SIZE)}
77
- >A</button>
83
+ >A<span class="sr-only"> {resetLabel}</span></button>
78
84
  <button
79
85
  type="button"
80
86
  class="ts-btn ts-increase"
81
- aria-label={increaseLabel}
82
87
  disabled={atMax}
83
88
  onclick={() => emit(increaseTextSize(current))}
84
- >A<span aria-hidden="true">+</span></button>
89
+ >A+<span class="sr-only"> {increaseLabel}</span></button>
85
90
  <span class="sr-only" aria-live="polite" data-testid="text-size-readout"
86
91
  >{current}%</span
87
92
  >
@@ -10,10 +10,6 @@
10
10
  @example Disabled
11
11
  <Toggle label="Notifications" checked disabled />
12
12
  -->
13
- <script module>
14
- let _toggleUid = 0;
15
- </script>
16
-
17
13
  <script>
18
14
  let {
19
15
  /** @type {boolean} */
@@ -31,9 +27,11 @@
31
27
  ...rest
32
28
  } = $props();
33
29
 
34
- const fallbackId = `toggle-${_toggleUid++}`;
35
- const toggleId = $derived(id ?? fallbackId);
36
-
30
+ // No auto-generated id: the switch self-names via aria-label, so it needs no
31
+ // id/labelledby plumbing. A module-counter fallback id (`toggle-N`) diverged
32
+ // between SSR and client (server counter ≠ fresh client counter), an id that
33
+ // could surface as a duplicate/parse error; an id is now emitted ONLY when a
34
+ // consumer passes one.
37
35
  function handleClick() {
38
36
  if (!disabled) {
39
37
  checked = !checked;
@@ -50,7 +48,7 @@
50
48
  override (e.g. type="submit" if they really want submit semantics).
51
49
  -->
52
50
  <button
53
- id={toggleId}
51
+ {id}
54
52
  class="toggle"
55
53
  class:toggle-on={checked}
56
54
  class:toggle-disabled={disabled}
@@ -58,15 +56,23 @@
58
56
  type="button"
59
57
  role="switch"
60
58
  aria-checked={checked}
61
- aria-labelledby={label ? `${toggleId}-label` : undefined}
59
+ aria-label={label}
62
60
  {...rest}
63
61
  onclick={handleClick}
64
62
  >
65
63
  <span class="toggle-knob"></span>
66
64
  </button>
67
65
  {#if label}
66
+ <!--
67
+ The switch now names itself directly via aria-label (was aria-labelledby →
68
+ this span). Stricter ACT accessible-name engines (QualWeb/AccessMonitor)
69
+ did not credit a name referenced through a role="none" target and read the
70
+ switch as nameless; a direct aria-label is unambiguous. This span is the
71
+ VISIBLE label only — role="none" keeps it out of the a11y tree so the name
72
+ isn't announced twice. Click still toggles (sighted affordance); the button
73
+ stays the keyboard/AT target.
74
+ -->
68
75
  <span
69
- id="{toggleId}-label"
70
76
  class="toggle-label"
71
77
  class:toggle-label-disabled={disabled}
72
78
  onclick={handleClick}
@@ -374,7 +374,8 @@ export function getHeatmapGradient(el) {
374
374
  */
375
375
  export function geoStylerToFlat(style) {
376
376
  if (!style || typeof style !== "object") return {};
377
- if (!Array.isArray(style.rules)) return /** @type {OverlayLayerStyle} */ (style);
377
+ if (!Array.isArray(style.rules))
378
+ return /** @type {OverlayLayerStyle} */ (style);
378
379
  const sym = style.rules?.[0]?.symbolizers?.[0];
379
380
  if (!sym || typeof sym !== "object") return {};
380
381
  /** @type {OverlayLayerStyle} */
@@ -389,8 +390,10 @@ export function geoStylerToFlat(style) {
389
390
  if (typeof sym.width === "number") flat.strokeWidth = sym.width;
390
391
  } else if (sym.kind === "Fill") {
391
392
  if (typeof sym.color === "string") flat.fillColor = sym.color;
392
- if (typeof sym.outlineColor === "string") flat.strokeColor = sym.outlineColor;
393
- if (typeof sym.outlineWidth === "number") flat.strokeWidth = sym.outlineWidth;
393
+ if (typeof sym.outlineColor === "string")
394
+ flat.strokeColor = sym.outlineColor;
395
+ if (typeof sym.outlineWidth === "number")
396
+ flat.strokeWidth = sym.outlineWidth;
394
397
  }
395
398
  return flat;
396
399
  }
@@ -449,7 +452,9 @@ export async function createOverlayLayers(defs, styles) {
449
452
  const point = new Style({
450
453
  image: new CircleStyle({
451
454
  radius: flat.pointRadius ?? 6,
452
- fill: new Fill({ color: flat.pointColor ?? flat.strokeColor ?? "#3366cc" }),
455
+ fill: new Fill({
456
+ color: flat.pointColor ?? flat.strokeColor ?? "#3366cc",
457
+ }),
453
458
  stroke,
454
459
  }),
455
460
  });
@@ -484,6 +489,39 @@ export async function createOverlayLayers(defs, styles) {
484
489
  });
485
490
  }
486
491
 
492
+ // ─── Hover tooltip label ─────────────────────────────────────────
493
+
494
+ /**
495
+ * Decide the hover-tooltip text for a feature under the pointer.
496
+ *
497
+ * Markers passed via the `markers` prop go through an OL Cluster source, so
498
+ * each rendered feature carries a `features` collection: one entry → that
499
+ * marker's label; many → "N items". Features served from an overlay `layers`
500
+ * def (a file/WFS source) are added to a plain VectorSource and have NO
501
+ * `features` collection (`undefined`) — for those, fall back to the feature's
502
+ * own descriptive props rather than rendering the misleading "0 items".
503
+ *
504
+ * @param {{ get: (key: string) => any }} feature — an OL Feature (or any
505
+ * object exposing `.get(key)`).
506
+ * @returns {string | null} the label to show, or null to hide the tooltip.
507
+ */
508
+ export function clusterTooltipLabel(feature) {
509
+ const clustered = feature.get("features");
510
+ if (clustered === undefined) {
511
+ const label =
512
+ feature.get("name") ??
513
+ feature.get("title") ??
514
+ feature.get("label") ??
515
+ feature.get("markerData")?.label;
516
+ return label != null && String(label) !== "" ? String(label) : null;
517
+ }
518
+ if (clustered.length === 1) {
519
+ const data = clustered[0].get("markerData");
520
+ return data?.label ?? null;
521
+ }
522
+ return `${clustered.length} items`;
523
+ }
524
+
487
525
  // ─── Boundary (#187 D-e prep) ────────────────────────────────────
488
526
 
489
527
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiaiai-pt/design-system",
3
- "version": "0.31.0",
3
+ "version": "0.32.0",
4
4
  "description": "Design system tokens and Svelte components for aiaiai products",
5
5
  "license": "MIT",
6
6
  "type": "module",