Skip to content

Commit

Permalink
Misc fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
mj12albert committed Sep 5, 2024
1 parent 2626864 commit 258295c
Show file tree
Hide file tree
Showing 8 changed files with 367 additions and 36 deletions.
189 changes: 189 additions & 0 deletions docs/app/experiments/accordion-animations.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import * as React from 'react';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import * as Accordion from '@base_ui/react/Accordion';

const DURATION = '300ms';

export default function App() {
return (
<div className="AnimatedAccordions">
<h3>CSS @keyframe animations + `hidden=&quot;until-found&quot;`</h3>
<Accordion.Root
className="MyAccordion-root"
aria-label="Uncontrolled Material UI Accordion"
htmlHidden="until-found"
>
{[0, 1, 2].map((index) => (
<Accordion.Section className="MyAccordion-section" key={index}>
<Accordion.Heading className="MyAccordion-heading">
<Accordion.Trigger className="MyAccordion-trigger">
<span className="trigger-text">Trigger {index + 1}</span>
<ExpandMoreIcon />
</Accordion.Trigger>
</Accordion.Heading>
<Accordion.Panel className="MyAccordion-panel cssanimation">
<p>
This is the contents of Accordion.Panel {index + 1}
<br />
It uses `hidden=&quot;until-found&quot;` and can be opened by the browser&apos;s
in-page search
</p>
</Accordion.Panel>
</Accordion.Section>
))}
</Accordion.Root>

<h3>CSS transitions</h3>
<Accordion.Root className="MyAccordion-root" aria-label="Uncontrolled Material UI Accordion">
{[0, 1, 2].map((index) => (
<Accordion.Section className="MyAccordion-section" key={index}>
<Accordion.Heading className="MyAccordion-heading">
<Accordion.Trigger className="MyAccordion-trigger">
<span className="trigger-text">Trigger {index + 1}</span>
<ExpandMoreIcon />
</Accordion.Trigger>
</Accordion.Heading>
<Accordion.Panel className="MyAccordion-panel csstransition">
<p>This is the contents of Accordion.Panel {index + 1}</p>
</Accordion.Panel>
</Accordion.Section>
))}
</Accordion.Root>
<MaterialStyles />
</div>
);
}

