Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
6293678
Sketch in stepper styles
jhgilbert Mar 3, 2026
c3820ad
Tweak styles
jhgilbert Mar 3, 2026
d06049f
Check off completed steps
jhgilbert Mar 3, 2026
54dc466
Flesh out example steps
jhgilbert Mar 3, 2026
165f00d
Make steps searchable
jhgilbert Mar 3, 2026
a1ffc1c
Nudge elements
jhgilbert Mar 3, 2026
be39def
Update example step
jhgilbert Mar 3, 2026
33e11e0
Tweak stepper behavior
jhgilbert Mar 4, 2026
62dd2de
Use a green checkmark circle to mark completed tasks
jhgilbert Mar 4, 2026
c0a7666
Tweak button wording
jhgilbert Mar 4, 2026
7f8519e
Tweak wording
jhgilbert Mar 4, 2026
627d8c5
Tweak stepper line width
jhgilbert Mar 4, 2026
b1eb45f
Tweak appearance
jhgilbert Mar 9, 2026
8673f1d
Improve focus visibility
jhgilbert Mar 9, 2026
c353840
Improve accessibility
jhgilbert Mar 9, 2026
844dedf
Improve accessibility
jhgilbert Mar 9, 2026
e834ed5
Tweak checkmark
jhgilbert Mar 9, 2026
5879ab1
Tweak button text size
jhgilbert Mar 9, 2026
6bbed70
Tweak loading behavior
jhgilbert Mar 9, 2026
0742832
Button tweaks
jhgilbert Mar 9, 2026
109f1ab
Tweaks
jhgilbert Mar 9, 2026
ab326d9
Update demo markup
jhgilbert Mar 11, 2026
6ceee61
[wip] Incorporate feedback
jhgilbert Mar 11, 2026
8a2ce38
Make the clicked step the active step
jhgilbert Mar 12, 2026
3094b0e
Prevent step titles from being hidden under the sticky menu
jhgilbert Mar 12, 2026
9720b32
Tweak reset behavior
jhgilbert Mar 12, 2026
8debd09
Style expand/collapse buttons as links
jhgilbert Mar 13, 2026
8a7b385
Improve responsiveness
jhgilbert Mar 13, 2026
482e70d
Tweak styles
jhgilbert Mar 13, 2026
05cf62f
Tweak icons
jhgilbert Mar 13, 2026
2617e44
Tweak spacing
jhgilbert Mar 13, 2026
c61a9ef
Fix stepper icon URLs
jhgilbert Mar 13, 2026
fedc140
Tone down expand/collapse toggle styling (#35284)
brett0000FF Mar 13, 2026
bf346ae
Tweaks
jhgilbert Mar 16, 2026
8c7c58a
Delete stepper demo file
jhgilbert Mar 16, 2026
ffe5f61
Revert changes in package.json
jhgilbert Mar 16, 2026
d67ee73
Merge master
jhgilbert Mar 16, 2026
68554c2
Implement Codex feedback
jhgilbert Mar 16, 2026
0f1ddb6
Fix bug
jhgilbert Mar 16, 2026
95c7e16
Update assets/styles/components/_collapsible-section.scss
jhgilbert Mar 16, 2026
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
209 changes: 209 additions & 0 deletions assets/scripts/components/stepper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
const STORAGE_PREFIX = 'stepper-progress-';
const MAX_STORED_STEPPERS = 10;

function getStorageKey(stepperId) {
return `${STORAGE_PREFIX}${location.pathname}:${stepperId}`;
}

function loadProgress(stepperId) {
try {
const data = localStorage.getItem(getStorageKey(stepperId));
return data ? JSON.parse(data) : null;
} catch {
return null;
}
}

function saveProgress(stepperId, state) {
try {
const key = getStorageKey(stepperId);
if (!localStorage.getItem(key)) {
pruneOldEntries();
}
localStorage.setItem(key, JSON.stringify({ ...state, timestamp: Date.now() }));
} catch {
// Ignore storage errors, the stepper will still work without localStorage persistence
}
}

function pruneOldEntries() {
try {
const entries = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith(STORAGE_PREFIX)) {
const data = JSON.parse(localStorage.getItem(key));
entries.push({ key, timestamp: data.timestamp || 0 });
}
}
// If at the cap, remove the oldest entry to make room
if (entries.length >= MAX_STORED_STEPPERS) {
entries.sort((a, b) => a.timestamp - b.timestamp);
const toRemove = entries.length - MAX_STORED_STEPPERS + 1;
for (let i = 0; i < toRemove; i++) {
localStorage.removeItem(entries[i].key);
}
}
} catch {
// Ignore storage errors, the stepper will still work without localStorage persistence
}
}

function setHidden(el, hidden) {
if (!el) return;
if (hidden) {
el.setAttribute('data-hidden', 'true');
} else {
el.removeAttribute('data-hidden');
}
}

