-
-
Notifications
You must be signed in to change notification settings - Fork 47
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add ToggleButton component and hooks
- Loading branch information
1 parent
26737f8
commit 6c0fe92
Showing
14 changed files
with
624 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,28 @@ | ||
{ | ||
"props": { | ||
"aria-label": { "type": { "name": "string" } }, | ||
"aria-labelledby": { "type": { "name": "string" } }, | ||
"className": { "type": { "name": "union", "description": "func<br>| string" } }, | ||
"defaultPressed": { "type": { "name": "bool" }, "default": "false" }, | ||
"disabled": { "type": { "name": "bool" }, "default": "false" }, | ||
"onPressedChange": { | ||
"type": { "name": "func" }, | ||
"signature": { | ||
"type": "function(pressed: boolean, event: Event) => void", | ||
"describedArgs": ["pressed", "event"] | ||
} | ||
}, | ||
"pressed": { "type": { "name": "bool" }, "default": "undefined" }, | ||
"render": { "type": { "name": "union", "description": "element<br>| func" } } | ||
}, | ||
"name": "ToggleButton", | ||
"imports": ["import { ToggleButton } from '@base_ui/react/ToggleButton';"], | ||
"classes": [], | ||
"spread": true, | ||
"themeDefaultProps": null, | ||
"muiName": "ToggleButton", | ||
"filename": "/packages/mui-base/src/ToggleButton/ToggleButton.tsx", | ||
"inheritance": null, | ||
"demos": "<ul><li><a href=\"/components/react-toggle-button/\">ToggleButton</a></li></ul>", | ||
"cssComponent": false | ||
} |
26 changes: 26 additions & 0 deletions
26
docs/data/components/toggle-button/ToggleButtonIntroduction.js
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,26 @@ | ||
'use client'; | ||
import * as React from 'react'; | ||
import { ToggleButton } from '@base_ui/react/ToggleButton'; | ||
import classes from './styles.module.css'; | ||
|
||
export default function ToggleButtonIntroduction() { | ||
const [pressed, setPressed] = React.useState(false); | ||
return ( | ||
<ToggleButton | ||
pressed={pressed} | ||
onPressedChange={setPressed} | ||
className={classes.button} | ||
aria-label="Bookmark this article" | ||
> | ||
<svg | ||
xmlns="http://www.w3.org/2000/svg" | ||
width="24" | ||
height="24" | ||
viewBox="0 0 24 24" | ||
className={classes.icon} | ||
> | ||
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" /> | ||
</svg> | ||
</ToggleButton> | ||
); | ||
} |
26 changes: 26 additions & 0 deletions
26
docs/data/components/toggle-button/ToggleButtonIntroduction.tsx
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,26 @@ | ||
'use client'; | ||
import * as React from 'react'; | ||
import { ToggleButton } from '@base_ui/react/ToggleButton'; | ||
import classes from './styles.module.css'; | ||
|
||
export default function ToggleButtonIntroduction() { | ||
const [pressed, setPressed] = React.useState(false); | ||
return ( | ||
<ToggleButton | ||
pressed={pressed} | ||
onPressedChange={setPressed} | ||
className={classes.button} | ||
aria-label="Bookmark this article" | ||
> | ||
<svg | ||
xmlns="http://www.w3.org/2000/svg" | ||
width="24" | ||
height="24" | ||
viewBox="0 0 24 24" | ||
className={classes.icon} | ||
> | ||
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" /> | ||
</svg> | ||
</ToggleButton> | ||
); | ||
} |
16 changes: 16 additions & 0 deletions
16
docs/data/components/toggle-button/ToggleButtonIntroduction.tsx.preview
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,16 @@ | ||
<ToggleButton | ||
pressed={pressed} | ||
onPressedChange={setPressed} | ||
className={classes.button} | ||
aria-label="Bookmark this article" | ||
> | ||
<svg | ||
xmlns="http://www.w3.org/2000/svg" | ||
width="24" | ||
height="24" | ||
viewBox="0 0 24 24" | ||
className={classes.icon} | ||
> | ||
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" /> | ||
</svg> | ||
</ToggleButton> |
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,38 @@ | ||
.button { | ||
--size: 2.5rem; | ||
--corner: 0.4rem; | ||
|
||
display: flex; | ||
flex-flow: row nowrap; | ||
justify-content: center; | ||
align-items: center; | ||
height: var(--size); | ||
width: var(--size); | ||
border: 1px solid var(--gray-outline-2); | ||
border-radius: var(--corner); | ||
background-color: var(--gray-container-1); | ||
color: var(--gray-text-1); | ||
} | ||
|
||
.button:hover { | ||
background-color: var(--gray-surface-1); | ||
color: var(--gray-text-2); | ||
} | ||
|
||
.button:focus-visible { | ||
outline: 2px solid var(--gray-900); | ||
} | ||
|
||
.icon { | ||
fill: none; | ||
stroke: currentColor; | ||
stroke-width: 2; | ||
stroke-linecap: round; | ||
stroke-linejoin: round; | ||
} | ||
|
||
.button[data-pressed] .icon { | ||
color: var(--gray-text-2); | ||
fill: currentColor; | ||
stroke-width: 0; | ||
} |
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,76 @@ | ||
--- | ||
productId: base-ui | ||
title: React ToggleButton component | ||
description: Toggle Buttons are a two-state button that can either be off (not pressed) or on (pressed). | ||
components: ToggleButton | ||
hooks: useToggleButton | ||
githubLabel: 'component: toggle button' | ||
waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/button/ | ||
packageName: '@base_ui/react' | ||
--- | ||
|
||
# ToggleButton | ||
|
||
<Description /> | ||
|
||
<ComponentLinkHeader design={false} /> | ||
|
||
<Demo demo="ToggleButtonIntroduction" defaultCodeOpen="false" bg="gradient" /> | ||
|
||
## Installation | ||
|
||
<InstallationInstructions componentName="ToggleButton" /> | ||
|
||
## Anatomy | ||
|
||
ToggleButton is a single component that renders a `<button>`: | ||
|
||
```tsx | ||
<ToggleButton>Toggle bookmark</ToggleButton> | ||
``` | ||
|
||
## Controlled | ||
|
||
Use the `pressed` and `onPressedChange` props to control the pressed state: | ||
|
||
```jsx | ||
const [pressed, setPressed] = React.useState(false); | ||
|
||
<ToggleButton pressed={pressed} onPressedChange={setPressed}> | ||
{/* children */} | ||
</ToggleButton>; | ||
``` | ||
|
||
## Uncontrolled | ||
|
||
Use the `defaultPressed` prop to set the initial pressed state of the component when uncontrolled: | ||
|
||
```jsx | ||
<ToggleButton defaultPressed={false}>{/* children */}</ToggleButton> | ||
``` | ||
|
||
## Accessibility | ||
|
||
ToggleButtons must be given an accessible name with `aria-label` or `aria-labelledby`. It is important that the label does not change based on the pressed state to ensure a smooth screen-reader experience, since announcing the pressed state is already handled by `aria-pressed`. | ||
|
||
## Styling | ||
|
||
Use the `[data-pressed]` attribute to apply styles based on the pressed state: | ||
|
||
```jsx | ||
<ToggleButton className="MyToggle">{/* children */}</ToggleButton> | ||
``` | ||
|
||
```css | ||
/* base state, not pressed */ | ||
.MyToggle { | ||
background: white; | ||
color: black; | ||
} | ||
|
||
/* pressed state */ | ||
.MyToggle[data-pressed] { | ||
background: black; | ||
color: white; | ||
} | ||
``` |
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
26 changes: 26 additions & 0 deletions
26
docs/data/translations/api-docs/toggle-button/toggle-button.json
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,26 @@ | ||
{ | ||
"componentDescription": "", | ||
"propDescriptions": { | ||
"aria-label": { "description": "The label for the ToggleButton." }, | ||
"aria-labelledby": { | ||
"description": "An id or space-separated list of ids of elements that label the ToggleButton." | ||
}, | ||
"className": { | ||
"description": "Class names applied to the element or a function that returns them based on the component's state." | ||
}, | ||
"defaultPressed": { | ||
"description": "The default pressed state. Use when the component is not controlled." | ||
}, | ||
"disabled": { "description": "If <code>true</code>, the component is disabled." }, | ||
"onPressedChange": { | ||
"description": "Callback fired when the pressed state is changed.", | ||
"typeDescriptions": { | ||
"pressed": "The new pressed state.", | ||
"event": "The event source of the callback." | ||
} | ||
}, | ||
"pressed": { "description": "If <code>true</code>, the component is pressed." }, | ||
"render": { "description": "A function to customize rendering of the component." } | ||
}, | ||
"classDescriptions": {} | ||
} |
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,30 @@ | ||
'use client'; | ||
import * as React from 'react'; | ||
import { ToggleButton } from '@base_ui/react/ToggleButton'; | ||
|
||
export default function ToggleButtonDemo() { | ||
const [pressed, setPressed] = React.useState(true); | ||
return ( | ||
<div> | ||
<ToggleButton | ||
pressed={pressed} | ||
onPressedChange={setPressed} | ||
// className={classes.button} | ||
> | ||
<svg | ||
xmlns="http://www.w3.org/2000/svg" | ||
width="24" | ||
height="24" | ||
viewBox="0 0 24 24" | ||
fill="none" | ||
stroke="currentColor" | ||
strokeWidth={2} | ||
strokeLinecap="round" | ||
strokeLinejoin="round" | ||
> | ||
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" /> | ||
</svg> | ||
</ToggleButton> | ||
</div> | ||
); | ||
} |
101 changes: 101 additions & 0 deletions
101
packages/mui-base/src/ToggleButton/ToggleButton.test.tsx
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,101 @@ | ||
import * as React from 'react'; | ||
import { expect } from 'chai'; | ||
import { spy } from 'sinon'; | ||
import { act } from '@mui/internal-test-utils'; | ||
import { ToggleButton } from '@base_ui/react/ToggleButton'; | ||
import { createRenderer, describeConformance } from '#test-utils'; | ||
|
||
describe('<ToggleButton />', () => { | ||
const { render } = createRenderer(); | ||
|
||
describeConformance(<ToggleButton />, () => ({ | ||
refInstanceof: window.HTMLButtonElement, | ||
render, | ||
})); | ||
|
||
describe('pressed state', () => { | ||
it('controlled', async () => { | ||
function App() { | ||
const [pressed, setPressed] = React.useState(false); | ||
return ( | ||
<div> | ||
<input type="checkbox" checked={pressed} onChange={() => setPressed(!pressed)} /> | ||
<ToggleButton pressed={pressed} />; | ||
</div> | ||
); | ||
} | ||
|
||
const { getByRole } = await render(<App />); | ||
const checkbox = getByRole('checkbox'); | ||
const button = getByRole('button'); | ||
|
||
expect(button).to.have.attribute('aria-pressed', 'false'); | ||
await act(async () => { | ||
checkbox.click(); | ||
}); | ||
|
||
expect(button).to.have.attribute('aria-pressed', 'true'); | ||
|
||
await act(async () => { | ||
checkbox.click(); | ||
}); | ||
|
||
expect(button).to.have.attribute('aria-pressed', 'false'); | ||
}); | ||
|
||
it('uncontrolled', async () => { | ||
const { getByRole } = await render(<ToggleButton defaultPressed={false} />); | ||
|
||
const button = getByRole('button'); | ||
|
||
expect(button).to.have.attribute('aria-pressed', 'false'); | ||
await act(async () => { | ||
button.click(); | ||
}); | ||
|
||
expect(button).to.have.attribute('aria-pressed', 'true'); | ||
|
||
await act(async () => { | ||
button.click(); | ||
}); | ||
|
||
expect(button).to.have.attribute('aria-pressed', 'false'); | ||
}); | ||
}); | ||
|
||
describe('prop: onPressedChange', () => { | ||
it('is called when the pressed state changes', async () => { | ||
const handlePressed = spy(); | ||
const { getByRole } = await render( | ||
<ToggleButton defaultPressed={false} onPressedChange={handlePressed} />, | ||
); | ||
|
||
const button = getByRole('button'); | ||
|
||
await act(async () => { | ||
button.click(); | ||
}); | ||
|
||
expect(handlePressed.callCount).to.equal(1); | ||
expect(handlePressed.firstCall.args[0]).to.equal(true); | ||
}); | ||
}); | ||
|
||
describe('prop: disabled', () => { | ||
it('disables the component', async () => { | ||
const handlePressed = spy(); | ||
const { getByRole } = await render(<ToggleButton disabled onPressedChange={handlePressed} />); | ||
|
||
const button = getByRole('button'); | ||
expect(button).to.have.attribute('disabled'); | ||
expect(button).to.have.attribute('aria-pressed', 'false'); | ||
|
||
await act(async () => { | ||
button.click(); | ||
}); | ||
|
||
expect(handlePressed.callCount).to.equal(0); | ||
expect(button).to.have.attribute('aria-pressed', 'false'); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.