Style footer buttons via class so custom buttons can opt out

This commit is contained in:
Kamran Ahmed
2026-06-25 02:55:41 +01:00
parent abc923b7fb
commit e1b00394ed
5 changed files with 121 additions and 12 deletions
+3
View File
@@ -297,6 +297,9 @@ You can add custom buttons using `onPopoverRender` callback. This callback is ca
onPopoverRender: (popover, { config, state }) => {
const firstButton = document.createElement("button");
firstButton.innerText = "Go to First";
// Add this class to give the button the default driver.js styling.
// Leave it out if you want to apply your own styles instead.
firstButton.classList.add("driver-popover-footer-btn");
popover.footerButtons.appendChild(firstButton);
firstButton.addEventListener("click", () => {
+5
View File
@@ -64,6 +64,11 @@ Here is the list of classes applied to the popover which you can use in conjunct
.driver-popover-prev-btn {}
.driver-popover-next-btn {}
/* Shared style for the footer buttons. The built-in prev/next buttons carry it,
and you can add it to custom buttons created in onPopoverRender to give them
the default button look (or leave it off to apply your own styles). */
.driver-popover-footer-btn {}
/* Added to the next button on the last step, when it acts as the "Done" button */
.driver-popover-next-btn.driver-popover-done-btn {}
```
+3 -3
View File
@@ -117,7 +117,7 @@
zoom: 1;
}
.driver-popover-footer button {
.driver-popover-footer-btn {
all: unset;
display: inline-block;
box-sizing: border-box;
@@ -148,8 +148,8 @@
pointer-events: none !important;
}
.driver-popover-footer button:hover,
.driver-popover-footer button:focus {
.driver-popover-footer-btn:hover,
.driver-popover-footer-btn:focus {
background-color: #f7f7f7;
}
+11 -9
View File
@@ -223,13 +223,15 @@ export function renderPopover(element: Element, step: DriveStep) {
return undefined;
},
target => {
// Only prevent the default action if we're clicking on a driver button
// This allows us to have links inside the popover title and description
return (
!popover?.description.contains(target) &&
!popover?.title.contains(target) &&
typeof target.className === "string" &&
target.className.includes("driver-popover")
// Only prevent the default action if we're clicking on a driver button.
// This allows links inside the popover title/description, as well as
// custom buttons added through onPopoverRender, to behave normally.
if (popover?.description.contains(target) || popover?.title.contains(target)) {
return false;
}
return !!target.closest(
".driver-popover-prev-btn, .driver-popover-next-btn, .driver-popover-close-btn"
);
}
);
@@ -293,12 +295,12 @@ function createPopover(): PopoverDOM {
const previousButton = document.createElement("button");
previousButton.type = "button";
previousButton.classList.add("driver-popover-prev-btn");
previousButton.classList.add("driver-popover-prev-btn", "driver-popover-footer-btn");
previousButton.innerHTML = "Previous";
const nextButton = document.createElement("button");
nextButton.type = "button";
nextButton.classList.add("driver-popover-next-btn");
nextButton.classList.add("driver-popover-next-btn", "driver-popover-footer-btn");
nextButton.innerHTML = "Next";
footerButtons.appendChild(previousButton);
+99
View File
@@ -0,0 +1,99 @@
import { readFileSync } from "fs";
import { resolve } from "path";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { createDriver, navButton, SAMPLE_STEPS, useDriverHarness } from "./utils";
useDriverHarness();
// The footer button styling lives in src/driver.css. jsdom resolves the
// selectors below through getComputedStyle, so we load the real stylesheet
// once into <head> (the harness only resets <body>, so it survives each test).
beforeAll(() => {
const style = document.createElement("style");
style.textContent = readFileSync(resolve(process.cwd(), "src/driver.css"), "utf8");
document.head.appendChild(style);
});
afterEach(() => {
vi.restoreAllMocks();
});
function addCustomFooterButton(opts: { keepDefaultStyle: boolean }): {
button: HTMLButtonElement;
onClick: ReturnType<typeof vi.fn>;
} {
const onClick = vi.fn();
const button = document.createElement("button");
button.id = "custom-footer-btn";
button.innerText = "Custom";
if (opts.keepDefaultStyle) {
button.classList.add("driver-popover-footer-btn");
}
button.addEventListener("click", onClick);
const d = createDriver({
animate: false,
steps: SAMPLE_STEPS,
onPopoverRender: popover => {
popover.footerButtons.appendChild(button);
},
});
d.drive();
return { button, onClick };
}
describe("footer button styling", () => {
it("styles the built-in navigation buttons through the footer button class", () => {
const d = createDriver({ animate: false, steps: SAMPLE_STEPS });
d.drive();
const next = navButton("next")!;
expect(next.classList.contains("driver-popover-footer-btn")).toBe(true);
expect(navButton("prev")!.classList.contains("driver-popover-footer-btn")).toBe(true);
const styles = getComputedStyle(next);
expect(styles.padding).toBe("3px 7px");
expect(styles.borderRadius).toBe("3px");
});
it("lets a custom footer button opt out of the default button styling", () => {
const { button } = addCustomFooterButton({ keepDefaultStyle: false });
const styles = getComputedStyle(button);
expect(styles.padding).not.toBe("3px 7px");
expect(styles.borderRadius).not.toBe("3px");
});
it("lets a custom footer button opt in to the default button styling", () => {
const { button } = addCustomFooterButton({ keepDefaultStyle: true });
const styles = getComputedStyle(button);
expect(styles.padding).toBe("3px 7px");
expect(styles.borderRadius).toBe("3px");
});
});
describe("custom footer button click handling", () => {
it("does not swallow clicks on a custom footer button that opted in to the style", () => {
const { button, onClick } = addCustomFooterButton({ keepDefaultStyle: true });
const event = new MouseEvent("click", { bubbles: true, cancelable: true });
button.dispatchEvent(event);
expect(onClick).toHaveBeenCalledTimes(1);
expect(event.defaultPrevented).toBe(false);
});
it("still intercepts clicks on the built-in navigation buttons", () => {
const onNextClick = vi.fn();
const d = createDriver({ animate: false, steps: SAMPLE_STEPS, onNextClick });
d.drive();
const event = new MouseEvent("click", { bubbles: true, cancelable: true });
navButton("next")!.dispatchEvent(event);
expect(onNextClick).toHaveBeenCalledTimes(1);
expect(event.defaultPrevented).toBe(true);
});
});