fix(a11y): dialog semantics, focus management, icon labels, html lang#1
fix(a11y): dialog semantics, focus management, icon labels, html lang#1JohnMcLear wants to merge 194 commits intodevelopfrom
Conversation
* docs: add AGENTS.MD for AI and developer guidance * docs: update project name from Etherpad Lite to Etherpad * docs: fix incorrect test directory path in AGENTS.MD * docs: correct test stack description in AGENTS.MD (Mocha is primary) * docs: fix incorrect easysync documentation path in AGENTS.MD * chore: add .pr_agent.toml to enable automatic PR review/description on push * docs: remove nodejs version from README and AGENTS.MD (prefer package.json) * docs: standardize all indentation to 2 spaces * chore: update src/package.json node engine to match root (>=20.0.0)
Bumps [docker/login-action](https://github.com/docker/login-action) from 3 to 4. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](docker/login-action@v3...v4) --- updated-dependencies: - dependency-name: docker/login-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6 to 7. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](docker/build-push-action@v6...v7) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5 to 6. - [Release notes](https://github.com/docker/metadata-action/releases) - [Commits](docker/metadata-action@v5...v6) --- updated-dependencies: - dependency-name: docker/metadata-action dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3 to 4. - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](docker/setup-buildx-action@v3...v4) --- updated-dependencies: - dependency-name: docker/setup-buildx-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [express-rate-limit](https://github.com/express-rate-limit/express-rate-limit) from 8.2.1 to 8.2.2. - [Release notes](https://github.com/express-rate-limit/express-rate-limit/releases) - [Commits](express-rate-limit/express-rate-limit@v8.2.1...v8.2.2) --- updated-dependencies: - dependency-name: express-rate-limit dependency-version: 8.2.2 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3 to 4. - [Release notes](https://github.com/docker/setup-qemu-action/releases) - [Commits](docker/setup-qemu-action@v3...v4) --- updated-dependencies: - dependency-name: docker/setup-qemu-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…er#7358) Bumps the dev-dependencies group with 16 updates: | Package | From | To | | --- | --- | --- | | [@types/formidable](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/formidable) | `3.4.6` | `3.5.0` | | [@types/jquery](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jquery) | `3.5.33` | `4.0.0` | | [@types/jsdom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jsdom) | `27.0.0` | `28.0.0` | | [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.2.3` | `25.3.5` | | [@types/supertest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/supertest) | `6.0.3` | `7.2.0` | | [@types/whatwg-mimetype](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/whatwg-mimetype) | `3.0.2` | `5.0.0` | | [eslint](https://github.com/eslint/eslint) | `10.0.0` | `10.0.3` | | [sinon](https://github.com/sinonjs/sinon) | `21.0.1` | `21.0.2` | | [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `8.55.0` | `8.56.1` | | [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `8.55.0` | `8.56.1` | | [eslint-plugin-react-refresh](https://github.com/ArnaudBarre/eslint-plugin-react-refresh) | `0.5.0` | `0.5.2` | | [i18next](https://github.com/i18next/i18next) | `25.8.8` | `25.8.16` | | [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `0.564.0` | `0.577.0` | | [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.71.1` | `7.71.2` | | [react-i18next](https://github.com/i18next/react-i18next) | `16.5.4` | `16.5.6` | | [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) | `7.13.0` | `7.13.1` | Updates `@types/formidable` from 3.4.6 to 3.5.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/formidable) Updates `@types/jquery` from 3.5.33 to 4.0.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jquery) Updates `@types/jsdom` from 27.0.0 to 28.0.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jsdom) Updates `@types/node` from 25.2.3 to 25.3.5 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@types/supertest` from 6.0.3 to 7.2.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/supertest) Updates `@types/whatwg-mimetype` from 3.0.2 to 5.0.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/whatwg-mimetype) Updates `eslint` from 10.0.0 to 10.0.3 - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](eslint/eslint@v10.0.0...v10.0.3) Updates `sinon` from 21.0.1 to 21.0.2 - [Release notes](https://github.com/sinonjs/sinon/releases) - [Changelog](https://github.com/sinonjs/sinon/blob/main/docs/changelog.md) - [Commits](sinonjs/sinon@v21.0.1...v21.0.2) Updates `@typescript-eslint/eslint-plugin` from 8.55.0 to 8.56.1 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.56.1/packages/eslint-plugin) Updates `@typescript-eslint/parser` from 8.55.0 to 8.56.1 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.56.1/packages/parser) Updates `eslint-plugin-react-refresh` from 0.5.0 to 0.5.2 - [Release notes](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/releases) - [Changelog](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/blob/main/CHANGELOG.md) - [Commits](ArnaudBarre/eslint-plugin-react-refresh@v0.5.0...v0.5.2) Updates `i18next` from 25.8.8 to 25.8.16 - [Release notes](https://github.com/i18next/i18next/releases) - [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md) - [Commits](i18next/i18next@v25.8.8...v25.8.16) Updates `lucide-react` from 0.564.0 to 0.577.0 - [Release notes](https://github.com/lucide-icons/lucide/releases) - [Commits](https://github.com/lucide-icons/lucide/commits/0.577.0/packages/lucide-react) Updates `react-hook-form` from 7.71.1 to 7.71.2 - [Release notes](https://github.com/react-hook-form/react-hook-form/releases) - [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md) - [Commits](react-hook-form/react-hook-form@v7.71.1...v7.71.2) Updates `react-i18next` from 16.5.4 to 16.5.6 - [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md) - [Commits](i18next/react-i18next@v16.5.4...v16.5.6) Updates `react-router-dom` from 7.13.0 to 7.13.1 - [Release notes](https://github.com/remix-run/react-router/releases) - [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md) - [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@7.13.1/packages/react-router-dom) --- updated-dependencies: - dependency-name: "@types/formidable" dependency-version: 3.5.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: "@types/jquery" dependency-version: 4.0.0 dependency-type: direct:development update-type: version-update:semver-major dependency-group: dev-dependencies - dependency-name: "@types/jsdom" dependency-version: 28.0.0 dependency-type: direct:development update-type: version-update:semver-major dependency-group: dev-dependencies - dependency-name: "@types/node" dependency-version: 25.3.5 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: "@types/supertest" dependency-version: 7.2.0 dependency-type: direct:development update-type: version-update:semver-major dependency-group: dev-dependencies - dependency-name: "@types/whatwg-mimetype" dependency-version: 5.0.0 dependency-type: direct:development update-type: version-update:semver-major dependency-group: dev-dependencies - dependency-name: eslint dependency-version: 10.0.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: sinon dependency-version: 21.0.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: "@typescript-eslint/eslint-plugin" dependency-version: 8.56.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: "@typescript-eslint/parser" dependency-version: 8.56.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: eslint-plugin-react-refresh dependency-version: 0.5.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: i18next dependency-version: 25.8.16 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: lucide-react dependency-version: 0.577.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: react-hook-form dependency-version: 7.71.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: react-i18next dependency-version: 16.5.6 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: react-router-dom dependency-version: 7.13.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [openapi-backend](https://github.com/openapistack/openapi-backend) from 5.15.0 to 5.16.1. - [Release notes](https://github.com/openapistack/openapi-backend/releases) - [Commits](openapistack/openapi-backend@5.15.0...5.16.1) --- updated-dependencies: - dependency-name: openapi-backend dependency-version: 5.16.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [jose](https://github.com/panva/jose) from 6.1.3 to 6.2.1. - [Release notes](https://github.com/panva/jose/releases) - [Changelog](https://github.com/panva/jose/blob/main/CHANGELOG.md) - [Commits](panva/jose@v6.1.3...v6.2.1) --- updated-dependencies: - dependency-name: jose dependency-version: 6.2.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [ejs](https://github.com/mde/ejs) from 4.0.1 to 5.0.1. - [Release notes](https://github.com/mde/ejs/releases) - [Changelog](https://github.com/mde/ejs/blob/main/RELEASE_NOTES_v4.md) - [Commits](mde/ejs@v4.0.1...v5.0.1) --- updated-dependencies: - dependency-name: ejs dependency-version: 5.0.1 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [oidc-provider](https://github.com/panva/node-oidc-provider) from 9.6.1 to 9.7.0. - [Release notes](https://github.com/panva/node-oidc-provider/releases) - [Changelog](https://github.com/panva/node-oidc-provider/blob/main/CHANGELOG.md) - [Commits](panva/node-oidc-provider@v9.6.1...v9.7.0) --- updated-dependencies: - dependency-name: oidc-provider dependency-version: 9.7.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [axios](https://github.com/axios/axios) from 1.13.5 to 1.13.6. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](axios/axios@v1.13.5...v1.13.6) --- updated-dependencies: - dependency-name: axios dependency-version: 1.13.6 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…r#7366) Bumps the dev-dependencies group with 7 updates: | Package | From | To | | --- | --- | --- | | [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.3.5` | `25.4.0` | | [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.0.18` | `4.1.0` | | [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `8.56.1` | `8.57.0` | | [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `8.56.1` | `8.57.0` | | [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) | `5.1.4` | `6.0.0` | | [i18next](https://github.com/i18next/i18next) | `25.8.16` | `25.8.18` | | [react-i18next](https://github.com/i18next/react-i18next) | `16.5.6` | `16.5.8` | Updates `@types/node` from 25.3.5 to 25.4.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `vitest` from 4.0.18 to 4.1.0 - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.0/packages/vitest) Updates `@typescript-eslint/eslint-plugin` from 8.56.1 to 8.57.0 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.57.0/packages/eslint-plugin) Updates `@typescript-eslint/parser` from 8.56.1 to 8.57.0 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.57.0/packages/parser) Updates `@vitejs/plugin-react` from 5.1.4 to 6.0.0 - [Release notes](https://github.com/vitejs/vite-plugin-react/releases) - [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@6.0.0/packages/plugin-react) Updates `i18next` from 25.8.16 to 25.8.18 - [Release notes](https://github.com/i18next/i18next/releases) - [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md) - [Commits](i18next/i18next@v25.8.16...v25.8.18) Updates `react-i18next` from 16.5.6 to 16.5.8 - [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md) - [Commits](i18next/react-i18next@v16.5.6...v16.5.8) --- updated-dependencies: - dependency-name: "@types/node" dependency-version: 25.4.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: vitest dependency-version: 4.1.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: "@typescript-eslint/eslint-plugin" dependency-version: 8.57.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: "@typescript-eslint/parser" dependency-version: 8.57.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: "@vitejs/plugin-react" dependency-version: 6.0.0 dependency-type: direct:development update-type: version-update:semver-major dependency-group: dev-dependencies - dependency-name: i18next dependency-version: 25.8.18 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: react-i18next dependency-version: 16.5.8 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](actions/upload-artifact@v6...v7) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [esbuild](https://github.com/evanw/esbuild) from 0.27.3 to 0.27.4. - [Release notes](https://github.com/evanw/esbuild/releases) - [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md) - [Commits](evanw/esbuild@v0.27.3...v0.27.4) --- updated-dependencies: - dependency-name: esbuild dependency-version: 0.27.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [express-rate-limit](https://github.com/express-rate-limit/express-rate-limit) from 8.2.2 to 8.3.1. - [Release notes](https://github.com/express-rate-limit/express-rate-limit/releases) - [Commits](express-rate-limit/express-rate-limit@v8.2.2...v8.3.1) --- updated-dependencies: - dependency-name: express-rate-limit dependency-version: 8.3.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…r#7368) Bumps the dev-dependencies group with 3 updates: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node), [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) and [vite-plugin-static-copy](https://github.com/sapphi-red/vite-plugin-static-copy). Updates `@types/node` from 25.4.0 to 25.5.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@vitejs/plugin-react` from 6.0.0 to 6.0.1 - [Release notes](https://github.com/vitejs/vite-plugin-react/releases) - [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@6.0.1/packages/plugin-react) Updates `vite-plugin-static-copy` from 3.2.0 to 3.3.0 - [Release notes](https://github.com/sapphi-red/vite-plugin-static-copy/releases) - [Changelog](https://github.com/sapphi-red/vite-plugin-static-copy/blob/main/CHANGELOG.md) - [Commits](https://github.com/sapphi-red/vite-plugin-static-copy/compare/vite-plugin-static-copy@3.2.0...vite-plugin-static-copy@3.3.0) --- updated-dependencies: - dependency-name: "@types/node" dependency-version: 25.5.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: "@vitejs/plugin-react" dependency-version: 6.0.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: vite-plugin-static-copy dependency-version: 3.3.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
) Bumps [rate-limiter-flexible](https://github.com/animir/node-rate-limiter-flexible) from 9.1.1 to 10.0.1. - [Release notes](https://github.com/animir/node-rate-limiter-flexible/releases) - [Commits](animir/node-rate-limiter-flexible@v9.1.1...v10.0.1) --- updated-dependencies: - dependency-name: rate-limiter-flexible dependency-version: 10.0.1 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [jsdom](https://github.com/jsdom/jsdom) from 28.1.0 to 29.0.0. - [Release notes](https://github.com/jsdom/jsdom/releases) - [Changelog](https://github.com/jsdom/jsdom/blob/v29.0.0/Changelog.md) - [Commits](jsdom/jsdom@v28.1.0...v29.0.0) --- updated-dependencies: - dependency-name: jsdom dependency-version: 29.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…r#7371) Bumps the dev-dependencies group with 2 updates: [sinon](https://github.com/sinonjs/sinon) and [zustand](https://github.com/pmndrs/zustand). Updates `sinon` from 21.0.2 to 21.0.3 - [Release notes](https://github.com/sinonjs/sinon/releases) - [Changelog](https://github.com/sinonjs/sinon/blob/main/docs/changelog.md) - [Commits](sinonjs/sinon@v21.0.2...v21.0.3) Updates `zustand` from 5.0.11 to 5.0.12 - [Release notes](https://github.com/pmndrs/zustand/releases) - [Commits](pmndrs/zustand@v5.0.11...v5.0.12) --- updated-dependencies: - dependency-name: sinon dependency-version: 21.0.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: zustand dependency-version: 5.0.12 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…her#7370) Bumps [reitzig/actions-asciidoctor](https://github.com/reitzig/actions-asciidoctor) from 2.0.3 to 2.0.4. - [Release notes](https://github.com/reitzig/actions-asciidoctor/releases) - [Changelog](https://github.com/reitzig/actions-asciidoctor/blob/master/CHANGELOG.md) - [Commits](reitzig/actions-asciidoctor@v2.0.3...v2.0.4) --- updated-dependencies: - dependency-name: reitzig/actions-asciidoctor dependency-version: 2.0.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [lru-cache](https://github.com/isaacs/node-lru-cache) from 11.2.6 to 11.2.7. - [Changelog](https://github.com/isaacs/node-lru-cache/blob/main/CHANGELOG.md) - [Commits](isaacs/node-lru-cache@v11.2.6...v11.2.7) --- updated-dependencies: - dependency-name: lru-cache dependency-version: 11.2.7 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…r#7376) Bumps the dev-dependencies group with 2 updates: [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) and [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser). Updates `@typescript-eslint/eslint-plugin` from 8.57.0 to 8.57.1 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.57.1/packages/eslint-plugin) Updates `@typescript-eslint/parser` from 8.57.0 to 8.57.1 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.57.1/packages/parser) --- updated-dependencies: - dependency-name: "@typescript-eslint/eslint-plugin" dependency-version: 8.57.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: "@typescript-eslint/parser" dependency-version: 8.57.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [oidc-provider](https://github.com/panva/node-oidc-provider) from 9.7.0 to 9.7.1. - [Release notes](https://github.com/panva/node-oidc-provider/releases) - [Changelog](https://github.com/panva/node-oidc-provider/blob/main/CHANGELOG.md) - [Commits](panva/node-oidc-provider@v9.7.0...v9.7.1) --- updated-dependencies: - dependency-name: oidc-provider dependency-version: 9.7.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…r#7381) Bumps the dev-dependencies group with 2 updates: [i18next](https://github.com/i18next/i18next) and [vite-plugin-babel](https://github.com/owlsdepartment/vite-plugin-babel). Updates `i18next` from 25.8.18 to 25.8.19 - [Release notes](https://github.com/i18next/i18next/releases) - [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md) - [Commits](i18next/i18next@v25.8.18...v25.8.19) Updates `vite-plugin-babel` from 1.5.1 to 1.6.0 - [Commits](https://github.com/owlsdepartment/vite-plugin-babel/commits) --- updated-dependencies: - dependency-name: i18next dependency-version: 25.8.19 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: vite-plugin-babel dependency-version: 1.6.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [jose](https://github.com/panva/jose) from 6.2.1 to 6.2.2. - [Release notes](https://github.com/panva/jose/releases) - [Changelog](https://github.com/panva/jose/blob/main/CHANGELOG.md) - [Commits](panva/jose@v6.2.1...v6.2.2) --- updated-dependencies: - dependency-name: jose dependency-version: 6.2.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…d unit (…" (ether#7582) This reverts commit 6bb879e.
|
/review |
Code Review by Qodo
1.
|
Review Summary by QodoAccessibility: dialog semantics, focus management, icon labels, and html lang
WalkthroughsDescription• Add lang and dir attributes to HTML templates negotiated from Accept-Language header • Implement dialog semantics (role, aria-modal, aria-labelledby/aria-label) on all popups • Add focus management: remember trigger element, focus first focusable on open, restore on close • Enable Escape key to close popups while preserving focus restoration • Convert icon-only controls to semantic buttons with accessible labels (chat icon, close/pin buttons, show-more, export links) • Fix invalid aria-role="document" typo on userlist; replace with role="region" and aria-live="polite" • Add comprehensive Playwright test suite covering dialog ARIA, lang attribute, focus behavior, and button semantics Diagramflowchart LR
A["Templates<br/>pad.html, index.html,<br/>timeslider.html"] -->|"Add lang/dir<br/>from Accept-Language"| B["HTML Element<br/>lang & dir attrs"]
C["Popup Elements<br/>#settings, #users,<br/>#embed, etc."] -->|"Add ARIA<br/>semantics"| D["Dialog Roles<br/>aria-modal, aria-labelledby"]
E["toggleDropDown<br/>in pad_editbar.ts"] -->|"Remember trigger &<br/>focus management"| F["Focus Restoration<br/>on popup close"]
G["Escape Key<br/>Handler"] -->|"Close popup<br/>& restore focus"| F
H["Icon-only Controls<br/>chat, export, show-more"] -->|"Convert to buttons<br/>with aria-label"| I["Semantic Buttons<br/>with accessible names"]
J["Tests<br/>a11y_dialogs.spec.ts"] -->|"Verify all<br/>changes"| K["Dialog & Focus<br/>Coverage"]
File Changes1. src/templates/pad.html
|
Code Review by Qodo
1. Escape won’t close userlist
|
) * fix(editor): preserve U+00A0 non-breaking space (ether#3037) Non-breaking spaces were silently normalized to regular spaces at every ingestion point, so typed/pasted/imported nbsps never reached the changeset and users could not glue words against line-wrap in French or other languages that require nbsp typography. Removed the four strip sites that replaced U+00A0 with U+0020: - src/node/db/Pad.ts cleanText - src/static/js/contentcollector.ts textify - src/static/js/ace2_inner.ts textify - src/static/js/ace2_inner.ts importText raw-text guard Updated both processSpaces functions (domline and ExportHtml) to tokenize U+00A0 as a separate unit, emit it verbatim as , and treat it as content (not whitespace) for the run-collapse bookkeeping so adjacent regular-space runs aren't miscounted. Added backend round-trip tests for spliceText and setText, and extended the cleanText case table. Updated the existing contentcollector and importexport specs whose expectations encoded the previous buggy behavior; they now assert genuine nbsp preservation. Verified manually in Firefox: clipboard U+00A0 → paste → pad → getText returns c2 a0; getHTML emits `100 km`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(contentcollector): collapse display-artifact nbsp runs on DOM read-back processSpaces is a lossy one-way display transform: leading/trailing spaces and all-but-the-last of a run get rendered as so HTML doesn't collapse them. When incorporateUserChanges reads text back from the DOM, those display-artifact nbsps were being stored in the changeset model instead of being normalized back to plain spaces. This broke handleReturnIndentation, whose /^ *(?:)/ regex only matches ASCII spaces: auto-indent after `foo:\n` produced 4 spaces instead of the expected prev-indent (2) + THE_TAB (4) = 6, because the previous line's model had nbsps where it used to have spaces. Fix: in contentcollector.textify, collapse any [ ]+ run back to plain spaces UNLESS the run is pure U+00A0 AND strictly interior to word chars. That preserves user-intended typographic nbsps like "100 km" while undoing the one-way display transform. Updated 7 contentcollector tests and 7 importexport tests whose assertions needed to reflect the new rule (boundary/mixed runs collapse; pure-interior nbsp runs preserve). Fixes the Playwright regression in indentation.spec.ts:117 that the previous commit introduced. * fix(contentcollector): canonicalize nbsp runs at line assembly, not per text node Addresses Qodo code review feedback on PR ether#7585. ## Bug fix — nbsp lost at DOM text-node boundary The previous approach ran the "collapse display-artifact nbsp" rule inside textify(), which is called per individual DOM TEXT_NODE. A user-intended nbsp sitting at a text-node boundary (e.g., <span>100</span><span> km </span>) was incorrectly seen as non-interior (before === '' for the second text node) and normalized back to a regular space. Fix: move the canonicalization out of textify() and run it on each fully assembled line string inside cc.finish(). The rule remains: [ ]+ run -> plain spaces UNLESS pure U+00A0 AND strictly interior to non-ws chars It is length-preserving, so attribute offsets and line lengths are unaffected. Added a regression test (contentcollector.spec.ts) for the cross-span case. ## Docs concern Reverted the type-only addition of spliceText to PadType. spliceText is an existing Pad runtime method; the backend test now uses a cast (`(pad as any).spliceText`) so the PR does not expand the declared public type surface, avoiding a separate documentation requirement. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tization (ether#7587) PadMessageHandler built the `pluginsSanitized` payload for clientVars by aliasing `plugins.plugins` and then mutating each entry's `package` field in place: let pluginsSanitized: any = plugins.plugins; Object.keys(plugins.plugins).forEach(function(element) { const p: any = plugins.plugins[element].package; pluginsSanitized[element].package = {name: p.name, version: p.version}; }); Because `pluginsSanitized` is a reference to `plugins.plugins`, the assignment clobbered the server-side plugin registry. After the first pad connection, every plugin's `package` object held only `{name, version}` — `realPath`, `path`, and `location` were gone. Minify.ts resolves `/static/plugins/ep_*/...` URLs via `plugin.package.realPath`. Once the field disappeared, every subsequent static asset request for a bundled plugin 500'd with: TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string. Received undefined at Object.join (node:path:1354:7) at _minify (src/node/utils/Minify.ts:181:23) Symptoms on Chromium: plugin CSS/JS assets fail to load (e.g. /static/plugins/ep_font_size/static/css/size.css returns 500), so plugins partially render or don't work at all. Firefox swallows the resulting console errors quietly. Fix: extract the sanitization into a pure helper `sanitizePluginsForWire` that returns a fresh object graph and never touches the input. The helper is covered by a new backend spec that: * verifies the sanitized output has only {name, version} in `package` * asserts the input registry's realPath/path/location survive the call * runs the call repeatedly and confirms non-destructiveness * mutates the returned copy and asserts the input is independent Verified live with the dev server: before the fix, `/static/plugins/ ep_font_size/static/css/size.css` 500'd after visiting any pad; after the fix it returns 200 both before and after pad connections.
Server-renders the html element with `lang` and `dir` matching the client's Accept-Language header (negotiated against availableLangs from i18n hooks). Falls back to `en`/`ltr` if no match. This gives screen readers a correct document language during the brief window before client-side html10n refines it (l10n.ts already sets both attributes after locale data loads). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds role=dialog, aria-modal=true, and either aria-labelledby (when an h1 is present) or aria-label (for popups without an h1) to: - #settings, #import_export, #embed, #skin-variants (labelledby) - #connectivity, #users, #mycolorpicker (aria-label) Fixes the invalid aria-role="document" attribute on #otherusers; it's now role=region with aria-live=polite so screen readers announce collaborator joins/leaves. Container aria-label values are English-only for now — Etherpad's html10n implementation only supports localizing specific attributes (title, alt, placeholder, etc), not aria-label on container nodes. Localization can follow once html10n grows that affordance. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three additions to toggleDropDown / _bodyKeyEvent:
- Remember the trigger element (document.activeElement) when opening
a popup, so we can restore focus when it closes.
- On open, focus the first focusable element inside the popup so
keyboard users land inside the dialog instead of staying on the
trigger button.
- Escape pressed while focus is inside a popup closes it, then the
restore-focus path runs and the trigger button is refocused.
Replaces the previous behavior where Escape from inside a popup did
nothing; users had to click outside to dismiss.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- #chaticon: <div onclick> → <button type=button> with aria-label - #titlecross / #titlesticky: <a onClick> → <button type=button> with aria-label (Close chat / Pin chat to screen) - Decorative chat-bubble glyph gets aria-hidden=true so it isn't read alongside the button label - #chatcounter labelled "Unread messages" - Inline onclick attributes moved to chat.init() handlers - CSS reset on the new buttons (transparent bg, no border, inherit font/color) so they match the prior visual design - :focus-visible outlines for keyboard users Existing test selectors (#chaticon, #titlecross, #titlesticky) are unchanged and continue to work — they never relied on element type. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Export links (#exportetherpada, #exporthtmla, #exportplaina, #exportworda, #exportpdfa, #exportopena): added aria-label so the link is announced as e.g. "Export as PDF". The inner icon span gets aria-hidden=true so screen readers don't read both the icon text and the link label. - Show-more toolbar toggle (.show-more-icon-btn): converted from <span> to <button type=button> with aria-label and aria-expanded. The click handler now toggles aria-expanded alongside the full-icons class so assistive tech reflects the open/closed state. - Theme switcher knob: aria-label changed from "theme-switcher-knob" (a class-style identifier, not human text) to "Toggle theme". Aria-label values are English-only for now. Etherpad's html10n implementation only localizes a fixed attribute list (title, alt, placeholder, value, innerHTML, textContent); aria-label is not included, so a clean l10n path requires a follow-up to either extend html10n or set aria-label client-side after locale loads. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New Playwright spec verifies the a11y guarantees added by this branch:
- <html> has a non-empty lang attribute
- settings/import_export/embed/users popups expose role=dialog,
aria-modal=true, and either aria-labelledby (when an h1 exists)
or aria-label (when none does)
- Escape from inside the settings popup closes it AND restores
focus to the trigger button
- Export links each carry a descriptive aria-label
- #chaticon is a real <button> with aria-label
- #titlecross / #titlesticky are real <button>s with aria-label
- #otherusers uses role=region + aria-live=polite + aria-label
(and the previous aria-role typo is gone)
- .show-more-icon-btn is a <button> with aria-label and
aria-expanded
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
818dceb to
68a0761
Compare
1. Users Escape close broken - toggleDropDown('none') intentionally
skips the users module so switching between other popups doesn't
hide the user list. That meant Escape couldn't dismiss the Users
popup either. The Escape branch now checks for #users as the
focused popup and closes it explicitly (respecting stickyUsers)
before falling through to the normal close-all path.
2. Embed focus overridden - the rAF auto-focus in toggleDropDown
grabbed the first focusable descendant, which stole focus from
command handlers that target a specific control (notably the Embed
command's #linkinput). rAF now bails out if focus is already
inside the newly-opened popup.
3. Button click blurs :focus before toggleDropDown captures trigger -
discovered while investigating the Firefox Playwright failure for
"settings popup Escape restores focus". Button.bind() calls
$(':focus').trigger('blur') before invoking the callback, so by
the time toggleDropDown() captured document.activeElement as the
restore target it was already <body>. The click handler now
stashes padeditbar._lastTrigger to the clicked <button> before
blur runs; toggleDropDown only falls back to activeElement when
the pre-stash didn't happen (keyboard shortcut path).
4. html10n overwrites aria-label - html10n unconditionally set
aria-label to the translated string, clobbering explicit aria-label
on elements that also carry data-l10n-id. setAttribute now only
fires when the element has no aria-label; explicit author labels
win, unlabelled translated elements still get a name.
5. Button visual reset - the show-more-icon-btn and #chaticon
conversions inherited UA default button border/background/padding,
shifting icon glyphs visibly off-centre. Added appearance /
background / border / padding resets.
6. Export links test assumes soffice is installed - #exportworda,
#exportpdfa, #exportopena are removed client-side by pad_impexp.ts
when clientVars.exportAvailable === 'no'. The test now skips links
absent at runtime.
Verified locally: all 10 a11y_dialogs specs pass on both Chromium and
Firefox; backend suite remains 799/799 passing; ts-check clean.
…layout Round 2 of ether#7584 review follow-ups. 1. Users popup Escape still didn't close the dialog (user-confirmed). Root cause: _bodyKeyEvent is bound to the OUTER document's body. When #users opens, the command handler tries to focus #myusernameedit but that input is `disabled`, so focus stays in the ace editor iframe. Keydown from inside the iframe does not bubble to the outer document, so Esc never reaches _bodyKeyEvent. Fix: in the open-popup rAF, if no command handler placed focus inside the dialog, focus the popup div itself (with tabindex=-1). That keeps subsequent keydown events on the outer document so Esc can dismiss the popup. Also broadened the Esc branch to fire whenever any popup is `.popup-show`, regardless of where :focus lives — some popups legitimately have no focusable content at open. Added a regression test that opens #users and asserts Esc closes it. Passes on both Chromium and Firefox. 2. Chat icon (#chaticon) visual still wrong after the first CSS fix. - My previous `border: 0` reset was overriding the intended `border: 1px solid #ccc; border-bottom: none` from the earlier rule. Removed `border: 0`; the earlier explicit border suffices to suppress UA defaults. - The `<span class="buttonicon">` inside `#chaticon` was picking up the global `.buttonicon { display: flex; }` rule meant for toolbar button instances, which broke the inline layout of the label + glyph + counter row. Added a scoped `#chaticon .buttonicon { display: inline; }` override. All 11 a11y_dialogs specs pass on Chromium and Firefox. Backend suite and ts-check remain clean.
Round 3 follow-up. The previous Button.bind() change stashed every clicked toolbar button as padeditbar._lastTrigger before blurring :focus. That was necessary for popup-opening buttons (settings, import_export, etc.) so Escape could return focus to them — but it also fired for non-popup toolbar buttons (list toggles, bold/italic, indent/outdent, clearauthorship). For those, the stash held a stale reference that interfered with subsequent editor interactions and regressed Playwright tests: ordered_list, unordered_list, undo_clear_authorship. Fix: only stash when the clicked command is a registered dropdown (settings, import_export, embed, showusers, savedrevision, connectivity). Other commands return focus to the ace editor as before and leave _lastTrigger alone. Verified locally on Chromium: - ordered_list.spec.ts: 6/6 pass (was 4/6) - unordered_list.spec.ts: 6/6 pass (was 4/6) - undo_clear_authorship.spec.ts: 2/2 pass (was 0/2) - a11y_dialogs.spec.ts: 11/11 pass (unchanged)
#1 Stale aria-label after relocalize html10n.translateNode() refused to overwrite any existing aria-label, which also skipped updates on language change (pad.applyLanguage() re-runs localize). Use a `data-l10n-aria-label="true"` marker: set aria-label + marker when html10n populates it, overwrite only if the marker is present. Explicit template-supplied aria-labels stay as-is; html10n-generated ones refresh on relocalize. #2 Escape won't close colorpicker _bodyKeyEvent caught Escape on any `.popup.popup-show` but only closed dropdown popups via toggleDropDown('none'). Popups opened outside the editbar framework (#mycolorpicker, toggled directly by pad_userlist.ts) stayed open while preventDefault() swallowed the key. Now the Escape branch manually closes any popup that toggleDropDown('none') cannot reach (non-dropdown ids, plus #users unless pinned) and leaves registered dropdowns for toggleDropDown to close so its focus-restore sees the transition. ether#3 Stale focus restoration toggleDropDown('none') restored focus to _lastTrigger even when no popup was open on entry, which meant background callers (connectivity setup, periodic state handling) could yank focus out of the editor to a stale toolbar button. Gated the restore on `wasAnyOpen === true` so it only fires when there was a popup to close. ether#11 English aria-label overrides i18n (export links, chat icon) Removed the hard-coded English aria-label from export anchors and removed aria-hidden from their inner localized spans. Screen readers now get the localized child text as the accessible name (Etherpad, HTML, PDF, etc.), matching the visible UI language. Removed the English aria-label from #chaticon and #titlesticky as well — both have data-l10n-id, so html10n populates a localized aria-label via the marker mechanism in #1. #titlecross keeps its static aria-label because it has no data-l10n-id yet. ether#4 4-space indent in a11y spec Two tests had continuation lines at 4-space indent violating the repo's 2-space rule. Folded the signatures onto one line. Updated a11y_dialogs.spec.ts to assert accessible-name presence rather than hard-coded English for elements whose names now come from the localized text. Still asserts static English for #titlecross (not localized yet). Verified locally (dev server restarted for each round): - a11y_dialogs.spec.ts: 11/11 on Chromium, 11/11 on Firefox - ordered_list + unordered_list + undo_clear_authorship: 13/13 on Chromium - Full backend suite: 799 passing, 0 failing - tsc --noEmit clean in our code ether#9 Popup behavior documentation: deferred to a follow-up doc PR so this PR stays focused on the a11y code changes. The new keyboard behavior (Escape-to-close, focus-restore-to-trigger) is small enough to summarize in a short doc/ addition.
…ether#7584) * fix(a11y): negotiate lang/dir per request and set on <html> Server-renders the html element with `lang` and `dir` matching the client's Accept-Language header (negotiated against availableLangs from i18n hooks). Falls back to `en`/`ltr` if no match. This gives screen readers a correct document language during the brief window before client-side html10n refines it (l10n.ts already sets both attributes after locale data loads). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(a11y): dialog semantics on popups; fix aria-role typo on userlist Adds role=dialog, aria-modal=true, and either aria-labelledby (when an h1 is present) or aria-label (for popups without an h1) to: - #settings, #import_export, #embed, #skin-variants (labelledby) - #connectivity, #users, #mycolorpicker (aria-label) Fixes the invalid aria-role="document" attribute on #otherusers; it's now role=region with aria-live=polite so screen readers announce collaborator joins/leaves. Container aria-label values are English-only for now — Etherpad's html10n implementation only supports localizing specific attributes (title, alt, placeholder, etc), not aria-label on container nodes. Localization can follow once html10n grows that affordance. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(a11y): focus management and Escape-to-close for popups Three additions to toggleDropDown / _bodyKeyEvent: - Remember the trigger element (document.activeElement) when opening a popup, so we can restore focus when it closes. - On open, focus the first focusable element inside the popup so keyboard users land inside the dialog instead of staying on the trigger button. - Escape pressed while focus is inside a popup closes it, then the restore-focus path runs and the trigger button is refocused. Replaces the previous behavior where Escape from inside a popup did nothing; users had to click outside to dismiss. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(a11y): make chaticon and chat header controls real buttons - #chaticon: <div onclick> → <button type=button> with aria-label - #titlecross / #titlesticky: <a onClick> → <button type=button> with aria-label (Close chat / Pin chat to screen) - Decorative chat-bubble glyph gets aria-hidden=true so it isn't read alongside the button label - #chatcounter labelled "Unread messages" - Inline onclick attributes moved to chat.init() handlers - CSS reset on the new buttons (transparent bg, no border, inherit font/color) so they match the prior visual design - :focus-visible outlines for keyboard users Existing test selectors (#chaticon, #titlecross, #titlesticky) are unchanged and continue to work — they never relied on element type. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(a11y): accessible names for icon-only toolbar/export controls - Export links (#exportetherpada, #exporthtmla, #exportplaina, #exportworda, #exportpdfa, #exportopena): added aria-label so the link is announced as e.g. "Export as PDF". The inner icon span gets aria-hidden=true so screen readers don't read both the icon text and the link label. - Show-more toolbar toggle (.show-more-icon-btn): converted from <span> to <button type=button> with aria-label and aria-expanded. The click handler now toggles aria-expanded alongside the full-icons class so assistive tech reflects the open/closed state. - Theme switcher knob: aria-label changed from "theme-switcher-knob" (a class-style identifier, not human text) to "Toggle theme". Aria-label values are English-only for now. Etherpad's html10n implementation only localizes a fixed attribute list (title, alt, placeholder, value, innerHTML, textContent); aria-label is not included, so a clean l10n path requires a follow-up to either extend html10n or set aria-label client-side after locale loads. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(a11y): cover dialog semantics, html lang, icon button labels New Playwright spec verifies the a11y guarantees added by this branch: - <html> has a non-empty lang attribute - settings/import_export/embed/users popups expose role=dialog, aria-modal=true, and either aria-labelledby (when an h1 exists) or aria-label (when none does) - Escape from inside the settings popup closes it AND restores focus to the trigger button - Export links each carry a descriptive aria-label - #chaticon is a real <button> with aria-label - #titlecross / #titlesticky are real <button>s with aria-label - #otherusers uses role=region + aria-live=polite + aria-label (and the previous aria-role typo is gone) - .show-more-icon-btn is a <button> with aria-label and aria-expanded Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(a11y): address Qodo review feedback from PR ether#7584 1. Users Escape close broken - toggleDropDown('none') intentionally skips the users module so switching between other popups doesn't hide the user list. That meant Escape couldn't dismiss the Users popup either. The Escape branch now checks for #users as the focused popup and closes it explicitly (respecting stickyUsers) before falling through to the normal close-all path. 2. Embed focus overridden - the rAF auto-focus in toggleDropDown grabbed the first focusable descendant, which stole focus from command handlers that target a specific control (notably the Embed command's #linkinput). rAF now bails out if focus is already inside the newly-opened popup. 3. Button click blurs :focus before toggleDropDown captures trigger - discovered while investigating the Firefox Playwright failure for "settings popup Escape restores focus". Button.bind() calls $(':focus').trigger('blur') before invoking the callback, so by the time toggleDropDown() captured document.activeElement as the restore target it was already <body>. The click handler now stashes padeditbar._lastTrigger to the clicked <button> before blur runs; toggleDropDown only falls back to activeElement when the pre-stash didn't happen (keyboard shortcut path). 4. html10n overwrites aria-label - html10n unconditionally set aria-label to the translated string, clobbering explicit aria-label on elements that also carry data-l10n-id. setAttribute now only fires when the element has no aria-label; explicit author labels win, unlabelled translated elements still get a name. 5. Button visual reset - the show-more-icon-btn and #chaticon conversions inherited UA default button border/background/padding, shifting icon glyphs visibly off-centre. Added appearance / background / border / padding resets. 6. Export links test assumes soffice is installed - #exportworda, #exportpdfa, #exportopena are removed client-side by pad_impexp.ts when clientVars.exportAvailable === 'no'. The test now skips links absent at runtime. Verified locally: all 10 a11y_dialogs specs pass on both Chromium and Firefox; backend suite remains 799/799 passing; ts-check clean. * fix(a11y): close popups with no focusable content; unbreak chat-icon layout Round 2 of ether#7584 review follow-ups. 1. Users popup Escape still didn't close the dialog (user-confirmed). Root cause: _bodyKeyEvent is bound to the OUTER document's body. When #users opens, the command handler tries to focus #myusernameedit but that input is `disabled`, so focus stays in the ace editor iframe. Keydown from inside the iframe does not bubble to the outer document, so Esc never reaches _bodyKeyEvent. Fix: in the open-popup rAF, if no command handler placed focus inside the dialog, focus the popup div itself (with tabindex=-1). That keeps subsequent keydown events on the outer document so Esc can dismiss the popup. Also broadened the Esc branch to fire whenever any popup is `.popup-show`, regardless of where :focus lives — some popups legitimately have no focusable content at open. Added a regression test that opens #users and asserts Esc closes it. Passes on both Chromium and Firefox. 2. Chat icon (#chaticon) visual still wrong after the first CSS fix. - My previous `border: 0` reset was overriding the intended `border: 1px solid #ccc; border-bottom: none` from the earlier rule. Removed `border: 0`; the earlier explicit border suffices to suppress UA defaults. - The `<span class="buttonicon">` inside `#chaticon` was picking up the global `.buttonicon { display: flex; }` rule meant for toolbar button instances, which broke the inline layout of the label + glyph + counter row. Added a scoped `#chaticon .buttonicon { display: inline; }` override. All 11 a11y_dialogs specs pass on Chromium and Firefox. Backend suite and ts-check remain clean. * fix(a11y): only stash _lastTrigger for dropdown-opening buttons Round 3 follow-up. The previous Button.bind() change stashed every clicked toolbar button as padeditbar._lastTrigger before blurring :focus. That was necessary for popup-opening buttons (settings, import_export, etc.) so Escape could return focus to them — but it also fired for non-popup toolbar buttons (list toggles, bold/italic, indent/outdent, clearauthorship). For those, the stash held a stale reference that interfered with subsequent editor interactions and regressed Playwright tests: ordered_list, unordered_list, undo_clear_authorship. Fix: only stash when the clicked command is a registered dropdown (settings, import_export, embed, showusers, savedrevision, connectivity). Other commands return focus to the ace editor as before and leave _lastTrigger alone. Verified locally on Chromium: - ordered_list.spec.ts: 6/6 pass (was 4/6) - unordered_list.spec.ts: 6/6 pass (was 4/6) - undo_clear_authorship.spec.ts: 2/2 pass (was 0/2) - a11y_dialogs.spec.ts: 11/11 pass (unchanged) * fix(a11y): address Qodo review round 4 for PR ether#7584 #1 Stale aria-label after relocalize html10n.translateNode() refused to overwrite any existing aria-label, which also skipped updates on language change (pad.applyLanguage() re-runs localize). Use a `data-l10n-aria-label="true"` marker: set aria-label + marker when html10n populates it, overwrite only if the marker is present. Explicit template-supplied aria-labels stay as-is; html10n-generated ones refresh on relocalize. #2 Escape won't close colorpicker _bodyKeyEvent caught Escape on any `.popup.popup-show` but only closed dropdown popups via toggleDropDown('none'). Popups opened outside the editbar framework (#mycolorpicker, toggled directly by pad_userlist.ts) stayed open while preventDefault() swallowed the key. Now the Escape branch manually closes any popup that toggleDropDown('none') cannot reach (non-dropdown ids, plus #users unless pinned) and leaves registered dropdowns for toggleDropDown to close so its focus-restore sees the transition. ether#3 Stale focus restoration toggleDropDown('none') restored focus to _lastTrigger even when no popup was open on entry, which meant background callers (connectivity setup, periodic state handling) could yank focus out of the editor to a stale toolbar button. Gated the restore on `wasAnyOpen === true` so it only fires when there was a popup to close. ether#11 English aria-label overrides i18n (export links, chat icon) Removed the hard-coded English aria-label from export anchors and removed aria-hidden from their inner localized spans. Screen readers now get the localized child text as the accessible name (Etherpad, HTML, PDF, etc.), matching the visible UI language. Removed the English aria-label from #chaticon and #titlesticky as well — both have data-l10n-id, so html10n populates a localized aria-label via the marker mechanism in #1. #titlecross keeps its static aria-label because it has no data-l10n-id yet. ether#4 4-space indent in a11y spec Two tests had continuation lines at 4-space indent violating the repo's 2-space rule. Folded the signatures onto one line. Updated a11y_dialogs.spec.ts to assert accessible-name presence rather than hard-coded English for elements whose names now come from the localized text. Still asserts static English for #titlecross (not localized yet). Verified locally (dev server restarted for each round): - a11y_dialogs.spec.ts: 11/11 on Chromium, 11/11 on Firefox - ordered_list + unordered_list + undo_clear_authorship: 13/13 on Chromium - Full backend suite: 799 passing, 0 failing - tsc --noEmit clean in our code ether#9 Popup behavior documentation: deferred to a follow-up doc PR so this PR stays focused on the a11y code changes. The new keyboard behavior (Escape-to-close, focus-restore-to-trigger) is small enough to summarize in a short doc/ addition. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses Qodo review on ether#7544: 1. Requirement gap (#1): Add stability detection to focusOnLine()'s reapply loop. When the target line's offsetTop has not changed for 3 consecutive 250ms ticks (~750ms), stop() is called early instead of running the full 10s window. This means once late content is no longer shifting layout, the loop releases the user immediately rather than waiting out maxSettleDuration. 2. Maintainability (ether#4): Add a comment explaining why the previous $.animate({scrollTop}) "needed for FF" path was replaced with a direct .scrollTop() call — the settle interval now covers the late-layout case Firefox originally needed animation for. Also adds a test that the reapply loop exits early so a user-initiated scrollTop=0 after ~2s is not reverted by another reapply tick. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round 2 of Qodo review on ether#7601. Addressing the action-required items: #1 Badge bypassed pad baseURL — derive basePath the same way padBootstrap.js does (`new URL('..', window.location.href).pathname`) and prefix the fetch with it. Subpath deployments now reach /<prefix>/api/version-status instead of 404ing. #2 Updater poller could get stuck — `getCurrentState()` is now inside the try/finally so a one-time loadState() rejection can't leave `checkInFlight=true` and permanently silence polling. ether#3 Updates off hung admin page — UpdatePage now self-fetches and renders explicit `disabled` (404), `unauthorized` (401/403), and `error` states instead of staying on "Loading...". Banner-driven prefetch is still honoured if it landed first. ether#11 NaN polling interval — coerce `checkIntervalHours` to a number, clamp to [1h, 168h], log a warning and fall back to 6h on non-finite input. Math.max(1, NaN) === NaN previously meant a malformed settings.json could turn the poller into a tight loop. ether#13 State validation accepted broken subfields — `isValid()` now inspects `latest.{version,tag,body,publishedAt,htmlUrl,prerelease}`, `vulnerableBelow[].{announcedBy,threshold}`, and `email.{severeAt,vulnerableAt,vulnerableNewReleaseTag}`. A hand-edited file with a number where a string is expected is now treated as corrupt and reset to EMPTY_STATE rather than crashing later in semver parsing or email rendering. ether#14 Badge cache stampede — wrap `computeOutdated()` in a single-flight promise so concurrent requests at cache expiry await one shared computation instead of fanning out into N redundant disk reads. Plus six new state.test.ts cases covering each new validation guard. Pushing back on the remaining items: ether#4 `updates.tier` defaults to `notify` — intentional. The whole point of tier 1 is to surface the "you are behind" signal to admins by default. Opt-in defeats the purpose; the existing failure mode (admin never hears about a security-relevant release) is exactly what this PR is fixing. ether#5/ether#8 Admin status endpoint admin-auth — `currentVersion` is already public via `/health`, so wrapping the route in admin-auth doesn't reduce the disclosure surface meaningfully. Operators who want it gated set `updates.requireAdminForStatus=true` (already wired and covered by the comment on the route handler). ether#10 Plain `https://` URLs in planning doc — planning markdown is viewed in editors and on GitHub where protocol-relative URLs would either render literally or break entirely. Keeping `https://`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-out Round 3 of Qodo review on ether#7544: ether#3 Early exit misses late shifts — image loads / plugin renders past my previous 750ms early-exit window were no longer corrected. Add a `minSettleDuration` of 2s before any early-exit can fire, and bump `stableTicksRequired` from 3 to 4. Hard ceiling stays 10s. ether#4 Offset equality prevents stability — strict === on `offset().top` never matched in the presence of sub-pixel rounding, so the loop ran the full 10s even on stable layouts. Switch to `Math.abs(...) < 1` tolerance. ether#7 Invalid anchors spin interval — when `getCurrentTargetOffset()` keeps returning null (the requested line never resolves), the loop used to run for the full 10s doing nothing. Track consecutive misses and `stop()` after `missingTicksRequired` (8 ticks ≈ 2s). Real "inner doc not yet rendered" cases get the first 2s window. Bump the early-exit test's wait from 2s → 3.5s to clear the new `minSettleDuration` + `stableTicksRequired` window before asserting. Pushing back on remaining Qodo items: #1 Defer scroll until layout settles — the design is "scroll once immediately so the user sees the line, then keep correcting". Deferring all scrolling until "stable" (which is unknowable up front) would visibly hang on `#L...` navigation for seconds while nothing happens. The reapply loop is the deferral. ether#6 FF rationale lost — already addressed in the previous commit (comment on the `scrollTop()` call explaining why the `$.animate({scrollTop})` "needed for FF" path was removed). Qodo's persistent review doesn't track resolution of items that aren't touched by the new commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round 3 of Qodo review on ether#7561: #2 Unknown update_type silently omitted — render() now validates each item's `update_type` against the GROUPS allowlist and raises ValueError. Without this, a typo'd entry (e.g. `external-pr` with a dash) would parse cleanly but never reach the rendered checklist, so the downstream would silently fall off the radar — exactly the failure mode the tracker exists to prevent. ether#5 Traceback on YAML read/parse — main()'s except clause only caught ValueError, so missing-file (FileNotFoundError/OSError) and malformed-YAML (yaml.YAMLError) errors still surfaced as full tracebacks. Broaden to catch all three with a single `type(e).__name__: message` line so the workflow log stays actionable. Plus three new test cases: unknown update_type, missing catalog file, malformed YAML. Pushing back again on: #1/ether#3 4-space → 2-space Python indent — Python files follow PEP 8. Forcing 2-space breaks formatter/linter/IDE defaults and is harder to read than the universal Python convention. The 2-space rule is for JS/TS source. ether#6 Workflow lacks feature flag — already addressed in the previous commit via opt-out gate `vars.SKIP_DOWNSTREAM_TRACKER`. Default stays opt-out because the tracker exists precisely to catch "forgot to enable it at release time" cases. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address remaining Qodo findings on the theme-color rollout: - (#1) Skip emitting the meta entirely when settings.skinName is not colibris — the helper only knows colibris's --bg-color values, so on no-skin or third-party skins the previous code would emit a white meta over a non-white toolbar. - (ether#4) Drop the prefers-color-scheme: dark variant. The pad's client-side dark mode is also gated on a localStorage white-mode override that no media query can express, so the dark meta could paint a dark address bar over a still-light toolbar. The single baseline meta always matches what the user sees on first paint. - (ether#8) Remove the redundant module.exports assignment; rely on the ES named export only (tsx handles the require() interop). - (ether#9) Iterate the toolbar variants in CSS source order and let the last match win, matching the cascade in pad-variants.css when multiple *-toolbar tokens are present. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… (ether#7636) * feat(pad): add <meta name="theme-color"> matching toolbar (ether#7606) Mobile browsers paint the address-bar / status-bar area above the viewport. Without theme-color this is a system color that does not match the Etherpad toolbar, leaving a visible gap above the pad. Render <meta name="theme-color"> server-side so the bar matches the configured toolbar on first paint. Light + dark variants are emitted with prefers-color-scheme media queries when dark mode is enabled. Colors are derived from settings.skinVariants via a new SkinColors helper (mirrors --bg-color in the colibris pad-variants.css). Closes ether#7606 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(timeslider): emit single theme-color matching configured toolbar Qodo flagged a mismatch: timeslider does not switch skin variants on prefers-color-scheme, so emitting a dark theme-color via media query would leave dark-mode devices with a dark address bar over a light toolbar. Drop the media-query metas on timeslider and emit one unconditional theme-color resolved from settings.skinVariants. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(pad): emit unconditional theme-color so dark-OS users still match Qodo flagged that gating the light theme-color on prefers-color-scheme: light leaves no applicable meta on dark-OS devices when enableDarkMode is false — the address bar then uses a system color while the toolbar stays light. Drop the light media query so the light theme-color is the baseline, and let the prefers-color-scheme: dark meta override it when dark mode is enabled. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(theme-color): align dark meta with client-side super-dark override Two related Qodo findings on the SkinColors helper: - The pad client's dark-mode auto-switch (pad.ts L650) forces super-dark-toolbar regardless of the configured skinVariants, so the prefers-color-scheme: dark meta must always be #485365 — not whichever dark variant the operator configured. - When skinVariants only carries a dark token (e.g. dark-toolbar), the previous helper left the baseline meta at #ffffff, so light-OS users would see white above a dark toolbar. Replace toolbarThemeColors() with configuredToolbarColor() (used as the unconditional baseline) and a fixed DARK_MODE_TOOLBAR_COLOR constant (used in the prefers-color-scheme: dark meta). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(theme-color): server-side only, drop fragile dark media query Address remaining Qodo findings on the theme-color rollout: - (#1) Skip emitting the meta entirely when settings.skinName is not colibris — the helper only knows colibris's --bg-color values, so on no-skin or third-party skins the previous code would emit a white meta over a non-white toolbar. - (ether#4) Drop the prefers-color-scheme: dark variant. The pad's client-side dark mode is also gated on a localStorage white-mode override that no media query can express, so the dark meta could paint a dark address bar over a still-light toolbar. The single baseline meta always matches what the user sees on first paint. - (ether#8) Remove the redundant module.exports assignment; rely on the ES named export only (tsx handles the require() interop). - (ether#9) Iterate the toolbar variants in CSS source order and let the last match win, matching the cascade in pad-variants.css when multiple *-toolbar tokens are present. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round 2 of Qodo review on ether#7601. Addressing the action-required items: #1 Badge bypassed pad baseURL — derive basePath the same way padBootstrap.js does (`new URL('..', window.location.href).pathname`) and prefix the fetch with it. Subpath deployments now reach /<prefix>/api/version-status instead of 404ing. #2 Updater poller could get stuck — `getCurrentState()` is now inside the try/finally so a one-time loadState() rejection can't leave `checkInFlight=true` and permanently silence polling. ether#3 Updates off hung admin page — UpdatePage now self-fetches and renders explicit `disabled` (404), `unauthorized` (401/403), and `error` states instead of staying on "Loading...". Banner-driven prefetch is still honoured if it landed first. ether#11 NaN polling interval — coerce `checkIntervalHours` to a number, clamp to [1h, 168h], log a warning and fall back to 6h on non-finite input. Math.max(1, NaN) === NaN previously meant a malformed settings.json could turn the poller into a tight loop. ether#13 State validation accepted broken subfields — `isValid()` now inspects `latest.{version,tag,body,publishedAt,htmlUrl,prerelease}`, `vulnerableBelow[].{announcedBy,threshold}`, and `email.{severeAt,vulnerableAt,vulnerableNewReleaseTag}`. A hand-edited file with a number where a string is expected is now treated as corrupt and reset to EMPTY_STATE rather than crashing later in semver parsing or email rendering. ether#14 Badge cache stampede — wrap `computeOutdated()` in a single-flight promise so concurrent requests at cache expiry await one shared computation instead of fanning out into N redundant disk reads. Plus six new state.test.ts cases covering each new validation guard. Pushing back on the remaining items: ether#4 `updates.tier` defaults to `notify` — intentional. The whole point of tier 1 is to surface the "you are behind" signal to admins by default. Opt-in defeats the purpose; the existing failure mode (admin never hears about a security-relevant release) is exactly what this PR is fixing. ether#5/ether#8 Admin status endpoint admin-auth — `currentVersion` is already public via `/health`, so wrapping the route in admin-auth doesn't reduce the disclosure surface meaningfully. Operators who want it gated set `updates.requireAdminForStatus=true` (already wired and covered by the comment on the route handler). ether#10 Plain `https://` URLs in planning doc — planning markdown is viewed in editors and on GitHub where protocol-relative URLs would either render literally or break entirely. Keeping `https://`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…es (ether#7601) * docs(updater): add four-tier auto-update design spec Four-tier opt-in self-update subsystem (off / notify / manual / auto / autonomous). GitHub Releases as source of truth; install-method auto-detection with admin override; in-process execution with supervisor restart; 60s drain + announce; auto-rollback on health-check failure with crash-loop guard. Pad-side severe/ vulnerable badge that does not leak the running version. Top-level adminEmail with escalating cadence (weekly while vulnerable, monthly while severe). Refs: docs/superpowers/specs/2026-04-25-auto-update-design.md * docs(updater): add PR 1 (Tier 1 notify) implementation plan Bite-sized TDD task breakdown for shipping Tier 1 notify only: - VersionChecker, InstallMethodDetector, UpdatePolicy, Notifier, state modules - /admin/update/status (admin-auth) and /api/version-status (public, no version leak) - Admin UI banner + read-only update page + nav link - Pad-side severe/vulnerable footer badge - Settings: updates.* block + top-level adminEmail - Tests: vitest unit + mocha integration + Playwright admin/pad - CHANGELOG + doc/admin/updates.md PRs 2-4 (manual/auto/autonomous) get their own plans after PR 1 lands. * feat(updater): add shared types for auto-update subsystem * feat(updater): clarify OutdatedLevel and EMPTY_STATE doc, drop path header Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(updater): add semver helpers and vulnerable-below parser Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(updater): tighten semver regex to reject four-part versions * feat(updater): add state persistence with schema validation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(updater): reject null email and array latest in state validation typeof null === 'object' meant {email:null} passed the old isValid check, which would crash downstream Notifier code reading email.severeAt. Likewise, an array would pass the typeof latest === 'object' branch. Introduce isPlainObject helper (null-safe, Array.isArray guard) and use it for both fields. Adds two regression tests covering the exact broken inputs. * feat(updater): add install-method detector with override * feat(updater): add policy evaluator * feat(updater): add GitHub Releases checker with ETag support Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(updater): validate release fields and preserve ETag on prerelease Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(updater): add email cadence decider Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(updater): tagChanged email fires regardless of cadence; drop unused field * feat(settings): add updates.* and adminEmail settings Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(updater): wire boot hook and periodic checker Register expressCreateServer/shutdown hooks in ep.json and implement the boot-wiring module that detects install method, starts the polling interval and runs the notifier dedupe pass each tick. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(updater): add /admin/update/status and /api/version-status endpoints Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * i18n(updater): add english strings for update banner, page, and pad badge * feat(updater): add pad footer badge for severe/vulnerable status * feat(admin-ui): add update banner, page, and nav link Add UpdateStatusPayload to the zustand store, a persistent UpdateBanner rendered in the App layout, a /update page showing version details and changelog, and a Bell nav link — all wired to the /admin/update/status endpoint added in Task 10. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(updater): add Playwright specs for admin banner/page and pad badge * docs(updater): document tier 1 settings, badge, email cadence * refactor(updater): dedupe helpers, fix misleading log, add banner styling - Export stateFilePath from index.ts and import it in updateStatus.ts (removes local duplicate) - Import getEpVersion from Settings.ts in both index.ts and updateStatus.ts (removes two local definitions) - Fix misleading 'backing off' log message — no backoff is implemented, just retries at next interval - Remove EMPTY_STATE_FOR_TESTS re-export from state.ts; state.test.ts now imports EMPTY_STATE directly from types.ts - Add .update-banner and .update-page CSS rules to admin/src/index.css Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(updater): address review feedback — async wrap, tier=off skip, poll race, opt-in admin gate - Wrap /api/version-status and /admin/update/status with a small async helper so a rejected promise becomes next(err) instead of an unhandled rejection. - Short-circuit route registration when updates.tier === 'off' so the heavier opt-out also removes the HTTP surface (matches pre-PR behavior for that case). - Add an in-flight guard around performCheck() so overlapping interval ticks can't race on update-state.json writes or duplicate email decisions; track the initial setTimeout handle and clear it in shutdown(). - Add updates.requireAdminForStatus (default false) so admins can lock /admin/update/status to authenticated admin sessions without disabling the updater. Default false preserves current behavior (the running version is already exposed publicly via /health). Backend specs cover unauth → 401, non-admin → 403, admin → 200. - Bump admin troubleshooting menu count test 5 → 6 to account for the new Update nav link. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(updater): address Qodo round-2 review feedback Round 2 of Qodo review on ether#7601. Addressing the action-required items: #1 Badge bypassed pad baseURL — derive basePath the same way padBootstrap.js does (`new URL('..', window.location.href).pathname`) and prefix the fetch with it. Subpath deployments now reach /<prefix>/api/version-status instead of 404ing. #2 Updater poller could get stuck — `getCurrentState()` is now inside the try/finally so a one-time loadState() rejection can't leave `checkInFlight=true` and permanently silence polling. ether#3 Updates off hung admin page — UpdatePage now self-fetches and renders explicit `disabled` (404), `unauthorized` (401/403), and `error` states instead of staying on "Loading...". Banner-driven prefetch is still honoured if it landed first. ether#11 NaN polling interval — coerce `checkIntervalHours` to a number, clamp to [1h, 168h], log a warning and fall back to 6h on non-finite input. Math.max(1, NaN) === NaN previously meant a malformed settings.json could turn the poller into a tight loop. ether#13 State validation accepted broken subfields — `isValid()` now inspects `latest.{version,tag,body,publishedAt,htmlUrl,prerelease}`, `vulnerableBelow[].{announcedBy,threshold}`, and `email.{severeAt,vulnerableAt,vulnerableNewReleaseTag}`. A hand-edited file with a number where a string is expected is now treated as corrupt and reset to EMPTY_STATE rather than crashing later in semver parsing or email rendering. ether#14 Badge cache stampede — wrap `computeOutdated()` in a single-flight promise so concurrent requests at cache expiry await one shared computation instead of fanning out into N redundant disk reads. Plus six new state.test.ts cases covering each new validation guard. Pushing back on the remaining items: ether#4 `updates.tier` defaults to `notify` — intentional. The whole point of tier 1 is to surface the "you are behind" signal to admins by default. Opt-in defeats the purpose; the existing failure mode (admin never hears about a security-relevant release) is exactly what this PR is fixing. ether#5/ether#8 Admin status endpoint admin-auth — `currentVersion` is already public via `/health`, so wrapping the route in admin-auth doesn't reduce the disclosure surface meaningfully. Operators who want it gated set `updates.requireAdminForStatus=true` (already wired and covered by the comment on the route handler). ether#10 Plain `https://` URLs in planning doc — planning markdown is viewed in editors and on GitHub where protocol-relative URLs would either render literally or break entirely. Keeping `https://`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Closing — the substantive a11y work (dialog semantics, focus management, icon-button conversions, html lang/dir negotiation, a11y_dialogs.spec.ts) is already on ether/etherpad develop via a separate route. The only piece not landed upstream is the export-anchor aria-label / aria-hidden cluster; opening a focused follow-up PR for just that. |
Summary
Highest-impact accessibility fixes from a fresh audit of Etherpad's pad surface. Four themes:
Dialog semantics — every
.popup(#settings,#import_export,#connectivity,#embed,#users,#mycolorpicker,#skin-variants) now exposesrole="dialog",aria-modal="true", and eitheraria-labelledby(when an<h1>is present) oraria-label. Fixed the invalidaria-role="document"on#otherusers(it'srole, notaria-role); nowrole="region"+aria-live="polite"so collaborator joins/leaves get announced.Focus management —
toggleDropDownnow remembers the trigger button, focuses the first focusable element inside the popup on open, and restores focus to the trigger on close. Escape while focus is inside an open popup closes it (previously did nothing — users had to click outside).Real buttons for icon-only controls —
<div id="chaticon" onclick="…">→<button type="button" id="chaticon" aria-label="Open chat"><a id="titlecross" onClick="…">→<button aria-label="Close chat"><a id="titlesticky" onClick="…">→<button aria-label="Pin chat to screen"><span class="show-more-icon-btn">→<button aria-label="Show more toolbar buttons" aria-expanded>(toggles aria-expanded on click)#exportetherpada…#exportopena) get a descriptivearia-label; the inner icon spans getaria-hidden="true"so SR doesn't double-read.aria-label="theme-switcher-knob"(a class-style identifier) →"Toggle theme"(human text).#chatcounterare now properly labelled (aria-hiddenon the icon,aria-label="Unread messages"on the counter).<html>lang/dir negotiated per request —pad.html,index.html,timeslider.htmlnow render withlanganddirmatching the client'sAccept-Language(negotiated againsti18n.availableLangs), falling back toen/ltr. Client-sidel10n.tsalready refines both attributes after html10n loads, so this just gives screen readers a correct language hint during the brief pre-localization window.Out of scope (deliberate)
:focus-visiblesweep.Test plan
// @ts-nocheckso no type regressions possible)#chaticon/#titlecrossselectors which are unchanged)tests/frontend-new/specs/a11y_dialogs.spec.tspasses — covers<html lang>, dialog ARIA, Escape-closes-and-restores-focus, export labels, chat button conversion, userlist region, show-more button🤖 Generated with Claude Code