Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 158 additions & 0 deletions docs/syntax/applies-switch.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,163 @@ Other content for version 9.0
:::::
::::::

## Dropdown appearance

Add `:appearance: dropdown` to render the switch as a compact dropdown instead of tabs. This works well for version-specific code examples: readers select the version they run, and the code block, including its callouts, updates to match.

Because each `applies-item` contains both the code block and its callout list, the callouts always match the selected version.

The dropdown appearance requires every `applies-item` to start with a code block: the selector chip attaches to the code block's top edge. A switch with other leading content falls back to the tabs appearance and docs-builder emits a warning.

Instead of badges, the selector shows a compact text form of each `applies_to` definition: Elastic Stack entries show only the version (`9.1+`, `=9.0`, `9.1-9.3`), other entries keep their product or deployment name (`Serverless`, `ECH 8.0+`), and the lifecycle appears in parentheses when it is not GA, for example `9.0 (preview)`. Hover the selector to see the full definition.

The appearance only changes the presentation. Switches with the same `applies_to` definitions stay in sync through the same [automatic grouping](#automatic-grouping) regardless of their appearance: selecting a version in a dropdown switch also updates tab switches on the page, and the other way around.

::::::{tab-set}
:::::{tab-item} Output

::::{applies-switch}
:appearance: dropdown

:::{applies-item} { serverless: ga, stack: ga 9.1+ }
:selected:
```console
PUT api/dashboards/dashboard/my-dashboard
{
"attributes": {
"title": "My dashboard", <1>
"panels": [
{
"type": "metric",
"config": {
"metrics": [ { "field": "system.cpu.usage" } ] <2>
}
}
]
}
}
```

1. The dashboard title, displayed in the dashboard listing.
2. In this version, the metric chart accepts multiple metrics.

:::

:::{applies-item} stack: preview =9.0
```console
PUT api/dashboards/dashboard/my-dashboard
{
"attributes": {
"title": "My dashboard", <1>
"panels": [
{
"type": "metric",
"config": {
"metric": { "field": "system.cpu.usage" } <2>
}
}
]
}
}
```

1. The dashboard title, displayed in the dashboard listing.
2. In this version, the metric chart accepts a single metric.

:::

::::

:::::
:::::{tab-item} Markdown

`````markdown
::::{applies-switch}
:appearance: dropdown

:::{applies-item} { serverless: ga, stack: ga 9.1+ }
:selected:
```console
PUT api/dashboards/dashboard/my-dashboard
{
"attributes": {
"title": "My dashboard", <1>
...
}
}
```

1. The dashboard title, displayed in the dashboard listing.

:::

:::{applies-item} stack: preview =9.0
```console
PUT api/dashboards/dashboard/my-dashboard
{
"attributes": { ... }
}
```

1. The dashboard title, displayed in the dashboard listing.

:::

::::
`````
:::::
::::::

## Default selection

By default, the first `applies-item` is selected. Add the `:selected:` option to an item to select a different one, for example to default to the newest version when older versions come first in the source. If multiple items have `:selected:`, only the first one is honored and docs-builder emits a warning.

::::::{tab-set}
:::::{tab-item} Output

::::{applies-switch}
:appearance: dropdown

:::{applies-item} stack: preview =9.0
```console
GET api/dashboards/dashboard
```
:::

:::{applies-item} stack: ga 9.1+
:selected:
```console
GET api/dashboards/dashboard?page=1
```
:::

::::

:::::
:::::{tab-item} Markdown

````markdown
::::{applies-switch}
:appearance: dropdown

:::{applies-item} stack: preview =9.0
```console
GET api/dashboards/dashboard
```
:::

:::{applies-item} stack: ga 9.1+
:selected:
```console
GET api/dashboards/dashboard?page=1
```
:::

::::
````
:::::
::::::

## Supported `applies_to` definitions

The `applies-item` directive accepts any valid applies_to definition that would work with the `{applies_to}` role.
Expand All @@ -153,3 +310,4 @@ Use applies switches when:
- You want to show applies_to badges as tab titles instead of text
- You need to group related content that differs by applicability
- You want to provide a clear visual indication of what each content section applies to
- You want to offer version-specific code examples without duplicating the surrounding prose: use the dropdown appearance
109 changes: 104 additions & 5 deletions src/Elastic.Documentation.Site/Assets/applies-switch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ function ready() {
}
}
})

// Reflect the (possibly restored) selection in dropdown content panes
$$optional('.applies-switch--dropdown').forEach(updateDropdownContents)
}

/**
Expand All @@ -72,20 +75,116 @@ function ready() {
* @this {HTMLElement} - The element that was clicked.
*/
function onAppliesSwitchLabelClick(this: HTMLLabelElement) {
const data = create_key(this)
syncSelection(this)
}

/**
* Activate the same applies_to selection in all other switches on the page
* and persist it. `label` is the trigger label of the newly selected option.
*/
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)
}
Comment on lines +85 to 102

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.


/**
* Reflect the checked input of a dropdown switch: show the matching content
* pane and hide the matching panel row (the current selection is already
* visible in the chip, the panel only lists the alternatives).
*
* Dropdown switches group their inputs and labels in a selector overlay, so
* the pure-CSS sibling selector used by the tabs appearance cannot reach the
* content panes.
*/
function updateDropdownContents(dropdown: Element) {
const checked = dropdown.querySelector('.applies-switch-input:checked')
if (!checked) return
const index = checked.getAttribute('data-index')
dropdown.querySelectorAll('.applies-switch-content').forEach((content) => {
content.classList.toggle(
'applies-switch-content--active',
content.getAttribute('data-index') === index
)
})
dropdown.querySelectorAll('.applies-switch-panel-row').forEach((row) => {
row.classList.toggle(
'applies-switch-panel-row--current',
row.getAttribute('data-index') === index
)
})
}

/**
* Open/close behavior for switches with the dropdown appearance.
*
* Delegated document-level listeners registered once at module scope so they
* survive htmx swaps (initAppliesSwitch re-runs on every htmx:load).
*/
function closeDropdowns(except?: Element | null) {
document
.querySelectorAll('.applies-switch--dropdown.open')
.forEach((dropdown) => {
if (dropdown !== except) dropdown.classList.remove('open')
})
}

document.addEventListener('click', (event) => {
const target = event.target as HTMLElement
// A click on a label also fires a synthetic click on its radio input;
// ignore it so it doesn't immediately close the dropdown we just opened.
if (target.closest('.applies-switch--dropdown .applies-switch-input')) {
return
}
// Selecting a panel row checks its radio natively (label for=); the
// change listener below does the rest, so only close the menu here.
if (target.closest('.applies-switch-panel-row')) {
closeDropdowns()
return
}
const label = target.closest(
'.applies-switch--dropdown .applies-switch-label'
)
if (!label) {
closeDropdowns()
return
}
const dropdown = label.closest('.applies-switch--dropdown')
if (!dropdown) return
dropdown.classList.toggle('open')
closeDropdowns(dropdown)
})

document.addEventListener('change', (event) => {
const input = (event.target as HTMLElement).closest(
'.applies-switch--dropdown .applies-switch-input'
)
const dropdown = input?.closest('.applies-switch--dropdown')
if (!input || !dropdown) return
updateDropdownContents(dropdown)
// Panel-row and keyboard selections bypass the trigger-label click
// handler, so propagate the sync from here.
const label = input.nextElementSibling
if (label instanceof HTMLElement) syncSelection(label)
})

document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') closeDropdowns()
})

export function initAppliesSwitch() {
ready()
}
Loading
Loading