From f800414013e5d31fee0e849cf2f28d8c741f004d Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Tue, 3 Dec 2024 22:38:23 +0800 Subject: [PATCH] [Collapsible][Accordion] Modernize implementation (#889) --- docs/data/api/accordion-panel.json | 1 + docs/data/api/accordion-root.json | 2 + docs/data/api/collapsible-panel.json | 1 + docs/data/api/collapsible-root.json | 2 +- .../accordion/AccordionIntroduction.js | 59 +++ .../accordion/AccordionIntroduction.tsx | 59 +++ .../UnstyledAccordionIntroduction.js | 61 --- .../UnstyledAccordionIntroduction.tsx | 61 --- docs/data/components/accordion/accordion.mdx | 38 +- .../components/accordion/styles.module.css | 11 +- ...oduction.js => CollapsibleIntroduction.js} | 2 +- ...uction.tsx => CollapsibleIntroduction.tsx} | 2 +- .../components/collapsible/collapsible.mdx | 35 +- .../accordion-panel/accordion-panel.json | 5 +- .../accordion-root/accordion-root.json | 6 + .../collapsible-panel/collapsible-panel.json | 5 +- docs/reference/generated/accordion-panel.json | 7 +- docs/reference/generated/accordion-root.json | 10 + .../generated/collapsible-panel.json | 7 +- .../reference/generated/collapsible-root.json | 2 +- .../app/experiments/accordion-animations.tsx | 16 +- docs/src/app/experiments/accordion.tsx | 220 ---------- .../app/experiments/collapsible-accordion.tsx | 164 -------- docs/src/app/experiments/collapsible-cls.tsx | 172 ++++---- .../app/experiments/collapsible-framer.tsx | 93 ++--- .../collapsible-hidden-until-found.tsx | 90 ++-- .../experiments/collapsible-transitions.tsx | 77 ---- .../app/experiments/collapsible.module.css | 140 ++++--- docs/src/app/experiments/collapsible.tsx | 269 ++++-------- docs/src/components/demo/Demo.tsx | 1 + .../accordion/header/AccordionHeader.test.tsx | 2 + .../src/accordion/item/AccordionItem.test.tsx | 1 + .../src/accordion/item/AccordionItem.tsx | 13 +- .../accordion/panel/AccordionPanel.test.tsx | 2 + .../src/accordion/panel/AccordionPanel.tsx | 45 +- .../src/accordion/root/AccordionRoot.test.tsx | 383 ++++++++---------- .../src/accordion/root/AccordionRoot.tsx | 71 +++- .../accordion/root/AccordionRootContext.ts | 1 + .../src/accordion/root/useAccordionRoot.ts | 65 ++- .../trigger/AccordionTrigger.test.tsx | 2 + .../accordion/trigger/AccordionTrigger.tsx | 8 +- .../panel/CollapsiblePanel.test.tsx | 93 ++++- .../collapsible/panel/CollapsiblePanel.tsx | 74 +++- .../collapsible/panel/useCollapsiblePanel.ts | 154 ++++--- .../collapsible/root/CollapsibleRoot.test.tsx | 105 ++--- .../src/collapsible/root/CollapsibleRoot.tsx | 32 +- .../collapsible/root/useCollapsibleRoot.ts | 18 +- .../trigger/CollapsibleTrigger.test.tsx | 1 + .../trigger/CollapsibleTrigger.tsx | 3 +- .../trigger/useCollapsibleTrigger.ts | 10 +- .../src/utils/collapsibleOpenStateMapping.ts | 17 +- 51 files changed, 1223 insertions(+), 1495 deletions(-) create mode 100644 docs/data/components/accordion/AccordionIntroduction.js create mode 100644 docs/data/components/accordion/AccordionIntroduction.tsx delete mode 100644 docs/data/components/accordion/UnstyledAccordionIntroduction.js delete mode 100644 docs/data/components/accordion/UnstyledAccordionIntroduction.tsx rename docs/data/components/collapsible/{UnstyledCollapsibleIntroduction.js => CollapsibleIntroduction.js} (95%) rename docs/data/components/collapsible/{UnstyledCollapsibleIntroduction.tsx => CollapsibleIntroduction.tsx} (95%) delete mode 100644 docs/src/app/experiments/accordion.tsx delete mode 100644 docs/src/app/experiments/collapsible-accordion.tsx delete mode 100644 docs/src/app/experiments/collapsible-transitions.tsx diff --git a/docs/data/api/accordion-panel.json b/docs/data/api/accordion-panel.json index 017e5cf1d1..3d4857e3d7 100644 --- a/docs/data/api/accordion-panel.json +++ b/docs/data/api/accordion-panel.json @@ -2,6 +2,7 @@ "props": { "className": { "type": { "name": "union", "description": "func
| string" } }, "hiddenUntilFound": { "type": { "name": "bool" }, "default": "false" }, + "keepMounted": { "type": { "name": "bool" }, "default": "false" }, "render": { "type": { "name": "union", "description": "element
| func" } } }, "name": "AccordionPanel", diff --git a/docs/data/api/accordion-root.json b/docs/data/api/accordion-root.json index dafacd6aa2..2da9cddd43 100644 --- a/docs/data/api/accordion-root.json +++ b/docs/data/api/accordion-root.json @@ -7,6 +7,8 @@ "default": "0" }, "disabled": { "type": { "name": "bool" }, "default": "false" }, + "hiddenUntilFound": { "type": { "name": "bool" }, "default": "false" }, + "keepMounted": { "type": { "name": "bool" }, "default": "false" }, "loop": { "type": { "name": "bool" }, "default": "true" }, "onValueChange": { "type": { "name": "func" } }, "openMultiple": { "type": { "name": "bool" }, "default": "true" }, diff --git a/docs/data/api/collapsible-panel.json b/docs/data/api/collapsible-panel.json index 1bc64944d2..77ef423ac2 100644 --- a/docs/data/api/collapsible-panel.json +++ b/docs/data/api/collapsible-panel.json @@ -2,6 +2,7 @@ "props": { "className": { "type": { "name": "union", "description": "func
| string" } }, "hiddenUntilFound": { "type": { "name": "bool" }, "default": "false" }, + "keepMounted": { "type": { "name": "bool" }, "default": "false" }, "render": { "type": { "name": "union", "description": "element
| func" } } }, "name": "CollapsiblePanel", diff --git a/docs/data/api/collapsible-root.json b/docs/data/api/collapsible-root.json index 07e96140d0..3d6a92195c 100644 --- a/docs/data/api/collapsible-root.json +++ b/docs/data/api/collapsible-root.json @@ -2,7 +2,7 @@ "props": { "animated": { "type": { "name": "bool" }, "default": "true" }, "className": { "type": { "name": "union", "description": "func
| string" } }, - "defaultOpen": { "type": { "name": "bool" }, "default": "true" }, + "defaultOpen": { "type": { "name": "bool" }, "default": "false" }, "disabled": { "type": { "name": "bool" }, "default": "false" }, "onOpenChange": { "type": { "name": "func" } }, "open": { "type": { "name": "bool" } }, diff --git a/docs/data/components/accordion/AccordionIntroduction.js b/docs/data/components/accordion/AccordionIntroduction.js new file mode 100644 index 0000000000..7c4583c34e --- /dev/null +++ b/docs/data/components/accordion/AccordionIntroduction.js @@ -0,0 +1,59 @@ +'use client'; +import * as React from 'react'; +import { Accordion } from '@base-ui-components/react/accordion'; +import classes from './styles.module.css'; + +export default function AccordionIntroduction() { + return ( + + + + + Trigger 1 + + + + + This is the contents of Accordion.Panel 1 + + + + + + Trigger 2 + + + + + This is the contents of Accordion.Panel 2 + + + + + + Trigger 3 + + + + + This is the contents of Accordion.Panel 3 + + + + ); +} + +function ExpandMoreIcon(props) { + return ( + + + + ); +} diff --git a/docs/data/components/accordion/AccordionIntroduction.tsx b/docs/data/components/accordion/AccordionIntroduction.tsx new file mode 100644 index 0000000000..c90b44b8a5 --- /dev/null +++ b/docs/data/components/accordion/AccordionIntroduction.tsx @@ -0,0 +1,59 @@ +'use client'; +import * as React from 'react'; +import { Accordion } from '@base-ui-components/react/accordion'; +import classes from './styles.module.css'; + +export default function AccordionIntroduction() { + return ( + + + + + Trigger 1 + + + + + This is the contents of Accordion.Panel 1 + + + + + + Trigger 2 + + + + + This is the contents of Accordion.Panel 2 + + + + + + Trigger 3 + + + + + This is the contents of Accordion.Panel 3 + + + + ); +} + +function ExpandMoreIcon(props: React.SVGProps) { + return ( + + + + ); +} diff --git a/docs/data/components/accordion/UnstyledAccordionIntroduction.js b/docs/data/components/accordion/UnstyledAccordionIntroduction.js deleted file mode 100644 index 893ff819a6..0000000000 --- a/docs/data/components/accordion/UnstyledAccordionIntroduction.js +++ /dev/null @@ -1,61 +0,0 @@ -'use client'; -import * as React from 'react'; -import { Accordion } from '@base-ui-components/react/accordion'; -import classes from './styles.module.css'; - -export default function UnstyledAccordionIntroduction() { - return ( -
- - - - - Trigger 1 - - - - - This is the contents of Accordion.Panel 1 - - - - - - Trigger 2 - - - - - This is the contents of Accordion.Panel 2 - - - - - - Trigger 3 - - - - - This is the contents of Accordion.Panel 3 - - - -
- ); -} - -function ExpandMoreIcon(props) { - return ( - - - - ); -} diff --git a/docs/data/components/accordion/UnstyledAccordionIntroduction.tsx b/docs/data/components/accordion/UnstyledAccordionIntroduction.tsx deleted file mode 100644 index 1732e7fe2d..0000000000 --- a/docs/data/components/accordion/UnstyledAccordionIntroduction.tsx +++ /dev/null @@ -1,61 +0,0 @@ -'use client'; -import * as React from 'react'; -import { Accordion } from '@base-ui-components/react/accordion'; -import classes from './styles.module.css'; - -export default function UnstyledAccordionIntroduction() { - return ( -
- - - - - Trigger 1 - - - - - This is the contents of Accordion.Panel 1 - - - - - - Trigger 2 - - - - - This is the contents of Accordion.Panel 2 - - - - - - Trigger 3 - - - - - This is the contents of Accordion.Panel 3 - - - -
- ); -} - -function ExpandMoreIcon(props: React.SVGProps) { - return ( - - - - ); -} diff --git a/docs/data/components/accordion/accordion.mdx b/docs/data/components/accordion/accordion.mdx index b988cab337..e731aa2cf3 100644 --- a/docs/data/components/accordion/accordion.mdx +++ b/docs/data/components/accordion/accordion.mdx @@ -15,7 +15,7 @@ packageName: '@base-ui-components/react' ## Introduction - + ## Installation @@ -31,7 +31,7 @@ Accordions are implemented using a collection of related components: - `` is a heading (`h3` by default) that wraps the `Trigger` - `` is the element that contains content in a `Item` -```tsx +```jsx @@ -57,7 +57,7 @@ The open state of the accordion is represented an array holding the `value`s of You can optionally specify a custom `value` prop on `Item`: -```tsx +```jsx @@ -78,7 +78,7 @@ You can optionally specify a custom `value` prop on `Item`: When uncontrolled, use the `defaultValue` prop to set the initial state of the accordion: -```tsx +```jsx @@ -115,7 +115,7 @@ When uncontrolled, use the `defaultValue` prop to set the initial state of the a When controlled, pass the `value` and `onValueChange` props to `Accordion.Root`: -```tsx +```jsx const [value, setValue] = React.useState(['a']); return ( @@ -142,7 +142,7 @@ return ( By default, all accordion items can be opened at the same time. Use the `openMultiple` prop to only allow one open item at a time: -```tsx +```jsx {/* subcomponents */} ``` @@ -150,7 +150,7 @@ By default, all accordion items can be opened at the same time. Use the `openMul Use controlled mode to always keep one `Item` open: -```tsx +```jsx const [value, setValue] = React.useState([0]); const handleValueChange = (newValue) => { @@ -168,9 +168,9 @@ return ( ## Horizontal -Use the `orientation` prop to configure a horizontal accordion. In a horizontal accordion, focus will move between `Accordion.Trigger`s with the Right Arrow and Left Arrow keys, instead of Down/Up. +Use the `orientation` prop to configure a horizontal accordion. In a horizontal accordion, focus will move between `Accordion.Trigger`s with the Right Arrow and Left Arrow keys, instead of Down/Up. -```tsx +```jsx {/* subcomponents */} ``` @@ -178,11 +178,21 @@ Use the `orientation` prop to configure a horizontal accordion. In a horizontal Use the `direction` prop to configure a RTL accordion: -```tsx +```jsx {/* subcomponents */} ``` -When a horizontal accordion is set to `direction="rtl"`, keyboard actions are reversed accordingly - Left Arrow moves focus to the next trigger and Right Arrow moves focus to the previous trigger. +When a horizontal accordion is set to `direction="rtl"`, keyboard actions are reversed accordingly - Left Arrow moves focus to the next trigger and Right Arrow moves focus to the previous trigger. + +## Hidden state + +``s are unmounted from the DOM by default when they are closed. The `keepMounted` prop can be used to keep them mounted when closed, and instead rely on the `hidden` attribute to hide the content: + +```jsx +{/* accordion items */} +``` + +Alternatively `keepMounted` can be passed to `Accordion.Panel`s directly to enable this for only one `Item` instead of the whole accordion. ## Improving searchability of hidden content @@ -198,6 +208,8 @@ Content hidden by `Accordion.Panel` components can be made accessible only to a {/* subcomponents */} ``` +When `hiddenUntilFound` is used, `Accordion.Panel`s remain mounted even when closed, overriding the `keepMounted` prop on the root or the panel. + Alternatively `hiddenUntilFound` can be passed to `Accordion.Panel`s directly to enable this for only one `Item` 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. @@ -290,13 +302,13 @@ When using CSS transitions, styles for the `Panel` must be applied to three stat ### JavaScript Animations -When using external libraries for animation, for example `framer-motion`, be aware that `Accordion.Item`s hides content using the HTML `hidden` attribute in the closed state, and does not unmount from the DOM. +Use the `keepMounted` prop lets an external library control the mounting, for example `framer-motion`: ```js function App() { const [value, setValue] = useState([0]); return ( - + Toggle diff --git a/docs/data/components/accordion/styles.module.css b/docs/data/components/accordion/styles.module.css index acf6b0c782..c78f0edf30 100644 --- a/docs/data/components/accordion/styles.module.css +++ b/docs/data/components/accordion/styles.module.css @@ -1,8 +1,3 @@ -.demo { - width: 40rem; - margin: 1rem; -} - .root { --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); @@ -11,9 +6,11 @@ box-shadow: var(--shadow); background-color: rgba(0, 0, 0, 0.12); border-radius: 0.3rem; + min-width: 22rem; } .item { + width: 100%; position: relative; background-color: #fff; color: rgba(0, 0, 0, 0.87); @@ -48,12 +45,14 @@ border: 0; border-radius: inherit; color: inherit; - padding: 0 1rem; + padding: 1rem; position: relative; width: 100%; display: flex; flex-flow: row nowrap; align-items: center; + font-size: 1rem; + line-height: 1.5; } .trigger:hover { diff --git a/docs/data/components/collapsible/UnstyledCollapsibleIntroduction.js b/docs/data/components/collapsible/CollapsibleIntroduction.js similarity index 95% rename from docs/data/components/collapsible/UnstyledCollapsibleIntroduction.js rename to docs/data/components/collapsible/CollapsibleIntroduction.js index 99de9bfa64..daf51f3321 100644 --- a/docs/data/components/collapsible/UnstyledCollapsibleIntroduction.js +++ b/docs/data/components/collapsible/CollapsibleIntroduction.js @@ -3,7 +3,7 @@ import * as React from 'react'; import { Collapsible } from '@base-ui-components/react/collapsible'; import classes from './styles.module.css'; -export default function UnstyledCollapsibleIntroduction() { +export default function CollapsibleIntroduction() { const [open, setOpen] = React.useState(true); return ( diff --git a/docs/data/components/collapsible/UnstyledCollapsibleIntroduction.tsx b/docs/data/components/collapsible/CollapsibleIntroduction.tsx similarity index 95% rename from docs/data/components/collapsible/UnstyledCollapsibleIntroduction.tsx rename to docs/data/components/collapsible/CollapsibleIntroduction.tsx index 5b3238355e..ee88faf4b1 100644 --- a/docs/data/components/collapsible/UnstyledCollapsibleIntroduction.tsx +++ b/docs/data/components/collapsible/CollapsibleIntroduction.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import { Collapsible } from '@base-ui-components/react/collapsible'; import classes from './styles.module.css'; -export default function UnstyledCollapsibleIntroduction() { +export default function CollapsibleIntroduction() { const [open, setOpen] = React.useState(true); return ( diff --git a/docs/data/components/collapsible/collapsible.mdx b/docs/data/components/collapsible/collapsible.mdx index 2b61fe5c67..d548f218ea 100644 --- a/docs/data/components/collapsible/collapsible.mdx +++ b/docs/data/components/collapsible/collapsible.mdx @@ -14,7 +14,7 @@ packageName: '@base-ui-components/react' - + ## Installation @@ -26,13 +26,24 @@ packageName: '@base-ui-components/react' - `` is the trigger element, a ` - - - - - - - Trigger 3 - - - - This is the contents of Accordion.Panel 3 - MUI - - - - - - - Trigger 4 - - - - This is the contents of Accordion.Panel 4 - - - - - - - Trigger 5 - - - - This is the contents of Accordion.Panel 5 - - - - -
- -

