{"_id":"@19h47/windowsplitter","name":"@19h47/windowsplitter","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"@19h47/windowsplitter","version":"1.0.0","description":"Window splitter controller with accessibility in mind","keywords":["windowsplitter","separator","accessibility","a11y","ES6"],"homepage":"https://github.com/19h47/19h47-windowsplitter#readme","bugs":{"url":"https://github.com/19h47/19h47-windowsplitter/issues"},"repository":{"type":"git","url":"git://github.com/19h47/19h47-windowsplitter.git"},"license":"ISC","author":{"name":"Jérémy Levron","email":"jeremylevron@19h47.fr","url":"https://19h47.fr"},"type":"module","main":"./dist/windowsplitter.umd.cjs","module":"./dist/windowsplitter.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/windowsplitter.js","require":"./dist/windowsplitter.umd.cjs"}},"scripts":{"dev":"vite","test":"vitest run","build":"vite build && tsc && vite build --config vite.config.docs.js","build:docs":"vite build --config vite.config.docs.js","deploy":"pnpm login && pnpm run build && pnpm publish --access public --no-git-checks"},"devDependencies":{"@19h47/prettier-config":"^2.0.0","happy-dom":"^20.10.6","prettier":"^3.9.5","typescript":"^7.0.2","vite":"^8.1.4","vitest":"^4.1.10"},"packageManager":"pnpm@10.12.1","prettier":"@19h47/prettier-config","gitHead":"ba9cbd1dc4d58ca5d4a60a063311de3c4fb20029","_id":"@19h47/windowsplitter@1.0.0","_nodeVersion":"22.22.0","_npmVersion":"11.18.0","dist":{"integrity":"sha512-wAhY9XydQhQ3+LjemyQwTS7rbKDeTEGTJo1qlfU1rM/1rP7WCs4bk8h/PaPZyyf+affcnLUqgERbvmOdFm5K4w==","shasum":"d085ab500ce600fef99833310f97994b0f037632","tarball":"https://registry.npmjs.org/@19h47/windowsplitter/-/windowsplitter-1.0.0.tgz","fileCount":8,"unpackedSize":33633,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIDqZI4A/ccqpQZ37+Ux0uNu55wBi/b6vIPfITJ+m8qCSAiEA2Afzbb0nHHIZ4l8NEIulz6Gj4k3yyJJoiShW3EPwGHw="}]},"_npmUser":{"name":"19h47","email":"jeremylevron@19h47.fr"},"directories":{},"maintainers":[{"name":"19h47","email":"jeremylevron@19h47.fr"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/windowsplitter_1.0.0_1784278118532_0.24490026215218297"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-17T08:48:38.202Z","1.0.0":"2026-07-17T08:48:38.675Z","modified":"2026-07-17T08:48:38.875Z"},"maintainers":[{"name":"19h47","email":"jeremylevron@19h47.fr"}],"description":"Window splitter controller with accessibility in mind","homepage":"https://github.com/19h47/19h47-windowsplitter#readme","keywords":["windowsplitter","separator","accessibility","a11y","ES6"],"repository":{"type":"git","url":"git://github.com/19h47/19h47-windowsplitter.git"},"author":{"name":"Jérémy Levron","email":"jeremylevron@19h47.fr","url":"https://19h47.fr"},"bugs":{"url":"https://github.com/19h47/19h47-windowsplitter/issues"},"license":"ISC","readme":"# @19h47/windowsplitter\n\n[![](https://img.shields.io/npm/v/@19h47/windowsplitter)](https://www.npmjs.com/package/@19h47/windowsplitter)\n[![](https://img.shields.io/npm/dm/@19h47/windowsplitter)](https://www.npmjs.com/package/@19h47/windowsplitter)\n\nWindow splitter behaviour for UIs where a focusable `role=\"separator\"` must resize a primary pane through **JavaScript orchestration**, while HTML remains the source of truth for ARIA.\n\nBased on the W3C APG Window Splitter pattern:\n[Window Splitter Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/)\n\n## Install\n\n```\npnpm add @19h47/windowsplitter\n```\n\n## HTML\n\nHTML is the source of truth. Set `role`, `tabindex`, `aria-orientation`, `aria-valuemin`, `aria-valuemax`, `aria-valuenow`, labelling attributes, and `aria-controls` (primary pane) in the markup; the library reads them and mirrors layout onto the separator and primary pane.\n\n`aria-valuemin` / `aria-valuemax` must match the **real** allowed range of the primary pane (not always `0`–`100`). If the pane cannot fully collapse, set min above `0`.\n\n> **Note on `role`:** the APG documents `role=\"separator\"`. In practice, JAWS / NVDA / VoiceOver often handle a focusable `role=\"slider\"` more reliably ([discussion](https://github.com/w3c/aria-practices/issues/130)). The library does not enforce the role — pick what works for your AT testing.\n\n```html\n<div class=\"panes\">\n\t<div id=\"toc\" class=\"pane pane--primary\">Table of contents</div>\n\t<div\n\t\trole=\"separator\"\n\t\ttabindex=\"0\"\n\t\taria-orientation=\"vertical\"\n\t\taria-valuemin=\"0\"\n\t\taria-valuemax=\"100\"\n\t\taria-valuenow=\"30\"\n\t\taria-controls=\"toc\"\n\t\taria-label=\"Table of Contents\"\n\t></div>\n\t<div class=\"pane pane--secondary\">Content</div>\n</div>\n```\n\n## JavaScript\n\n```javascript\nimport WindowSplitter from '@19h47/windowsplitter';\n\nconst $separator = document.querySelector('[role=\"separator\"]');\nconst splitter = new WindowSplitter($separator);\n\nsplitter.init();\n```\n\n## Options\n\n```ts\ntype Orientation = 'horizontal' | 'vertical';\ntype Mode = 'resize' | 'clip' | 'none';\n\ntype SizeContext = {\n\tvalue: number;\n\tratio: number;\n\toffset: number;\n\tlength: number;\n};\n\ntype Options = {\n\torientation?: Orientation; // default: aria-orientation, then 'vertical'\n\tmode?: Mode; // default: 'resize'\n\tstep?: number; // default: 1\n\tpage?: number; // default: 10\n\tfixed?: boolean; // default: false (Enter-only when true)\n\tcontainer?: HTMLElement | null; // default: el.parentElement\n\tformatSize?: (context: SizeContext) => string; // default: `${value}%`\n\tformatValue?: (value: number) => string; // default: String(value) → aria-valuetext\n};\n```\n\n| Option        | Description                                                                 |\n| ------------- | --------------------------------------------------------------------------- |\n| `orientation` | `vertical` = left/right move; `horizontal` = up/down move                   |\n| `mode`        | `resize` sets primary width/height; `clip` sets `clip-path`; `none` events only |\n| `step`        | Arrow key increment                                                         |\n| `page`        | PageUp / PageDown / Shift+Arrow increment                                   |\n| `fixed`       | Toggle-only splitter (no arrow keys; pointer toggles like Enter)        |\n| `container`   | Element used for drag bounds (defaults to parent)                           |\n| `formatSize`  | CSS size for the primary pane in `resize` mode (unit, not a label)          |\n| `formatValue` | `aria-valuetext` string (localization, units in the announced name)         |\n\n```javascript\n// CSS pixels instead of percent\nnew WindowSplitter($separator, {\n\tformatSize: ({ offset }) => `${offset}px`,\n});\n\n// French typography for screen readers (space before %)\nnew WindowSplitter($separator, {\n\tformatValue: value => `${value} %`,\n});\n```\n\n### Comparison images (`mode: 'clip'`)\n\nSame interaction as a before/after slider: overlay panes and clip the primary (before) layer.\n\n```javascript\nconst splitter = new WindowSplitter($separator, {\n\tmode: 'clip',\n\tstep: 5,\n\tpage: 20,\n});\n\nsplitter.init();\n```\n\n## Keyboard Support\n\n| Key         | Function                                                                 |\n| ----------- | ------------------------------------------------------------------------ |\n| Left Arrow  | Moves a **vertical** splitter to the left.                               |\n| Right Arrow | Moves a **vertical** splitter to the right.                              |\n| Up Arrow    | Moves a **horizontal** splitter up.                                      |\n| Down Arrow  | Moves a **horizontal** splitter down.                                    |\n| Enter       | Collapses the primary pane, or restores the previous position.           |\n| Home        | Moves splitter to `aria-valuemin` (optional in APG).                     |\n| End         | Moves splitter to `aria-valuemax` (optional in APG).                     |\n| Page Up     | Increases value by `page`.                                               |\n| Page Down   | Decreases value by `page`.                                               |\n| Shift+Arrow | Uses `page` as the step size.                                            |\n\nA fixed splitter (`fixed: true`) omits arrow keys and only implements Enter (and pointer toggle).\n\n`F6` (optional pane cycling in the APG) is **not** implemented: it conflicts with browser chrome shortcuts and belongs to the app, not this controller.\n\n## Role, Property, State, and Tabindex Attributes\n\n| Role        | Attribute               | Element | Usage                                                                 |\n| ----------- | ----------------------- | ------- | --------------------------------------------------------------------- |\n| `separator` |                         | focusable splitter | Identifies the element as a window splitter.                 |\n|             | `tabindex=\"0\"`          | splitter | Includes the separator in the page tab sequence.                     |\n|             | `aria-orientation`      | splitter | `vertical` or `horizontal`.                                          |\n|             | `aria-valuenow`         | splitter | Current size of the primary pane. Written by the library on change.  |\n|             | `aria-valuetext`        | splitter | Mirrored from `aria-valuenow` by the library (`sync` / `setValue`).  |\n|             | `aria-valuemin`         | splitter | Minimum primary pane size (typically `0`).                           |\n|             | `aria-valuemax`         | splitter | Maximum primary pane size (typically `100`).                         |\n|             | `aria-controls`         | splitter | References the primary pane `id`.                                    |\n|             | `aria-label` / `aria-labelledby` | splitter | Accessible name matching the primary pane.                  |\n|             | `aria-disabled=\"true\"`  | splitter | Disables pointer and keyboard interaction. Prefer `tabindex=\"-1\"`. |\n\n## Methods\n\n| Method        | Description                                      | Arguments                                              |\n| ------------- | ------------------------------------------------ | ------------------------------------------------------ |\n| `init()`      | Bind events and sync from HTML                   |                                                       |\n| `destroy()`   | Remove event listeners                           |                                                       |\n| `sync()`      | Re-apply HTML ? layout                           |                                                       |\n| `setValue()`  | Set `aria-valuenow` / `aria-valuetext` and update layout | `value`, `trigger` (optional, default `true`) |\n| `collapse()`  | Collapse primary pane to `aria-valuemin`         | `trigger` (optional, default `true`)                   |\n| `restore()`   | Restore previous value after collapse            | `trigger` (optional, default `true`)                   |\n| `toggle()`    | Collapse or restore                              | `trigger` (optional, default `true`)                   |\n\n```javascript\nsplitter.setValue(40);\nsplitter.collapse();\nsplitter.restore();\nsplitter.destroy();\n```\n\n## Getters\n\n| Getter       | Type      | Source                                      |\n| ------------ | --------- | ------------------------------------------- |\n| `value`      | `number`  | `aria-valuenow`                             |\n| `min`        | `number`  | `aria-valuemin`                             |\n| `max`        | `number`  | `aria-valuemax`                             |\n| `ratio`      | `number`  | normalized `01` between min and max        |\n| `disabled`   | `boolean` | `aria-disabled=\"true\"`                      |\n| `collapsed`  | `boolean` | `value === min`                             |\n| `vertical`   | `boolean` | orientation is `vertical`                   |\n| `$primary`   | element   | pane referenced by `aria-controls`          |\n| `$container` | element   | drag bounds container                       |\n\n## Sync classes & CSS variables\n\n`sync()` / `setValue()` mirror state onto optional CSS hooks and keep `aria-valuetext` in sync with `aria-valuenow`:\n\n| Class / variable              | When / meaning                          |\n| ----------------------------- | --------------------------------------- |\n| `is-focus`                    | Separator has focus                     |\n| `is-dragging`                 | Pointer drag in progress                |\n| `is-collapsed`                | Primary pane at `aria-valuemin`         |\n| `is-disabled`                 | `aria-disabled=\"true\"`                  |\n| `--windowsplitter-value`      | Current value on the container          |\n| `--windowsplitter-ratio`      | `01` ratio on the container            |\n| `--windowsplitter-offset`     | Pixel offset of the separator           |\n\nThe library does not ship styles for these classes  use them in your own CSS.\n\n## Events\n\nEvents are dispatched on the separator element.\n\n### `WindowSplitter.change`\n\n```javascript\n$separator.addEventListener('WindowSplitter.change', event => {\n\tconst { value, min, max, ratio, collapsed } = event.detail;\n\n\tconsole.log({ value, min, max, ratio, collapsed });\n});\n```\n\nNative-like `input` and `change` events are also fired when `trigger` is `true`.\n\n## Example\n\nAn interactive demo is at [19h47.github.io/19h47-windowsplitter](https://19h47.github.io/19h47-windowsplitter/) when published.\n\nRun tests with `pnpm test`.\n\n## References\n\n- [Window Splitter Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/)\n- [ARIA `separator` role](https://www.w3.org/TR/wai-aria-1.2/#separator)\n- [APG example tracking (#130)](https://github.com/w3c/aria-practices/issues/130) — still open; no official example yet\n- [Pattern draft notes (#129)](https://github.com/w3c/aria-practices/issues/129) — `aria-expanded` removed in ARIA 1.1; value attrs required\n","readmeFilename":"README.md","_rev":"1-933df056c35b48f6898376a1933c0224"}