Skip to content

Commit 8aa1399

Browse files
committed
collapsible2
1 parent a3ada88 commit 8aa1399

18 files changed

+1337
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
.Root {
2+
--width: 320px;
3+
--duration: 800ms;
4+
5+
width: var(--width);
6+
7+
& + .Root {
8+
margin-top: 2rem;
9+
}
10+
}
11+
12+
.Panel {
13+
display: grid;
14+
box-sizing: border-box;
15+
width: 100%;
16+
grid-template-rows: 1fr;
17+
18+
transition: all var(--duration);
19+
20+
&[data-starting-style],
21+
&[data-ending-style] {
22+
opacity: 0;
23+
grid-template-rows: 0fr;
24+
}
25+
}
26+
27+
.Content {
28+
display: flex;
29+
flex-direction: column;
30+
gap: 0.5rem;
31+
margin-top: 0.25rem;
32+
padding: 0.5rem;
33+
border-radius: 0.25rem;
34+
background-color: var(--color-gray-200);
35+
cursor: text;
36+
37+
& p {
38+
overflow-wrap: break-word;
39+
}
40+
}
41+
42+
.Trigger {
43+
display: flex;
44+
width: 100%;
45+
align-items: center;
46+
padding: 0.25rem 0.5rem;
47+
border-radius: 0.25rem;
48+
background-color: var(--color-gray-200);
49+
color: var(--color-gray-900);
50+
51+
&[data-panel-open] .Icon {
52+
transform: rotate(90deg);
53+
}
54+
}
55+
56+
.Icon {
57+
width: 0.75rem;
58+
height: 0.75rem;
59+
transition: transform var(--duration) ease-out;
60+
}
61+
62+
.grid {
63+
display: grid;
64+
grid-template-columns: 1fr 1fr;
65+
grid-gap: 5rem;
66+
}
67+
68+
.wrapper {
69+
font-family: system-ui, sans-serif;
70+
line-height: 1.4;
71+
display: flex;
72+
flex-flow: column nowrap;
73+
align-items: stretch;
74+
gap: 1rem;
75+
align-self: flex-start;
76+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
'use client';
2+
import * as React from 'react';
3+
import { Collapsible } from '@base-ui-components/react/collapsible2';
4+
import { ChevronIcon } from '../collapsible/_icons';
5+
import styles from './transitions.module.css';
6+
7+
export default function CssTransitions() {
8+
return (
9+
<div className={styles.grid}>
10+
<div className={styles.wrapper}>
11+
<pre>keepMounted: true</pre>
12+
<Collapsible.Root className={styles.Root} defaultOpen>
13+
<Collapsible.Trigger className={styles.Trigger}>
14+
<ChevronIcon className={styles.Icon} />
15+
Trigger
16+
</Collapsible.Trigger>
17+
<Collapsible.Panel className={styles.Panel} keepMounted>
18+
<div className={styles.Content}>
19+
<p>
20+
He rubbed his eyes, and came close to the picture, and examined it
21+
again. There were no signs of any change when he looked into the
22+
actual painting, and yet there was no doubt that the whole expression
23+
had altered. It was not a mere fancy of his own. The thing was
24+
horribly apparent.
25+
</p>
26+
</div>
27+
</Collapsible.Panel>
28+
</Collapsible.Root>
29+
30+
<Collapsible.Root className={styles.Root} defaultOpen={false}>
31+
<Collapsible.Trigger className={styles.Trigger}>
32+
<ChevronIcon className={styles.Icon} />
33+
Trigger
34+
</Collapsible.Trigger>
35+
<Collapsible.Panel className={styles.Panel} keepMounted>
36+
<div className={styles.Content}>
37+
<p>
38+
He rubbed his eyes, and came close to the picture, and examined it
39+
again. There were no signs of any change when he looked into the
40+
actual painting, and yet there was no doubt that the whole expression
41+
had altered. It was not a mere fancy of his own. The thing was
42+
horribly apparent.
43+
</p>
44+
</div>
45+
</Collapsible.Panel>
46+
</Collapsible.Root>
47+
<small>———</small>
48+
</div>
49+
50+
<div className={styles.wrapper}>
51+
<pre>keepMounted: false</pre>
52+
<Collapsible.Root className={styles.Root} defaultOpen>
53+
<Collapsible.Trigger className={styles.Trigger}>
54+
<ChevronIcon className={styles.Icon} />
55+
Trigger
56+
</Collapsible.Trigger>
57+
<Collapsible.Panel className={styles.Panel} keepMounted={false}>
58+
<div className={styles.Content}>
59+
<p>
60+
He rubbed his eyes, and came close to the picture, and examined it
61+
again. There were no signs of any change when he looked into the
62+
actual painting, and yet there was no doubt that the whole expression
63+
had altered. It was not a mere fancy of his own. The thing was
64+
horribly apparent.
65+
</p>
66+
</div>
67+
</Collapsible.Panel>
68+
</Collapsible.Root>
69+
70+
<Collapsible.Root className={styles.Root} defaultOpen={false}>
71+
<Collapsible.Trigger className={styles.Trigger}>
72+
<ChevronIcon className={styles.Icon} />
73+
Trigger
74+
</Collapsible.Trigger>
75+
<Collapsible.Panel className={styles.Panel} keepMounted={false}>
76+
<div className={styles.Content}>
77+
<p>
78+
He rubbed his eyes, and came close to the picture, and examined it
79+
again. There were no signs of any change when he looked into the
80+
actual painting, and yet there was no doubt that the whole expression
81+
had altered. It was not a mere fancy of his own. The thing was
82+
horribly apparent.
83+
</p>
84+
</div>
85+
</Collapsible.Panel>
86+
</Collapsible.Root>
87+
<small>———</small>
88+
</div>
89+
</div>
90+
);
91+
}

packages/react/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"./checkbox": "./src/checkbox/index.ts",
3434
"./checkbox-group": "./src/checkbox-group/index.ts",
3535
"./collapsible": "./src/collapsible/index.ts",
36+
"./collapsible2": "./src/collapsible2/index.ts",
3637
"./dialog": "./src/dialog/index.ts",
3738
"./direction-provider": "./src/direction-provider/index.ts",
3839
"./field": "./src/field/index.ts",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { CollapsibleRoot as Root } from './root/CollapsibleRoot';
2+
export { CollapsibleTrigger as Trigger } from './trigger/CollapsibleTrigger';
3+
export { CollapsiblePanel as Panel } from './panel/CollapsiblePanel';
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * as Collapsible from './index.parts';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import * as React from 'react';
2+
import { expect } from 'chai';
3+
import { spy } from 'sinon';
4+
import { act, fireEvent, flushMicrotasks } from '@mui/internal-test-utils';
5+
import { Collapsible } from '@base-ui-components/react/collapsible';
6+
import { createRenderer, describeConformance, isJSDOM } from '#test-utils';
7+
8+
const PANEL_CONTENT = 'This is panel content';
9+
10+
describe('<Collapsible.Panel />', () => {
11+
const { render } = createRenderer();
12+
13+
describeConformance(<Collapsible.Panel />, () => ({
14+
refInstanceof: window.HTMLDivElement,
15+
render: (node) => {
16+
return render(<Collapsible.Root defaultOpen>{node}</Collapsible.Root>);
17+
},
18+
}));
19+
20+
describe('prop: keepMounted', () => {
21+
it('does not unmount the panel when true', async () => {
22+
function App() {
23+
const [open, setOpen] = React.useState(false);
24+
return (
25+
<Collapsible.Root open={open} onOpenChange={setOpen}>
26+
<Collapsible.Trigger />
27+
<Collapsible.Panel keepMounted>{PANEL_CONTENT}</Collapsible.Panel>
28+
</Collapsible.Root>
29+
);
30+
}
31+
32+
const { queryByText, getByRole } = await render(<App />);
33+
34+
const trigger = getByRole('button');
35+
36+
expect(trigger).to.have.attribute('aria-expanded', 'false');
37+
expect(trigger.getAttribute('aria-controls')).to.equal(
38+
queryByText(PANEL_CONTENT)?.getAttribute('id'),
39+
);
40+
expect(queryByText(PANEL_CONTENT)).to.not.equal(null);
41+
expect(queryByText(PANEL_CONTENT)).not.toBeVisible();
42+
expect(queryByText(PANEL_CONTENT)).to.have.attribute('data-closed');
43+
44+
fireEvent.click(trigger);
45+
await flushMicrotasks();
46+
47+
expect(trigger).to.have.attribute('aria-expanded', 'true');
48+
49+
expect(queryByText(PANEL_CONTENT)).toBeVisible();
50+
expect(queryByText(PANEL_CONTENT)).to.have.attribute('data-open');
51+
expect(trigger).to.have.attribute('data-panel-open');
52+
53+
fireEvent.click(trigger);
54+
await flushMicrotasks();
55+
56+
expect(trigger).to.have.attribute('aria-expanded', 'false');
57+
expect(trigger.getAttribute('aria-controls')).to.equal(
58+
queryByText(PANEL_CONTENT)?.getAttribute('id'),
59+
);
60+
expect(queryByText(PANEL_CONTENT)).not.toBeVisible();
61+
expect(queryByText(PANEL_CONTENT)).to.have.attribute('data-closed');
62+
});
63+
});
64+
65+
// we test firefox in browserstack which does not support this yet
66+
describe.skipIf(!('onbeforematch' in window) || isJSDOM)('prop: hiddenUntilFound', () => {
67+
it('uses `hidden="until-found" to hide panel when true', async () => {
68+
const handleOpenChange = spy();
69+
70+
const { queryByText } = await render(
71+
<Collapsible.Root defaultOpen={false} onOpenChange={handleOpenChange}>
72+
<Collapsible.Trigger />
73+
<Collapsible.Panel hiddenUntilFound keepMounted>
74+
{PANEL_CONTENT}
75+
</Collapsible.Panel>
76+
</Collapsible.Root>,
77+
);
78+
79+
const panel = queryByText(PANEL_CONTENT);
80+
81+
act(() => {
82+
const event = new window.Event('beforematch', {
83+
bubbles: true,
84+
cancelable: false,
85+
});
86+
panel?.dispatchEvent(event);
87+
});
88+
89+
expect(handleOpenChange.callCount).to.equal(1);
90+
expect(panel).to.have.attribute('data-open');
91+
});
92+
});
93+
});

0 commit comments

Comments
 (0)