Skip to content

WCAG accessibility support (a11y phase 1 + 2) - #23

Merged
nexus merged 68 commits into
mainfrom
WCAG
Jun 30, 2026
Merged

WCAG accessibility support (a11y phase 1 + 2)#23
nexus merged 68 commits into
mainfrom
WCAG

Conversation

@McSpace

@McSpace McSpace commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds WCAG/accessibility support to the seatmap (opt-in via wcag config). Unit 598/598, e2e 123/123 (against the WCAG demo).

Highlights

  • Grid semantics + roving tabindex; arrow-key seat navigation (skips aisles/empties); arrow keys remap to on-screen direction in horizontal mode (P1a).
  • Tooltip-as-dialog keyboard ownership (focus trapped, Escape closes); visible focus rings.
  • Accessible list view (row numbers, conditional columns, position dropdown, merged status+price action button).
  • Skip-link handling; landmark/labelling fixes; colour-contrast pass.
  • Includes the horizontal React-parity layout work as its base.

Test plan

  • Unit: npx ng test seatmap-lib --watch=false → 598/598.
  • e2e (a11y + axe + full suite) against the WCAG demo → 123/123.

⚠️ Merge order / reconciliation

🤖 Generated with Claude Code

Andrey Vilchinskiy and others added 30 commits June 4, 2026 18:09
Plan covers gap analysis, target architecture, 17-commit roadmap, progress
tracking, decisions log and verification protocol. Lives in the repo so any
agent/session working on the WCAG branch can pick up where the previous one
left off. Source of truth: docs/wcag/PLAN.md.
LiveAnnouncer from @angular/cdk/a11y will back the screen-reader
announcements added in later WCAG commits. Added as peerDependency of
the library (host provides it) and as devDependency of the root
workspace so the demo and tests build locally. Bundle cost ~30 KB, but
the trade is a battle-tested live-region implementation that the ACR
can point at.

Commit 1 of the docs/wcag/PLAN.md roadmap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Commit 1 (chore(a11y): add @angular/cdk@^21.2.0 peer dep) shipped as
7d0b368. Status section now points at commit 2 (hide decorative
graphics from AT).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Marks purely decorative graphics aria-hidden so screen readers stop
reading the visual chrome and inert duplicates of seat metadata. The
chair SVG, passenger badge, price pill and unavailable cross inside
each seat hide here because commit 5 will move all that information
into the seat button's aria-label as the single source of truth.

Decorative components hidden at the root / via host:
- jets-nose, jets-tail, jets-wing, jets-deck-separator (single-div
  templates)
- jets-deck-exit, jets-bulk (host attribute on the @for root)

Tooltip iconography:
- close-btn inner <svg> (button keeps aria-label="Close")
- amenity-icon span (text sibling already conveys the meaning)
- dim-icon div (label + value siblings already convey the meaning)

Per-seat decorative blocks:
- .jets-seat__svg (chair illustration)
- .jets-seat__cross (visual indicator of unavailable)
- .jets-seat__passenger (initials badge)
- .jets-seat__price (price pill)

Deck-selector intentionally skipped: it is the interactive button
itself, hiding its SVG would orphan the control without an accessible
name. Commit 12 reworks deck-selector with role=switch / role=tablist
plus an explicit aria-label.

Trade-off: between this commit and commit 5 (seat as button with
aria-label), AT users see only the seat number text. Current AT
experience is uninformative anyway, so muting the visual chrome is
a strict improvement rather than a regression.

ng build seatmap-lib: green.
ng test seatmap-lib: 296/296 pass.

Commit 2 of the docs/wcag/PLAN.md roadmap.
Plus: noted that `vitest run` directly fails on baseline because the
init-testbed setup is injected by `ng test` only. Use `npm test` /
`ng test seatmap-lib` going forward; running raw vitest gives
misleading TestBed errors that are not regressions.
Add Active claims section to PLAN.md so parallel agents/sessions
can see who is working on what. Reserving commit 3
(accessible-name builder + locale keys) for this session in the
main repo on branch WCAG. Other agent already shipped commit 2
from .claude/worktrees/wcag-commit-2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add the pure utility module that the seat-button (commit 5) and the
LiveAnnouncer (commit 9) will use to compose accessible names for
seats:

- computeSeatPosition(seat, row) returns window | aisle | middle | null
  by looking at seat-type cells and aisle neighbours in the row.
- buildSeatAriaLabel(seat, position, locale) returns a comma-separated
  string like "14C, aisle, extra legroom, available, €12". Selected
  seats include the attached passenger label; restricted seats fall
  back to a passenger-type sentence; unavailable seats stop at the
  status word. Missing locale entries fall back to the key.

A11Y_LOCALE_KEYS centralises the locale-key names so the table can be
extended once and consumed in three places without typos.

Extend LOCALES_MAP across all 18 supported languages (EN/RU/CN/DE/FR/
ES/IT/PT/PT-BR/AR/JA/KO/TR/NL/PL/CS/UK/VI) with the keys the builder
reads and the list-view (commit 13) headers — seatPosition{Window,
Aisle,Middle}, seatExtraLegroom, seatExitRow, seat{Available,
Unavailable,Selected,SelectedFor,RestrictedFor}, close, moveToSeat,
gridLabel, allSeats and the table column headers row/seat/cabin/
position/features/price/status/action.

Unit tests cover position detection edge cases (window, aisle
neighbour, single-seat row, non-seat cell), every status branch of
the label builder, the {type} placeholder template, and the
missing-key fallback.

Commit 3 of the docs/wcag/PLAN.md roadmap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Commit 3 (feat(a11y): accessible-name builder + locale keys) shipped
as 7a70627. Release the active claim, point Status at commit 4 with
the visual-breaking-change reminder.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
_jumpToSeat now passes behavior: 'auto' to scrollIntoView when the user
has requested reduced motion (window.matchMedia, SSR-guarded), and the
seat :hover brightness filter is wrapped in
@media (prefers-reduced-motion: no-preference) so it only triggers for
users who have not opted out of motion.

Commit 14 of the docs/wcag/PLAN.md roadmap.
Add @media (forced-colors: active) blocks to the components whose
inline SVG fills/strokes would otherwise be neutralised by the browser
in Windows High Contrast mode, leaving invisible or ambiguous UI:

- jets-seat.component.scss — CanvasText outline on .jets-seat--seat /
  .jets-seat--unavailable so the seat boundary survives once the
  parameterised SVG fill is stripped; restate color: CanvasText on
  .jets-seat__cross (already currentColor) and force Highlight /
  HighlightText on the passenger badge so the selected indicator
  stays perceivable.