function MaterialStyles() {
return (
<style suppressHydrationWarning>
{`
.AnimatedAccordions {
width: 40rem;
margin: 1rem;
display: flex;
flex-direction: column;
gap: 2rem;
}
.MyAccordion-root {
--Paper-shadow:
0px 2px 1px -1px rgba(0, 0, 0, 0.2),
0px 1px 1px 0px rgba(0, 0, 0, 0.14),
0px 1px 3px 0px rgba(0, 0, 0, 0.12);
font-family: system-ui, sans-serif;
box-shadow: var(--Paper-shadow);
background-color: rgba(0,0,0,0.12);
border-radius: .3rem;
}
.MyAccordion-section {
position: relative;
background-color: #fff;
color: rgba(0, 0, 0, .87);
}
.MyAccordion-section:not(:first-of-type) {
margin-top: 1px;
}
.MyAccordion-section:first-of-type {
border-top-left-radius: .25rem;
border-top-right-radius: .25rem;
}
.MyAccordion-section:last-of-type {
border-bottom-left-radius: .25rem;
border-bottom-right-radius: .25rem;
}
.MyAccordion-heading {
margin: 0;
}
.MyAccordion-trigger {
appearance: none;
background-color: transparent;
border: 0;
color: inherit;
cursor: pointer;
padding: 0 1rem;
position: relative;
width: 100%;
display: flex;
flex-flow: row nowrap;
align-items: center;
}
.MyAccordion-trigger:focus-visible {
outline: 0;
background-color: rgba(0,0,0,0.12);
}
.MyAccordion-trigger .trigger-text {
font-size: 1rem;
line-height: 1.5;
margin: 12px auto 12px 0;
}
.MyAccordion-trigger svg {
transition: transform 300ms;
}
.MyAccordion-trigger[data-collapsible="open"] svg {
transform: rotate(180deg);
}
.MyAccordion-panel {
overflow: hidden;
}
.MyAccordion-panel p {
margin: 0;
padding: 1rem;
}
.MyAccordion-panel.cssanimation[data-collapsible="open"] {
animation: slideDown ${DURATION} ease-out;
}
.MyAccordion-panel.cssanimation[data-collapsible="closed"] {
animation: slideUp ${DURATION} ease-out;
}
@keyframes slideDown {
from {
height: 0;
}
to {
height: var(--accordion-content-height);
}
}
@keyframes slideUp {
from {
height: var(--accordion-content-height);
}
to {
height: 0;
}
}
.MyAccordion-panel.csstransition[data-collapsible="open"] {
height: var(--accordion-content-height);
transition: height ${DURATION} ease-out;
}
.MyAccordion-panel.csstransition[data-collapsible="closed"] {
height: 0;
transition: height ${DURATION} ease-in;
}
.MyAccordion-panel.csstransition[data-entering] {
height: 0;
}
`}
</style>
);
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
'use client';
import * as React from 'react';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import * as Accordion from '@base_ui/react/Accordion';
Expand Down Expand Up @@ -122,7 +123,7 @@ function Styles() {
margin: 12px auto 12px 0;
}
.Accordion-trigger[data-state="open"] svg {
.Accordion-trigger[data-collapsible="open"] svg {
transform: rotate(180deg);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
'use client';
import * as React from 'react';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import * as Accordion from '@base_ui/react/Accordion';
Expand Down Expand Up @@ -122,7 +123,7 @@ function Styles() {
margin: 12px auto 12px 0;
}
.Accordion-trigger[data-state="open"] svg {
.Accordion-trigger[data-collapsible="open"] svg {
transform: rotate(180deg);
}
Expand Down
157 changes: 148 additions & 9 deletions docs/data/components/accordion/accordion.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -103,27 +103,21 @@ When uncontrolled, use the `defaultValue` prop to set the initial state of the a

```tsx
<Accordion.Root defaultValue={[0]}>
<Accordion.Section>
{' '}
{/* `value={0}` by default */}
<Accordion.Section {/* `value={0}` by default */}>
<Accordion.Heading>
<Accordion.Trigger>Toggle one</Accordion.Trigger>
</Accordion.Heading>
<Accordion.Panel>Section one content</Accordion.Panel>
</Accordion.Section>
<Accordion.Section>
{' '}
{/* `value={1}` by default */}
<Accordion.Section {/* `value={1}` by default */}>
<Accordion.Heading>
<Accordion.Trigger>Toggle two</Accordion.Trigger>
</Accordion.Heading>
<Accordion.Panel>Section two content</Accordion.Panel>
</Accordion.Section>
</Accordion.Root>;

{
/* with custom `value`s */
}
// with custom `value`s
<Accordion.Root defaultValue={['a']}>
<Accordion.Section value="a">
<Accordion.Heading>
Expand Down Expand Up @@ -212,3 +206,148 @@ Use the `direction` prop to configure a RTL accordion:
```

When a horizontal accordion is set to `direction="rtl"`, keyboard actions are reversed accordingly - <kbd class="key">Left Arrow</kbd> moves focus to the next trigger and <kbd class="key">Right Arrow</kbd> moves focus to the previous trigger.

## Improving searchability of hidden content

:::warning
This is [not yet supported](https://caniuse.com/mdn-html_global_attributes_hidden_until-found_value) in Safari and Firefox as of August 2024 and will fall back to the default `hidden` behavior.

:::

Content hidden by `Accordion.Panel` components can be made accessible only to a browser's find-in-page functionality with the `htmlHidden` prop to improve searchability:

```js
<Accordion.Root htmlHidden="until-found">{/* subcomponents */}</Accordion.Root>
```

Alternatively `htmlHidden` can be passed to `Accordion.Panel` directly to enable this for only one section instead of the whole accordion.

We recommend using [CSS animations](#css-animations) for animated accordions that use this feature. Currently there is browser bug that does not highlight the found text inside elements that have a [CSS transition](#css-transitions) applied.

This relies on the HTML `hidden="until-found"` attribute which only has [partial browser support](https://caniuse.com/mdn-html_global_attributes_hidden_until-found_value) as of August 2024, but automatically falls back to the default `hidden` state in unsupported browsers.

## Animations

Accordion uses [`Collapsible`](/base-ui/react-collapsible/) internally, and can be animated in a [similar way](/base-ui/react-collapsible/#animations).

Four states are available as data attributes to animate the `Accordion.Panel`:

- `[data-collapsible="open"]` - `open` state is `true`.
- `[data-collapsible="closed"]` - `open` state is `false`. Can still be mounted to the DOM if closing.
- `[data-entering]` - the `hidden` attribute was just removed from the DOM and the content element participates in page layout. The `data-entering` attribute will be removed 1 animation frame later.
- `[data-exiting]` - the content element is in the process of being hidden from the DOM, but is still mounted.

The component can be animate when opening or closing using either:

- CSS animations
- CSS transitions
- JavaScript animations

The dimensions of the `Accordion.Panel` subcomponent are provided as the `--accordion-content-height` and `--accordion-content-width` CSS variables.

### CSS Animations

CSS animations can be used with two declarations:

```css
.Accordion-panel {
overflow: hidden;
}

.Accordion-panel[data-collapsible='open'] {
animation: slideDown 300ms ease-out;
}

.Accordion-panel[data-collapsible='closed'] {
animation: slideUp 300ms ease-in;
}

@keyframes slideDown {
from {
height: 0;
}
to {
height: var(--accordion-content-height);
}
}

@keyframes slideUp {
from {
height: var(--accordion-content-height);
}
to {
height: 0;
}
}
```

### CSS Transitions

When using CSS transitions, styles for the `Panel` must be applied to three states:

- The closed styles with `[data-collapsible="closed"]`
- The open styles with `[data-collapsible="open"]`
- The entering styles with `[data-entering]`

```css
.Accordion-panel {
overflow: hidden;
}

.Accordion-panel[data-collapsible='open'] {
height: var(--accordion-content-height);
transition: height 300ms ease-out;
}

.Accordion-panel[data-entering] {
height: 0;
}

.Accordion-panel[data-collapsible='closed'] {
height: 0;
transition: height 300ms ease-in;
}
```

### JavaScript Animations

When using external libraries for animation, for example `framer-motion`, be aware that `Accordion.Section`s hides content using the html `hidden` attribute in the closed state, and does not unmount from the DOM.

```js
function App() {
const [value, setValue] = useState([0]);
return (
<Accordion.Root value={value} onOpenChange={setValue}>
<Accordion.Section>
<Accordion.Heading>
<Accordion.Trigger>Toggle</Accordion.Trigger>
</Accordion.Heading>
<Accordion.Panel
render={
<motion.div
key="AccordionPanel"
initial={false}
animate={open ? 'open' : 'closed'}
exit={!open ? 'open' : 'closed'}
variants={{
open: {
height: 'auto',
transition: { duration: 0.3, ease: 'ease-out' },
},
closed: {
height: 0,
transition: { duration: 0.3, ease: 'ease-in' },
transitionEnd: { display: 'revert-layer' },
},
}}
/>
}
>
This is the content
</Accordion.Panel>
</Accordion.Section>
{/* more accordion sections */}
</Accordion.Root>
);
}
```
Loading

0 comments on commit 258295c

Please sign in to comment.