Controlled

- - - - - - Trigger 1 - - - - This is the contents of Accordion.Panel 1, the value is "one" - - - - - - - Trigger 2 - - - - This is the contents of Accordion.Panel 2, the value is "two" - - - - - - - Trigger 3 - - - - This is the contents of Accordion.Panel 3, the value is "three" - - - - -
- -

Controlled, at least one section must remain open

- - { - if (Array.isArray(newValue) && newValue.length > 0) { - setVal2(newValue); - } - }} - aria-label="Controlled Accordion, one section must remain open" - openMultiple={openMultiple} - > - - - - Trigger 1 - - - - This is the contents of Accordion.Panel 1, the value is "one" - - - - - - - Trigger 2 - - - - This is the contents of Accordion.Panel 2, the value is "two" - - - - - - - Trigger 3 - - - - This is the contents of Accordion.Panel 3, the value is "three" - - - - - ); -} - -function CheckIcon(props: React.SVGProps) { - return ( - - - - ); -} - -export function ExpandMoreIcon(props: React.SVGProps) { - return ( - - - - ); -} diff --git a/docs/src/app/experiments/collapsible-accordion.tsx b/docs/src/app/experiments/collapsible-accordion.tsx deleted file mode 100644 index ec252edbf9..0000000000 --- a/docs/src/app/experiments/collapsible-accordion.tsx +++ /dev/null @@ -1,164 +0,0 @@ -'use client'; -import * as React from 'react'; -import { Collapsible } from '@base-ui-components/react/collapsible'; - -const TRANSITION_DURATION = '350ms'; - -function AccordionSection(props: { - index: number; - openIndex: number; - setOpen: (nextOpenIndex: number) => void; -}) { - const { index, openIndex, setOpen } = props; - const isOpen = index === openIndex; - return ( - setOpen(isOpen ? -1 : index)} - > - - - - - - - {isOpen ? 'Close' : 'Open'} Panel {index} - - -

This is the collapsed content of Panel {index}

-

This is the second paragraph

-

This is the third paragraph

-
- -
- ); -} - -export default function CollapsibleAccordion() { - const [openIndex, setOpen] = React.useState(-1); - return ( -
-
-        A crude accordion where only 1 of the 3 {``}s can be open at
-        any time
-        
- Animated using CSS transitions -
- {[0, 1, 2].map((index) => ( - - ))} -
- ); -} - -const grey = { - 50: '#F3F6F9', - 100: '#E5EAF2', - 200: '#DAE2ED', - 300: '#C7D0DD', - 400: '#B0B8C4', - 500: '#9DA8B7', - 600: '#6B7A90', - 700: '#434D5B', - 800: '#303740', - 900: '#1C2025', -}; - -export function Styles() { - return ( - - ); -} diff --git a/docs/src/app/experiments/collapsible-cls.tsx b/docs/src/app/experiments/collapsible-cls.tsx index 88885d529c..8631756a68 100644 --- a/docs/src/app/experiments/collapsible-cls.tsx +++ b/docs/src/app/experiments/collapsible-cls.tsx @@ -1,95 +1,103 @@ 'use client'; import * as React from 'react'; import { Collapsible } from '@base-ui-components/react/collapsible'; -import classes from './collapsible.module.css'; - -function classNames(...c: Array) { - return c.filter(Boolean).join(' '); -} +import c from './collapsible.module.css'; // https://github.com/mui/base-ui/issues/740 export default function AnimatedCollapsibles() { return ( -
- - - - Trigger 1A (CSS Animation) - - -