- jets-deck-exit.component.ts — set .jets-exit__icon { color:
  CanvasText } so the inline arrow SVG (fill="currentColor") remains
  visible.
- jets-deck-selector.component.ts — render the toggle as a
  ButtonFace / ButtonText box (with forced-color-adjust: none and
  border) so the clickable area stays operable until commit 12 gives
  it real button semantics; force SVG path fill to ButtonText.
- jets-deck-separator.component.ts — force background: CanvasText so
  the inter-deck divider doesn't disappear when the themed background
  is overridden.

Audited but deliberately left untouched:
- seat-template.service.ts seat-shape SVG templates — parameterised
  by consumer theme; the browser normalises fills under forced-colors
  anyway, the new .jets-seat outline above guarantees the seat
  boundary stays visible.
- nose-template.service.ts, jets-tail, jets-bulk, jets-wing — all
  aria-hidden decorative chrome; relying on the browser's default
  forced-colors handling is acceptable for non-interactive graphics.
- jets-seat .jets-seat__cross inline SVG — already uses
  stroke="currentColor", no template change needed.
- jets-deck-exit arrow SVGs — already fill="currentColor".

Commit 15 of the docs/wcag/PLAN.md roadmap.
Wave A complete. Status section now records orchestration model
(Claude as conductor of parallel sub-agents in per-commit worktrees;
sub-agents commit code only, orchestrator updates PLAN.md).

Commit 4 (default colors) approved by user — visual breaking with
snapshot regeneration allowed. Wave B starts: commits 4, 9, 10 in
parallel.
Recompute DEFAULT_COLOR_THEME so the out-of-the-box look meets WCAG
2.1/2.2 AA without forcing consumers to supply a custom colorTheme.
Verified contrast pairs (computed against the standard relative-luminance
formula, opaque hex only):

  Text on seat fill (SC 1.4.3, ≥ 4.5:1):
    seatLabelColor #1a1a1a (was #ffffff) on:
      seatAvailableColor      #A5D6A7 (was #4CAF50)  10.59:1 (was 2.78:1)
      seatSelectedColor       #90CAF9 (was #2196F3)   9.95:1 (was 3.12:1)
      seatPreferredColor      #FFCC80 (was #FF9800)  11.77:1 (was 2.16:1)
      seatExtraColor          #CE93D8 (was #9C27B0)   7.28:1 (was 6.30:1)
      notAvailableSeatsColor  #BDBDBD (was dimgrey)   9.26:1 (was 5.49:1)
      seatUnavailableColor    #BDBDBD (was #b0bec5)   9.26:1 (was 9.13:1)

  UI boundary (SC 1.4.11, ≥ 3:1):
    seatStrokeColor #757575 (was rgb(237,237,237)) on
      seatMapBackgroundColor #ffffff               4.61:1 (was 1.17:1)

  Passenger badge text (SC 1.4.3, ≥ 4.5:1):
    defaultPassengerBadgeLabelColor #ffffff on
      defaultPassengerBadgeColor #1565C0 (was #2196F3)  6.73:1 (was 3.12:1)
    defaultPassengerBadgeBorderColor #0D47A1 (was #1976D2) — boundary

  Incidental text on map background:
    cabinTitlesLabelColor #0277BD (was #00BFFF) on
      seatMapBackgroundColor #ffffff               4.80:1 (was 2.12:1)

  Tooltip text + buttons (already AA-compliant, kept as-is and documented):
    tooltipFontColor #333333 on tooltipBackgroundColor #ffffff       12.63:1
    tooltipSelectButtonTextColor #ffffff on rgb(0,68,153)             9.18:1
    tooltipCancelButtonTextColor #333333 on tooltipCancelBg #f0f0f0  11.09:1

A comment block above DEFAULT_COLOR_THEME enumerates every verified
pair so future maintainers can spot a contrast regression at review
time.

Disabled seat state: dropped the `opacity: 0.6` hack from
.jets-seat--unavailable's CSS (jets-seat.component.scss). Opacity made
the effective label/boundary contrast dependent on whatever sits behind
the seatmap, which is uncertifiable on a dynamic host background. The
discrete look now comes from the theme — disabled seats already resolve
to `notAvailableSeatsColor` via seat-template.service, which clears
9.26:1 against the dark seat label and 3:1 against the white map
background. The semantic distinction between unavailable and disabled
is carried by `aria-disabled` (added in commit 5).

Removed the default `text-shadow` on .jets-seat__number (it was tuned
for white-on-coloured text and only blurs dark-on-light text); custom
themes can still opt into a shadow via the existing
`colorTheme.seatLabelTextShadow` input.

Visual breaking change for consumers who rely on the default theme.
Consumers who pass their own `colorTheme` to <sm-jets-seat-map> are
unaffected — `IColorTheme`'s public shape did not change, only the
defaults. User approval to regenerate Playwright golden snapshots was
explicitly given on 2026-06-04 (PLAN.md Decisions log).

Regenerated 99 Playwright snapshots across 19 e2e folders so the
existing visual regression suite reflects the new defaults.

Commit 4 of the docs/wcag/PLAN.md roadmap.
Inject CDK LiveAnnouncer into JetsSeatMapComponent and announce the
following status messages on a polite live region (never assertive —
the seat-map is not an emergency UI):

  - onTooltipSelect:   "Seat {n} selected for {passengerLabel}, {ccy}{price}"
                       Falls back to passenger.abbr, then literal "passenger"
                       when no human-readable label is available.
  - onTooltipUnselect: "Seat {n} cleared"
  - _jumpToSeat:       "Move to seat {n}" once the seat element is located,
                       or "Seat {label} not found" if the label is unknown.

Strings are pulled from LOCALES_MAP via a private _a11yLocale() helper
with an English fallback so a missing key in any of the 18 locales
never silently swallows the announcement. announce calls go through
a try/catch wrapper so a torn-down live-region (stray timers after
component teardown, host-page DOM swaps) does not surface as a hard
error.

Left a TODO(commit 10) note inside onTooltipSelect for the
restriction-reasoning hook — commit 10 owns the tooltip flow and
will expose the reason string for routing through this same
LiveAnnouncer once that hook exists.

Tests: new "LiveAnnouncer (a11y)" describe block in
jets-seat-map.component.spec.ts asserts each announcement fires
with the exact English string. The restriction announcement is
intentionally out of scope for this commit.

