Priority Task Scheduler
advancedA concurrency-limited scheduler with priorities and cancellation — the pool question upgraded to the API-design round.
Build a star rating component in vanilla JavaScript — hover preview vs committed state, event delegation, keyboard support, and the radio-group ARIA pattern.
Build a 5-star rating widget, no framework: hover previews a rating, click commits it, clicking the same star clears it, keyboard works, and a screen reader can use it.
This is the most-assigned small machine-coding widget (Atlassian and Amazon loops love it) because it's graded on exactly the things tutorials skip: the two-state model (hover preview vs committed value), event delegation instead of five listeners, and the part that decides senior-vs-not — keyboard and ARIA. The rendering is trivial; the state discipline isn't.
Two independent pieces of state, one render function:
value — the committed rating (survives mouse leave).hovered — the preview (0 when the pointer is outside).hovered || value — one expression encodes the entire UX: preview while hovering, fall back to committed on leave.Candidates who merge these into one variable end up restoring state in mouseleave handlers and chasing flicker bugs. Separate state + derived render is the whole lesson — it's the memoizeLast/React model in miniature: state changes, render derives.
function createStarRating(container, {
max = 5,
value = 0,
onChange = () => {},
} = {}) {
let committed = value;
let hovered = 0;
// --- render: build once, then only toggle classes/attributes ---
container.classList.add("star-rating");
container.setAttribute("role", "radiogroup");
container.setAttribute("aria-label", "Rating");
const stars = Array.from({ length: max }, (_, i) => {
const star = document.createElement("button");
star.type = "button"; // never submit a surrounding form
star.className = "star";
star.dataset.value = String(i + 1);
star.setAttribute("role", "radio");
star.setAttribute("aria-label", `${i + 1} of ${max} stars`);
star.textContent = "★";
container.append(star);
return star;
});
function render() {
const shown = hovered || committed; // the one-expression UX model
stars.forEach((star, i) => {
star.classList.toggle("filled", i < shown);
star.setAttribute("aria-checked", String(i + 1 === committed));
// Roving tabindex: ONE tab stop for the whole group
star.tabIndex = i + 1 === (committed || 1) ? 0 : -1;
});
}
function commit(next) {
committed = next === committed ? 0 : next; // click current star = clear
hovered = 0;
render();
onChange(committed);
}
// --- events: THREE delegated listeners, not 3×max ---
container.addEventListener("click", (e) => {
const star = e.target.closest(".star");
if (star) commit(Number(star.dataset.value));
});
container.addEventListener("mouseover", (e) => {
const star = e.target.closest(".star");
if (star) { hovered = Number(star.dataset.value); render(); }
});
container.addEventListener("mouseleave", () => {
hovered = 0;
render(); // fall back to committed
});
container.addEventListener("keydown", (e) => {
const delta = { ArrowRight: 1, ArrowUp: 1, ArrowLeft: -1, ArrowDown: -1 }[e.key];
if (delta !== undefined) {
e.preventDefault(); // don't scroll the page
const next = Math.min(max, Math.max(1, (committed || 0) + delta));
commit(next === committed ? committed : next); // arrows never clear
stars[next - 1].focus();
}
});
render();
return {
getValue: () => committed,
setValue: (v) => { committed = Math.min(max, Math.max(0, v)); render(); },
destroy: () => container.replaceChildren(), // listeners on container die with nodes? No—
// container listeners persist; a real destroy also removes them (kept simple here;
// see the teardown note below)
};
}.star { background: none; border: 0; font-size: 1.5rem; color: #555; cursor: pointer; }
.star.filled { color: gold; }Wait — that inline destroy comment is doing real work: replaceChildren removes the stars but the four listeners live on the container, which survives. A correct teardown stores the handler references and removeEventListeners them, or the widget owns (and removes) a wrapper element. Interviewers ask "what leaks?" — the emitter-leak answer applies to DOM widgets identically.
<button role="radio"> elements, not <span>s — buttons are focusable, clickable via keyboard, and announced by default; a <div onclick> widget fails the accessibility grading line before ARIA is even discussed. (type="button" because inside a review form, default type="submit" submits it on every star click — a classic real bug.)aria-checked tracks the committed value, never the hover preview (announcing every hover is noise).tabindex="0" makes keyboard users press Tab five times to cross your widget; it's the detail that distinguishes "added ARIA" from "knows the patterns."mouseover instead of mouseenter per star — mouseenter doesn't bubble (the delegation trap); mouseover does. This widget is the delegation article's exam question.mousemove + offsetX < width/2 for pointer precision, or double the radio count (10 half-star buttons, visually overlapped) which keeps keyboard/ARIA coherent for free. The second is the better answer because of accessibility — a genuinely nice design fork.committed into a hidden <input name="rating"> so plain form submission works; the widget stays progressive-enhancement-friendly.dir="rtl"; naming it is plenty.5 × 3 bindings), or mouseenter delegation that silently never fires.<span> stars with click handlers — unfocusable, unannounced, auto-fail on a11y.type="button" inside forms.destroy that removes nodes but leaves container listeners (the leak above).interactive: false option that skips listeners and sets role="img" + aria-label="4.5 out of 5 stars" — the display case is a different ARIA pattern, and knowing that is the answer.useState for both states, the derived-render expression unchanged; the controlled/uncontrolled prop question (value vs defaultValue) is where the interview goes next — mirror the debounce-in-React identity discipline for onChange.PATCH behind, roll back on failure (retry with backoff if transient) — three sentences of systems glue that upgrade the widget answer.color/transform with prefers-reduced-motion respected — one media query that reads as production experience.A concurrency-limited scheduler with priorities and cancellation — the pool question upgraded to the API-design round.
IntersectionObserver, in-flight guards, end-of-data states, and the sentinel pattern — the pagination question every feed team asks.
Debounce, cancellation, and stale-response guards composed into a search box that never shows the wrong results.
O(1) get/put with least-recently-used eviction — the Map insertion-order trick, and the linked-list version interviewers ask about.