This is the collapsed content

-

- You can find the Base UI repository{' '} - - here - -

-
-
+
+
+
+ + + + Trigger 1A (CSS Animation) + + +

This is the collapsed content

+

+ You can find the Base UI repository{' '} + + here + +

+
+
+
- - - - Trigger 1B (CSS Animation) - - -

This is the collapsed content

-

- You can find the Base UI repository{' '} - - here - -

-
-
+
+ + + + Trigger 1B (CSS Animation) + + +

This is the collapsed content

+

+ You can find the Base UI repository{' '} + + here + +

+
+
+
+
- - - - Trigger 2A (CSS Transition) - - -

This is the collapsed content

-

- You can find the Base UI repository{' '} - - here - -

-
-
+
+
+ + + + Trigger 2A (CSS Transition) + + +

This is the collapsed content

+

+ You can find the Base UI repository{' '} + + here + +

+
+
+
- - - - Trigger 2B (CSS Transition) - - -

This is the collapsed content

-

- You can find the Base UI repository{' '} - - here - -

-
-
+
+ + + + Trigger 2B (CSS Transition) + + +

This is the collapsed content

+

+ You can find the Base UI repository{' '} + + here + +

+
+
+
+
); } diff --git a/docs/src/app/experiments/collapsible-framer.tsx b/docs/src/app/experiments/collapsible-framer.tsx index 9b21eba1f6..64f032aedd 100644 --- a/docs/src/app/experiments/collapsible-framer.tsx +++ b/docs/src/app/experiments/collapsible-framer.tsx @@ -2,56 +2,57 @@ import * as React from 'react'; import { Collapsible } from '@base-ui-components/react/collapsible'; import { motion } from 'framer-motion'; -import classes from './collapsible.module.css'; - -function classNames(...c: Array) { - return c.filter(Boolean).join(' '); -} +import c from './collapsible.module.css'; export default function CollapsibleFramer() { const [open, setOpen] = React.useState(false); return ( -
- - - - Trigger - - - } - > -

This is the collapsed content

-

- Your Choice of Fried Chicken (Half), Chicken Sandwich, With Shredded - cabbage & carrot with mustard mayonnaise And Potato Wedges -

-

demo: https://codepen.io/aardrian/pen/QWjBNQG

-

https://adrianroselli.com/2020/05/disclosure-widgets.html

-
-
+
+
+
+ + + + Trigger + + + } + > +