Commit 9 of the docs/wcag/PLAN.md roadmap.
getSelectDisabledReason() returns {disabled, reason, message};
visible reason text under disabled Select; aria-describedby ties them;
selectAttemptBlocked Output emits when user clicks a disabled-by-aria
Select button (gated manually so the click event still fires).
isSelectDisabled() preserved as boolean facade for existing templates
and overrides.

Commit 10 of the docs/wcag/PLAN.md roadmap.
Wave B integrated end-to-end via orchestration:
- commit 4 default colors (d49e100) — sub-agent + user-approved snapshot regen
- commit 9 LiveAnnouncer (d85a86a) — sub-agent + orchestrator-side push
- commit 10 restriction reasoning (eb4d17f) — sub-agent + orchestrator-side push

Status section now lists two operational blockers learned in Wave B:
sub-agent shared-branch push denial and the .env.local Playwright
auth requirement. Both captured in Claude memory for future waves.

Wave C will dispatch commits 5 (seat → button, DOM-breaking) and
12 (landmarks + skip-link + deck-selector semantics).
JetsSeatMapComponent now wraps the seat-map in a proper landmark for AT
users:

- The grid is wrapped in a <section role="region"> labelled by a
  visually-hidden <h2> ("Seat map — <cabin>") so screen readers
  can announce/jump to the seat map as a named landmark.
- A focusable-on-Tab skip link is rendered at the top of the region
  and jumps focus past the entire grid to a tabindex=-1 anchor after
  the section. Visible-on-focus with a clear contrasting style.