function initStepper(stepper) {
const stepperId = stepper.id;
const steps = stepper.querySelectorAll('.stepper__step');
const finishedEl = stepper.querySelector('.stepper__finished');
const resetEl = stepper.querySelector('.stepper__reset');
const showAllBtn = stepper.querySelector('.stepper__show-all-btn');
const collapseBtn = stepper.querySelector('.stepper__collapse-btn');

if (!steps.length) return;

// Set step numbers as data attributes for CSS
// (CSS counters don't work reliably when steps use display:none)
steps.forEach((step, i) => {
step.dataset.stepNumber = String(i + 1);
});

// Mark first/last steps for CSS line endpoints
steps[0].classList.add('stepper__step--first');
steps[steps.length - 1].classList.add('stepper__step--last');

let currentIndex = 0;
let finished = false;
let isAllExpanded = stepper.classList.contains('stepper--open');

// Restore saved progress
const saved = loadProgress(stepperId);
if (saved) {
if (saved.finished) {
finished = true;
} else if (typeof saved.stepIndex === 'number' && saved.stepIndex >= 0 && saved.stepIndex < steps.length) {
currentIndex = saved.stepIndex;
}
if (typeof saved.isAllExpanded === 'boolean') {
isAllExpanded = saved.isAllExpanded;
}
}

function persist() {
saveProgress(stepperId, {
stepIndex: currentIndex,
finished,
isAllExpanded
});
}

function render() {
stepper.classList.toggle('stepper--all-expanded', isAllExpanded);

// Toggle viz control buttons
setHidden(showAllBtn, isAllExpanded);
setHidden(collapseBtn, !isAllExpanded);

steps.forEach((step, i) => {
const isActive = !finished && i === currentIndex;
const isCompleted = finished || i < currentIndex;

step.classList.toggle('stepper__step--active', isActive);
step.classList.toggle('stepper__step--completed', isCompleted);

// The step title, number, etc. should always be visible,
// even if the step body itself is not visible
setHidden(step, false);

// Nav: hidden when expanded or finished, visible only for active step
const nav = step.querySelector('.stepper__nav');
if (nav) {
setHidden(nav, isAllExpanded || !isActive || finished);
}
});

// Finished message
setHidden(finishedEl, !finished);

// Start over: visible only when finished
setHidden(resetEl, !finished);
}

function goToStep(index) {
finished = false;
currentIndex = Math.max(0, Math.min(index, steps.length - 1));
persist();
render();
}

function handleFinish() {
finished = true;
persist();
render();
}

function handleReset() {
finished = false;
currentIndex = 0;
persist();
render();
}

// Clicking a step title in accordion (collapsed) mode makes it the active step
steps.forEach((step, i) => {
const title = step.querySelector('.stepper__step-title');
if (title) {
title.addEventListener('click', (e) => {
e.preventDefault();
if (isAllExpanded) return;
goToStep(i);
});
}
});

stepper.addEventListener('click', (e) => {
const btn = e.target.closest('.stepper__btn');
if (!btn) return;

if (btn.classList.contains('stepper__next-btn')) {
goToStep(currentIndex + 1);
} else if (btn.classList.contains('stepper__prev-btn')) {
goToStep(currentIndex - 1);
} else if (btn.classList.contains('stepper__finish-btn')) {
handleFinish();
} else if (btn.classList.contains('stepper__reset-btn')) {
handleReset();
} else if (btn.classList.contains('stepper__show-all-btn')) {
isAllExpanded = true;
persist();
render();
} else if (btn.classList.contains('stepper__collapse-btn')) {
isAllExpanded = false;
persist();
render();
}
});

// Initial render
render();
stepper.classList.add('stepper--initialized');
}

function initAllSteppers() {
const steppers = document.querySelectorAll('.stepper');
steppers.forEach(initStepper);
}

if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initAllSteppers);
} else {
initAllSteppers();
}

export { initAllSteppers, initStepper };
1 change: 1 addition & 0 deletions assets/scripts/main-dd-js.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import './components/mobile-nav'; // should move this to websites-modules
import './components/accordion-auto-open';
import './components/signup';
import './components/conversational-search';
import './components/stepper';

// Add Bootstrap Tooltip across docs
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
Expand Down
8 changes: 6 additions & 2 deletions assets/styles/_bootstrap-custom.scss
Original file line number Diff line number Diff line change
Expand Up @@ -255,10 +255,14 @@ button.list-group-item-white:hover {
}
}

// remove outlines
button:focus {
// Hide focus outlines for mouse/touch only; keep for keyboard
button:focus:not(:focus-visible) {
outline: none;
}
button:focus-visible {
outline: 2px solid $ddpurple;
outline-offset: 2px;
}
Comment on lines +258 to +265
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jhgilbert hello! these styles seem to be a duplicate of what's in _global.scss. could we remove them and still have the intended output?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

by the order of imports here https://github.com/DataDog/documentation/blob/master/assets/styles/style.scss, the pages/global styles would overwrite the bootstrap-custom styles

.form-control:focus {
box-shadow: none;
}
Expand Down
10 changes: 9 additions & 1 deletion assets/styles/components/_collapsible-section.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
align-items: center;
padding: 8px;
cursor: pointer;
outline: none;
background-color: #ffffff00;
border-radius: 7px 7px 7px 7px;
transition: background-color 0.3s,
Expand All @@ -22,6 +21,15 @@
display: none;
}

.collapsible-header:focus:not(:focus-visible) {
outline: none;
}

.collapsible-header:focus-visible {
outline: 2px solid $ddpurple;
outline-offset: -2px;
}

.collapsible-header:hover {
background-color: #eae2f8;
}
Expand Down
Loading
Loading