This is the collapsed content

+

+ Your Choice of Fried Chicken (Half), Chicken Sandwich, With Shredded + cabbage & carrot with mustard mayonnaise And Potato Wedges +

+

demo: https://codepen.io/aardrian/pen/QWjBNQG

+

https://adrianroselli.com/2020/05/disclosure-widgets.html

+
+
+
+
); } diff --git a/docs/src/app/experiments/collapsible-hidden-until-found.tsx b/docs/src/app/experiments/collapsible-hidden-until-found.tsx index ef9b4c7691..4b11a3a5c9 100644 --- a/docs/src/app/experiments/collapsible-hidden-until-found.tsx +++ b/docs/src/app/experiments/collapsible-hidden-until-found.tsx @@ -1,16 +1,12 @@ 'use client'; import * as React from 'react'; import { Collapsible } from '@base-ui-components/react/collapsible'; -import classes from './collapsible.module.css'; - -function classNames(...c: Array) { - return c.filter(Boolean).join(' '); -} +import c from './collapsible.module.css'; export default function CollapsibleHiddenUntilFound() { return ( -
+      
         All 3 Collapsibles contain the text "May the force be with you" but
         only the content in the 2nd and 3rd Collapsible will be revealed by the
         browser's in-page search (e.g. Ctrl/Cmd-F) in{' '}
@@ -29,47 +25,51 @@ export default function CollapsibleHiddenUntilFound() {
         that instance is matched. It only occurs with transitions, not @keyframe
         animations.
       
-
- - - - Trigger 1 - - -

This is the collapsed content

-

May the force be with you

-
-
+
+
+
+ + + + Trigger 1 + + +

This is the collapsed content

+

May the force be with you

+
+
+
+
- - - - Trigger 2 - - -

This is the collapsed content

-

May the force be with you

-
-
+
+
+ + + + Trigger 2 + + +

This is the collapsed content

+

May the force be with you

+
+
+
+
- - - - Trigger 3 - - -

This is the collapsed content

-

May the force be with you

-
-
+
+
+ + + + Trigger 3 + + +

This is the collapsed content

+

May the force be with you

+
+
+
+
); diff --git a/docs/src/app/experiments/collapsible-transitions.tsx b/docs/src/app/experiments/collapsible-transitions.tsx deleted file mode 100644 index ed115f4422..0000000000 --- a/docs/src/app/experiments/collapsible-transitions.tsx +++ /dev/null @@ -1,77 +0,0 @@ -'use client'; -import * as React from 'react'; -import { Collapsible } from '@base-ui-components/react/collapsible'; -import classes from './collapsible.module.css'; - -function classNames(...c: Array) { - return c.filter(Boolean).join(' '); -} - -export default function CollapsibleTransitions() { - return ( -
-
- - - - Trigger 1A - - -

This is the collapsed content

-

- You can find the Base UI repository{' '} - - here - -

-
-
-
- -
- - - - Trigger 1B - - -

This is the collapsed content

-

- You can find the Base UI repository{' '} - - here - -

-
-
-
-
- ); -} - -function ExpandMoreIcon(props: React.SVGProps) { - return ( - - - - ); -} diff --git a/docs/src/app/experiments/collapsible.module.css b/docs/src/app/experiments/collapsible.module.css index eb8c31e283..a2af2bf0db 100644 --- a/docs/src/app/experiments/collapsible.module.css +++ b/docs/src/app/experiments/collapsible.module.css @@ -1,82 +1,101 @@ .wrapper { --width: 320px; - --duration: 300ms; + --duration: 600ms; font-family: system-ui, sans-serif; line-height: 1.4; display: flex; flex-flow: column nowrap; align-items: stretch; - width: var(--width); - margin: 2rem; -} + gap: 3rem; -.pre { - line-height: 1.5; - max-width: 75ch; - white-space: pre-wrap; - margin: 1rem 1rem 2rem; + & h3 { + margin-bottom: -2rem; + } } -.trigger { - display: flex; - align-items: center; - padding-left: 0; -} +.grid { + --width: 20rem; + --height: 11.25rem; -.trigger:not(:first-of-type) { - margin-top: 3rem; + display: grid; + grid-template-columns: repeat(2, var(--width)); + grid-template-rows: repeat(2, minmax(var(--height), 1fr)); + grid-gap: 3rem; } -.icon { - transform: rotate(-90deg); - transition: transform var(--duration) ease-in; -} +.collapsible { + width: var(--width); -.trigger[data-panel-open] .icon { - transform: rotate(0); - transition: transform var(--duration) ease-out; -} + /* trigger */ + & .trigger { + display: flex; + width: 100%; + align-items: center; + padding-left: 0; + } -.panel { - background-color: #eaeaea; - overflow: hidden; - box-sizing: border-box; - width: var(--width); - padding-left: 1rem; - padding-right: 1rem; -} + & .trigger:not(:first-of-type) { + margin-top: 3rem; + } -.panel.animation[data-open] { - animation: slideDown var(--duration) ease-out; -} + & .trigger svg { + transform: rotate(-90deg); + transition: transform var(--duration) ease-in; + } -.panel.animation { - animation: slideUp var(--duration) ease-in; -} + & .trigger[data-panel-open] svg { + transform: rotate(0); + transition: transform var(--duration) ease-out; + } -.panel.transition[data-open] { - height: var(--collapsible-panel-height); - transition: height var(--duration) ease-out; -} + /* panel */ + & .panel { + background-color: #eaeaea; + overflow: hidden; + box-sizing: border-box; + width: var(--width); + padding-left: 1rem; + padding-right: 1rem; + } -.panel.transition { - height: 0; - transition: height var(--duration) ease-in; + & .panel p { + margin: 1.25rem auto; + overflow-wrap: break-word; + } } -.panel.transition[data-starting-style] { - height: 0; +.transition { + & .panel[data-open] { + height: var(--collapsible-panel-height); + transition: height var(--duration) ease-out; + } + + & .panel { + height: 0; + transition: height var(--duration) ease-in; + } + + & .panel[data-starting-style] { + height: 0; + } } -.panel.framer { - display: flex; - flex-direction: column; +.animation { + & .panel[data-open] { + animation: slideDown var(--duration) ease-out; + } + + & .panel { + animation: slideUp var(--duration) ease-in; + } } -.panel p { - margin: 1.25rem auto; - overflow-wrap: break-word; +.framer { + & .panel { + display: flex; + flex-direction: column; + } } @keyframes slideDown { @@ -97,14 +116,9 @@ } } -.grid { - --width: 320px; - --duration: 2000ms; - - font-family: system-ui, sans-serif; - line-height: 1.4; - - display: grid; - grid: var(--width) var(--width) / var(--width) var(--width); - grid-gap: 4rem; +.pre { + line-height: 1.5; + max-width: 75ch; + white-space: pre-wrap; + margin: 1rem 1rem 2rem; } diff --git a/docs/src/app/experiments/collapsible.tsx b/docs/src/app/experiments/collapsible.tsx index 5d68a3f6c5..f7cbf4c863 100644 --- a/docs/src/app/experiments/collapsible.tsx +++ b/docs/src/app/experiments/collapsible.tsx @@ -1,205 +1,96 @@ 'use client'; import * as React from 'react'; import { Collapsible } from '@base-ui-components/react/collapsible'; +import c from './collapsible.module.css'; -const DURATION = '350ms'; +function classNames(...classes: Array) { + return classes.filter(Boolean).join(' '); +} -export default function CollapsibleDemo() { +export default function CollapsibleTransitions() { return ( -
-
- - - - - - - - Trigger (CSS animation) - - -

This is the collapsed content

-

This component is animated with CSS @keyframe animations

-

demo: https://codepen.io/aardrian/pen/QWjBNQG

-

https://adrianroselli.com/2020/05/disclosure-widgets.html

-
-
+
+

Transitions

+
+ {CONFIG.map((entry, i) => { + const [defaultOpen, keepMounted] = entry; + return ( +
+ + + + Trigger {i} (keepMounted={String(keepMounted)}) + + +

This is the collapsed content

+

+ You can find the Base UI repository{' '} + + here + +

+
+
+
+ ); + })}
- -
- - - - - - - - Trigger (CSS transition) - - -

This is the collapsed content

-

This component is animated with CSS transitions

-

demo: https://codepen.io/aardrian/pen/QWjBNQG

-

https://adrianroselli.com/2020/05/disclosure-widgets.html

-
-
+

Animations

+
+ {CONFIG.map((entry, i) => { + const [defaultOpen, keepMounted] = entry; + return ( +
+ + + + Trigger {i} (keepMounted={String(keepMounted)}) + + +

This is the collapsed content

+

+ You can find the Base UI repository{' '} + + here + +

+
+
+
+ ); + })}
- - } className="MyCollapsible-root"> - - - - - - - Trigger (root renders a span + CSS transition) - - -

This is the collapsed content

-

This component is animated with CSS transitions

-

demo: https://codepen.io/aardrian/pen/QWjBNQG

-

https://adrianroselli.com/2020/05/disclosure-widgets.html

-
-
-
); } -const grey = { - 50: '#F3F6F9', - 100: '#E5EAF2', - 200: '#DAE2ED', - 300: '#C7D0DD', - 400: '#B0B8C4', - 500: '#9DA8B7', - 600: '#6B7A90', - 700: '#434D5B', - 800: '#303740', - 900: '#1C2025', -}; +const CONFIG = [ + // [defaultOpen, keepMounted] + [false, false], + [true, false], + [false, true], + [true, true], +]; -export function Styles() { +function ExpandMoreIcon(props: React.SVGProps) { return ( - + + + ); } diff --git a/docs/src/components/demo/Demo.tsx b/docs/src/components/demo/Demo.tsx index fb57779959..8d86a5dc7a 100644 --- a/docs/src/components/demo/Demo.tsx +++ b/docs/src/components/demo/Demo.tsx @@ -64,6 +64,7 @@ export function Demo({ className, defaultOpen = false, title, ...props }: DemoPr