Skip to content

Commit 443a077

Browse files
mdmower-csnwwerdnanoslenbrandonlenz
authored
feat: upgrade to USWDS 3.8.2 (#3263)
Co-authored-by: Andrew Nelson <andy@andyhub.com> Co-authored-by: Brandon Lenz <15805554+brandonlenz@users.noreply.github.com>
1 parent 104e08a commit 443a077

File tree

20 files changed

+340
-139
lines changed

20 files changed

+340
-139
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@
8585
"peerDependencies": {
8686
"@types/react": "^16.x || ^17.x || ^18.x || ^19.x",
8787
"@types/react-dom": "^16.x || ^17.x || ^18.x || ^19.x",
88-
"@uswds/uswds": "^3.7.1",
88+
"@uswds/uswds": "^3.8.2",
8989
"focus-trap-react": "^10.2.3",
9090
"react": "^16.x || ^17.x || ^18.x || ^19.x",
9191
"react-dom": "^16.x || ^17.x || ^18.x || ^19.x"
@@ -109,7 +109,7 @@
109109
"@types/react-dom": "^19.0.4",
110110
"@typescript-eslint/eslint-plugin": "^8.29.0",
111111
"@typescript-eslint/parser": "^8.29.0",
112-
"@uswds/uswds": "3.7.1",
112+
"@uswds/uswds": "3.8.2",
113113
"@vitejs/plugin-react": "^5.0.0",
114114
"@vitest/coverage-istanbul": "^4.0.8",
115115
"@vitest/eslint-plugin": "^1.1.44",

src/components/InPageNavigation/InPageNavigation.stories.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,17 @@ export default {
2020
title: {
2121
control: 'text',
2222
},
23+
headingElements: {
24+
control: 'check',
25+
options: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
26+
},
2327
},
2428
args: {
2529
headingLevel: 'h4',
2630
rootMargin: '0px 0px 0px 0px',
2731
threshold: 1,
2832
title: 'On this page',
33+
headingElements: ['h2', 'h3'],
2934
},
3035
parameters: {
3136
docs: {
@@ -46,6 +51,7 @@ type StorybookArguments = {
4651
scrollOffset: string
4752
threshold: number
4853
title: string
54+
headingElements: HeadingLevel[]
4955
}
5056

5157
export const Default = (argTypes: StorybookArguments): JSX.Element => (
@@ -56,6 +62,7 @@ export const Default = (argTypes: StorybookArguments): JSX.Element => (
5662
rootMargin={argTypes.rootMargin}
5763
threshold={argTypes.threshold}
5864
title={argTypes.title}
65+
headingElements={argTypes.headingElements}
5966
/>
6067
)
6168

@@ -70,5 +77,6 @@ export const ScrollOffset = (argTypes: StorybookArguments): JSX.Element => (
7077
scrollOffset="2rem"
7178
threshold={argTypes.threshold}
7279
title={argTypes.title}
80+
headingElements={argTypes.headingElements}
7381
/>
7482
)

src/components/InPageNavigation/InPageNavigation.test.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react'
2-
import { screen, render, getByRole } from '@testing-library/react'
2+
import { screen, render, getByRole, within } from '@testing-library/react'
33
import { userEvent } from '@testing-library/user-event'
44
import { InPageNavigation } from './InPageNavigation'
55
import { HeadingLevel } from '../../types/headingLevel'
@@ -13,14 +13,15 @@ describe('InPageNavigation component', () => {
1313
title: 'What do we have <i>here</i>?',
1414
}
1515

16-
const setup = (plain?: boolean) => {
16+
const setup = (plain?: boolean, headingElements?: HeadingLevel[]) => {
1717
const utils = plain
1818
? render(<InPageNavigation content={props.content} />)
1919
: render(
2020
<InPageNavigation
2121
content={props.content}
2222
headingLevel={props.headingLevel}
2323
title={props.title}
24+
headingElements={headingElements}
2425
/>
2526
)
2627
const nav = screen.getByTestId('InPageNavigation')
@@ -70,4 +71,22 @@ describe('InPageNavigation component', () => {
7071
})
7172
expect(heading).toBeInTheDocument()
7273
})
74+
75+
describe('lists the right heading types if', () => {
76+
it('is undefined', () => {
77+
const { nav } = setup(true)
78+
const contentHeadingsTwo = screen.getAllByRole('heading', { level: 2 })
79+
const contentHeadingsThree = screen.getAllByRole('heading', { level: 3 })
80+
const contentHeadings = contentHeadingsTwo.concat(contentHeadingsThree)
81+
const headingLinks = within(nav).getAllByRole('link')
82+
expect(contentHeadings.length).toBe(headingLinks.length)
83+
})
84+
85+
it('is defined', () => {
86+
const { nav } = setup(false, ['h2' as HeadingLevel])
87+
const contentHeadingsTwo = screen.getAllByRole('heading', { level: 2 })
88+
const headingLinks = within(nav).getAllByRole('link')
89+
expect(contentHeadingsTwo.length).toBe(headingLinks.length)
90+
})
91+
})
7392
})

src/components/InPageNavigation/InPageNavigation.tsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export type InPageNavigationProps = {
1414
scrollOffset?: string
1515
threshold?: number
1616
title?: string
17+
headingElements?: HeadingLevel[]
1718
} & Omit<JSX.IntrinsicElements['div'], 'content'>
1819

1920
export const InPageNavigation = ({
@@ -26,6 +27,7 @@ export const InPageNavigation = ({
2627
scrollOffset,
2728
threshold = 1,
2829
title = 'On this page',
30+
headingElements = ['h2', 'h3'],
2931
...divProps
3032
}: InPageNavigationProps): JSX.Element => {
3133
const asideClasses = classnames('usa-in-page-nav', styles.target, className)
@@ -38,8 +40,11 @@ export const InPageNavigation = ({
3840
'--margin-offset': scrollOffset,
3941
} as React.CSSProperties
4042
const [currentSection, setCurrentSection] = useState('')
43+
headingElements = !headingElements.length
44+
? ['h2', 'h3']
45+
: headingElements.sort()
4146
const sectionHeadings: JSX.Element[] = content.props.children.filter(
42-
(el: JSX.Element) => el.type === 'h2' || el.type === 'h3'
47+
(el: JSX.Element) => headingElements.includes(el.type)
4348
)
4449
const handleIntersection = (entries: IntersectionObserverEntry[]) => {
4550
entries.forEach((entry) => {
@@ -55,7 +60,9 @@ export const InPageNavigation = ({
5560
}
5661
const observer = new IntersectionObserver(handleIntersection, observerOptions)
5762
useEffect(() => {
58-
document.querySelectorAll('h2,h3').forEach((h) => observer.observe(h))
63+
document
64+
.querySelectorAll(headingElements.join(','))
65+
.forEach((h) => observer.observe(h))
5966
document.querySelector('html')?.classList.add(styles['smooth-scroll'])
6067
return () => {
6168
document.querySelector('html')?.classList.remove(styles['smooth-scroll'])
@@ -75,16 +82,17 @@ export const InPageNavigation = ({
7582
<ul className="usa-in-page-nav__list">
7683
{sectionHeadings.map((el: JSX.Element) => {
7784
const heading: JSX.Element = el.props.children
78-
const href: string = el.props.id
85+
const href: string = el.props.id ?? ''
7986
const hClass = classnames('usa-in-page-nav__item', {
80-
'usa-in-page-nav__item--sub-item': el.type === 'h3',
87+
'usa-in-page-nav__item--primary':
88+
el.type === headingElements[0],
8189
})
8290
const lClass = classnames('usa-in-page-nav__link', {
83-
'usa-current': href === currentSection,
91+
'usa-current': !!href && href === currentSection,
8492
})
8593
return (
8694
<li key={`usa-in-page-nav__item_${heading}`} className={hClass}>
87-
<Link href={`#${href}`} className={lClass}>
95+
<Link href={`#${CSS.escape(href)}`} className={lClass}>
8896
{heading}
8997
</Link>
9098
</li>

src/components/Pagination/Pagination.tsx

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import React, { type JSX } from 'react'
22
import classnames from 'classnames'
33
import { Icon } from '../Icon/Icons'
44
import { Link } from '../Link/Link'
5-
import { Button } from '../Button/Button'
65

76
export type PaginationProps = {
87
pathname: string // pathname of results page
@@ -34,24 +33,28 @@ const PaginationPage = ({
3433
const linkClasses = classnames('usa-pagination__button', {
3534
'usa-current': isCurrent,
3635
})
36+
const buttonClasses = classnames(
37+
linkClasses,
38+
'bg-transparent',
39+
'cursor-pointer'
40+
)
3741

3842
return (
3943
<li
4044
key={`pagination_page_${page}`}
4145
className="usa-pagination__item usa-pagination__page-no">
4246
{onClickPageNumber ? (
43-
<Button
47+
<button
4448
type="button"
45-
unstyled
4649
data-testid="pagination-page-number"
47-
className={linkClasses}
50+
className={buttonClasses}
4851
aria-label={`Page ${page}`}
4952
aria-current={isCurrent ? 'page' : undefined}
5053
onClick={(event) => {
5154
onClickPageNumber(event, page)
5255
}}>
5356
{page}
54-
</Button>
57+
</button>
5558
) : (
5659
<Link
5760
href={`${pathname}?page=${page}`}
@@ -161,26 +164,48 @@ export const Pagination = ({
161164
const prevPage = !isOnFirstPage && currentPage - 1
162165
const nextPage = !isOnLastPage && currentPage + 1
163166

167+
const prevLinkClasses = classnames(
168+
'usa-pagination__link',
169+
'usa-pagination__previous-page'
170+
)
171+
const prevButtonClasses = classnames(
172+
prevLinkClasses,
173+
'border-0',
174+
'padding-0',
175+
'bg-transparent',
176+
'cursor-pointer'
177+
)
178+
const nextLinkClasses = classnames(
179+
'usa-pagination__link',
180+
'usa-pagination__next-page'
181+
)
182+
const nextButtonClasses = classnames(
183+
nextLinkClasses,
184+
'border-0',
185+
'padding-0',
186+
'bg-transparent',
187+
'cursor-pointer'
188+
)
189+
164190
return (
165191
<nav aria-label="Pagination" className={navClasses} {...props}>
166192
<ul className="usa-pagination__list">
167193
{prevPage && (
168194
<li className="usa-pagination__item usa-pagination__arrow">
169195
{onClickPrevious ? (
170-
<Button
196+
<button
171197
type="button"
172-
unstyled
173-
className="usa-pagination__link usa-pagination__previous-page"
198+
className={prevButtonClasses}
174199
aria-label="Previous page"
175200
data-testid="pagination-previous"
176201
onClick={onClickPrevious}>
177202
<Icon.NavigateBefore aria-hidden={true} />
178203
<span className="usa-pagination__link-text">Previous</span>
179-
</Button>
204+
</button>
180205
) : (
181206
<Link
182207
href={`${pathname}?page=${prevPage}`}
183-
className="usa-pagination__link usa-pagination__previous-page"
208+
className={prevLinkClasses}
184209
aria-label="Previous page">
185210
<Icon.NavigateBefore aria-hidden={true} />
186211
<span className="usa-pagination__link-text">Previous</span>
@@ -206,20 +231,19 @@ export const Pagination = ({
206231
{nextPage && (
207232
<li className="usa-pagination__item usa-pagination__arrow">
208233
{onClickNext ? (
209-
<Button
234+
<button
210235
type="button"
211-
unstyled
212-
className="usa-pagination__link usa-pagination__next-page"
236+
className={nextButtonClasses}
213237
aria-label="Next page"
214238
data-testid="pagination-next"
215239
onClick={onClickNext}>
216240
<span className="usa-pagination__link-text">Next</span>
217241
<Icon.NavigateNext aria-hidden={true} />
218-
</Button>
242+
</button>
219243
) : (
220244
<Link
221245
href={`${pathname}?page=${nextPage}`}
222-
className="usa-pagination__link usa-pagination__next-page"
246+
className={nextLinkClasses}
223247
aria-label="Next page">
224248
<span className="usa-pagination__link-text">Next</span>
225249
<Icon.NavigateNext aria-hidden={true} />

src/components/Table/Table.stories.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,15 @@ Source: https://designsystem.digital.gov/components/table/
2929
stackedStyle: {
3030
control: {
3131
type: 'select',
32-
options: ['default', 'headers'],
32+
options: ['default', 'headers', 'none'],
3333
},
3434
},
35+
stickyHeader: {
36+
control: {
37+
type: 'boolean',
38+
},
39+
description: 'This is not compatible with stacked and scrollable',
40+
},
3541
},
3642
args: {
3743
stackedStyle: 'default',
@@ -41,7 +47,8 @@ Source: https://designsystem.digital.gov/components/table/
4147
type StorybookArguments = {
4248
bordered: boolean
4349
striped: boolean
44-
stackedStyle: 'default' | 'headers'
50+
stackedStyle: 'default' | 'headers' | 'none'
51+
stickyHeader: boolean
4552
}
4653

4754
const testContent = (
@@ -341,6 +348,14 @@ export const Scrollable = (): JSX.Element => (
341348
</>
342349
)
343350

351+
export const StickyHeader = {
352+
render: (argTypes: StorybookArguments): React.ReactElement => (
353+
<Table stickyHeader bordered={argTypes.bordered}>
354+
{testContent}
355+
</Table>
356+
),
357+
}
358+
344359
export const Striped = {
345360
render: (argTypes: StorybookArguments): JSX.Element => (
346361
<Table

src/components/Table/Table.test.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,12 @@ describe('Table component', () => {
104104
)
105105
})
106106

107+
it('renders sticky header table', () => {
108+
const { getByRole } = render(<Table stickyHeader>{testContent}</Table>)
109+
110+
expect(getByRole('table')).toHaveClass('usa-table--sticky-header')
111+
})
112+
107113
it('passes the class onto the root table element', () => {
108114
const { getByRole } = render(
109115
<Table className="custom-class">{testContent}</Table>

src/components/Table/Table.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export type TableProps = {
1414
striped?: boolean
1515
compact?: boolean
1616
stackedStyle?: 'none' | 'default' | 'headers'
17+
stickyHeader?: boolean
1718
}
1819

1920
export const Table = ({
@@ -27,6 +28,7 @@ export const Table = ({
2728
striped,
2829
compact,
2930
stackedStyle = 'none',
31+
stickyHeader,
3032
}: TableProps): JSX.Element => {
3133
const classes = classnames(
3234
'usa-table',
@@ -38,6 +40,7 @@ export const Table = ({
3840
'usa-table--compact': compact,
3941
'usa-table--stacked': stackedStyle === 'default',
4042
'usa-table--stacked-header': stackedStyle === 'headers',
43+
'usa-table--sticky-header': stickyHeader,
4144
},
4245
className
4346
)
@@ -48,6 +51,12 @@ export const Table = ({
4851
)
4952
}
5053

54+
if (stickyHeader && (scrollable || stackedStyle !== 'none')) {
55+
console.warn(
56+
'USWDS states that sticky headers are not compatible with scrollable or stacked variants. See USWDS Table component, Table variants for more information: https://designsystem.digital.gov/components/table'
57+
)
58+
}
59+
5160
const table = (
5261
<table className={classes} data-testid="table">
5362
{caption && <caption>{caption}</caption>}

0 commit comments

Comments
 (0)