Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

USWDS 3.4.1 #5181

Closed
wants to merge 20 commits into from
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/usa-form/src/styles/_usa-form.scss
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@
}
}

a {
// Avoids links styled as buttons
a:where(:not(.usa-button)) {
@include typeset-link;
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/usa-header/src/styles/_usa-header.scss
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ $z-index-overlay: 400;
@include at-media($theme-header-min-width) {
font-size: font-size($theme-header-font-family, "xl");
margin: units(4) 0 units(3);
max-width: 50%;
max-width: $theme-header-logo-text-width;
}
}

Expand Down
61 changes: 57 additions & 4 deletions packages/usa-in-page-navigation/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,39 @@ const getSectionAnchors = () => {
return sectionAnchors;
};

/**
* Generates a unique ID for the given heading element.
*
* @param {HTMLHeadingElement} heading
*
* @return {string} - Unique ID
*/
const getHeadingId = (heading) => {
const baseId = heading.textContent
.toLowerCase()
// Replace non-alphanumeric characters with dashes
.replace(/[^a-z\d]/g, "-")
// Replace a sequence of two or more dashes with a single dash
.replace(/-{2,}/g, "-")
// Trim leading or trailing dash (there should only ever be one)
.replace(/^-|-$/g, "");

let id;
let suffix = 0;
do {
id = baseId;

// To avoid conflicts with existing IDs on the page, loop and append an
// incremented suffix until a unique ID is found.
suffix += 1;
if (suffix > 1) {
id += `-${suffix}`;
}
} while (document.getElementById(id));

return id;
};

/**
* Return a section id/anchor hash without the number sign
*
Expand Down Expand Up @@ -98,6 +131,23 @@ const handleScrollToSection = (el) => {
top: el.offsetTop - inPageNavScrollOffset,
block: "start",
});

if (window.location.hash.slice(1) !== el.id) {
window.history.pushState(null, "", `#${el.id}`);
}
};

/**
* Scrolls the page to the section corresponding to the current hash fragment, if one exists.
*/
const scrollToCurrentSection = () => {
const hashFragment = window.location.hash.slice(1);
if (hashFragment) {
const anchorTag = document.getElementById(hashFragment);
if (anchorTag) {
handleScrollToSection(anchorTag);
}
}
};

/**
Expand Down Expand Up @@ -140,7 +190,7 @@ const createInPageNav = (inPageNavEl) => {
inPageNavList.classList.add(IN_PAGE_NAV_LIST_CLASS);
inPageNav.appendChild(inPageNavList);

sectionHeadings.forEach((el, i) => {
sectionHeadings.forEach((el) => {
const listItem = document.createElement("li");
const navLinks = document.createElement("a");
const anchorTag = document.createElement("a");
Expand All @@ -152,11 +202,13 @@ const createInPageNav = (inPageNavEl) => {
listItem.classList.add(SUB_ITEM_CLASS);
}

navLinks.setAttribute("href", `#section_${i}`);
const headingId = getHeadingId(el);

navLinks.setAttribute("href", `#${headingId}`);
navLinks.setAttribute("class", IN_PAGE_NAV_LINK_CLASS);
navLinks.textContent = textContentOfLink;

anchorTag.setAttribute("id", `section_${i}`);
anchorTag.setAttribute("id", headingId);
anchorTag.setAttribute("class", IN_PAGE_NAV_ANCHOR_CLASS);
el.insertAdjacentElement("afterbegin", anchorTag);

Expand Down Expand Up @@ -206,7 +258,7 @@ const handleEnterFromLink = (event) => {
} else {
// throw an error?
}
handleScrollToSection(target);
handleScrollToSection(targetAnchor);
};

const inPageNavigation = behavior(
Expand All @@ -228,6 +280,7 @@ const inPageNavigation = behavior(
init(root) {
selectOrMatches(`.${IN_PAGE_NAV_CLASS}`, root).forEach((inPageNavEl) => {
createInPageNav(inPageNavEl);
scrollToCurrentSection();
});
},
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
@use "uswds-core" as *;
@use "sass:color";
@use "sass:list";

.usa-in-page-nav-container {
align-items: flex-start;
Expand Down Expand Up @@ -72,6 +73,19 @@
$preferred-link-color: $theme-in-page-nav-link-color,
$context: "In-page-navigation link"
);

&:visited {
color: color(
list.nth(
get-link-tokens-from-bg(
$bg-color: $theme-in-page-nav-background-color,
$preferred-link-token: $theme-in-page-nav-link-color,
$context: "In-page-navigation link"
),
1
)
);
}
}

&.usa-current {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const sinon = require("sinon");
const behavior = require("../index");

const HIDE_MAX_WIDTH = 639;
const OFFSET_PER_SECTION = 100;
const TEMPLATE = fs.readFileSync(path.join(__dirname, "/template.html"));
const STYLES = fs.readFileSync(
`${__dirname}/../../../../dist/css/uswds.min.css`
Expand Down Expand Up @@ -48,26 +49,64 @@ tests.forEach(({ name, selector: containerSelector }) => {

let theNav;
let theList;
let originalOffsetTop;

before(() => {
originalOffsetTop = Object.getOwnPropertyDescriptor(
HTMLElement.prototype,
"offsetTop"
);
Object.defineProperty(HTMLElement.prototype, "offsetTop", {
get() {
// Since JSDOM doesn't emulate positions, create a fake offset using
// the heading's index to be used to test scrolling behavior.
const heading = this.closest("h2,h3");

let index = 0;
let sibling = heading;
while (true) {
sibling = sibling.previousElementSibling;
if (sibling) {
index += 1;
} else {
break;
}
}

return index * OFFSET_PER_SECTION;
},
});
const observe = sinon.spy();
const mockIntersectionObserver = sinon.stub().returns({ observe });
window.IntersectionObserver = mockIntersectionObserver;
sinon.stub(window, "scroll");
});

beforeEach(() => {
body.innerHTML = TEMPLATE;

behavior.on(containerSelector());

theNav = document.querySelector(THE_NAV);
theList = document.querySelector(PRIMARY_CONTENT_SELECTOR);

window.innerWidth = 1024;
behavior.on(containerSelector());
});

afterEach(() => {
sinon.resetHistory();
behavior.off(containerSelector(body));
body.innerHTML = "";
window.location.hash = "";
});

after(() => {
Object.defineProperty(
HTMLElement.prototype,
"offsetTop",
originalOffsetTop
);
sinon.restore();
});

it("defines a max width", () => {
Expand All @@ -84,5 +123,59 @@ tests.forEach(({ name, selector: containerSelector }) => {
resizeTo(1024);
assertHidden(theList, false);
});

it("assigns id to section headings", () => {
// Tests that new anchor children are created in the fixture template in
// the expected locations.
const ok = [
"h2 > .usa-anchor#section-1",
"h2 ~ h3 > .usa-anchor#section-1-1",
"h2 ~ h3 ~ h3 > .usa-anchor#section-1-2-2",
"h2 ~ h3 ~ h3 ~ h3 > .usa-anchor#section-1-3",
].every((selector) => document.querySelector(selector));

assert(ok);
});

it("scrolls to section", () => {
const firstLink = theNav.querySelector("a[href='#section-1']");
firstLink.click();

assert(window.scroll.calledOnceWith(sinon.match({ top: 80 })));
});

it("updates url when scrolling to section", () => {
// Activate by click
const firstLink = theNav.querySelector("a[href='#section-1']");
firstLink.click();

assert.equal(window.location.hash, "#section-1");

// Activate by Enter press
const secondLink = theNav.querySelector("a[href='#section-1-1']");
const event = new KeyboardEvent("keydown", {
bubbles: true,
key: "Enter",
keyCode: 13,
});
secondLink.dispatchEvent(event);

assert.equal(window.location.hash, "#section-1-1");
});

it("does not scroll to section on initialization", () => {
assert.equal(window.scroll.called, false);
});

context("with initial hash URL", () => {
before(() => {
sinon.stub(window, "location").value({ hash: undefined });
sinon.stub(window.location, "hash").get(() => "#section-1");
});

it("scrolls to section on initialization", () => {
assert(window.scroll.calledOnceWith(sinon.match({ top: 80 })));
});
});
});
});
31 changes: 8 additions & 23 deletions packages/usa-in-page-navigation/src/test/template.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,15 @@
<main id="main-content" class="main-content">
<h1>In page navigation test heading</h1>

<h2><a id="section_0" class="usa-anchor-tag"></a>Section 1</h2>
<h2>Section 1</h2>
<p></p>
<h3><a id="section_1" class="usa-anchor-tag"></a>Section 1.1</h3>
<h3>Section 1.1</h3>
<p>Section 1.1 content</p>
<h3><a id="section_2" class="usa-anchor-tag"></a>Section 1.2</h3>
<p>Section 1.2. content</p>
<h3>Section 1.2</h3>
<p id="section-1-2">Section 1.2 content</p>
<h3>? Section 1.3.</h3>
<p>Section 1.3 content</p>
</main>

<aside class="usa-in-page-nav" data-title-text="On this page" data-title-heading-level="h4" data-scroll-offset="20">
<nav aria-label="In-page navigation">
<h4 class="usa-in-page-nav__heading">On this page</h4>
<ul class="usa-in-page-nav__list">
<li class="usa-in-page-nav__item"><a href="#section_0" class="usa-in-page-nav__link usa-current">Section 1</a></li>
<li class="usa-in-page-nav__item usa-in-page-nav__item--sub-item"><a href="#section_1" class="usa-in-page-nav__link">Section 1.1</a></li>
<li class="usa-in-page-nav__item usa-in-page-nav__item--sub-item"><a href="#section_2" class="usa-in-page-nav__link">Section 1.2</a></li>
</ul>
</nav>
</aside>
<main id="main-content" class="main-content">
<h1>In page navigation test heading</h1>
<h2><a id="section_0" class="usa-anchor-tag"></a>Section 1</h2>
<h3><a id="section_1" class="usa-anchor-tag"></a>Section 1.1</h3>
<p>Section 1.1 content</p>
<h3><a id="section_2" class="usa-anchor-tag"></a>Section 1.2</h3>
<p>Section 1.2. content</p>
</main>
</div>
<aside class="usa-in-page-nav" data-title-text="On this page" data-title-heading-level="h4" data-scroll-offset="20"></aside>
</div>