UNPKG

smdm-slide-menu

Version:

A library agnostic multilevel page menu with a smooth slide effect based on CSS transitions, focused on accessibility.

95 lines (59 loc) 6.53 kB
# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Commands This project uses **ddev** for local development. There is a dedicated `cypress` ddev service for running e2e tests. ```sh ddev start # start all services (frontend + cypress containers) ddev ssh -s frontend # shell into the frontend container ddev ssh -s cypress # shell into the cypress container ``` Inside the frontend container: ```sh npm run watch # dev server at http://frontend:8080 (slide-menu.frontend.ddev.site:9999) npm run build # tsc compile + vite bundle → dist/ npm run typecheck # type-check only, no emit npm run lint # oxlint on the three core source files (0 warnings tolerance) npm run format # oxfmt on src/ (configured via .oxfmtrc.json, single quotes) ``` Tests use a custom ddev host command that auto-starts the Vite dev server if needed: ```sh ddev test # starts dev server if not running, then runs cypress in the cypress container npm run test # alias for ddev test ``` **Release flow:** `npm run version-patch|minor|major` runs lint + typecheck + format + tests + build then bumps version; `npm run publish-version` pushes and publishes to npm. Tests target `http://frontend:8080` and require the dev server (`npm run watch`) to be running in the frontend container first. ## Two build outputs There are two distinct outputs from a single source: | Output | Entry point | Purpose | | -------------------------------------------- | -------------------------- | ------------------------------------------------------------------------------- | | `dist/slide-menu.js` + `dist/slide-menu.css` | `src/lib/SlideMenu.min.ts` | Standalone browser bundle (Vite) — exposes `window.SlideMenu`, fires `sm.ready` | | `dist/index.js` + type declarations | `src/index.ts` | ES module for npm consumers (tsc only) | `src/lib/SlideMenu.min.ts` just imports the SCSS and re-exports `SlideMenu`. The Vite config (`vite.config.js`) takes this as its entry and roots at `src/`. `tsconfig.json` roots at `src/index.ts` only and outputs to `dist/`. ## Architecture ### Core classes **`src/lib/SlideMenu.ts`** — main class (~850 lines). Orchestrates everything: DOM construction, navigation state, animation, fold behaviour, keyboard/focus management. Also contains module-level side effects: a global `document` click dispatcher that routes `data-action` buttons to menu instances, `window.SlideMenu = SlideMenu`, and `window.dispatchEvent(new Event('sm.ready'))`. **`src/lib/Slide.ts`** — represents one `<ul>` submenu panel. Constructed by `SlideMenu.initSlides()` for every `<a>` that has a sibling `<ul>`. Handles its own activation state, tabbing (via `inert`), back-link injection, and navigator button injection. Stores a back-reference on the DOM node (`menuElem._slide`). **`src/lib/SlideMenuOptions.ts`** — options interface, enums (`Action`, `MenuPosition`), and the `CLASSES` / `NAMESPACE` constants that drive all CSS class names. **`src/lib/utils/dom.ts`** — small DOM helpers: `trapFocus`, `focusFirstTabAbleElemIn`, `parentsOne`, `validateQuery`, `alignTop`. ### Slide graph On init, `SlideMenu` walks all `<a>` elements and creates a `Slide` for each that has a sibling `<ul>`. The root `<ul>` also gets a `Slide`. All slides are appended into `.slide-menu__slider__wrapper` (or the `.slide-menu__foldable__wrapper` for foldable slides). The graph is parent-linked: `Slide.parent` points to the containing slide, resolved at construction time via `anchorElem.closest('ul')._slide`. ### Navigation model `navigateTo(target)` is the single entry point for all navigation. It: 1. Resolves `target` to a `Slide` via `findNextMenu` 2. Determines direction (forward/back) by comparing parent chains 3. Calls `activateMenus`, `setTabbing`, `setSlideLevel`, `updateMenuTitle` 4. Schedules a post-animation timeout (stored as `navigateTimeoutId`) to focus the first element and deactivate the previous slide Navigation depth is tracked as a CSS variable `--smdm-sm-menu-level` set on the menu element, which drives the `translateX` of `.slide-menu__slider__wrapper` via a calc chain in SCSS. ### Foldable submenus Slides whose anchor has class `slide-menu__item--has-foldable-submenu` are `isFoldable = true`. When `window.innerWidth >= options.minWidthFold`, `canFold()` returns true and foldable slides are moved into `.slide-menu__foldable__wrapper` (rendered side-by-side) rather than the slider. A `ResizeObserver` on `document.body` triggers `openFold`/`closeFold` at the breakpoint. Fold state affects `setTabbing`, `activateMenus`, `setSlideLevel`, and `updateMenuTitle` throughout. ### CSS variables Instance-specific variables (`--smdm-sm-menu-width`, `--smdm-sm-min-width-fold`, `--smdm-sm-transition-duration`, `--smdm-sm-menu-level`) are set on the `.slide-menu` element directly so multiple instances on one page don't conflict. The `:root` block in SCSS provides fallback defaults for user-overridable theme vars only. The derived calc vars (`--smdm-sm-calc-menu-width`, `--smdm-sm-calc-slide-width`, `--smdm-sm-calc-wrapper-translation`) are declared inside `.slide-menu {}` in SCSS — not `:root` — so Chrome recomputes them per-instance using each element's own inline-style values. Declaring them in `:root` would cause `var()` references inside the formula to capture `:root`'s values at declaration time, breaking per-instance isolation. ### Data-action dispatch The module-level click listener handles all `[data-action]` buttons. Valid actions are defined in the `ALLOWED_ACTIONS` set. `data-target` on the button identifies which menu instance to control (falls back to closest ancestor `.slide-menu`). `data-arg` passes an optional argument to the method. ### Accessibility - `inert` attribute controls tabbing — slides are marked inert when not active/visible - Focus is trapped inside the open menu via `trapFocus` in the menu's keydown handler - WCAG: `<a role="button">` elements respond to Space via a keydown handler - `aria-expanded` is maintained on anchors or navigator buttons depending on mode - Controls with `slide-menu--hidden-on-root-level` / `slide-menu--invisible-on-root-level` have `tabindex="-1"` injected at root level