Skip to content

Commit 23b3d7e

Browse files
committed
Rename footer regions into sections
1 parent 2a84390 commit 23b3d7e

7 files changed

Lines changed: 82 additions & 70 deletions

File tree

.changeset/bright-dots-rest.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66
Updated `MinimalFooter` visual design and added new content slot and back to top control.
77

88
- Added optional `MinimalFooter.Content` slot for custom content and optional `MinimalFooter.BackToTop` control.
9-
- Replaced the full GitHub logo with the GitHub logomark and reorganized the footer into top, content, and bottom regions.
9+
- Replaced the full GitHub logo with the GitHub logomark and reorganized the footer into top, content, and bottom sections.

apps/next-docs/content/components/MinimalFooter/index.mdx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ Use up to five `MinimalFooter.Link` children. Links can render as anchors or but
7777

7878
### Custom content
7979

80-
Use `MinimalFooter.Content` to render an opt-in slot between the top and the bottom regions. Use this page-specific controls or information such as a language selector, theme toggle, or status indicator, not to recreate top navigation.
80+
Use `MinimalFooter.Content` to render an opt-in slot between the top and bottom sections. Use this for page-specific controls or information such as a language selector, theme toggle, or status indicator, not to recreate top navigation.
8181

8282
```jsx live
8383
<MinimalFooter>
@@ -124,16 +124,16 @@ Use `scrollBehavior` to customize the DOM [scroll behavior](https://developer.mo
124124
| :------------------- | :------------------------------------------------------------------------------------------------------------------ | :-----------------------------------------------------------------------------------: | :------------------------------------------------------------ |
125125
| `className` | `string` | | Sets a custom class on the root element |
126126
| `children` | <MinimalFooterChildrenProp /> | `undefined` | Valid child components |
127-
| `copyrightStatement` | `string` \| `ReactElement` | Current-year GitHub copyright statement | The copyright statement displayed in the bottom region. |
127+
| `copyrightStatement` | `string` \| `ReactElement` | Current-year GitHub copyright statement | The copyright statement displayed in the bottom section. |
128128
| `logoHref` | `string` | `'https://github.com'` | The href for the GitHub logomark. |
129129
| `socialLinks` | `Array<'x' \| 'github' \| 'linkedin' \| 'youtube' \| 'facebook' \| 'twitch' \| 'tiktok' \| 'instagram'>` \| `false` | `['x', 'github', 'linkedin', 'youtube', 'facebook', 'twitch', 'tiktok', 'instagram']` | Ordered subset of social links, or `false` to hide all links. |
130130

131131
### MinimalFooter.Link
132132

133-
| Name | Type | Default | Description |
134-
| :--------- | :---------------- | :---------: | :------------------------------------ |
135-
| `as` | `'a'`, `'button'` | `'a'` | Applies the underlying HTML element |
136-
| `children` | `string` | `undefined` | Label for a link in the bottom region |
133+
| Name | Type | Default | Description |
134+
| :--------- | :---------------- | :---------: | :------------------------------------- |
135+
| `as` | `'a'`, `'button'` | `'a'` | Applies the underlying HTML element |
136+
| `children` | `string` | `undefined` | Label for a link in the bottom section |
137137

138138
### MinimalFooter.Footnotes
139139

packages/react/src/MinimalFooter/MinimalFooter.features.stories.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,11 @@ export const DefaultNarrow: Story = {
130130
</MinimalFooter>
131131
),
132132
play: async ({canvasElement}) => {
133-
const bottom = canvasElement.querySelector('[data-footer-layout="social"]')
133+
const socialLinks = within(canvasElement).getByRole('list')
134+
const bottom = socialLinks.closest('section')
134135
const container = bottom?.firstElementChild
135-
const socialLinks = bottom?.querySelector('ul')
136136

137-
if (!container || !socialLinks) {
137+
if (!container) {
138138
throw new Error('Expected the social footer layout to render')
139139
}
140140

packages/react/src/MinimalFooter/MinimalFooter.module.css

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
}
5050

5151
/**
52-
* Footer top region
52+
* Footer top section
5353
*/
5454
.Footer__top {
5555
padding-top: var(--base-size-48);
@@ -80,17 +80,21 @@
8080
}
8181
}
8282

83-
[data-footer-region='content'] > .Footer__container {
83+
.Footer__section {
84+
min-width: 0;
85+
}
86+
87+
.Footer__section--content > .Footer__container {
8488
padding-top: var(--base-size-32);
8589
padding-bottom: var(--base-size-32);
8690
}
8791

88-
.Footer [data-footer-region='content'] + [data-footer-region='bottom'] {
92+
.Footer__section--content + .Footer__section--bottom {
8993
border-top: var(--brand-borderWidth-thin) solid var(--brand-color-border-subtle);
9094
}
9195

9296
/**
93-
* Footer bottom region
97+
* Footer bottom section
9498
*/
9599
.Footer__bottom-row,
96100
.Footer__copyright-and-links {
@@ -151,11 +155,11 @@
151155
gap: var(--base-size-20);
152156
}
153157

154-
[data-footer-layout='no-social'] .Footer__copyright-and-links {
158+
.Footer__bottom--no-social .Footer__copyright-and-links {
155159
width: 100%;
156160
}
157161

158-
[data-footer-layout='no-social'] .Footer__links {
162+
.Footer__bottom--no-social .Footer__links {
159163
width: 100%;
160164
justify-content: space-between;
161165
order: -1;
@@ -222,11 +226,11 @@
222226
flex-wrap: wrap;
223227
}
224228

225-
[data-footer-layout='no-social'] .Footer__copyright-and-links {
229+
.Footer__bottom--no-social .Footer__copyright-and-links {
226230
justify-content: space-between;
227231
}
228232

229-
[data-footer-layout='no-social'] .Footer__links {
233+
.Footer__bottom--no-social .Footer__links {
230234
width: auto;
231235
justify-content: flex-start;
232236
order: initial;

packages/react/src/MinimalFooter/MinimalFooter.module.css.d.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ declare const styles: {
44
readonly "Footer__backToTop-content": string;
55
readonly "Footer__backToTop-icon": string;
66
readonly "Footer__bottom": string;
7+
readonly "Footer__bottom--no-social": string;
78
readonly "Footer__bottom-row": string;
89
readonly "Footer__container": string;
910
readonly "Footer__copyright": string;
@@ -12,6 +13,9 @@ declare const styles: {
1213
readonly "Footer__link-text": string;
1314
readonly "Footer__links": string;
1415
readonly "Footer__logo": string;
16+
readonly "Footer__section": string;
17+
readonly "Footer__section--bottom": string;
18+
readonly "Footer__section--content": string;
1519
readonly "Footer__social-icon": string;
1620
readonly "Footer__social-link": string;
1721
readonly "Footer__social-links": string;
@@ -21,4 +25,3 @@ declare const styles: {
2125
readonly "Footer__top-row": string;
2226
};
2327
export = styles;
24-

packages/react/src/MinimalFooter/MinimalFooter.test.tsx

Lines changed: 34 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -443,8 +443,9 @@ describe('MinimalFooter', () => {
443443

444444
expect(sections[0]).toContainElement(container.querySelector('.Footer__terms'))
445445

446-
const topRegion = container.querySelector('[data-footer-region="top"]')
447-
expect(topRegion).toBeInTheDocument()
446+
const topSection = container.querySelector('.Footer__top')?.parentElement
447+
expect(topSection).toBeInTheDocument()
448+
expect(topSection).toHaveClass('Footer__section')
448449
})
449450

450451
it('renders component with only footnotes', () => {
@@ -691,31 +692,30 @@ describe('MinimalFooter', () => {
691692
expect(queryByRole('link', {name: 'Link 6'})).not.toBeInTheDocument()
692693
})
693694

694-
it('omits the content region when no Content is provided', () => {
695+
it('omits the content section when no Content is provided', () => {
695696
const {container} = render(<MinimalFooter />)
696697

697-
const regions = Array.from(container.querySelectorAll('[data-footer-region]')).map(region =>
698-
region.getAttribute('data-footer-region'),
699-
)
700-
701-
expect(regions).toEqual(['top', 'bottom'])
698+
const footer = container.querySelector('footer')
699+
expect(footer?.children[0]).toHaveClass('Footer__section')
700+
expect(footer?.children[0]).toContainElement(container.querySelector('.Footer__top'))
701+
expect(footer?.children[1]).toHaveClass('Footer__section--bottom')
702702
})
703703

704-
it('renders top, content, and bottom regions in the approved order', () => {
704+
it('renders top, content, and bottom sections in the approved order', () => {
705705
const {container, getByText} = render(
706706
<MinimalFooter>
707707
<MinimalFooter.Content>Custom content</MinimalFooter.Content>
708708
</MinimalFooter>,
709709
)
710710

711-
const regions = Array.from(container.querySelectorAll('[data-footer-region]')).map(region =>
712-
region.getAttribute('data-footer-region'),
713-
)
714-
715-
expect(regions).toEqual(['top', 'content', 'bottom'])
716-
expect(container.querySelector('[data-footer-region="top"] .Footer__logo')).toBeInTheDocument()
717-
expect(getByText('Custom content').closest('[data-footer-region="bottom"]')).not.toBeInTheDocument()
718-
expect(getByText('Custom content').closest('[data-footer-region="content"]')).toBeInTheDocument()
711+
const footer = container.querySelector('footer')
712+
expect(footer?.children[0]).toHaveClass('Footer__section')
713+
expect(footer?.children[0]).toContainElement(container.querySelector('.Footer__top'))
714+
expect(footer?.children[1]).toHaveClass('Footer__section--content')
715+
expect(footer?.children[2]).toHaveClass('Footer__section--bottom')
716+
expect(container.querySelector('.Footer__top .Footer__logo')).toBeInTheDocument()
717+
expect(getByText('Custom content').closest('.Footer__section--bottom')).not.toBeInTheDocument()
718+
expect(getByText('Custom content').closest('.Footer__section--content')).toBeInTheDocument()
719719
})
720720

721721
it('places the logomark and Back to Top in the same top row', () => {
@@ -731,18 +731,18 @@ describe('MinimalFooter', () => {
731731
expect(logo.closest('.Footer__top-row')).toBe(backToTop.closest('.Footer__top-row'))
732732
})
733733

734-
it('places footer links and social links in the bottom region', () => {
734+
it('places footer links and social links in the bottom section', () => {
735735
const {getByRole} = render(
736736
<MinimalFooter socialLinks={['x']}>
737737
<MinimalFooter.Link href="/test">Test Link</MinimalFooter.Link>
738738
</MinimalFooter>,
739739
)
740740

741-
expect(getByRole('link', {name: 'Test Link'}).closest('[data-footer-region="bottom"]')).toBeInTheDocument()
742-
expect(getByRole('link', {name: 'GitHub on X'}).closest('[data-footer-region="bottom"]')).toBeInTheDocument()
741+
expect(getByRole('link', {name: 'Test Link'}).closest('.Footer__section--bottom')).toBeInTheDocument()
742+
expect(getByRole('link', {name: 'GitHub on X'}).closest('.Footer__section--bottom')).toBeInTheDocument()
743743
})
744744

745-
it('places Footnotes ahead of the mapped regions and preserves full region order when every optional child is present', () => {
745+
it('places Footnotes ahead of the mapped sections and preserves full section order when every optional child is present', () => {
746746
const {getByRole} = render(
747747
<MinimalFooter>
748748
<MinimalFooter.Footnotes>
@@ -755,11 +755,14 @@ describe('MinimalFooter', () => {
755755
)
756756

757757
const footer = getByRole('contentinfo')
758-
const topLevelRegionNames = Array.from(footer.children).map(
759-
element => element.getAttribute('data-footer-region') ?? 'footnotes',
760-
)
758+
const topLevelSectionNames = Array.from(footer.children).map(element => {
759+
if (element.classList.contains('Footer__section--content')) return 'content'
760+
if (element.classList.contains('Footer__section--bottom')) return 'bottom'
761+
if (element.querySelector('.Footer__top')) return 'top'
762+
return 'footnotes'
763+
})
761764

762-
expect(topLevelRegionNames).toEqual(['footnotes', 'top', 'content', 'bottom'])
765+
expect(topLevelSectionNames).toEqual(['footnotes', 'top', 'content', 'bottom'])
763766
})
764767

765768
it('does not impose internal layout on MinimalFooter.Content', () => {
@@ -773,7 +776,7 @@ describe('MinimalFooter', () => {
773776
</MinimalFooter>,
774777
)
775778

776-
// The region wrapper only positions Content within the footer; it does not alter or
779+
// The section wrapper only positions Content within the footer; it does not alter or
777780
// wrap the consumer's own children.
778781
expect(getByTestId('consumer-layout').parentElement).toHaveClass('Footer__container')
779782
})
@@ -789,23 +792,17 @@ describe('MinimalFooter', () => {
789792
expect(queryAllByRole('link', {name: isSocialLink})).toHaveLength(0)
790793
})
791794

792-
it('marks the bottom region with stable social and no-social layout hooks', () => {
795+
it('applies a no-social modifier class to the bottom section', () => {
793796
const {container, rerender} = render(
794797
<MinimalFooter>
795798
<MinimalFooter.Link href="/test">Test Link</MinimalFooter.Link>
796799
</MinimalFooter>,
797800
)
798801

799-
expect(container.querySelector('[data-footer-region="bottom"] section')).toHaveAttribute(
800-
'data-footer-layout',
801-
'social',
802-
)
802+
expect(container.querySelector('.Footer__bottom')).not.toHaveClass('Footer__bottom--no-social')
803803

804804
rerender(<MinimalFooter socialLinks={false} />)
805-
expect(container.querySelector('[data-footer-region="bottom"] section')).toHaveAttribute(
806-
'data-footer-layout',
807-
'no-social',
808-
)
805+
expect(container.querySelector('.Footer__bottom')).toHaveClass('Footer__bottom--no-social')
809806
})
810807

811808
it('preserves logo href, analytics, accessibility, and semantic styling', () => {
@@ -891,15 +888,15 @@ describe('MinimalFooter', () => {
891888
expect(queryByRole('button', {name: 'Second'})).not.toBeInTheDocument()
892889
})
893890

894-
it('renders Back to Top within the top region', () => {
891+
it('renders Back to Top within the top section', () => {
895892
const {getByRole} = render(
896893
<MinimalFooter>
897894
<MinimalFooter.BackToTop>Back to top</MinimalFooter.BackToTop>
898895
</MinimalFooter>,
899896
)
900897

901898
const button = getByRole('button', {name: 'Back to top'})
902-
expect(button.closest('[data-footer-region="top"]')).toBeInTheDocument()
899+
expect(button.closest('.Footer__section')).toBeInTheDocument()
903900
})
904901

905902
it('forwards ARIA, data, analytics, and native button attributes', () => {

packages/react/src/MinimalFooter/MinimalFooter.tsx

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -131,11 +131,11 @@ function parseRootChildren(children: React.ReactNode) {
131131
}
132132

133133
/**
134-
* The footer's top-level regions, in rendering order. `top` and `bottom`
134+
* The footer's top-level sections, in rendering order. `top` and `bottom`
135135
* are always populated, while `content` is only rendered when a
136136
* `MinimalFooter.Content` child is present.
137137
*/
138-
type FooterRegionName = 'top' | 'content' | 'bottom'
138+
type FooterSectionName = 'top' | 'content' | 'bottom'
139139

140140
function Root({
141141
className,
@@ -152,7 +152,7 @@ function Root({
152152
const renderedSocialLinks = resolvedSocialLinks === false ? [] : resolvedSocialLinks
153153
const hasSocialLinks = renderedSocialLinks.length > 0
154154

155-
const topRegion = (
155+
const topSection = (
156156
<section className={styles.Footer__top}>
157157
<div className={styles.Footer__container}>
158158
<div className={styles['Footer__top-row']}>
@@ -163,8 +163,8 @@ function Root({
163163
</section>
164164
)
165165

166-
const bottomRegion = (
167-
<section className={styles.Footer__bottom} data-footer-layout={hasSocialLinks ? 'social' : 'no-social'}>
166+
const bottomSection = (
167+
<section className={clsx(styles.Footer__bottom, !hasSocialLinks && styles['Footer__bottom--no-social'])}>
168168
<div className={styles.Footer__container}>
169169
<div className={styles['Footer__bottom-row']}>
170170
<div className={styles['Footer__copyright-and-links']}>
@@ -179,20 +179,28 @@ function Root({
179179
</section>
180180
)
181181

182-
const regions: {name: FooterRegionName; node: React.ReactNode}[] = [
183-
{name: 'top', node: topRegion},
184-
{name: 'content', node: content},
185-
{name: 'bottom', node: bottomRegion},
182+
const sections: {name: FooterSectionName; node: React.ReactNode; className: string}[] = [
183+
{name: 'top', node: topSection, className: styles.Footer__section},
184+
{
185+
name: 'content',
186+
node: content,
187+
className: clsx(styles.Footer__section, styles['Footer__section--content']),
188+
},
189+
{
190+
name: 'bottom',
191+
node: bottomSection,
192+
className: clsx(styles.Footer__section, styles['Footer__section--bottom']),
193+
},
186194
]
187195

188196
return (
189197
<footer className={clsx(styles.Footer, className)} {...rest}>
190198
{footnotes}
191-
{regions
192-
.filter(region => Boolean(region.node))
193-
.map(region => (
194-
<div key={region.name} data-footer-region={region.name}>
195-
{region.node}
199+
{sections
200+
.filter(section => Boolean(section.node))
201+
.map(section => (
202+
<div key={section.name} className={section.className}>
203+
{section.node}
196204
</div>
197205
))}
198206
</footer>

0 commit comments

Comments
 (0)