Add dropdown appearance to applies-switch for versioned code examples - #3739
Add dropdown appearance to applies-switch for versioned code examples#3739florent-leborgne wants to merge 1 commit into
Conversation
18a8335 to
29605d1
Compare
Documentation increasingly shows API payloads that differ across versions and deployment types. The tabs appearance spends a full-width tab bar per snippet and scales poorly on pages with many versioned examples. :appearance: dropdown renders an applies-switch as a compact selector chip attached to the top-right corner of each item's code block, with a floating panel listing the alternatives. Items accept the full applies_to syntax and each carries its own code block and callout list, so callouts always match the selected version. Selections render as compact text labels (9.1+, ECH 8.0+, 9.0 (preview)) derived from the same applicability pipeline as badges, sync across all switches on the page regardless of appearance, and persist per session. Guard rails: every item must start with a code block, otherwise the switch warns and falls back to tabs; unknown :appearance: values do the same. :selected: now marks the default item (previously parsed but ignored); multiple :selected: items warn. Hidden radios no longer intercept pointer events in the selector, and selection sync now also covers keyboard and panel-row selections via change events. Also hardens llms.txt generation against code blocks with no content lines, which crashed the LLM exporter with a NullReferenceException. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
29605d1 to
9d85de5
Compare
reakaleek
left a comment
There was a problem hiding this comment.
Thanks for this. The problem statement is clear, and the guardrails (code-block-first fallback, unknown appearance → tabs, sync across appearances, LLM empty-fence fix) make the feature feel safe to try.
Most of the inline notes are about aligning the dropdown skin with existing markdown chrome, plus a couple of accessibility/validity checks on the custom menu. None of that questions the overall approach.
Happy to chat through the left-vs-right chip placement if useful. Left would match tabs and LTR scanning habits, while keeping the chevron on the right of the label text.
| border-top-right-radius: 0.5rem; | ||
| box-shadow: 0 -2px 6px rgb(0 0 0 / 0.05); |
There was a problem hiding this comment.
These 0.5rem corners (and the panel's rounded-lg / row rounded-md below) read softer than the surrounding markdown chrome. Tabs and applies-switch tabs are square, code blocks use rounded-t-sm, and {dropdown} uses rounded-sm + shadow-xs.
Matching that smaller radius family would help this sit next to existing snippets without looking like a different component set.
| transform: translateY(-4px); | ||
| pointer-events: none; | ||
| transition: | ||
| opacity 0.16s ease, | ||
| transform 0.16s ease; | ||
| } | ||
|
|
||
| .applies-switch-panel-row { | ||
| @apply text-ink-light flex cursor-pointer items-center rounded-md px-3 text-sm whitespace-nowrap; | ||
| height: 1.875rem; | ||
| transition: background-color 0.12s ease; | ||
|
|
||
| &:hover { | ||
| @apply bg-grey-10 text-black; |
There was a problem hiding this comment.
Closed state only uses opacity: 0 and pointer-events: none, so the panel can still sit in the accessibility tree. Screen readers may announce options while the menu looks closed.
visibility: hidden, the hidden attribute, or inert when not .open would keep the closed state truly closed for AT as well.
|
|
||
| .applies-switch-select { | ||
| @apply border-grey-30 absolute top-0 right-0 z-10 flex w-max cursor-pointer flex-col items-stretch overflow-hidden border-1 border-b-0 bg-white; | ||
| border-top-left-radius: 0.5rem; |
There was a problem hiding this comment.
The chip sits on the trailing edge (right-0), while the tabs appearance and {tabs} both start their selectors on the leading edge. In LTR layouts that left edge is also where readers scan first (NN/g on right-justified menus).
Would anchoring with left-0 (and flipping rounded-tr-none → rounded-tl-none on the code corner) keep this aligned with the rest of the switch family? Chevron inside the chip can stay on the right of the label.
| /* Floating options panel, right-aligned 4px below the chip. */ | ||
| .applies-switch-panel { | ||
| @apply absolute right-0 z-20 w-max rounded-lg bg-white p-1; | ||
| top: calc(2.25rem + 1px + 4px); |
There was a problem hiding this comment.
The markdown {dropdown} rotates its chevron with no transition (dropdown.css only sets transform on [open]). The animated rotate here feels a bit busier next to that pattern.
Dropping the chevron transition (instant open/close) would match the existing dropdown. If the panel fade stays, a prefers-reduced-motion off-switch would be good.
| the top padding matches the chip's outer height exactly | ||
| (2.25rem label + 1px top border) so the block's top edge runs | ||
| at the same line beside and under the chip. */ |
There was a problem hiding this comment.
Hiding the current row keeps the panel short, though it also removes the selected item from the list. NN/g generally prefers keeping options in place (and VersionDropdown shows the current version with a check).
With only two items the panel becomes a single alternative, and with one item it can open empty. Showing all rows and marking the current one might be clearer.
| <div class="applies-switch@(Model.IsDropdown ? " applies-switch--dropdown" : "")"> | ||
| @if (Model.IsDropdown) | ||
| { | ||
| <div class="applies-switch-select" title="Select another version"> |
There was a problem hiding this comment.
Open/close is driven by a CSS class on the wrapper, and the accessible name is only a title on this non-focusable div. Assistive tech won't get a clear disclosure control.
Wiring aria-expanded / aria-controls on the trigger (and toggling them from the click handler) would make the open state explicit. Escape close is a nice touch already.
| var id = $"applies-switch-item-{Model.AppliesSwitchIndex}-{Model.Index}"; | ||
| } | ||
|
|
||
| <input class="applies-switch-input" checked="@(Model.Checked ? "checked" : "")" id="@id" name="applies-switch-set-@Model.AppliesSwitchIndex" type="radio" tabindex="0" data-index="@Model.Index"> |
There was a problem hiding this comment.
When Checked is false this can render checked="". For boolean HTML attributes, presence alone can mark the radio checked, so :selected: on a later item might leave more than one input looking selected in the DOM.
Worth confirming Razor omits the attribute when false (e.g. pass null, or a conditional attribute). The tests assert the checked="checked" string, which wouldn't catch an empty checked="".
| function syncSelection(label: HTMLElement) { | ||
| const data = create_key(label) | ||
| if (!data) return | ||
| const [group, id, key] = data | ||
| for (const label of as_id_to_elements[key]) { | ||
| if (label === this) { | ||
| for (const other of as_id_to_elements[key] ?? []) { | ||
| if (other === label) { | ||
| continue | ||
| } | ||
| if (label.previousElementSibling instanceof HTMLInputElement) { | ||
| label.previousElementSibling.checked = true | ||
| if (other.previousElementSibling instanceof HTMLInputElement) { | ||
| other.previousElementSibling.checked = true | ||
| // Setting .checked programmatically fires no change event, so | ||
| // synced dropdowns need their content panes updated explicitly | ||
| const dropdown = other.closest('.applies-switch--dropdown') | ||
| if (dropdown) updateDropdownContents(dropdown) | ||
| } | ||
| } | ||
| window.sessionStorage.setItem(storageKeyPrefix + group, id) | ||
| } |
There was a problem hiding this comment.
Nice catch that programmatic .checked skips the change event. Updating dropdown panes from here keeps tabs ↔ dropdown sync working without fighting the CSS sibling selector used by the tabs appearance.

Docs-eng: treat this as a suggestion/idea
Why
docs-content needs to document API payloads that changed across versions — for example the dashboards API, with a 9.0 technical preview shape and breaking changes in 9.1/serverless. Duplicating a snippet's surrounding prose per version doesn't scale, and the tabs appearance spends a full-width tab bar on every snippet, which gets noisy on pages with many versioned examples.
What
{applies-switch}accepts:appearance: dropdown: a compact selector chip attached to the upper-right corner of each item's code block, opening a floating panel with the alternatives.applies_tosyntax; each item carries its own code block and callout list, so callouts always match the selected version.9.1+,ECH 8.0+,9.0 (preview)) derived from the same applicability pipeline as badges; the full definition shows on hover.applies_todefinitions stay in sync page-wide regardless of appearance (dropdown ↔ tabs), and selection persists per session.:appearance:values). Future content types can be supported by adapting the chip's anchoring, without new syntax.:selected:now sets the default item (previously parsed but ignored); multiple:selected:warn. Also fixes hidden radios intercepting pointer events on the selector, and extends selection sync to keyboard and panel-row selections.NullReferenceException.The default tabs appearance is untouched; docs-content has no existing
:selected:usage, so behavior of existing content is unchanged.Docs: new sections with live demos in
syntax/applies-switch.md. Tests: 18 new (appearance parsing, guard fallback, default selection, panel markup, label format, LLM export).Screenshots
Closed chip and open panel captures to be attached below.
🤖 Generated with Claude Code