-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
b413cc0
commit 389c03c
Showing
6 changed files
with
192 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import classnames from 'classnames'; | ||
import type { JSX } from 'preact'; | ||
|
||
import type { PresentationalProps } from '../../types'; | ||
import { downcastRef } from '../../util/typing'; | ||
|
||
import ButtonBase from '../input/ButtonBase'; | ||
|
||
type ComponentProps = { | ||
/** | ||
* Text string representing the content of the tab when selected. The tab | ||
* button will be sized to accommodate this string in bold text. This can | ||
* prevent tab jiggle. | ||
*/ | ||
textContent?: string; | ||
selected?: boolean; | ||
variant?: 'basic'; | ||
}; | ||
|
||
type HTMLAttributes = Omit< | ||
JSX.HTMLAttributes<HTMLButtonElement>, | ||
'size' | 'icon' | 'title' | ||
>; | ||
|
||
export type TabProps = PresentationalProps & ComponentProps & HTMLAttributes; | ||
|
||
/** | ||
* Render a button with appropriate ARIA tab affordances | ||
*/ | ||
const TabNext = function Tab({ | ||
children, | ||
classes, | ||
elementRef, | ||
|
||
textContent, | ||
selected = false, | ||
variant = 'basic', | ||
|
||
...htmlAttributes | ||
}: TabProps) { | ||
return ( | ||
<ButtonBase | ||
{...htmlAttributes} | ||
classes={classnames( | ||
'enabled:hover:text-brand-dark', | ||
{ | ||
'font-bold': selected && variant === 'basic', | ||
}, | ||
classes | ||
)} | ||
elementRef={downcastRef(elementRef)} | ||
aria-selected={selected} | ||
role="tab" | ||
data-component="Tab" | ||
> | ||
<span | ||
data-content={textContent} | ||
data-testid="sizing-wrapper" | ||
className={classnames({ | ||
// Set the content of this span's ::before pseudo-element to | ||
// `textContent` and make it bold. | ||
'before:content-[attr(data-content)] before:font-bold': textContent, | ||
// Make the ::before occupy space within the button, but make it | ||
// invisible. This ensures that the tab button is wide enough to show | ||
// bolded text even if the visible text is not currently bold. See | ||
// https://css-tricks.com/bold-on-hover-without-the-layout-shift/ | ||
'before:block before:invisible before:h-0 before:overflow-hidden': | ||
textContent, | ||
})} | ||
> | ||
{children} | ||
</span> | ||
</ButtonBase> | ||
); | ||
}; | ||
|
||
export default TabNext; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import classnames from 'classnames'; | ||
import type { JSX } from 'preact'; | ||
|
||
import { useArrowKeyNavigation } from '../../hooks/use-arrow-key-navigation'; | ||
import { useSyncedRef } from '../../hooks/use-synced-ref'; | ||
import type { PresentationalProps } from '../../types'; | ||
import { downcastRef } from '../../util/typing'; | ||
|
||
type HTMLAttributes = Omit<JSX.HTMLAttributes<HTMLElement>, 'size'>; | ||
|
||
export type TabListProps = PresentationalProps & HTMLAttributes; | ||
|
||
/** | ||
* Render a tablist container for a set of tabs, with arrow key navigation per | ||
* https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/ | ||
*/ | ||
const TabListNext = function TabList({ | ||
children, | ||
classes, | ||
elementRef, | ||
|
||
...htmlAttributes | ||
}: TabListProps) { | ||
const tabListRef = useSyncedRef(elementRef); | ||
|
||
useArrowKeyNavigation(tabListRef, { | ||
selector: 'button', | ||
horizontal: true, | ||
vertical: false, | ||
}); | ||
|
||
return ( | ||
<div | ||
{...htmlAttributes} | ||
ref={downcastRef(tabListRef)} | ||
className={classnames('flex', classes)} | ||
role="tablist" | ||
data-component="TabList" | ||
> | ||
{children} | ||
</div> | ||
); | ||
}; | ||
|
||
export default TabListNext; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { mount } from 'enzyme'; | ||
|
||
import { testPresentationalComponent } from '../../test/common-tests'; | ||
|
||
import Tab from '../Tab'; | ||
|
||
const contentFn = (Component, props = {}) => { | ||
return mount( | ||
<div role="tablist"> | ||
<Component {...props}>This is child content</Component> | ||
</div> | ||
); | ||
}; | ||
|
||
describe('Tab', () => { | ||
testPresentationalComponent(Tab, { | ||
createContent: contentFn, | ||
elementSelector: 'button[data-component="Tab"]', | ||
}); | ||
|
||
it('sets `aria-selected` when selected', () => { | ||
const tab1 = contentFn(Tab, { selected: true }); | ||
const tab2 = contentFn(Tab, { selected: false }); | ||
|
||
assert.equal( | ||
tab1.find('button').getDOMNode().getAttribute('aria-selected'), | ||
'true' | ||
); | ||
assert.equal( | ||
tab2.find('button').getDOMNode().getAttribute('aria-selected'), | ||
'false' | ||
); | ||
}); | ||
|
||
it('sets content data attribute on sizing span when `textContent` provided', () => { | ||
const wrapper = contentFn(Tab, { textContent: 'Tab Label' }); | ||
assert.equal( | ||
wrapper | ||
.find('[data-testid="sizing-wrapper"]') | ||
.getDOMNode() | ||
.getAttribute('data-content'), | ||
'Tab Label' | ||
); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { mount } from 'enzyme'; | ||
|
||
import { testPresentationalComponent } from '../../test/common-tests'; | ||
|
||
import TabList from '../TabList'; | ||
|
||
/** | ||
* An element with `role="tablist"` needs at least one `role="tab"` child. | ||
* Accessibility tests will fail without it. | ||
*/ | ||
const contentFn = (Component, props = {}) => { | ||
return mount( | ||
<Component {...props}> | ||
<button role="tab">Tab 1</button> | ||
</Component> | ||
); | ||
}; | ||
|
||
describe('TabList', () => { | ||
testPresentationalComponent(TabList, { createContent: contentFn }); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -41,4 +41,6 @@ export { | |
Link, | ||
LinkBase, | ||
LinkButton, | ||
Tab, | ||
TabList, | ||
} from './components/navigation/'; |