Skip to content

Commit 2341c14

Browse files
Fix/docs (#112)
1 parent e152944 commit 2341c14

3 files changed

Lines changed: 174 additions & 35 deletions

File tree

docs/astro.config.mjs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,11 @@ export default defineConfig({
113113
window.overscrollTimeout = setTimeout(function() {
114114
pullDistance = 0;
115115
updateOverscroll(0);
116-
}, 300);
116+
}, 5000);
117+
} else if (e.deltaY < 0) {
118+
clearTimeout(window.overscrollTimeout);
119+
pullDistance = 0;
120+
updateOverscroll(0);
117121
}
118122
}
119123

docs/src/components/InstallSelector.astro

Lines changed: 149 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,16 @@ const defaultOption = highlightedOptions[defaultIndex];
251251
background: rgba(255, 255, 255, 0.1);
252252
font-weight: 500;
253253
}
254+
255+
.dropdown-option:focus {
256+
outline: none;
257+
background: rgba(255, 255, 255, 0.12);
258+
}
259+
260+
.dropdown-option:focus-visible {
261+
outline: 2px solid rgba(255, 255, 255, 0.4);
262+
outline-offset: -2px;
263+
}
254264
</style>
255265

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

267278
if (!trigger || !installBox) return;
268279

269-
// Toggle dropdown
270-
trigger.addEventListener('click', function(e) {
271-
e.stopPropagation();
272-
const isOpen = installBox.classList.contains('open');
280+
/**
281+
* Selects an option: updates UI, closes dropdown, returns focus to trigger.
282+
*/
283+
function selectOption(option) {
284+
const newLabel = option.getAttribute('data-label');
285+
const newCommand = option.getAttribute('data-command');
286+
const newHighlighted = option.getAttribute('data-highlighted');
287+
288+
if (label && newLabel) label.textContent = newLabel;
289+
if (commandText && newHighlighted) {
290+
commandText.innerHTML = newHighlighted;
291+
}
292+
if (commandText && newCommand) {
293+
commandText.setAttribute('data-command', newCommand);
294+
}
295+
if (copyBtn && newCommand) {
296+
copyBtn.setAttribute('data-command', newCommand);
297+
}
273298

274-
// Close all other dropdowns
299+
// Update selected state
300+
options.forEach(function(opt) {
301+
opt.classList.remove('selected');
302+
opt.setAttribute('aria-selected', 'false');
303+
});
304+
option.classList.add('selected');
305+
option.setAttribute('aria-selected', 'true');
306+
307+
// Close dropdown and return focus
308+
installBox.classList.remove('open');
309+
trigger.setAttribute('aria-expanded', 'false');
310+
trigger.focus();
311+
}
312+
313+
/**
314+
* Opens the dropdown and focuses the selected or first option.
315+
*/
316+
function openDropdown() {
317+
// Close all other dropdowns first
275318
document.querySelectorAll('.install-box.open').forEach(function(box) {
276319
if (box !== installBox) box.classList.remove('open');
277320
});
278321

279-
installBox.classList.toggle('open', !isOpen);
280-
trigger.setAttribute('aria-expanded', (!isOpen).toString());
322+
installBox.classList.add('open');
323+
trigger.setAttribute('aria-expanded', 'true');
324+
325+
// Focus the currently selected option, or first option
326+
const selectedOption = selector.querySelector('.dropdown-option.selected') || options[0];
327+
if (selectedOption) selectedOption.focus();
328+
}
329+
330+
/**
331+
* Closes the dropdown and returns focus to trigger.
332+
*/
333+
function closeDropdown() {
334+
installBox.classList.remove('open');
335+
trigger.setAttribute('aria-expanded', 'false');
336+
trigger.focus();
337+
}
338+
339+
// Toggle dropdown on click
340+
trigger.addEventListener('click', function(e) {
341+
e.stopPropagation();
342+
const isOpen = installBox.classList.contains('open');
343+
344+
if (isOpen) {
345+
closeDropdown();
346+
} else {
347+
openDropdown();
348+
}
281349
});
282350

283-
// Handle option selection
284-
options.forEach(function(option) {
351+
// Keyboard navigation on trigger
352+
trigger.addEventListener('keydown', function(e) {
353+
const isOpen = installBox.classList.contains('open');
354+
355+
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
356+
e.preventDefault();
357+
if (!isOpen) {
358+
openDropdown();
359+
} else {
360+
// Focus first or last option based on direction
361+
const targetOption = e.key === 'ArrowDown' ? options[0] : options[options.length - 1];
362+
if (targetOption) targetOption.focus();
363+
}
364+
}
365+
366+
if (e.key === 'Enter' || e.key === ' ') {
367+
e.preventDefault();
368+
if (!isOpen) {
369+
openDropdown();
370+
}
371+
}
372+
373+
if (e.key === 'Home' && isOpen) {
374+
e.preventDefault();
375+
if (options[0]) options[0].focus();
376+
}
377+
378+
if (e.key === 'End' && isOpen) {
379+
e.preventDefault();
380+
if (options[options.length - 1]) options[options.length - 1].focus();
381+
}
382+
});
383+
384+
// Handle option click and keyboard navigation
385+
options.forEach(function(option, index) {
285386
option.addEventListener('click', function() {
286-
const newLabel = option.getAttribute('data-label');
287-
const newCommand = option.getAttribute('data-command');
288-
const newHighlighted = option.getAttribute('data-highlighted');
387+
selectOption(option);
388+
});
389+
390+
option.addEventListener('keydown', function(e) {
391+
if (e.key === 'ArrowDown') {
392+
e.preventDefault();
393+
const nextIndex = index < optionsArray.length - 1 ? index + 1 : 0;
394+
optionsArray[nextIndex].focus();
395+
}
289396

290-
if (label && newLabel) label.textContent = newLabel;
291-
if (commandText && newHighlighted) {
292-
commandText.innerHTML = newHighlighted;
397+
if (e.key === 'ArrowUp') {
398+
e.preventDefault();
399+
const prevIndex = index > 0 ? index - 1 : optionsArray.length - 1;
400+
optionsArray[prevIndex].focus();
293401
}
294-
if (commandText && newCommand) {
295-
commandText.setAttribute('data-command', newCommand);
402+
403+
if (e.key === 'Home') {
404+
e.preventDefault();
405+
options[0].focus();
406+
}
407+
408+
if (e.key === 'End') {
409+
e.preventDefault();
410+
options[options.length - 1].focus();
296411
}
297-
if (copyBtn && newCommand) {
298-
copyBtn.setAttribute('data-command', newCommand);
412+
413+
if (e.key === 'Enter' || e.key === ' ') {
414+
e.preventDefault();
415+
selectOption(option);
299416
}
300417

301-
// Update selected state
302-
options.forEach(function(opt) {
303-
opt.classList.remove('selected');
304-
opt.setAttribute('aria-selected', 'false');
305-
});
306-
option.classList.add('selected');
307-
option.setAttribute('aria-selected', 'true');
418+
if (e.key === 'Escape') {
419+
e.preventDefault();
420+
closeDropdown();
421+
}
308422

309-
// Close dropdown
310-
installBox.classList.remove('open');
311-
trigger.setAttribute('aria-expanded', 'false');
423+
if (e.key === 'Tab') {
424+
// Close dropdown when tabbing away
425+
closeDropdown();
426+
}
312427
});
313428
});
314429

@@ -336,13 +451,16 @@ const defaultOption = highlightedOptions[defaultIndex];
336451
}
337452
});
338453

339-
// Close on escape
454+
// Close on escape (global handler for when focus is elsewhere)
340455
document.addEventListener('keydown', function(e) {
341456
if (e.key === 'Escape') {
342457
document.querySelectorAll('.install-box.open').forEach(function(box) {
343458
box.classList.remove('open');
344459
const trigger = box.querySelector('.dropdown-trigger');
345-
if (trigger) trigger.setAttribute('aria-expanded', 'false');
460+
if (trigger) {
461+
trigger.setAttribute('aria-expanded', 'false');
462+
trigger.focus();
463+
}
346464
});
347465
}
348466
});

docs/src/styles/custom.css

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -235,20 +235,37 @@ site-search button kbd {
235235

236236
.hero-docs-link {
237237
color: rgba(255, 255, 255, 0.7) !important;
238-
text-decoration: none;
238+
text-decoration: none !important;
239239
font-weight: 400;
240240
font-size: 0.9rem;
241241
transition: color 0.2s ease;
242242
margin-top: 0.75rem;
243+
position: relative;
244+
display: inline-block;
245+
}
246+
247+
.hero-docs-link::after {
248+
content: "";
249+
position: absolute;
250+
bottom: -3px;
251+
right: 0;
252+
width: 7.5em; /* Width of "documentation." */
253+
height: 1px;
254+
background: currentColor;
255+
transition: width 0.3s ease;
243256
}
244257

245258
.hero-docs-link:hover {
246259
color: #fff !important;
247260
}
248261

262+
.hero-docs-link:hover::after {
263+
width: 100%;
264+
}
265+
266+
/* Remove the static underline from the span since pseudo-element handles it */
249267
.hero-docs-link .underline {
250-
text-decoration: underline;
251-
text-underline-offset: 3px;
268+
text-decoration: none;
252269
}
253270

254271
/* Stack container - page layout for splash */

0 commit comments

Comments
 (0)