Skip to content
Merged
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
2 changes: 1 addition & 1 deletion static/css/v3/account-connections.css
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@

.account-connections__platform {
display: flex;
align-items: center;
align-items: baseline;
gap: var(--space-default, 8px);
flex-shrink: 0;
}
Expand Down
43 changes: 27 additions & 16 deletions static/css/v3/install-card.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@
Tabs + dropdown selector + command display for getting-started install instructions.
Uses CSS radio inputs for tab switching and option selection (no JS required).
Builds on .card (card.css), .dropdown (forms.css), .code-block (code-block.css).
The tabs are styled in tab.css
Tabs styled by tab.css.

Architecture — two levels of radio-driven show/hide:
1. Tab radios (.install-card__radio--pkg / --sys) are direct children of <section>.
Static :checked ~ sibling rules below toggle .install-card__panel visibility.
2. Option radios per tab (e.g. Vcbkg, Conan) are siblings of .code-block inside each panel.
Dynamic :checked ~ rules are generated by Django in _install_card.html's
inline <style> block because the number of options is data-driven.
*/

.install-card {
Expand All @@ -27,22 +34,18 @@
.install-card__radio:focus-visible
~ .install-card__body
.tab__list
[for="install-tab-pkg"],
.install-card__radio:focus-visible
~ .install-card__body
.tab__list
[for="install-tab-sys"] {
.tab__trigger {
outline: none;
}

#install-tab-pkg:focus-visible
.install-card__radio--pkg:focus-visible
~ .install-card__body
.tab__list
[for="install-tab-pkg"],
#install-tab-sys:focus-visible
.tab__trigger:first-child,
.install-card__radio--sys:focus-visible
~ .install-card__body
.tab__list
[for="install-tab-sys"] {
.tab__trigger:last-child {
outline: medium auto -webkit-focus-ring-color;
}

Expand Down Expand Up @@ -100,6 +103,10 @@
border: 1px solid var(--color-surface-strong);
}

.install-card__dropdown .dropdown__trigger:focus-visible {
outline: -webkit-focus-ring-color auto 1px !important;
}

/* ── details/summary dropdown overrides ───────────────────── */
.install-card__dropdown summary {
list-style: none;
Expand Down Expand Up @@ -138,22 +145,26 @@
}

/* Tab panel switching */
#install-tab-pkg:checked ~ .install-card__body .install-card__panel--pkg {
.install-card__radio--pkg:checked
~ .install-card__body
.install-card__panel--pkg {
display: block;
}
#install-tab-sys:checked ~ .install-card__body .install-card__panel--sys {
.install-card__radio--sys:checked
~ .install-card__body
.install-card__panel--sys {
display: block;
}

/* Tab active styling */
#install-tab-pkg:checked
.install-card__radio--pkg:checked
~ .install-card__body
.tab__list
[for="install-tab-pkg"],
#install-tab-sys:checked
.tab__trigger:first-child,
.install-card__radio--sys:checked
~ .install-card__body
.tab__list
[for="install-tab-sys"] {
.tab__trigger:last-child {
border-bottom-color: var(--color-text-secondary);
color: var(--color-text-primary);
}
Expand Down
143 changes: 64 additions & 79 deletions static/js/install-card.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,106 +2,91 @@
* Install Card — Keyboard navigation & UX enhancements
* Core interactions (tabs, dropdown, option selection) work without JS via
* CSS radio inputs and <details>/<summary>. This script adds:
* - Arrow-key navigation between tabs
* - aria-selected sync on tab labels
* - Escape to close dropdown
* - Enter/Space to select dropdown items
* - Auto-close dropdown after option selection
* - ARIA attribute updates on state changes
* - aria-selected sync on dropdown items
* Supports multiple install cards on the same page via querySelectorAll.
*/
document.addEventListener("DOMContentLoaded", function () {
var card = document.querySelector("[data-install-card]");
if (!card) return;

var tabLabels = card.querySelectorAll('.tab__list [role="tab"]');

// --- Arrow-key tab navigation ---
tabLabels.forEach(function (label) {
label.addEventListener("keydown", function (e) {
var tabs = Array.from(tabLabels);
var idx = tabs.indexOf(label);
var target = null;

if (e.key === "ArrowRight") target = tabs[(idx + 1) % tabs.length];
if (e.key === "ArrowLeft")
target = tabs[(idx - 1 + tabs.length) % tabs.length];

if (target) {
e.preventDefault();
// Check the associated radio to switch tab
var radio = document.getElementById(target.getAttribute("for"));
if (radio) radio.checked = true;
target.focus();
updateTabAria();
}
});
document.querySelectorAll("[data-install-card]").forEach(function (card) {
initInstallCard(card);
});

// --- Tab ARIA updates on radio change ---
card.querySelectorAll('input[name="install-tab"]').forEach(function (radio) {
radio.addEventListener("change", updateTabAria);
});
function initInstallCard(card) {
var tabLabels = card.querySelectorAll('.tab__list [role="tab"]');

function updateTabAria() {
tabLabels.forEach(function (label) {
var radio = document.getElementById(label.getAttribute("for"));
var isActive = radio && radio.checked;
label.setAttribute("aria-selected", isActive ? "true" : "false");
label.setAttribute("tabindex", isActive ? "0" : "-1");
// Sync aria-selected on tab labels when tab radio changes
card.querySelectorAll("input.install-card__radio").forEach(function (radio) {
radio.addEventListener("change", function () {
tabLabels.forEach(function (label) {
var r = document.getElementById(label.getAttribute("for"));
label.setAttribute("aria-selected", r && r.checked ? "true" : "false");
});
});
});
}

// Set initial ARIA state
updateTabAria();
// --- Dropdown enhancements per panel ---
card.querySelectorAll(".install-card__panel").forEach(function (panel) {
var details = panel.querySelector("details.install-card__dropdown");
if (!details) return;

// --- Dropdown enhancements per panel ---
card.querySelectorAll(".install-card__panel").forEach(function (panel) {
var details = panel.querySelector("details.install-card__dropdown");
if (!details) return;
var summary = details.querySelector("summary");
var items = panel.querySelectorAll(".dropdown__item");

var summary = details.querySelector("summary");
var items = panel.querySelectorAll(".dropdown__item");
// Auto-close <details> after selecting an option
items.forEach(function (item) {
item.addEventListener("click", function () {
details.removeAttribute("open");
updateOptionAria(panel);
/* Defer focus so it runs after the <label>'s native focus-the-input behavior.
This ensures focus goes back to the summary, not the radio input (which is visually hidden) */
setTimeout(function () {
summary.focus({ focusVisible: true });
}, 0);
});

// Auto-close <details> after selecting an option
items.forEach(function (item) {
item.addEventListener("click", function () {
details.removeAttribute("open");
summary.focus();
updateOptionAria(panel);
// Enter/Space to select
item.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
item.click();
/* Defer focus so it runs after the <label>'s native focus-the-input behavior.
This ensures focus goes back to the summary, not the radio input (which is visually hidden) */
setTimeout(function () {
summary.focus({ focusVisible: true });
}, 0);
}
});
});

// Enter/Space to select
item.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") {
// Escape to close dropdown
details.addEventListener("keydown", function (e) {
if (e.key === "Escape" && details.hasAttribute("open")) {
e.preventDefault();
item.click();
details.removeAttribute("open");
summary.focus({ focusVisible: true });
}
});
});

// Escape to close dropdown
details.addEventListener("keydown", function (e) {
if (e.key === "Escape" && details.hasAttribute("open")) {
e.preventDefault();
details.removeAttribute("open");
summary.focus();
}
});

// Update ARIA on radio change
panel.querySelectorAll('input[type="radio"]').forEach(function (radio) {
radio.addEventListener("change", function () {
updateOptionAria(panel);
// Update ARIA on radio change
panel.querySelectorAll('input[type="radio"]').forEach(function (radio) {
radio.addEventListener("change", function () {
updateOptionAria(panel);
});
});
});
});

function updateOptionAria(panel) {
var items = panel.querySelectorAll(".dropdown__item");
items.forEach(function (item) {
var radioId = item.getAttribute("for");
var radio = document.getElementById(radioId);
var isSelected = radio && radio.checked;
item.setAttribute("aria-selected", isSelected ? "true" : "false");
});
// Sync aria-selected on dropdown items to match the checked option radio.
function updateOptionAria(panel) {
var items = panel.querySelectorAll(".dropdown__item");
items.forEach(function (item) {
var radioId = item.getAttribute("for");
var radio = document.getElementById(radioId);
var isSelected = radio && radio.checked;
item.setAttribute("aria-selected", isSelected ? "true" : "false");
});
}
}
});
7 changes: 6 additions & 1 deletion templates/v3/examples/_v3_example_section.html
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,12 @@ <h4 class="content-story-section__title">Content detail card</h4>
<div class="v3-examples-section__block">
<h3 class="block-title">Install Card</h3>
<div class="v3-examples-section__example-box">
{% include "v3/includes/_install_card.html" with title=install_card_title %}
{% include "v3/includes/_install_card.html" with title=install_card_title card_id="install-1" %}
</div>

<h3 class="block-title">Install Card 2 (just a test for having multiple Install Cards on the same page)</h3>
<div class="v3-examples-section__example-box">
{% include "v3/includes/_install_card.html" with title=install_card_title card_id="install-2" %}
</div>
</div>

Expand Down
Loading
Loading