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
6 changes: 5 additions & 1 deletion docs/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,11 @@ export default defineConfig({
window.overscrollTimeout = setTimeout(function() {
pullDistance = 0;
updateOverscroll(0);
}, 300);
}, 5000);
} else if (e.deltaY < 0) {
clearTimeout(window.overscrollTimeout);
pullDistance = 0;
updateOverscroll(0);
}
}

Expand Down
180 changes: 149 additions & 31 deletions docs/src/components/InstallSelector.astro
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,16 @@ const defaultOption = highlightedOptions[defaultIndex];
background: rgba(255, 255, 255, 0.1);
font-weight: 500;
}

.dropdown-option:focus {
outline: none;
background: rgba(255, 255, 255, 0.12);
}

.dropdown-option:focus-visible {
outline: 2px solid rgba(255, 255, 255, 0.4);
outline-offset: -2px;
}
</style>

<script is:inline>
Expand All @@ -263,52 +273,157 @@ const defaultOption = highlightedOptions[defaultIndex];
const commandText = selector.querySelector('.command-text');
const copyBtn = selector.querySelector('.copy-btn');
const options = selector.querySelectorAll('.dropdown-option');
const optionsArray = Array.from(options);

if (!trigger || !installBox) return;

// Toggle dropdown
trigger.addEventListener('click', function(e) {
e.stopPropagation();
const isOpen = installBox.classList.contains('open');
/**
* Selects an option: updates UI, closes dropdown, returns focus to trigger.
*/
function selectOption(option) {
const newLabel = option.getAttribute('data-label');
const newCommand = option.getAttribute('data-command');
const newHighlighted = option.getAttribute('data-highlighted');

if (label && newLabel) label.textContent = newLabel;
if (commandText && newHighlighted) {
commandText.innerHTML = newHighlighted;
}
if (commandText && newCommand) {
commandText.setAttribute('data-command', newCommand);
}
if (copyBtn && newCommand) {
copyBtn.setAttribute('data-command', newCommand);
}

// Close all other dropdowns
// Update selected state
options.forEach(function(opt) {
opt.classList.remove('selected');
opt.setAttribute('aria-selected', 'false');
});
option.classList.add('selected');
option.setAttribute('aria-selected', 'true');

// Close dropdown and return focus
installBox.classList.remove('open');
trigger.setAttribute('aria-expanded', 'false');
trigger.focus();
}

/**
* Opens the dropdown and focuses the selected or first option.
*/
function openDropdown() {
// Close all other dropdowns first
document.querySelectorAll('.install-box.open').forEach(function(box) {
if (box !== installBox) box.classList.remove('open');
});

installBox.classList.toggle('open', !isOpen);
trigger.setAttribute('aria-expanded', (!isOpen).toString());
installBox.classList.add('open');
trigger.setAttribute('aria-expanded', 'true');

// Focus the currently selected option, or first option
const selectedOption = selector.querySelector('.dropdown-option.selected') || options[0];
if (selectedOption) selectedOption.focus();
}

/**
* Closes the dropdown and returns focus to trigger.
*/
function closeDropdown() {
installBox.classList.remove('open');
trigger.setAttribute('aria-expanded', 'false');
trigger.focus();
}

// Toggle dropdown on click
trigger.addEventListener('click', function(e) {
e.stopPropagation();
const isOpen = installBox.classList.contains('open');

if (isOpen) {
closeDropdown();
} else {
openDropdown();
}
});

// Handle option selection
options.forEach(function(option) {
// Keyboard navigation on trigger
trigger.addEventListener('keydown', function(e) {
const isOpen = installBox.classList.contains('open');

if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
e.preventDefault();
if (!isOpen) {
openDropdown();
} else {
// Focus first or last option based on direction
const targetOption = e.key === 'ArrowDown' ? options[0] : options[options.length - 1];
if (targetOption) targetOption.focus();
}
}

if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
if (!isOpen) {
openDropdown();
}
}

if (e.key === 'Home' && isOpen) {
e.preventDefault();
if (options[0]) options[0].focus();
}

if (e.key === 'End' && isOpen) {
e.preventDefault();
if (options[options.length - 1]) options[options.length - 1].focus();
}
});

// Handle option click and keyboard navigation
options.forEach(function(option, index) {
option.addEventListener('click', function() {
const newLabel = option.getAttribute('data-label');
const newCommand = option.getAttribute('data-command');
const newHighlighted = option.getAttribute('data-highlighted');
selectOption(option);
});

option.addEventListener('keydown', function(e) {
if (e.key === 'ArrowDown') {
e.preventDefault();
const nextIndex = index < optionsArray.length - 1 ? index + 1 : 0;
optionsArray[nextIndex].focus();
}

if (label && newLabel) label.textContent = newLabel;
if (commandText && newHighlighted) {
commandText.innerHTML = newHighlighted;
if (e.key === 'ArrowUp') {
e.preventDefault();
const prevIndex = index > 0 ? index - 1 : optionsArray.length - 1;
optionsArray[prevIndex].focus();
}
if (commandText && newCommand) {
commandText.setAttribute('data-command', newCommand);

if (e.key === 'Home') {
e.preventDefault();
options[0].focus();
}

if (e.key === 'End') {
e.preventDefault();
options[options.length - 1].focus();
}
if (copyBtn && newCommand) {
copyBtn.setAttribute('data-command', newCommand);

if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
selectOption(option);
}

// Update selected state
options.forEach(function(opt) {
opt.classList.remove('selected');
opt.setAttribute('aria-selected', 'false');
});
option.classList.add('selected');
option.setAttribute('aria-selected', 'true');
if (e.key === 'Escape') {
e.preventDefault();
closeDropdown();
}

// Close dropdown
installBox.classList.remove('open');
trigger.setAttribute('aria-expanded', 'false');
if (e.key === 'Tab') {
// Close dropdown when tabbing away
closeDropdown();
}
});
});

Expand Down Expand Up @@ -336,13 +451,16 @@ const defaultOption = highlightedOptions[defaultIndex];
}
});

// Close on escape
// Close on escape (global handler for when focus is elsewhere)
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
document.querySelectorAll('.install-box.open').forEach(function(box) {
box.classList.remove('open');
const trigger = box.querySelector('.dropdown-trigger');
if (trigger) trigger.setAttribute('aria-expanded', 'false');
if (trigger) {
trigger.setAttribute('aria-expanded', 'false');
trigger.focus();
}
});
}
});
Expand Down
23 changes: 20 additions & 3 deletions docs/src/styles/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -235,20 +235,37 @@ site-search button kbd {

.hero-docs-link {
color: rgba(255, 255, 255, 0.7) !important;
text-decoration: none;
text-decoration: none !important;
font-weight: 400;
font-size: 0.9rem;
transition: color 0.2s ease;
margin-top: 0.75rem;
position: relative;
display: inline-block;
}

.hero-docs-link::after {
content: "";
position: absolute;
bottom: -3px;
right: 0;
width: 7.5em; /* Width of "documentation." */
height: 1px;
background: currentColor;
transition: width 0.3s ease;
}

.hero-docs-link:hover {
color: #fff !important;
}

.hero-docs-link:hover::after {
width: 100%;
}

/* Remove the static underline from the span since pseudo-element handles it */
.hero-docs-link .underline {
text-decoration: underline;
text-underline-offset: 3px;
text-decoration: none;
}

/* Stack container - page layout for splash */
Expand Down
Loading