Skip to content
12 changes: 6 additions & 6 deletions packages/documentation-v7/.storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import type { Preview } from '@storybook/web-components';

import DocsLayout from './blocks/layout';
import { format } from 'prettier';
import { badgesConfig, prettierOptions, resetComponents } from './helpers';
import './helpers/register-web-components';
import {
extractArgTypesFactory,
extractArgTypes,
Comment thread
imagoiq marked this conversation as resolved.
extractComponentDescription,
setStencilDocJson,
} from '@pxtrn/storybook-addon-docs-stencil';
import { StencilJsonDocs } from '@pxtrn/storybook-addon-docs-stencil/dist/types';
import { format } from 'prettier';
import DocsLayout from './blocks/layout';
import { badgesConfig, prettierOptions, resetComponents } from './helpers';
import './helpers/register-web-components';

import './styles/preview.scss';
import themes from './styles/themes';
Expand Down Expand Up @@ -58,7 +58,7 @@ const preview: Preview = {
transform: (snippet: string) => format(snippet, prettierOptions),
},
components: resetComponents,
extractArgTypes: extractArgTypesFactory({ dashCase: true }),
extractArgTypes,
extractComponentDescription,
},
actions: { argTypesRegex: '^on[A-Z].*' },
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { spread } from '@open-wc/lit-helpers';
import { useArgs } from '@storybook/preview-api';
import { Meta, StoryContext, StoryObj } from '@storybook/web-components';
import { html } from 'lit';
import { unsafeHTML } from 'lit/directives/unsafe-html.js';

import { definedProperties } from '../../../utils';

const meta: Meta<HTMLPostCollapsibleElement> = {
title: 'Hidden/demos/components/Collapsible',
component: 'post-collapsible',
args: {
innerHTML: `<span slot='header'>Titulum</span><p>Contentus momentus vero siteos et accusam iretea et justo.</p>`,
},
argTypes: {
innerHTML: {
description:
'Defines the HTML markup contained in the collapsible.<br/>' +
'Elements with a `slot="header"` attribute are displayed in the header while others are shown in the body.',
table: {
category: 'content',
type: {
summary: 'string',
},
},
},
},
render: (args, context) => defaultRender(args, context),
};

export default meta;

type Story = StoryObj<HTMLPostCollapsibleElement>;

function defaultRender(
args: HTMLPostCollapsibleElement,
context: StoryContext<HTMLPostCollapsibleElement>,
) {
const hasHeader = args.innerHTML.indexOf('slot="header"') > -1;
const collapsibleId = `collapsible-example--${context.name.replace(/ /g, '-').toLowerCase()}`;

const collapsibleProperties = definedProperties({
'collapsed': args.collapsed,
'heading-level': args.headingLevel,
'id': hasHeader ? undefined : collapsibleId,
});

const collapsibleComponent = html`
<post-collapsible ${spread(collapsibleProperties)}>
${unsafeHTML(args.innerHTML)}
</post-collapsible>
`;

const [currentArgs, updateArgs] = useArgs();

const toggleCollapse = (open?: boolean) => {
const collapsible = document.querySelector(`#${collapsibleId}`) as HTMLPostCollapsibleElement;
collapsible.toggle(open).then((isOpen: boolean) => {
if (typeof currentArgs.collapsed !== 'undefined') updateArgs({ collapsed: !isOpen });
});
};

const togglers = [
['Toggle', () => toggleCollapse()],
['Show', () => toggleCollapse(true)],
['Hide', () => toggleCollapse(false)],
];

const togglersHtml = html`
<div class="d-flex gap-mini mb-regular">
${togglers.map(
([label, listener]) =>
html`
<button
aria-controls="${collapsibleId}"
aria-expanded="${!args.collapsed}"
class="btn btn-secondary"
@click="${listener}"
>
${label}
</button>
`,
)}
</div>
`;

return html`
${hasHeader ? null : togglersHtml} ${collapsibleComponent}
`;
}

export const Default: Story = {};

export const InitiallyCollapsed: Story = {
args: { collapsed: true },
};

export const HeadingLevel: Story = {
args: { headingLevel: 6 },
};

export const IntricateContent: Story = {
args: {
innerHTML: `<p>I am part of the body</p>
<span slot='header'>Customus<em>&nbsp;Titulum</em></span>
<small slot='header' class='text-muted'>&nbsp;- I am part of the header</small>
<p>I am part of the body too!</p>`,
},
};

export const CustomTrigger: Story = {
args: {
innerHTML: `<p class='border rounded p-large'>Contentus momentus vero siteos et accusam iretea et justo.</p>`,
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Canvas, Controls, Meta } from '@storybook/blocks';
import { BADGE } from '../../../../.storybook/constants';
import * as CollapsibleStories from './collapsible.demo.stories';

<Meta title="Components/Collapsible" parameters={{ badges: [BADGE.BETA, BADGE.NEEDS_REVISION] }} />

# Collapsible

<p className="lead">Toggle the visibility of content across your project.</p>

The `<post-collapsible>` component is used to show and hide content.
Collapsing an element will animate the height from its current value to 0.

<Canvas of={CollapsibleStories.Default} />
<Controls of={CollapsibleStories.Default} />

## Examples

The following examples show different use cases for the `<post-collapsible>` component.

### Initially Collapsed

To make the collapsible content hidden by default, just use the `collapsible="true"` property.

<Canvas of={CollapsibleStories.InitiallyCollapsed} />

### Heading Level

Use the `heading-level` property to define the hierarchical level of the collapsible header within the structure of a document.

<Canvas of={CollapsibleStories.HeadingLevel} />

### Intricate Content

The collapsible component uses [HTML slot elements](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/slot)
to allow any HTML content to be used as part of its header and body.<br/>
By default, all children are appended to the component's body.
For a specific child to be displayed in the header, a `slot="header"` attribute must be added.

<Canvas of={CollapsibleStories.IntricateContent} />

### Custom Trigger

The `<post-collapsible>` component exposes a `.toggle()` method that allows to trigger the collapse programmatically.
This method is asynchronous and returns a promise that resolves with the current open state.
It optionally takes a boolean parameter that forces open when `true` or close when `false`.

```typescript
const collapsible = document.querySelector('#collapsibleId') as HTMLPostCollapsibleElement;
collapsible.toggle(/* open: boolean */).then((/* isOpen: boolean */) => {});
```

A header is then not mandatory and can be replaced by an external control.

In this case, to ensure good accessibility, identify the collapsible with an `id`,
then add an `aria-controls` attribute to your control element referencing this `id`.
Also make sure to add an `aria-expanded` attribute to the control element:
if the collapsible element is closed, the attribute on the control element must have a value of `aria-expanded="false"`
and `aria-expanded="true"` otherwise.

<Canvas of={CollapsibleStories.CustomTrigger} />