Skip to content
Closed
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
133 changes: 130 additions & 3 deletions static/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ var isDetailPage = typeof PROJECT_ID !== "undefined";
// classList.toggle returns true if class was added, false if removed
var isOpen = menu.classList.toggle("open");
toggle.classList.toggle("open", isOpen);
// Keep aria-expanded in sync so screen readers announce state changes
toggle.setAttribute("aria-expanded", String(isOpen));
// Keep aria-expanded in sync so screen readers know if menu is open or closed
toggle.setAttribute("aria-expanded", isOpen);
});
Expand All @@ -40,8 +42,19 @@ var isDetailPage = typeof PROJECT_ID !== "undefined";
link.addEventListener("click", function () {
menu.classList.remove("open");
toggle.classList.remove("open");
toggle.setAttribute("aria-expanded", "false");
});
});

// Also close the menu when Escape is pressed
document.addEventListener("keydown", function (evt) {
if (evt.key === "Escape" && menu.classList.contains("open")) {
menu.classList.remove("open");
toggle.classList.remove("open");
toggle.setAttribute("aria-expanded", "false");
toggle.focus();
}
});
})();


Expand Down Expand Up @@ -245,6 +258,7 @@ if (isIndexPage) {
}
});

// Add skill on quick-pick chip click; also support Space/Enter for keyboard users
// Add/toggle skill on quick-pick chip click
quickPickChips.forEach(function (chip) {
chip.addEventListener("click", function () {
Expand Down Expand Up @@ -290,6 +304,23 @@ if (isIndexPage) {
});
}

// Add skill on quick-pick chip click
quickPickChips.forEach(function (chip) {
chip.addEventListener("click", function () {
addSkill(chip.getAttribute("data-skill"));
hideSuggestions();
skillsTextInput.value = "";
});
// Keyboard users expect buttons to respond to Enter and Space by default
// (they already do because these are <button> elements, but make sure)
chip.addEventListener("keydown", function (evt) {
if (evt.key === "Enter" || evt.key === " ") {
evt.preventDefault();
addSkill(chip.getAttribute("data-skill"));
chip.classList.add("active");
}
});
});

document.addEventListener("click", function (evt) {
if (skillWrap && !skillWrap.contains(evt.target)) {
Expand Down Expand Up @@ -567,6 +598,20 @@ if (isIndexPage) {
return text.length > maxLength ? text.slice(0, maxLength) + "..." : text;
}

// Wire up the "Try Different Inputs" button (onclick removed from HTML)
var btnTryAgain = document.getElementById("btn-try-again");
if (btnTryAgain) {
btnTryAgain.addEventListener("click", function () {
var target = document.getElementById("find-project");
if (target) target.scrollIntoView({ behavior: "smooth" });
// Move focus to the skills input so keyboard users land in the form
setTimeout(function () {
var skillsInput = document.getElementById("skills-input");
if (skillsInput) skillsInput.focus();
}, 400);
});
}

} // end isIndexPage


Expand All @@ -586,6 +631,8 @@ if (isDetailPage) {
// Cache flag so code is only fetched once per page load
var codeFetched = false;

// Tracks which element triggered the panel so focus can return on close
var panelTrigger = null;
function openCodePanel() {
// Panel element might not exist on every detail page, so check first
if (!codePanel) return;
Expand All @@ -606,6 +653,7 @@ if (isDetailPage) {
document.body.style.overflow = "";
}

// Fetch the starter code from the server (only once per page load)
function fetchStarterCode() {
// Show a loading message while we wait for the API response
if (codeContentEl) codeContentEl.textContent = "Loading starter code...";
Expand All @@ -629,20 +677,99 @@ if (isDetailPage) {
});
}

// Collect all focusable elements inside the panel for the focus trap
function getFocusableInPanel() {
return Array.from(
codePanel.querySelectorAll(
'a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])'
)
).filter(function (el) { return !el.hidden && el.offsetParent !== null; });
}
// Attach open/close handlers
if (btnViewCode) btnViewCode.addEventListener("click", openCodePanel);
if (btnViewCodeSm) btnViewCodeSm.addEventListener("click", openCodePanel);
if (btnClosePanel) btnClosePanel.addEventListener("click", closeCodePanel);

if (codePanelOverlay) {
codePanelOverlay.addEventListener("click", closeCodePanel);
function openCodePanel(triggerEl) {
if (!codePanel) return;
panelTrigger = triggerEl || document.activeElement;
codePanel.classList.add("active");
if (codePanelOverlay) codePanelOverlay.classList.add("active");
document.body.style.overflow = "hidden";

// Update aria-expanded on both trigger buttons
if (btnViewCode) btnViewCode.setAttribute("aria-expanded", "true");
if (btnViewCodeSm) btnViewCodeSm.setAttribute("aria-expanded", "true");

if (!codeFetched) fetchStarterCode();

// Move focus to the close button so keyboard users are immediately inside
// the panel's focus trap
setTimeout(function () {
var focusable = getFocusableInPanel();
if (focusable.length) focusable[0].focus();
}, 60);
}

function closeCodePanel() {
if (!codePanel) return;
codePanel.classList.remove("active");
if (codePanelOverlay) codePanelOverlay.classList.remove("active");
document.body.style.overflow = "";

// Update aria-expanded on both trigger buttons
if (btnViewCode) btnViewCode.setAttribute("aria-expanded", "false");
if (btnViewCodeSm) btnViewCodeSm.setAttribute("aria-expanded", "false");

// Return focus to whichever button opened the panel
if (panelTrigger && typeof panelTrigger.focus === "function") {
panelTrigger.focus();
}
panelTrigger = null;
}

// Focus trap: while the panel is open, Tab/Shift+Tab must stay inside it
// Let keyboard users close the panel with Escape — important for accessibility
document.addEventListener("keydown", function (evt) {
if (evt.key === "Escape") closeCodePanel();
if (!codePanel || !codePanel.classList.contains("active")) return;

if (evt.key === "Escape") {
closeCodePanel();
return;
}

if (evt.key !== "Tab") return;

var focusable = getFocusableInPanel();
if (!focusable.length) { evt.preventDefault(); return; }

var first = focusable[0];
var last = focusable[focusable.length - 1];

if (evt.shiftKey) {
// Shift+Tab: if focus is on the first element, wrap to last
if (document.activeElement === first) {
evt.preventDefault();
last.focus();
}
} else {
// Tab: if focus is on the last element, wrap to first
if (document.activeElement === last) {
evt.preventDefault();
first.focus();
}
}
});

// Attach open/close handlers — pass the trigger element so focus returns correctly
if (btnViewCode) btnViewCode.addEventListener("click", function () { openCodePanel(btnViewCode); });
if (btnViewCodeSm) btnViewCodeSm.addEventListener("click", function () { openCodePanel(btnViewCodeSm); });
if (btnClosePanel) btnClosePanel.addEventListener("click", closeCodePanel);

if (codePanelOverlay) {
codePanelOverlay.addEventListener("click", closeCodePanel);
}

// ----------------------------------------------------------
// Copy Code button
// ----------------------------------------------------------
Expand Down
187 changes: 187 additions & 0 deletions static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,193 @@ a { color: var(--indigo-500); text-decoration: none; }
a:hover { text-decoration: underline; }
ul, ol { list-style: none; }

/* ---- Skip Navigation Link --------------------------------- */
.skip-link {
position: absolute;
top: -100%;
left: 50%;
transform: translateX(-50%);
padding: 10px 24px;
background: var(--indigo-700);
color: var(--white);
font-family: var(--font-display);
font-size: 0.9rem;
font-weight: 700;
border-radius: 0 0 var(--r-sm) var(--r-sm);
z-index: 9999;
text-decoration: none;
transition: top 0.15s ease;
}
.skip-link:focus {
top: 0;
outline: 3px solid var(--yellow-400);
outline-offset: 2px;
}

/* ---- Global Focus-Visible Ring ----------------------------- */
/*
* We suppress the default browser outline on all elements globally
* and re-apply a polished custom ring only when navigating via
* keyboard (`:focus-visible`). This keeps the UI clean for mouse
* users while fully satisfying WCAG 2.1 SC 2.4.7 (Focus Visible).
*/
*:focus { outline: none; }

*:focus-visible {
outline: 2.5px solid var(--indigo-500);
outline-offset: 3px;
border-radius: 4px;
}

/* Dark-background contexts — use a lighter, high-contrast ring */
.navbar *:focus-visible,
.hero *:focus-visible,
.detail-hero *:focus-visible,
.cta-section *:focus-visible,
.footer *:focus-visible,
.code-panel *:focus-visible {
outline-color: var(--yellow-400);
}

/* ---- Per-component Focus Overrides ------------------------ */
/* These extend the ring for elements that need rounded outlines */

.nav-logo:focus-visible,
.nav-link:focus-visible,
.nav-btn-outline:focus-visible,
.nav-mobile-link:focus-visible {
border-radius: var(--r-full);
outline: 2.5px solid var(--yellow-400);
outline-offset: 3px;
}

.btn-hero-primary:focus-visible,
.btn-hero-ghost:focus-visible {
outline: 2.5px solid var(--yellow-400);
outline-offset: 3px;
border-radius: var(--r-full);
}

.skill-chip:focus-visible {
outline: 2.5px solid var(--indigo-500);
outline-offset: 2px;
border-radius: var(--r-full);
}

.skill-chip-remove:focus-visible {
outline: 2.5px solid var(--indigo-500);
outline-offset: 2px;
border-radius: 50%;
}

.feature-card-link:focus-visible {
outline: 2.5px solid var(--indigo-600);
outline-offset: 2px;
border-radius: var(--r-full);
}

.btn-submit:focus-visible {
outline: 2.5px solid var(--indigo-500);
outline-offset: 3px;
border-radius: var(--r-sm);
box-shadow: 0 0 0 5px rgba(79, 110, 247, 0.2), 0 4px 20px rgba(35, 53, 194, 0.35);
}

.btn-try-again:focus-visible {
outline: 2.5px solid var(--indigo-500);
outline-offset: 2px;
border-radius: var(--r-full);
}

.btn-details:focus-visible {
outline: 2.5px solid var(--indigo-500);
outline-offset: 2px;
border-radius: var(--r-xs);
background: var(--indigo-600);
color: var(--white);
}

.btn-cta:focus-visible {
outline: 2.5px solid var(--yellow-400);
outline-offset: 3px;
border-radius: var(--r-full);
}

.btn-download:focus-visible,
.btn-view-code:focus-visible {
outline: 2.5px solid var(--yellow-400);
outline-offset: 3px;
border-radius: var(--r-xs);
}

.btn-view-code-sm:focus-visible {
outline: 2.5px solid var(--indigo-500);
outline-offset: 2px;
border-radius: var(--r-xs);
background: var(--indigo-600);
color: var(--white);
}

.btn-download-sm:focus-visible {
outline: 2.5px solid var(--yellow-400);
outline-offset: 2px;
border-radius: var(--r-xs);
}

.resource-link:focus-visible {
outline: 2.5px solid var(--indigo-500);
outline-offset: 2px;
border-radius: var(--r-xs);
background: var(--indigo-100);
}

.code-panel-download:focus-visible {
outline: 2.5px solid var(--yellow-400);
outline-offset: 2px;
border-radius: var(--r-full);
}

.code-panel-close:focus-visible {
outline: 2.5px solid var(--yellow-400);
outline-offset: 2px;
border-radius: var(--r-xs);
background: rgba(255,255,255,0.18);
}

.btn-copy-code:focus-visible {
outline: 2.5px solid var(--indigo-500);
outline-offset: 2px;
border-radius: var(--r-xs);
}

.nav-mobile-toggle:focus-visible {
outline: 2.5px solid var(--yellow-400);
outline-offset: 3px;
border-radius: var(--r-xs);
}

select:focus-visible {
outline: 2.5px solid var(--indigo-500);
outline-offset: 0;
border-color: var(--indigo-500);
box-shadow: 0 0 0 4px rgba(79, 110, 247, 0.15);
}

.breadcrumb a:focus-visible {
outline: 2.5px solid var(--yellow-400);
outline-offset: 2px;
border-radius: 3px;
color: var(--white);
}

.footer-links-list a:focus-visible,
.footer-bottom-links a:focus-visible {
outline: 2.5px solid var(--yellow-400);
outline-offset: 2px;
border-radius: 3px;
color: var(--white);
}

/* ---- Shared utilities -------------------------------------- */
.container {
max-width: 1140px;
Expand Down
Loading
Loading