refactor(Button,Textarea): deprecate three props that render nothing, and close the #63 test gaps - #529
Conversation
Closes the two halves of #63 that hold up, and records why the third one does not need doing. **Accessibility (#63 §1).** Six component specs had no `axe` case. Four get one here: `Advice`, `Countdown`, `ProsePrompt`, `TableWrapper`. `ScrollArea` and `Theme` are left alone on purpose — `nuxt/ui@v4` skips both for the same reason, `Theme` being `<template><slot /></template>`, so an `axe` run on it measures the slot content and not the component. Each of the four was verified to go red on a mutation of its own component: a stripped `:label` on the prompt's action buttons (`button-name`), `role="img"` on the decorative SVG in `Advice` and `Countdown` (`svg-img-alt`), `role="table"` on the `TableWrapper` root (`aria-required-children`). **Prop coverage (#63 §2).** Of 1140 declared props across the library, 242 were never mentioned in their own spec, and the concentration is in props `nuxt/ui` does not have — so nothing that arrives with an upstream sync will ever cover them. This adds cases for the Bitrix24-only props of `Input`, `Textarea`, `Button`, `Select` and `InputNumber`, keeping to the ones that change rendered markup; props that only move focus or need real layout are listed in a comment instead of pinned by a snapshot that would prove nothing. `activeColor` and `activeDepth` get behavioural tests rather than snapshot cases: both land on a colour the matrix already renders, so a `renderEach` case for either collides with a sibling (#454). The assertion that matters is the override — the prop applies only while the button is active — and it goes red when the `active &&` guard is dropped. No standing prop-coverage utility is added. The scan was a one-off; the result is these tests. **Behavioural tests (#63 §3).** Already done: 36 of 110 component specs carry behavioural assertions against 28 of 116 upstream. Of the issue's initial set, `Select`, `SelectMenu` and `Modal` are covered; `Accordion` is snapshot-only upstream too. Two findings surfaced by the scan, neither fixed here: - `Textarea`'s `fixed` prop is inert. It works through `fixed x size` compound variants and `Textarea` deliberately has no `size`, so the prop is typed, documented and incapable of doing anything. Removing it is a public API change. - `depth` and `activeDepth` on `Button` have compound variants only for the legacy colour names, so they render nothing on the `air-*` colours — which includes the default. Snapshot corpus 27,473,481 -> 27,816,139 bytes (+335 KB, +1.2%). Refs #63
Review of the first commit found that two of the four new accessibility tests were green by vacancy, and that my mutation check on one of them had passed for the wrong reason. Measured by reading axe's own result object rather than only its pass/fail: | Case | rules run, before | after | | --- | --- | --- | | `TableWrapper` | 0 | 5 | | `Countdown` | 0 | 3 | | `Advice` | 1 | 3 | | `ProsePrompt` | 3 | 3 | - `TableWrapper` handed its table markup to the slot as a **string**, which Vue escapes to text, so no table ever reached the DOM. Rebuilt with `h`. The earlier `role="table"` mutation did go red, but on `aria-required-children` — there were no rows to find because there was no table. It now fails on a `role="row"` wrapper for the right reason, with `aria-required-parent` alongside it. - `Countdown`'s ring and digits match no axe rule at all. The case now mounts with an avatar, which renders an `<img>` and brings in `image-alt` and `nested-interactive`. - `Advice` ran one rule. An `icon` sets `isLeading`, which wins the `v-else-if` and drops the avatar entirely, so the case now passes the avatar instead. All three go red on a real regression: stripping `:alt` off the avatar image in `Avatar.vue` fails `Advice` and `Countdown` on `image-alt`. Also adds the `rounded` cases missed in `Input`, `InputNumber` and `Select` — the prop is declared on all four of the affected components, changes markup, and only `Textarea` had got a case for it. Refs #63
All three are typed, documented and forwarded to the theme, and none of them changes a single byte of rendered markup. Surfaced while closing the prop coverage gap in #63 and measured by mounting each component with and without the prop and diffing the HTML. `Button.depth` already carried a deprecation note — spelled `@depricate`, so no IDE, no `vue-tsc` and no docs generator ever saw it. The intent was real: `docs/app/components/content/ComponentProps.vue` filters both `depth` and `activeDepth` out of the props table by name, with a `@memo remove depricate props` comment beside it. This spells the tag correctly and gives it a reason. - `Button.depth`, `Button.activeDepth` — the `depth` compound variants in `src/theme/button.ts` are keyed on the legacy colour names (`default`, `danger`, `success`, `primary`, …), not on `air-*`. The default colour is `air-secondary-no-accent`, so both props are inert on it. Measured: `depth: 'dark'` changes the DOM on `color: 'primary'` and does not on the default. - `Textarea.fixed` — works through `fixed` x `size` compound variants; `src/theme/input.ts` has five of them, `src/theme/textarea.ts` has none, because `Textarea` deliberately has no `size` (`// @memo we remove size`). `fixed` is declared on ten components. Only `Textarea`'s is inert — the other nine were probed the same way and all nine render. The tag goes on `Textarea` alone. Nothing is removed and no behaviour changes; both are 3.0.0 work. The existing tests that exercise `depth` on legacy colours stay, so the behaviour remains pinned until then. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012MsMuj8Fic9tjWVjyEyrxc
…ions Panel review pointed out that every pre-existing `@deprecated` block in this repo pairs the prose with a separate `@removed 3.0.0` tag — see `SidebarSection.vue:7-10` and the other nine `Sidebar*`/`Navbar*` components. The three prop-level deprecations added here folded the removal version into the prose only. Nothing reads `@removed` today; this is house style, kept so a future tool that keys off it finds every deprecation rather than most of them. Checked before adding it: `ComponentPropsLinks.vue` filters `prop.tags` down to `@link` and renders nothing otherwise, so the extra tag changes no docs output. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012MsMuj8Fic9tjWVjyEyrxc
Review panel — five reviewers, run concurrentlyConvened because the PR grew a public-API deprecation; a tests-only change would have needed Outcome: no blocking findings. Two items need a maintainer decision, both about visibility rather than correctness. Verified independentlyThe QA reviewer re-ran every claim in the commit messages rather than reading them:
834/834 across the ten touched specs plus the guard. The engineer reviewer confirmed the deprecation premise from the theme source and strengthened one claim: Security: nothing. The Acted on
Open for the maintainer — 1: the deprecation is invisible on the docs siteFound independently by the documentation and engineer reviewers. Nothing under Adding Teaching the props table to render Open for the maintainer — 2: no paper trail for the 242 uncovered propsThe CTO's one ask. The scan that found them was a one-off script, run by hand and discarded — deliberately, to avoid standing machinery. With a single maintainer, the concern is that the finding leaves with the session. The numbers and the method are recorded in this PR body and in comments in the touched specs, so it is not lost outright. Whether that is enough, or whether it wants a line in #75 (the sync-pipeline tracker, where "an incomplete port shows up as a prop that exists in the types with no behaviour" already belongs), is a maintainer call. Noted, not acted on
Generated by Claude Code |
… by name Nothing under `docs/app` or `docs/server` read the `@deprecated` tag — zero grep hits before this. The two props that carried a deprecation were removed from the output by a hardcoded name list in two places instead: - `docs/app/components/content/ComponentProps.vue` - `docs/server/utils/transformMDC.ts` (`propItemHandler`) That left `Textarea`'s `fixed` rendering as an ordinary prop while its JSDoc said it does nothing, and it could not be fixed by extending the lists: they match on prop name with no component scoping, so adding `fixed` would also have hidden it from the nine components where it works. Both paths now read the tag: - **Props table** — a new `ComponentPropsDeprecated` component, built to mirror the existing `ComponentPropsLinks`. Renders a badge carrying the `@removed` version when there is one, and the `@deprecated` prose through `MDC` like any other description. - **`/raw/` markdown**, which is what `llms.txt` serves — the JSDoc block now carries `@deprecated` and `@removed` lines, so a consumer reading the raw page learns a prop is on its way out instead of silently not seeing it. Both hardcoded lists are gone. `depth` and `activeDepth` therefore reappear in the docs, marked, rather than vanishing — which is the point: someone with `depth="dark"` in their code needs somewhere to find out it is going away. Verified rather than assumed: - The tags survive the pipeline. Ran `vue-component-meta`'s `createChecker` over `Textarea.vue` and `Button.vue` directly: `props[].tags` carries both `deprecated` (full text, newlines intact) and `removed` (`3.0.0`) for all three props. `compactProp` drops only `defaultValue`, so nothing filters them out downstream. - The emitted markdown is right. Ran the new `propItemHandler` block against those exact tag objects: multi-line `@deprecated` text indents its continuation lines, `@removed` follows it, and a prop with no deprecation emits exactly what it did before. Not verified: the rendered docs page. The docs dev server does not start in this environment — `@nuxtjs/mcp-toolkit` throws `completable is not defined` from its own bundle, unrelated to this diff. `nuxt typecheck docs` and `eslint` both pass over the changed files. Refs #63 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012MsMuj8Fic9tjWVjyEyrxc
Both open items closed1 — the deprecation is now visible in the docs (
|
Linked issue
Refs #63 (closes §1 and §2; §3 was already done — see below)
Type of change
revert(Scope): ...)Description
Three parts, in the order they happened: the tests came first, the deprecations are what the tests turned up, and the docs change is what the review panel found missing.
Reviewed by the five-reviewer panel — findings and what was verified independently, and how the two open items were closed.
Part 1 — three props deprecated
All three are typed, JSDoc'd and forwarded to the theme. None of them changes a single byte of rendered markup. Measured, not inferred: each component was mounted with and without the prop and the HTML diffed.
Button.depthdepthcompound variants insrc/theme/button.tsare keyed on the legacy colour names (default,danger,success,primary, …), never onair-*. The default colour isair-secondary-no-accent.Button.activeDepthTextarea.fixedfixed×sizecompound variants.src/theme/input.tshas five of them;src/theme/textarea.tshas none — and declaresfixed: { false: '' }with notruebranch at all — becauseTextareadeliberately has nosize(// @memo we remove size).depth: 'dark'changes the DOM oncolor: 'primary'and does not on the default — that is the measurement, not a reading of the theme.Button.depthwas already meant to be deprecated. It carried the note as@depricate, misspelled, so no IDE, novue-tscand no docs generator ever saw it. This spells the tag correctly, gives it a reason, and adds the@removed 3.0.0tag the repo's existing deprecation blocks all carry.fixedis declared on ten components. OnlyTextarea's is inert — the other nine (Input,InputDate,InputMenu,InputNumber,InputTags,InputTime,PinInput,Select,SelectMenu) were probed the same way and all nine render. The tag goes onTextareaalone.Nothing is removed and no behaviour changes — both are
3.0.0work. The existing tests that exercisedepthon legacy colours stay, so the behaviour is pinned until then.Part 2 — the docs now read the tag
Raised by two reviewers independently: the deprecation would have been invisible where people actually look. Nothing under
docs/appordocs/serverread@deprecated— zero grep hits. The two props that carried a deprecation were dropped from the output by a hardcoded['depth', 'activeDepth']list in two places (ComponentProps.vueandtransformMDC.ts), which leftTextarea.fixedrendering as an ordinary prop while its JSDoc said it does nothing.Extending those lists was never an option — they match on prop name with no component scoping, so adding
fixedwould have hidden it from the nine components where it works.Both paths now read the tag:
ComponentPropsDeprecated.vue, built to mirror the existingComponentPropsLinks.vue: a badge carrying the@removedversion, and the@deprecatedprose throughMDClike any other description./raw/markdown, the sourcellms.txtserves — the emitted JSDoc block now carries@deprecatedand@removedlines.Both hardcoded lists are gone, so
depthandactiveDepthreappear marked rather than vanishing. That is the point: someone withdepth="dark"in their code needs somewhere to learn it is going away.Verified rather than assumed —
vue-component-meta'screateCheckerrun directly over both components returnsdeprecated(full text, newlines intact) andremoved(3.0.0) inprops[].tagsfor all three props, and the new emitter produces:A prop with no deprecation emits byte-for-byte what it did before.
Not verified: the rendered page. The docs dev server does not start in this environment —
@nuxtjs/mcp-toolkitthrowscompletable is not definedfrom its own bundle, which nothing in this diff imports.nuxt typecheck docs,eslintand the four docs-facing specs all pass. Worth a look at the props table on the first docs deploy after merge.Part 3 — the #63 test gaps
§1 — the missing
axecasesSix component specs had no accessibility case. Four get one:
Advice,Countdown,ProsePrompt,TableWrapper.ScrollAreaandThemeare deliberately left without one.nuxt/ui@v4skips exactly those two as well (its own gaps areDataSlot,Icon,ScrollArea,Theme), and the reason is visible in the source —Theme.vueis<template><slot /></template>, a renderless provider, so anaxerun on it measures whatever the test happens to put in the slot rather than the component.An axe test that runs no rules is green for free, so each case was checked by reading axe's own result object, not just its verdict. The first commit got two of the four wrong; the second fixes them:
TableWrapperCountdownAdviceProsePromptThose figures count
results.passes. Three of the four cases additionally report oneincompleterule —color-contrast, which happy-dom cannot evaluate without real layout — not counted above.TableWrapperhanded its table markup to the slot as a string, which Vue escapes to text — no table ever reached the DOM. Rebuilt withh.Countdown's ring and digits match no axe rule at all; the case now mounts with an avatar, which renders an<img>.Adviceran one rule: aniconsetsisLeading, which wins thev-else-ifand drops the avatar. The case passes the avatar instead.Then each was checked against a real regression in its own component:
Advice,Countdown:altstripped from the avatar<img>inAvatar.vueimage-altProsePrompt:labelstripped from the action buttonsbutton-nameTableWrapperrole="row"on the wrapper rootaria-required-children,aria-required-parentWorth recording: the first
TableWrappermutation check did go red, onaria-required-children— but for the wrong reason. There were no rows to find because the escaped string meant there was no table. A mutation going red is not by itself proof the test works.These four cases are narrow by construction — each exercises the one prop combination that makes some rule fire. They do not speak for other configurations of the same components.
§2 — the untested props
A one-off scan over
src/runtime/components/*.vuecompared each component's declared props against its spec: 1140 props declared, 242 never mentioned in their own spec (21 %), and that is a floor — "mentioned" is not "exercised".The concentration is the interesting part. The densest offenders are props
nuxt/uidoes not have:An upstream prop arrives with an upstream test. A Bitrix24-only prop arrives with nothing. So this adds cases for the Bitrix24-only props of the five densest components —
Input,Textarea,Button,Select,InputNumber.Only props that change rendered markup are pinned. Each candidate was measured first; the ones that render nothing (
autofocus,autofocusDelay,autoresizeDelay,maxrows,loadingAuto, Select'scontent) are named in a comment in each spec instead. The three deprecations above came out of the same pass.activeColorandactiveDepthget behavioural tests rather than snapshot cases. Both resolve to a colour the snapshot matrix already renders, so arenderEachcase for either one collides with a sibling — the #454 guard caught this and rejected the first draft. What is worth asserting is the override: the prop applies only while the button is active. Both tests go red when theactive &&guard is dropped fromButton.vue.No prop-coverage utility is added. The scan was run once by hand; the result is these tests. A standing checker would be another config to maintain, and #87 already settled the direction on home-grown test machinery. The measurement, the script and the caveats are recorded as item 6 on the sync-pipeline tracker so the finding outlives this PR.
§3 — behavioural tests
Already done, and ahead of upstream: 36 of 110 of our component specs carry behavioural assertions (
trigger/keydown/emitted/focus) against 28 of 116 innuxt/ui@v4. The issue's initial set is covered apart fromAccordion, which is snapshot-only upstream too.Numbers
main: 340 files, 7804 passed, 6 skippedpnpm lintandpnpm typecheckgreen (typecheckneedspnpm dev:preparefirst — without it three pre-existing#apperrors appear on cleanmainas well)Checklist
🤖 Generated with Claude Code
https://claude.ai/code/session_012MsMuj8Fic9tjWVjyEyrxc