Skip to content

Commit

Permalink
Update tests and docs
Browse files Browse the repository at this point in the history
  • Loading branch information
mj12albert committed Aug 15, 2024
1 parent c48fa8d commit 939767f
Show file tree
Hide file tree
Showing 14 changed files with 105 additions and 13 deletions.
2 changes: 1 addition & 1 deletion docs/data/base/components/collapsible/collapsible.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ Content hidden in the `Collapsible.Content` component can be made accessible onl
</Collapsible.Root>
```
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 will fall back to the default `hidden` state in unsupported browsers.
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
Expand Down
4 changes: 4 additions & 0 deletions docs/pages/base-ui/api/collapsible-content.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
{
"props": {
"className": { "type": { "name": "union", "description": "func<br>&#124;&nbsp;string" } },
"htmlHidden": {
"type": { "name": "enum", "description": "'hidden'<br>&#124;&nbsp;'until-found'" },
"default": "'hidden'"
},
"render": { "type": { "name": "union", "description": "element<br>&#124;&nbsp;func" } }
},
"name": "CollapsibleContent",
Expand Down
1 change: 1 addition & 0 deletions docs/pages/base-ui/api/collapsible-root.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"props": {
"animated": { "type": { "name": "bool" }, "default": "true" },
"defaultOpen": { "type": { "name": "bool" }, "default": "true" },
"disabled": { "type": { "name": "bool" }, "default": "false" },
"onOpenChange": { "type": { "name": "func" } },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"className": {
"description": "Class names applied to the element or a function that returns them based on the component&#39;s state."
},
"htmlHidden": { "description": "The hidden state when closed" },
"render": { "description": "A function to customize rendering of the component." }
},
"classDescriptions": {}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{
"componentDescription": "",
"propDescriptions": {
"animated": {
"description": "If <code>true</code>, the component supports CSS/JS-based animations and transitions."
},
"defaultOpen": {
"description": "If <code>true</code>, the Collapsible is initially open. This is the uncontrolled counterpart of <code>open</code>."
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { describeConformance } from '../../../test/describeConformance';
import type { CollapsibleContextValue } from '../Root/CollapsibleRoot.types';

const contextValue: CollapsibleContextValue = {
animated: false,
contentId: 'ContentId',
disabled: false,
mounted: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ const CollapsibleContent = React.forwardRef(function CollapsibleContent(
) {
const { className, htmlHidden, render, ...otherProps } = props;

const { mounted, open, contentId, setContentId, setMounted, setOpen, ownerState } =
const { animated, mounted, open, contentId, setContentId, setMounted, setOpen, ownerState } =
useCollapsibleContext();

const { getRootProps, height } = useCollapsibleContent({
animated,
htmlHidden,
id: contentId,
mounted,
Expand Down Expand Up @@ -57,6 +58,11 @@ CollapsibleContent.propTypes /* remove-proptypes */ = {
* Class names applied to the element or a function that returns them based on the component's state.
*/
className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
/**
* The hidden state when closed
* @default 'hidden'
*/
htmlHidden: PropTypes.oneOf(['hidden', 'until-found']),
/**
* A function to customize rendering of the component.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ export interface CollapsibleContentProps
Pick<UseCollapsibleContentParameters, 'htmlHidden'> {}

export interface UseCollapsibleContentParameters {
/**
* If `true`, the component supports CSS/JS-based animations and transitions.
* @default false
*/
animated?: boolean;
/**
* The hidden state when closed
* @default 'hidden'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ function supportsHiddenUntilFound(element: HTMLElement) {
typeof CSS.supports === 'function' &&
CSS.supports('content-visibility', 'hidden');
const supportsOnBeforeMatch = 'onbeforematch' in ownerWindow(element);
cachedSupportsHiddenUntilFound = supportsCssContentVisibility && supportsOnBeforeMatch;
cachedSupportsHiddenUntilFound =
process.env.NODE_ENV === 'test'
? supportsOnBeforeMatch
: supportsCssContentVisibility && supportsOnBeforeMatch;
}
return cachedSupportsHiddenUntilFound;
}
Expand All @@ -47,6 +50,7 @@ function useCollapsibleContent(
parameters: UseCollapsibleContentParameters,
): UseCollapsibleContentReturnValue {
const {
animated = false,
htmlHidden = 'hidden',
id: idParam,
open,
Expand Down Expand Up @@ -92,7 +96,7 @@ function useCollapsibleContent(

const runOnceAnimationsFinish = useAnimationsFinished(contentRef);

const isOpen = open || contextMounted;
const isOpen = animated ? open || contextMounted : open;

const isInitialOpenRef = React.useRef(isOpen);

Expand Down Expand Up @@ -217,7 +221,7 @@ function useCollapsibleContent(
// There is a bug in react that forces string values for the `hidden` attribute to a boolean
// so we have to force it back to `'until-found'` in the DOM when applicable
// https://github.com/facebook/react/issues/24740
React.useEffect(() => {
useEnhancedEffect(() => {
const { current: element } = contentRef;

if (
Expand Down
45 changes: 41 additions & 4 deletions packages/mui-base/src/Collapsible/Root/CollapsibleRoot.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from 'react';
import { expect } from 'chai';
import { createRenderer } from '@mui/internal-test-utils';
import { spy } from 'sinon';
import { createRenderer, act } from '@mui/internal-test-utils';
import * as Collapsible from '@base_ui/react/Collapsible';

describe('<Collapsible.Root />', () => {
Expand All @@ -27,7 +28,7 @@ describe('<Collapsible.Root />', () => {
describe('open state', () => {
it('controlled mode', async () => {
const { getByTestId, getByRole, setProps } = await render(
<Collapsible.Root open={false}>
<Collapsible.Root open={false} animated={false}>
<Collapsible.Trigger />
<Collapsible.Content data-testid="content" />
</Collapsible.Root>,
Expand All @@ -50,11 +51,12 @@ describe('<Collapsible.Root />', () => {

expect(trigger).to.have.attribute('aria-expanded', 'false');
expect(content).to.have.attribute('data-state', 'closed');
expect(content).to.have.attribute('hidden');
});

it('uncontrolled mode', async () => {
const { getByTestId, getByRole, user } = await render(
<Collapsible.Root defaultOpen={false}>
<Collapsible.Root defaultOpen={false} animated={false}>
<Collapsible.Trigger />
<Collapsible.Content data-testid="content" />
</Collapsible.Root>,
Expand All @@ -77,14 +79,15 @@ describe('<Collapsible.Root />', () => {

expect(trigger).to.have.attribute('aria-expanded', 'false');
expect(content).to.have.attribute('data-state', 'closed');
expect(content).to.have.attribute('hidden');
});
});

describe('keyboard interactions', () => {
['Enter', 'Space'].forEach((key) => {
it(`key: ${key} should toggle the Collapsible`, async () => {
const { getByTestId, getByRole, user } = await render(
<Collapsible.Root defaultOpen={false}>
<Collapsible.Root defaultOpen={false} animated={false}>
<Collapsible.Trigger>Trigger</Collapsible.Trigger>
<Collapsible.Content data-testid="content" />
</Collapsible.Root>,
Expand All @@ -109,7 +112,41 @@ describe('<Collapsible.Root />', () => {

expect(trigger).to.have.attribute('aria-expanded', 'false');
expect(content).to.have.attribute('data-state', 'closed');
expect(content).to.have.attribute('hidden');
});
});
});

describe('prop: htmlHidden', () => {
it('supports "hidden until found" state', async function test() {
// we test firefox in browserstack which does not support this yet
if (!('onbeforematch' in window)) {
this.skip();
}

const handleOpenChange = spy();

const { getByTestId } = await render(
<Collapsible.Root defaultOpen={false} animated={false} onOpenChange={handleOpenChange}>
<Collapsible.Trigger />
<Collapsible.Content data-testid="content" htmlHidden="until-found" />
</Collapsible.Root>,
);

const content = getByTestId('content');

expect(content).to.have.attribute('data-state', 'closed');

act(() => {
const event = new window.Event('beforematch', {
bubbles: true,
cancelable: false,
});
content.dispatchEvent(event);
});

expect(handleOpenChange.callCount).to.equal(1);
expect(content).to.have.attribute('data-state', 'open');
});
});
});
8 changes: 7 additions & 1 deletion packages/mui-base/src/Collapsible/Root/CollapsibleRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import { CollapsibleContext } from './CollapsibleContext';
import { CollapsibleContextValue, CollapsibleRootProps } from './CollapsibleRoot.types';

function CollapsibleRoot(props: CollapsibleRootProps) {
const { open, defaultOpen, onOpenChange, disabled, children } = props;
const { animated, open, defaultOpen, onOpenChange, disabled, children } = props;

const collapsible = useCollapsibleRoot({
animated,
open,
defaultOpen,
onOpenChange,
Expand All @@ -35,6 +36,11 @@ CollapsibleRoot.propTypes /* remove-proptypes */ = {
// │ These PropTypes are generated from the TypeScript type definitions. │
// │ To update them, edit the TypeScript types and run `pnpm proptypes`. │
// └─────────────────────────────────────────────────────────────────────┘
/**
* If `true`, the component supports CSS/JS-based animations and transitions.
* @default true
*/
animated: PropTypes.bool,
/**
* @ignore
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ export interface CollapsibleRootProps extends UseCollapsibleRootParameters {
}

export interface UseCollapsibleRootParameters {
/**
* If `true`, the component supports CSS/JS-based animations and transitions.
* @default true
*/
animated?: boolean;
/**
* If `true`, the Collapsible is initially open.
* This is the controlled counterpart of `defaultOpen`.
Expand All @@ -37,6 +42,7 @@ export interface UseCollapsibleRootParameters {
}

export interface UseCollapsibleRootReturnValue {
animated: boolean;
contentId: React.HTMLAttributes<Element>['id'];
/**
* The disabled state of the Collapsible
Expand Down
23 changes: 20 additions & 3 deletions packages/mui-base/src/Collapsible/Root/useCollapsibleRoot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@ import {
function useCollapsibleRoot(
parameters: UseCollapsibleRootParameters,
): UseCollapsibleRootReturnValue {
const { open: openParam, defaultOpen = true, onOpenChange, disabled = false } = parameters;
const {
animated = true,
open: openParam,
defaultOpen = true,
onOpenChange,
disabled = false,
} = parameters;

const [open, setOpenState] = useControlled({
controlled: openParam,
Expand All @@ -30,7 +36,7 @@ function useCollapsibleRoot(
state: 'open',
});

const { mounted, setMounted, transitionStatus } = useTransitionStatus(open, true); // TODO: the 2nd argument should be an `animated` prop?
const { mounted, setMounted, transitionStatus } = useTransitionStatus(open, animated);

const [contentId, setContentId] = React.useState<string | undefined>(useId());

Expand All @@ -41,6 +47,7 @@ function useCollapsibleRoot(

return React.useMemo(
() => ({
animated,
contentId,
disabled,
mounted,
Expand All @@ -50,7 +57,17 @@ function useCollapsibleRoot(
setOpen,
transitionStatus,
}),
[contentId, disabled, mounted, open, setContentId, setMounted, setOpen, transitionStatus],
[
animated,
contentId,
disabled,
mounted,
open,
setContentId,
setMounted,
setOpen,
transitionStatus,
],
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { describeConformance } from '../../../test/describeConformance';
import type { CollapsibleContextValue } from '../Root/CollapsibleRoot.types';

const contextValue: CollapsibleContextValue = {
animated: false,
contentId: 'ContentId',
disabled: false,
mounted: true,
Expand Down

0 comments on commit 939767f

Please sign in to comment.