Skip to content

Commit

Permalink
feat: add sd-expandable (#1214)
Browse files Browse the repository at this point in the history
  • Loading branch information
Vahid1919 authored Aug 2, 2024
1 parent d7069ef commit 3668556
Show file tree
Hide file tree
Showing 7 changed files with 658 additions and 2 deletions.
251 changes: 251 additions & 0 deletions packages/components/src/components/expandable/expandable.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
import '../../solid-components';
import { html } from 'lit';
import { storybookDefaults, storybookHelpers, storybookTemplate } from '../../../scripts/storybook/helper';
import { userEvent } from '@storybook/test';
import { waitUntil } from '@open-wc/testing-helpers';

const { argTypes, parameters } = storybookDefaults('sd-expandable');
const { overrideArgs } = storybookHelpers('sd-expandable');
const { generateTemplate } = storybookTemplate('sd-expandable');

export default {
title: 'Components/sd-expandable',
component: 'sd-expandable',
args: overrideArgs([
{ type: 'slot', name: 'default', value: '<div class="slot slot--border slot--text h-16">Default slot</div>' }
]),
argTypes,
parameters: { ...parameters }
};

/**
* Expandable shows a brief summary and expands to show additional content.
*/
export const Default = {
render: (args: any) => {
return generateTemplate({
args
});
}
};

/**
* Use the inverted attribute to make an expandable with inverted colors.
*/
export const Inverted = {
parameters: { controls: { exclude: 'inverted' } },
render: (args: any) => {
return generateTemplate({
axis: {
y: { type: 'attribute', name: 'inverted' }
},
args,
options: {
templateBackgrounds: { alternate: 'y', colors: ['rgb(var(--sd-color-primary, 0 53 142))', 'white'] }
}
});
}
};

/**
* Use the `default`, `toggle-open` and `toggle-closed` slots to add content to the expandable.
*/
export const Slots = {
parameters: {
controls: { exclude: ['default', 'toggle-open', 'toggle-closed'] }
},
render: (args: any) => {
return html`
${['default', 'toggle-open', 'toggle-closed'].map(slot =>
generateTemplate({
axis: {
x: {
type: 'slot',
name: slot,
title: 'slot=...',
values: [
{
value:
slot === 'default'
? `<div class="slot slot--border slot--background slot--text h-full">Default slot</div>`
: `<div slot='${slot}' class="slot slot--border slot--background slot--text h-12"></div>`,
title: slot
}
]
}
},
args
})
)}
`;
}
};

/**
* Use the `content`, `toggle`, `summary` and `details` parts to style the expandable.
*/
export const Parts = {
parameters: {
controls: {
exclude: ['open', 'content', 'toggle', 'summary', 'details']
}
},
render: (args: any) => {
return generateTemplate({
axis: {
y: {
type: 'template',
name: 'sd-expandable::part(...){outline: solid 2px red}',
values: ['content', 'toggle', 'summary', 'details'].map(part => {
return {
title: part,
value: `<style>#part-${part} sd-expandable::part(${part}){outline: solid 2px red; outline-offset: -2px;}</style><div id="part-${part}">%TEMPLATE%</div>`
};
})
}
},
args
});
}
};

/**
* sd-expandable is fully accessibile via keyboard.
*/

export const Mouseless = {
render: (args: any) => {
return html`<div class="mouseless">${generateTemplate({ args })}</div>`;
},

play: async ({ canvasElement }: { canvasElement: HTMLUnknownElement }) => {
const el = canvasElement.querySelector('.mouseless sd-expandable');
await waitUntil(() => el?.shadowRoot?.querySelector('button'));
await userEvent.type(el!.shadowRoot!.querySelector('button')!, '{return}', { pointerEventsCheck: 0 });
}
};

/**
* Expandable can be used with background options of white, neutral-100 and primary-100. When using these options, use the `--gradient-color-start` and `--gradient-color-end` CSS variables to align the gradient colors.
*
* The inverted attribute can be used when the background is primary. The default slot can be used with 2 variants for alternate expandable experiences: lead text and paragraph.
*/
export const Samples = {
render: (args: any) => {
return html`
<style>
.background-sample {
padding: 16px;
margin-bottom: 32px;
width: 100%;
box-sizing: border-box;
}
</style>
<div class="p-4 mb-8 bg-neutral-100 text-left text-[14px] font-bold box-border">Background white</div>
<div class="background-sample">
${generateTemplate({
args: { ...args }
})}
</div>
<div class="w-full p-4 mb-8 bg-neutral-100 text-left text-[14px] font-bold box-border">
Background neutral-100
</div>
<div class="background-sample bg-neutral-100">
${generateTemplate({
args: { ...args },
constants: [
{
type: 'cssProperty',
name: '--gradient-color-start',
value: 'rgba(246, 246, 246, 0)'
},
{
type: 'cssProperty',
name: '--gradient-color-end',
value: 'rgba(246, 246, 246, 1)'
}
]
})}
</div>
<div class="w-full p-4 mb-8 bg-neutral-100 text-left text-[14px] font-bold box-border">
Background primary-100
</div>
<div class="background-sample bg-primary-100">
${generateTemplate({
args: { ...args },
constants: [
{
type: 'cssProperty',
name: '--gradient-color-start',
value: 'rgba(236, 240, 249, 0)'
},
{
type: 'cssProperty',
name: '--gradient-color-end',
value: 'rgba(236, 240, 249, 1)'
}
]
})}
</div>
<div class="w-full p-4 mb-8 bg-neutral-100 text-left text-[14px] font-bold box-border">
Background primary, inverted
</div>
<div class="background-sample bg-primary">
${generateTemplate({
args: { ...args, inverted: true }
})}
</div>
<div class="w-full p-4 mb-8 bg-neutral-100 text-left text-[14px] font-bold box-border">Lead Text Example</div>
<div class="background-sample bg-neutral-100 ">
${generateTemplate({
args: overrideArgs([
{
type: 'slot',
name: 'default',
value:
'<div class="sd-leadtext">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nibh justo ullamcorper odio tempor molestie phasellus dui vel id. Velit in sed non orci pellentesque vivamus nunc. At non tortor, sit neque tristique. Facilisis commodo integer hendrerit tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nibh justo ullamcorper odio tempor molestie phasellus dui vel id. Velit in sed non orci pellentesque vivamus nunc. At non tortor, sit neque tristique. Facilisis commodo integer hendrerit tortor.</div>'
},
{
type: 'attribute',
name: 'variant',
value: 'leadtext'
},
{
type: 'cssProperty',
name: '--gradient-color-start',
value: 'rgba(246, 246, 246, 0)'
},
{
type: 'cssProperty',
name: '--gradient-color-end',
value: 'rgba(246, 246, 246, 1)'
}
])
})}
</div>
<div class="w-full p-4 mb-8 bg-neutral-100 text-left text-[14px] font-bold box-border">Paragraph Example</div>
<div class="background-sample bg-neutral-100 ">
${generateTemplate({
args: overrideArgs([
{
type: 'slot',
name: 'default',
value:
'<div class="sd-paragraph">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nibh justo ullamcorper odio tempor molestie phasellus dui vel id. Velit in sed non orci pellentesque vivamus nunc. At non tortor, sit neque tristique. Facilisis commodo integer hendrerit tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nibh justo ullamcorper odio tempor molestie phasellus dui vel id. Velit in sed non orci pellentesque vivamus nunc. At non tortor, sit neque tristique. Facilisis commodo integer hendrerit tortor.</div>'
},
{
type: 'cssProperty',
name: '--gradient-color-start',
value: 'rgba(246, 246, 246, 0)'
},
{
type: 'cssProperty',
name: '--gradient-color-end',
value: 'rgba(246, 246, 246, 1)'
}
])
})}
</div>
`;
}
};
108 changes: 108 additions & 0 deletions packages/components/src/components/expandable/expandable.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
import sinon from 'sinon';
import type SdExpandable from './expandable';

describe('<sd-expandable>', () => {
it('should be accessible', async () => {
const el = await fixture<SdExpandable>(html`
<sd-expandable>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat.
</sd-expandable>
`);

await expect(el).to.be.accessible();
});

it('should be visible when the open attribute is set', async () => {
const el = await fixture<SdExpandable>(html`
<sd-expandable open>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat.
</sd-expandable>
`);

expect(el.open).to.be.true;
expect(el.shadowRoot!.querySelector<HTMLElement>('details')?.getAttribute('open')).to.equal('true');
});

it('should not be visible without the open attribute', async () => {
const el = await fixture<SdExpandable>(html`
<sd-expandable>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat.
</sd-expandable>
`);

expect(el.open).to.be.false;
expect(el.shadowRoot!.querySelector<HTMLElement>('details')?.getAttribute('open')).to.equal('false');
});

it('should emit sd-show and sd-after-show when opened programmatically', async () => {
const el = await fixture<SdExpandable>(html`
<sd-expandable>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat.
</sd-expandable>
`);
const showHandler = sinon.spy();
const afterShowHandler = sinon.spy();

el.addEventListener('sd-show', showHandler);
el.addEventListener('sd-after-show', afterShowHandler);

el.show(); // Programmatically open the expandable
await el.updateComplete;

await waitUntil(() => showHandler.calledOnce);
await waitUntil(() => afterShowHandler.calledOnce);

expect(showHandler).to.have.been.calledOnce;
expect(afterShowHandler).to.have.been.calledOnce;
expect(el.shadowRoot!.querySelector<HTMLElement>('details')?.getAttribute('open')).to.equal('true');
});

it('should emit sd-hide and sd-after-hide when closed programmatically', async () => {
const el = await fixture<SdExpandable>(html`
<sd-expandable open>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat.
</sd-expandable>
`);
const hideHandler = sinon.spy();
const afterHideHandler = sinon.spy();

el.addEventListener('sd-hide', hideHandler);
el.addEventListener('sd-after-hide', afterHideHandler);
el.hide(); // Programmatically close the expandable

await waitUntil(() => hideHandler.calledOnce);
await waitUntil(() => afterHideHandler.calledOnce);

expect(hideHandler).to.have.been.calledOnce;
expect(afterHideHandler).to.have.been.calledOnce;
expect(el.shadowRoot!.querySelector<HTMLElement>('details')?.getAttribute('open')).to.equal('false');
});

it('should toggle open state when toggle button is clicked', async () => {
const el = await fixture<SdExpandable>(html`
<sd-expandable>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat.
</sd-expandable>
`);
const toggleButton = el.shadowRoot?.querySelector('button');
toggleButton?.click(); // Simulate user clicking the toggle button

expect(el.open).to.be.true;

toggleButton?.click(); // Click again to close
expect(el.open).to.be.false;
});
});
Loading

0 comments on commit 3668556

Please sign in to comment.