- The main grid div (#mapContainer) gets role="tabpanel" + an id +
  aria-labelledby pointing at the active deck-tab id so the tablist
  pairing is wired correctly when there are 3+ decks.

JetsDeckSelectorComponent now exposes real semantics depending on N:

- N = 2: rendered as <button role="switch"> with aria-checked
  reflecting the active deck and an aria-label describing the
  switching action ("Switch to <other deck title>"). Click, Enter
  and Space all toggle and emit selectDeck.
- N ≥ 3: rendered as <div role="tablist"> containing one
  <button role="tab"> per deck. Roving tabindex (selected tab is
  0, others -1), arrow-left/right wrap around the list, Home/End
  jump to first/last; all paths emit selectDeck and move DOM focus
  to the new tab.
- Existing rotating SVG icon is preserved on both branches, so the
  visual identity is unchanged for sighted users.
- New aria-controls on each tab points at the deck-panel id rendered
  by the seat-map (via the new deckPanelIdBase Input).

After a deck switch, focus is moved to the first interactive seat
([data-seat-number]) in the newly active deck via a deferred
setTimeout(0). Commit 7 will refine this with the proper roving-
tabindex manager.

Locale keys missing from constants.ts (English fallbacks used):
- skipSeatmap            → 'Skip seat map'
- switchToDeck           → 'Switch to'
- deckSelector           → 'Deck selector' (tablist aria-label)
'gridLabel' (commit 3) is reused for the region heading base text.
TODO comments mark the missing keys for commit 17 docs.

Per-instance ARIA ids generated via a module-level counter so multiple
seat maps on the same page get distinct heading / skip-link / tabpanel
ids.

A few Playwright screenshots were regenerated by the run helper, which
overwrites the PNG on every test (it doesn't compare). No visual
regressions inspected.

Commit 12 of the docs/wcag/PLAN.md roadmap.
Make every interactive seat a real <button type="button"> instead of a
<div>, so screen readers identify it correctly and keyboard activation
(Enter/Space) works natively. Non-seat cells (aisle/empty/index) stay
as <div> because they are decorative.

New optional inputs on JetsSeatComponent:
  - ariaLabel:       → [attr.aria-label]; omitted when unset
  - ariaSelected:    → [attr.aria-selected]; tri-state, omitted when null
  - ariaDisabled:    → [attr.aria-disabled]="true"; suppresses click /
                       mouseenter / mouseleave emissions
  - rovingTabindex:  → [attr.tabindex]; defaults to 0 so a stand-alone
                       seat stays focusable; commit 7 manages roving
  - colIndex:        → [attr.aria-colindex] when provided
  - rowIndex:        → [attr.aria-rowindex] when provided

aria-disabled vs native disabled: deliberately NOT using the native
disabled attribute. native disabled removes focusability, which would
break the roving-tabindex grid pattern landing in commits 6/7. The
click handler is updated to early-return when ariaDisabled is true, so
the semantic gate moves from "no event because the browser blocks it"
to "no emit because we choose to ignore it".

JetsRowComponent gains matching @input pass-throughs (ariaLabel,
ariaSelected, ariaDisabled, rovingTabindex, colIndex, rowIndex) for
both the native <sm-jets-seat> branch and the *ngComponentOutlet
override branch. Parent map (jets-seat-map) does not supply real
values yet — that lands in commit 6.

SCSS:
  - Narrow native-button reset on .jets-seat (appearance:none,
    background:transparent, border:0, padding:0, font:inherit,
    color:inherit, text-align:inherit, -webkit-tap-highlight-color:
    transparent). Chose narrow reset over `all: unset` so font and
    colour still inherit and the rendered output stays visually
    identical to the previous <div> render.
  - New .jets-seat:focus-visible rule (2px currentColor outline,
    offset 2px). currentColor pairs with the AA-contrast seat label
    introduced in commit 4.
  - Forced-colors block extended with .jets-seat:focus-visible using
    Highlight outline so Windows High Contrast users still see focus.

Preserved (regression guards covered by new specs):
  - .jets-seat class survives on the button — external CSS that
    targets .jets-seat continues to apply.
  - data-seat-number attribute survives — _jumpToSeat still works.
  - All seat-variant classes (jets-seat--seat, jets-seat--available,
    jets-seat--selected, jets-seat--unavailable, jets-seat--disabled).

Specs: 15 new cases in jets-seat.component.spec.ts cover button-vs-div
rendering, aria-label / aria-selected / aria-disabled reflection,
default tabindex, colIndex / rowIndex plumbing, click + mouse
suppression when aria-disabled is true, and the regression guards
above. Total: 343 tests, all green.

Snapshots: no Playwright screenshot updates needed. Isolated re-run
of every flake-suspect spec produced byte-identical baselines, so the
visual output of the seat is unchanged. The two consistent e2e
failures (colorTheme/field-seatArmrestColor, customCabinTitles/short
& default) reproduce on baseline (origin/WCAG @ 9eb0b26) — they are
pre-existing and unrelated to this commit.

DOM-breaking change: consumers that select `div.jets-seat` must
switch to `button.jets-seat` or just `.jets-seat`. Library-internal
selectors and the demo already use the class form, so the in-tree
impact is zero.

Commit 5 of the docs/wcag/PLAN.md roadmap. DOM-breaking change —
awaiting user approval before push to origin/WCAG.
…lakes

Wave C integrated:
- commit 5 seat → button (df885db) — DOM-breaking, user-approved
- commit 12 landmarks + skip-link + deck-selector (f216a55)

Plus a new Status note for the 4 pre-existing e2e flakes both Wave C
sub-agents reproduced on the WCAG baseline (colorTheme · field-
seatArmrestColor, · field-seatStrokeWidth, customCabinTitles ·
default, · short). They pass in isolated single-worker runs. Not
caused by any WCAG commit — to be triaged separately.

Wave D will dispatch commits 6 (grid scaffolding, file-disjoint from
jets-seat-map) and 13 (alternative list view, new component + minimal
seat-map wire-up).
Per APG Layout Grid pattern (Decisions log 2026-06-04 — full grid,
every position is a gridcell so the geometry doesn't lie to AT):

JetsDeckComponent (host bindings):
- role="grid"
- aria-label: "Seat map — {{ deck.title || 'Deck N' }}" (locale via
  LOCALES_MAP['gridLabel']; English fallback)
- aria-rowcount: deck.rows.length
- aria-colcount: max seats-per-row (includes aisle/empty cells for
  honest geometry)
- propagates rowIndex (1-based, $index + 1) and lang to each <sm-jets-row>

JetsRowComponent (host bindings):
- role="row"
- aria-rowindex: rowIndex input
- propagates per-cell colIndex = $index + 1 to each <sm-jets-seat>
  (the row-level @input colIndex from commit 5 is now @deprecated —
  preserved for API stability but no longer used; per-seat values
  are computed from the iteration index)
- new @input lang propagated to each seat for non-seat aria-labels

JetsSeatComponent:
- role="gridcell" on both the <button> (seat) branch and the <div>
  (aisle/empty/index) branch
- aria-label on non-seat cells via new nonSeatAriaLabel getter:
  aisle/empty fall back to English literals; index surfaces
  "Row {number}". TODO(commit 17 docs): add 'aisle' / 'empty' to
  every locale in LOCALES_MAP.
- new effectiveTabindex getter: seat cells default to rovingTabindex
  (??0); non-seat cells are always -1 (reachable via arrow nav from
  commit 7, never via Tab).
- aria-rowindex / aria-colindex applied to both branches via existing
  inputs from commit 5.
- new @input lang for locale lookup on the nonSeatAriaLabel getter.

Specs:
- JetsDeckComponent: 4 new cases (role=grid, aria-rowcount/colcount,
  aria-label includes deck title, per-row aria-rowindex).
- JetsRowComponent: 4 new cases (role=row, aria-rowindex reflects
  input, omitted when undefined, per-seat aria-colindex from @for
  index).
- JetsSeatComponent: 7 new cases under "Grid cell semantics" (role
  on button + div branches, aisle/empty/index aria-labels, tabindex
  defaults and rovingTabindex override via setInput pattern for OnPush).

Keyboard navigation (Arrow / Home / End / PageUp-Down / Ctrl-skim)
lands in commit 7 — this commit only sets up the structural scaffold.

373/373 unit tests pass. ng build seatmap-lib green.

Commit 6 of the docs/wcag/PLAN.md roadmap.
New standalone component JetsSeatListComponent renders all seats as a
semantic HTML <table> with filter checkboxes (Window, Aisle, Extra
legroom, Exit row) and a sort <select> (Row / Price asc / Price desc).
Closes WCAG 2.5.8 (target size) and gives a non-visual model for
screen readers that prefer linear navigation.

Wiring:
- IConfig.alternativeView?: 'grid' | 'list' | 'auto' (default 'grid'
  preserves existing behaviour).
- JetsSeatMapComponent gains effectiveView getter that resolves
  config + viewOverride + matchMedia('(max-width: 480px)') for 'auto'.
- _initViewportWatcher / _teardownViewportWatcher subscribe to the
  matchMedia change event; SSR-safe via typeof window guard.
- toggleView flips the user override; the toggle button renders only
  when alternativeView is 'auto' or unset (so explicit pinning by
  the host wins).
- When effectiveView === 'list', the template renders <sm-jets-seat-list>
  instead of the visual plane-body block. Outputs are wired to the
  same onTooltipSelect / onTooltipUnselect handlers, so the
  LiveAnnouncer announcements from commit 9 fire for free.

Specs (jets-seat-list.component.spec.ts): renders one <tr> per real
seat (skips aisle/empty), semantic table caption + scope=col headers,
Select / Unselect emissions, Select disabled when no passenger
available, priceAsc sort orders rows correctly.

Public API: exports JetsSeatListComponent.

Locale keys with English fallbacks (TODO commit 17 docs):
viewAsList, viewAsMap, allSeats, filters, sortByPriceAsc,
sortByPriceDesc.

364/364 tests pass. ng build seatmap-lib green.

Commit 13 of the docs/wcag/PLAN.md roadmap.
Wave D integrated:
- commit 6 grid scaffolding (49c34f6) — role=grid/row/gridcell
  + aria-rowindex/colindex/rowcount/colcount across deck/row/seat
- commit 13 alternative list view (b81fead) — JetsSeatListComponent
  + config.alternativeView + matchMedia(480px) for 'auto'

Both Wave D sub-agents stalled by API stream-timeout (~115 minutes
of run-time each) mid-implementation. Orchestrator picked up partial
work in both worktrees and finished the implementations + new specs
(spec for jets-deck/row grid host bindings, spec for JetsSeatList
table/select/sort behaviour).

Status section updated with the new operational lesson: split large
commit prompts into 2+ narrower sub-tasks or accept partial work and
finish in the orchestrator session. Wave E will return to bottleneck-
sequential since commits 7/8/11 all touch jets-seat-map.ts.
…board nav)

Pure-function service for keyboard navigation in the seat grid.
classifyKey() maps KeyboardEvent -> TGridKey (handling Ctrl/Meta
modifiers and PageUp/Down); move() computes the next focused cell
per the WAI-ARIA APG Layout Grid pattern:

  - Arrow*  : +/-1 along the axis; ArrowUp/Down clamp to the target
              row's last column when it is shorter; edges hold.
  - Home/End: first/last column of current row.
  - CtrlHome/CtrlEnd: first/last cell of the current deck.
  - PageUp/PageDown: -/+5 rows, clamped to deck edges.
  - CtrlArrowLeft/Right: "skim" mode — hop to the next interactive
              seat in the row (available/selected/preferred/extra),
              skipping aisle/empty/unavailable; stays put when no
              further interactive cell exists in that direction.

initialCell() picks the first interactive seat in a deck, falling
back to (deckIdx, 0, 0). isInteractive() exposes the same predicate
externally so the orchestrator can decide whether Enter should
trigger the seat's click handler.

Cross-deck navigation is intentionally OUT OF SCOPE and left to the
orchestrator wiring in JetsSeatMapComponent (also part of commit 7).

Spec coverage (66 cases): classifyKey x 22, initialCell x 4, arrows
x 8, Home/End x 4, CtrlHome/CtrlEnd x 4, PageUp/PageDown x 6, skim
x 5, isInteractive x 11, defensive x 2.

Commit 7 (part A: pure service) of docs/wcag/PLAN.md.
…mponent

Sub-agent's pure SeatGridNavigationService (commit 61b3b78) wired into
the map component. Adds:

- focusedCell: ICellPos state tracking which gridcell is the current
  Tab anchor (default {0,0,0}; re-seeded on map init and deck switch
  via gridNav.initialCell()).
- onGridKeydown(event): classifies the KeyboardEvent via gridNav, calls
  gridNav.move(), updates focusedCell, applies the roving tabindex
  (every cell -> tabindex=-1 except the focused one -> tabindex=0),
  imperatively focuses the new cell. Escape with an open tooltip closes
  the tooltip without moving focus. Keys not classified (typing, Tab,
  etc.) bubble normally.
- onGridFocusin(event): if focus lands on any [role=gridcell] with
  aria-rowindex/colindex (commit 6 scaffolding), updates focusedCell
  so subsequent arrow nav continues from where the user clicked.
- _applyRovingTabindex(): cheap DOM walk over [role=gridcell]; flips
  tabindex on each so only the focused cell is a Tab stop. Called
  whenever focusedCell changes.
- _focusCell(pos): imperative focus via [role=gridcell][aria-rowindex
  ="N"][aria-colindex="M"] selector — works for both seat buttons and
  aisle/empty divs because commit 6 attached role=gridcell uniformly.
- _resetGridFocus(): seeds the anchor on the first interactive seat
  of the active deck after _loadSeatMap completes.
- onDeckSelect refined to use gridNav.initialCell + _focusCell instead
  of the commit-12 placeholder querySelector('[data-seat-number]').

Template wiring: <div #mapContainer (keydown)="onGridKeydown($event)"
(focusin)="onGridFocusin($event)">. Listeners are on the container so
key/focus events from any descendant seat bubble up and the grid
manages them centrally.

Spec coverage (7 new cases): ArrowRight/Down move + preventDefault,
Home/Ctrl+End, Escape closes tooltip, unrelated keys are no-op,
focusin syncs focusedCell. Existing 445 tests stay green.

Skim-mode (Ctrl+Arrow), PageUp/Down, cross-deck (deck N edge ->
deck N±1) handled by the navigation service per APG; cross-deck
transition wiring is deferred — the seatmap currently shows one
deck at a time (singleDeckMode default) so the bottleneck is the
deck-selector switch, not in-grid nav.

Commit 7 (part B: wiring) of docs/wcag/PLAN.md.
Wave E integrated:
- commit 7 SeatGridNavigationService + wiring (61b3b78 + c055e2a) —
  split-then-wire tactic: sub-agent did pure service + 66 specs,
  orchestrator did the JetsSeatMapComponent integration in one pass.
  No stream-timeout this time.

Wave F (sequential, can't parallelise): commits 8 → 11, both touch
tooltip + seat-map.
Implements WCAG 2.1 SC 1.4.13 (Content on Hover or Focus) for the
hover-tooltip path. Four behaviour changes:

  1. Focus triggers the tooltip when tooltipOnHover is on. onGridFocusin
     now resolves the seat at focusedCell and opens the tooltip on
     interactive seats (available / selected / preferred / extra), so
     keyboard / AT users arrow-navigating the grid (commit 7) get the
     same affordance mouse users do. Suppressed on touch and skipped
     when the same seat already owns the tooltip (avoids flicker).
  2. Tooltip is now "hoverable": onSeatMouseLeave no longer closes the
     tooltip synchronously — it schedules an 80ms close via a new
     _hoverCloseTimer. New onTooltipMouseEnter cancels the pending
     close so the cursor can land on the tooltip body without dismissing
     it; onTooltipMouseLeave re-arms the same delayed close.
  3. Escape closes the tooltip even when focus is inside it. The grid's
     onGridKeydown already handled Escape from the grid (commit 7); this
     commit adds (keydown.escape) bindings on the tooltip element / host
     div with stopPropagation, covering the case where focus has moved
     into the tooltip's Select / Cancel buttons.
  4. The componentOverride branch is wrapped in a host <div> so it
     carries the same mouseenter / mouseleave / keydown.escape bindings
     as the built-in tooltip — Angular *ngComponentOutlet can't take
     event bindings directly, so consumers using JetsTooltip override
     get the new behaviour via the wrapper.

_showTooltip also cancels any pending _hoverCloseTimer up front, so
re-entering a seat (or focus bouncing back) aborts a pending close.
ngOnDestroy clears the timer to prevent a stray activeTooltipChanged
emission after teardown.

Existing "opens/closes tooltip on hover" test updated to advance fake
timers for the new deferred close path; six new specs cover the
focusin-opens-tooltip, focusin-no-op-when-off, deferred-close,
tooltip-mouseenter-cancels, tooltip-mouseleave-rearms, and
destroy-clears-timer paths.

Commit 8 of the docs/wcag/PLAN.md roadmap.
<body: role=dialog (no aria-modal, no cdkTrapFocus per Decisions log
2026-06-04); aria-labelledby on header; aria-describedby on amenities
/dimensions when present; sidePanel branch uses role=region instead;
auto-focus primary action button (Select / Unselect / Cancel) on view
init; keydown.escape inside tooltip emits close + stopPropagation;
JetsSeatMapComponent tracks _lastTriggerElement to restore focus on
onTooltipClose / Select / Unselect; close button aria-label localised
via LOCALES_MAP['close'] with English fallback (TODO commit 17 for
locale backfill).>

Commit 11 of the docs/wcag/PLAN.md roadmap.
Wave F:
- commit 8 1.4.13 hover-tooltip focus-aware + dismissable (8ea5e36)
- commit 11 tooltip non-modal dialog (8ce3f3f)

Both sub-agents finished cleanly without stream-timeout.

15 of 17 commits done. Wave G dispatches the final two in parallel
(file-disjoint): commit 16 axe + keyboard tests, commit 17 README
+ ACR docs.
README gets a new Accessibility section (what's built in, override
responsibility with a 9-line minimum-contract snippet, testing your
integration, known host responsibilities). docs/ACR.md is a fresh full
WCAG 2.2 A+AA conformance matrix grouped by principle (59 success
criteria covered, of which 33 Supports, 18 Not applicable, 6 Host
responsibility, 2 Partially supports — 2.4.11 and 2.5.8), with an
Override Responsibility appendix detailing per-component obligations
for hosts swapping JetsSeat / JetsTooltip / JetsTooltipView /
JetsNotInit via config.componentOverrides, plus a Known Limitations
appendix flagging the forced-colors inline-SVG residual and the
pre-existing e2e flakes.

CHANGELOG.md is new (first formal entry) and records the WCAG branch's
three breaking changes — peer dep (commit 1, 7d0b368), default colors
(commit 4, d49e100), DOM tag (commit 5, df885db) — plus the flat
Added / Changed / Fixed / Docs roll-up of all 17 commits with SHAs.

Commit 17 of the docs/wcag/PLAN.md roadmap. Closes the WCAG 2.2 AA
refit.
Adds the first formal accessibility test scaffolding for the seatmap
library — both jsdom-level (jest-axe) and chromium-level
(@axe-core/playwright) — wired through dedicated npm scripts.

devDependencies added:
  - jest-axe@^9.0.0
  - @types/jest-axe@^3.5.9
  - @axe-core/playwright@^4.10.0

Unit spec (`jets-seat-map.a11y.spec.ts`, 4 cases):
  1. default-themed single-deck seatmap renders without violations
  2. open tooltip (role="dialog" + aria-labelledby) renders without
     violations
  3. alternative list view (viewOverride='list') renders without
     violations
  4. 3-deck selector (role="tablist") renders without violations

Color-contrast is disabled for the jsdom pass — jsdom has no real
layout/paint so the rule is unreliable; the e2e suite covers real
contrast in chromium.

E2E spec (`projects/seatmap-demo/e2e/a11y/seatmap.a11y.spec.ts`):
  - AxeBuilder full-page scan inside .demo-seatmap-wrapper
  - Tab cycle eventually lands focus inside the seatmap region
  - First interactive seat is focusable
  - ArrowRight x3 advances aria-colindex
  - Enter (or fallback native click) on a seat opens role="dialog"
  - Escape closes the dialog

No real axe violations were surfaced against production code. One
authoring fix to the unit spec's deck factory: the original
two-row-bucketing helper produced an empty second row for scenarios
with <6 seats, which axe flagged as `aria-required-children`. The
factory now buckets seats into rows of at most 3 and skips empty
buckets. The library code is unchanged.

E2E `color-contrast` is currently disabled via `.disableRules` — the
deck-floor CSS variable handling is followed up in a later commit;
flagging it here would be a false signal.

Pre-existing flakes (`colorTheme · field-seatArmrestColor`,
`customCabinTitles · default/short`) are not in this commit's scope
and were not exercised.

npm scripts added:
  - `npm run test:a11y` — runs only *.a11y.spec.ts under seatmap-lib
  - `npm run e2e:a11y` — runs only the e2e/a11y/ suite

Commit 16 of the docs/wcag/PLAN.md roadmap.
All 17 commits of the WCAG roadmap are now in origin/WCAG. Wave G:
- commit 17 README + ACR + CHANGELOG (5d3a35a)
- commit 16 jest-axe unit + @axe-core/playwright e2e (9a9ab70)

Final tally:
- 17 feature/test/docs commits + 7 docs(wcag) progress commits
- 476/476 unit tests green; 3/3 e2e a11y tests green
- ACR covers 59 success criteria: 33 Supports, 18 N/A, 6 Host
  responsibility, 2 Partially supports (focus-not-obscured Min and
  target-size Min — both due to host responsibility for sticky
  overlays and forced 'grid' mode respectively), 0 Does not support
- 3 breaking changes (peer dep / default colors / DOM tag) all
  documented in CHANGELOG and ACR with rationale

Ready for code review, stakeholder sign-off, merge to main, release.
McSpace and others added 20 commits June 18, 2026 15:05
…elect-enabled

- isSelectAvailable is now a computed getter on JetsSeatMapComponent instead of
  a flag only refreshed in _showTooltip — in list view (no tooltip) it was stuck
  false, so every Select button was disabled even for available seats.
- List view drops the separate Status and Price columns; the last column's
  button now encodes everything: disabled 'Unavailable' for unavailable seats,
  the formatted price for paid available seats, 'Select' for free ones, and
  'Unselect' for an occupied seat. Unavailable seats are no longer selectable.
571/571 unit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The list-view action buttons (Select / Unselect / Unavailable) and the
filter select/checkboxes had no :focus-visible outline, so keyboard focus was
invisible. Add the same 2px ring used by the grid seats.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`_resolveBulkOverlaps` measured a row's bbox via `getNativeRowHeight`,
which only inspects per-seat-type heights and ignores per-seat
`topOffset`. On staggered Business pods (LH470 row 4: seat 4D at
topOffset=-172, 4C/4H at +45) the real bottom edge of the row was
under-reported, the galley bulk just below it looked merely lightly
grazed, and the resolver shrunk its native height to ~22% — landing
just above the 20% safety floor and rendering as an invisible 11-px
strip while only the galley sticker stayed visible.

Add `getNativeRowBBox` to walk non-aisle seats with their `topOffset`
included and use it in `_resolveBulkOverlaps`. The LH470 case now
overlaps the bulk deeply enough to trip the safety floor, so the
partition keeps its original geometry — matching the React renderer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirror the React lib: in horizontal mode the nose and tail flip 180deg when
not rightToLeft, so the cabin points the same way as the React reference.
Adds horizontal/rightToLeft inputs to JetsNose/JetsTail and a transform
binding; covered by unit tests (RED->GREEN).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nose/tail order)

Mirror React PlaneBody/index.js for horizontal LTR: rotate the deck wrapper
180deg, render the tail first / nose last, and thread horizontal/rightToLeft
into the nose and tail so they flip too. Gated on horizontal && !rightToLeft.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire the resolved config flags into the plane body so the nose/tail/deck
flip logic activates. Verified live: rightToLeft now controls the nose
direction in horizontal mode (false=nose left, true=nose right), matching
the React reference.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… (P1b)

Move the horizontal rotate(90deg) onto an inner .jets-seat-map__rotor wrapper
so the tooltip stays a sibling in the un-rotated container (mirrors the React
structure). The tooltip is then anchored by an explicit screen-space left/top
and a --horizontal CSS rule frees the right:8px constraint that was squeezing
it to a sliver. Previously the tooltip rendered far off-screen (x~2476).

_buildLayoutData now measures the rotor so the layoutUpdated payload is
unchanged. Vertical tooltip (arrow, positioning) is unaffected. Unit + smoke
e2e green; verified on-screen in the demo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirror React PlaneBody decks.reverse(): in horizontal && !rightToLeft, a
stacked multi-deck cabin reverses its deck order so the decks read correctly
after the rotor's 90deg rotation. Single-deck mode is unchanged (the active
deck is shown regardless of orientation). getDeckIndex still returns the
original content index, so data-deck-index / _buildLayoutData are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In horizontal mode the container kept its tall vertical height (e.g. 400x3139)
while the rotated rotor rendered 3139x359, leaving a huge gap below the cabin.
Measure the rotor's transform-immune offset size post-render and swap it onto
the container (width = rotor height, height = rotor width), mirroring React's
outer 'width: scaledTotalDecksHeight'. A --horizontal modifier aligns the rotor
to the start (it pivots top-left) and drops the vertical min-height.

Also simplifies mapTransform (the scaleType ternary had identical branches;
Angular bakes scale into deck rendering, so the offset is plain translateY(-100%)).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Behavioural guards for the horizontal React-parity work: nose points left in
LTR and right in RTL (rightToLeft drives the direction), and the built-in
tooltip stays fully within the viewport in horizontal mode (P1b regression).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The rotor refactor (e3043bb) made _buildLayoutData measure the rotor instead
of the container, shifting widthInPx by the fuselage inset (~400->359) and
breaking the seatMapInited React-parity payload test. Measure the container
again; in horizontal it now carries the swapped dimensions, so the payload
stays correct without the rotor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts:
#	projects/seatmap-lib/src/lib/components/jets-nose/jets-nose.component.ts
#	projects/seatmap-lib/src/lib/components/jets-plane-body/jets-plane-body.component.ts
#	projects/seatmap-lib/src/lib/components/jets-seat-map/jets-seat-map.component.html
#	projects/seatmap-lib/src/lib/components/jets-tail/jets-tail.component.ts
#	projects/seatmap-lib/src/lib/components/jets-tooltip/jets-tooltip.component.ts
…al (P1a)

In horizontal mode the cabin is rotated 90deg (and flipped 180deg in LTR), so
the grid's model-space arrow logic no longer matched what a sighted keyboard
user sees: ArrowRight moved focus visually up/down instead of right. Remap the
four arrows to the model key that produces the matching visual movement
(opposite rotation for LTR vs RTL). Non-arrow keys and vertical mode are
unchanged. Verified on the demo: all four arrows now move focus on-screen in
the pressed direction, both LTR and RTL.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ArrowRight must move focus visually rightward when the cabin is rotated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The nose & tail outline paths start 1.5 viewBox units inside the SVG border,
so they rendered slightly narrower than the fuselage and left a visible step
where they meet it (most noticeable on wide cabins like QT888). Scale each up
by 200/(200 - 2*effectiveInset), where effectiveInset = 1.5 - strokeWidth/2,
so the outline centre lines up with the fuselage border centre. Mirrors the
intent of React's Nose/Tail `distanceFromBorder` compensation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
resolvedConfig rebuilt the merged colorTheme (mergeColorThemeWithConstraints)
on every access, so resolvedConfig.colorTheme changed identity each CD. Every
seat then saw a new colorTheme input, ran ngOnChanges and rebuilt its inner
SVG — replacing the <path> between mousedown and mouseup, so the FIRST click on
a seat was swallowed and the tooltip only opened on the second click. Cache the
merged theme by the raw config.colorTheme reference so it stays stable.

Pre-existing on the WCAG branch (origin/WCAG shows it before this work); main
and dev are unaffected (their colorTheme is already stable).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ilability

The keyboard-navigation test needs to reach an interactive seat and open its
dialog. It rendered with the shared sandbox's live availability, so it flaked
whenever the flight had few free seats left. Pass an empty availability
override so every seat is available (interactive) and the test is deterministic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In horizontal mode the rotated cabin can be far wider than the controls
column, so the demo's flex-row layout squeezed the "Send Events" panel and
event log into a sliver pushed off-screen to the right (the e2e helper even
needs `force:true` to click through the overlap).

Drive a `--horizontal` modifier from the active config's `horizontal` flag:
switch `.demo-content` to a vertical stack and let the wide map scroll inside
its own wrapper (`overflow-x:auto`) so the controls stay full-width and
on-screen below the map.

Also make the e2e playwright config honour `PW_BASE_URL` so the suite can
target an already-running dev server (e.g. the dev worktree on :4201) without
editing the file.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…toggle

Toggling `horizontal` (or other layout-affecting flags) via a config change
rather than a full flight reload skipped `_updateHorizontalDims()`, so the
swapped container width/height stayed null from the initial vertical load. The
`.jets-seat-map` container then collapsed to the rotor's un-rotated layout
height (~3139px for a long cabin), leaving a large empty gap above the page
controls.

Re-run the swapped-dimension measurement after the view re-renders on a
settings-only config change so the container reserves the wide, short
horizontal footprint.

Covered by a new e2e that toggles horizontal on an already-loaded map and
asserts the reserved height stays smaller than the width.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s, wings, nose

The horizontal container only reserved the fuselage box (`offsetWidth`), so the
absolutely-positioned side cabin labels and wings (~20px past the fuselage on
each side) and the nose/tail caps were sheared off once the wrapper clipped
overflow: cabin titles were cut at the bottom and the nose ran off the
top-left.

Measure the union of every rotor descendant's painted rect to reserve the true
footprint, and nudge the rotor (a leading `translate` in the transform) so the
top-left overflow is pulled back inside the box. The offset compensates for the
already-applied shift so repeated measurements converge instead of oscillating.

Covered by a new e2e asserting the reserved height exceeds the fuselage strip
and that the nose and cabin labels stay within the container bounds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@McSpace
McSpace requested a review from nexus June 24, 2026 14:21
Comment thread projects/seatmap-demo/e2e/comparison/dom-diff.spec.ts Fixed

@nexus nexus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

hi @McSpace ,
Please fix github-advanced-security [bot] findings and also, please pay attention to the following points:

[
  {
    "file": "projects/seatmap-lib/src/lib/components/jets-plane-body/jets-plane-body.component.ts",
    "line": 174,
    "summary": "fuselageFill getter dropped the hullColor fallback — consumers using the still-public hullColor alias now get default grey fuselage instead of their branded color.",
    "failure_scenario": "Old getter: `hullColor ?? fuselageFillColor ?? default`. New getter: `fuselageFillColor ?? default`. IColorTheme.hullColor is still declared in types.ts (line 93, marked @deprecated but not removed). Wings still fall back to hullColor correctly — so a consumer with `colorTheme: { hullColor: '#002244' }` would see branded navy wings but default grey fuselage, a visible mismatch."
  },
  {
    "file": "projects/seatmap-lib/src/lib/components/jets-seat-map/jets-seat-map.component.ts",
    "line": 1005,
    "summary": "onGridFocusin always records focusedCell.deckIdx = activeDeckIndex, ignoring which physical deck the focused seat belongs to when singleDeckMode=false.",
    "failure_scenario": "With singleDeckMode=false (valid config, tested in spec) all decks are rendered simultaneously. User Tab-focuses a seat in deck 1 while activeDeckIndex=0 → focusedCell.deckIdx=0 recorded → SeatGridNavigationService.move() does `decks[0].rows` → arrow navigation jumps to a seat in the wrong deck. The correct deck index is available in the DOM via `[attr.data-deck-index]` on the ancestor deck-wrapper but is never read."
  },
  {
    "file": "projects/seatmap-lib/src/lib/components/jets-seat-map/jets-seat-map.component.ts",
    "line": 1552,
    "summary": "_prepareSeatForEmit casts a formatted price string (e.g. 'USD 29') to number via `as unknown as number`, making seat.price a string at runtime while the type says number|string.",
    "failure_scenario": "priceStr is built as `` `${currency} ${numericPrice}` `` (e.g. 'USD 29') then emitted as `price: priceStr as unknown as number`. Any integrator doing `if (seat.price > 0)` or `seat.price * quantity` gets NaN. The raw numeric value is available in the separate `priceValue` field added in this PR, but nothing in the type signature directs consumers toward it."
  },
  {
    "file": "projects/seatmap-lib/src/lib/components/jets-seat-map/jets-seat-map.component.ts",
    "line": 590,
    "summary": "getDeckFloorLiningWidth mixes coordinate spaces — subtracts unscaled fuselageStrokeWidth from a scaled pixel width, producing a lining that is slightly wrong at non-default displayScale values.",
    "failure_scenario": "Line 590: `const innerW = this.fuselageBodyWidth - 2 * stroke` where fuselageBodyWidth is in scaled CSS pixels but stroke is the raw config value. The CSS border is `stroke * displayScale` (correctly scaled in jets-plane-body). The mismatch introduces an error of `2 * stroke * (displayScale - 1)` in innerW — typically a few pixels at displayScale < 1, causing the lining to slightly overflow the fuselage outline. Fix: use `2 * stroke * this.displayScale` on line 590."
  }
]

McSpace and others added 2 commits June 26, 2026 10:09
Resolves PR #23 conflicts after #22 (React-parity seat colouring) landed
on main. #22 removed the colorfulSeatsByClass/colorfulSeatsByScore flags,
seatClassTints and color-tint util; this branch still carried them plus
the WCAG a11y work on top.

Conflict resolutions:
- nose/tail/tooltip components: keep WCAG a11y attributes (aria-hidden,
  role/aria-labelledby/aria-describedby, escape/keydown handlers).
- seat-map.component.ts: drop colorfulSeatsBy* (adopt #22) and keep WCAG's
  memoised _mergedThemeValue colourTheme.
- seat-map.component.html: keep WCAG template (a11y wrappers, list view,
  rotor); drop the deck's [colorfulSeatsByClass] binding (#22 removal).
- jets-seat.component.spec.ts: keep ARIA + grid-cell tests; drop the
  obsolete colorfulSeatsByClass describe block (#22 removed the feature).
- horizontalParity.spec.ts: keep WCAG's P1a arrow-key test.
- re-add IPassenger import to jets-tooltip.component.ts (used by WCAG's
  buildRestrictedPassengerTypeMessage; was unused on main).
- e2e baseline screenshots taken from main; regenerated separately.

Verified: build:lib OK, unit tests 587/587 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reviewer (nexus):
- jets-plane-body: restore hullColor fallback in fuselageFill getter so a
  consumer theming only the deprecated hullColor gets a branded fuselage
  (the wings already honour it); refresh the @deprecated note in types.ts.
- jets-seat-map onGridFocusin: record the focused seat's real deck from the
  ancestor .deck-wrapper[data-deck-index] instead of always activeDeckIndex,
  so arrow nav works with singleDeckMode=false (all decks rendered).
- jets-seat-map _prepareSeatForEmit: drop the misleading
  `priceStr as unknown as number` cast — price is typed number|string, so
  the formatted string is assigned honestly; priceValue carries the number.
- jets-seat-map getDeckFloorLiningWidth: scale fuselageStrokeWidth by
  displayScale before subtracting from the scaled fuselageBodyWidth, fixing a
  ~few-px lining error at non-default displayScale.

CodeQL:
- #44 seat-grid-navigation skim(): remove the dead lastInteractive assignment
  (the loop returns on the first interactive cell, so the tail was unreachable).
- #43 dom-diff.spec.ts: drop unused setAvailability import.
- #42 jets-seat-map-preparer: drop unused getNativeRowHeight import.

Adds a focusin regression test for the multi-deck deck-index fix.
build:lib green; 588/588 unit tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@McSpace

McSpace commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

Fixed all four review findings and the three CodeQL alerts (#42/#43/#44) in 09529cb. build:lib is green and 588/588 unit tests pass.

Stamp the WCAG 2.2 AA refit + PR #23 review fixes as 0.0.3 and date the
CHANGELOG entry. The publish workflow's version-guard requires
projects/seatmap-lib/package.json `version` to equal the release tag
(v0.0.3) minus the leading `v`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@nexus
nexus merged commit 2c43cc3 into main Jun 30, 2026
5 checks passed
@nexus
nexus deleted the WCAG branch June 30, 2026 17:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants