Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/add-drawer-top-bottom-placement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@solid-design-system/components': minor
'@solid-design-system/styles': minor
'@solid-design-system/tokens': minor
'@solid-design-system/docs': patch
---

Added `top` and `bottom` placement options to `sd-drawer`, enabling vertical slide-in animations from the top and bottom of the screen. A new `--height` CSS custom property controls the panel height for these placements (default: `25rem`).
50 changes: 50 additions & 0 deletions packages/components/src/components/drawer/drawer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,4 +232,54 @@ describe('<sd-drawer>', () => {
const activeElementInsideTestElement = testElement.shadowRoot!.activeElement;
expect(activeElementInsideTestElement).to.equal(trigger);
});

it('should render panel at the top with placement="top"', async () => {
const el = await fixture<SdDrawer>(html` <sd-drawer open placement="top">Lorem ipsum dolor sit amet.</sd-drawer> `);
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part~="panel"]')!;

expect(panel.classList.contains('top-0')).to.be.true;
expect(panel.classList.contains('bottom-0')).to.be.false;
});

it('should render panel at the bottom with placement="bottom"', async () => {
const el = await fixture<SdDrawer>(html`
<sd-drawer open placement="bottom">Lorem ipsum dolor sit amet.</sd-drawer>
`);
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part~="panel"]')!;

expect(panel.classList.contains('bottom-0')).to.be.true;
expect(panel.classList.contains('top-0')).to.be.false;
});

it('should emit sd-show and sd-after-show when opening with placement="top"', async () => {
const el = await fixture<SdDrawer>(html` <sd-drawer placement="top">Lorem ipsum dolor sit amet.</sd-drawer> `);
const showHandler = sinon.spy();
const afterShowHandler = sinon.spy();

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

await waitUntil(() => showHandler.calledOnce);
await waitUntil(() => afterShowHandler.calledOnce, 'sd-after-show did not fire', { timeout: 3000 });

expect(showHandler).to.have.been.calledOnce;
expect(afterShowHandler).to.have.been.calledOnce;
});

it('should emit sd-show and sd-after-show when opening with placement="bottom"', async () => {
const el = await fixture<SdDrawer>(html` <sd-drawer placement="bottom">Lorem ipsum dolor sit amet.</sd-drawer> `);
const showHandler = sinon.spy();
const afterShowHandler = sinon.spy();

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

await waitUntil(() => showHandler.calledOnce);
await waitUntil(() => afterShowHandler.calledOnce, 'sd-after-show did not fire', { timeout: 3000 });

expect(showHandler).to.have.been.calledOnce;
expect(afterShowHandler).to.have.been.calledOnce;
});
});
51 changes: 47 additions & 4 deletions packages/components/src/components/drawer/drawer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,21 @@ import SolidElement from '../../internal/solid-element';
* @csspart body - The drawer's body.
* @csspart footer - The drawer's footer.
*
* @cssproperty --width - The preferred width of the drawer.
* depending on its `placement`. Note that the drawer will shrink to accommodate smaller screens.
* @cssproperty --width - The preferred width of the drawer. Only applies to `start` and `end` placements.
* Note that the drawer will shrink to accommodate smaller screens.
* @cssproperty --height - The preferred height of the drawer. Only applies to `top` and `bottom` placements.
* Note that the drawer will shrink to accommodate smaller screens.
* @cssproperty --sd-panel-color-border - The border color of the drawer panel.
* @cssproperty --sd-overlay-color-background - The background color of the drawer overlay.
*
* @animation drawer.showEnd - The animation to use when showing a drawer with `end` placement.
* @animation drawer.showStart - The animation to use when showing a drawer with `start` placement.
* @animation drawer.showTop - The animation to use when showing a drawer with `top` placement.
* @animation drawer.showBottom - The animation to use when showing a drawer with `bottom` placement.
* @animation drawer.hideEnd - The animation to use when hiding a drawer with `end` placement.
* @animation drawer.hideStart - The animation to use when hiding a drawer with `start` placement.
* @animation drawer.hideTop - The animation to use when hiding a drawer with `top` placement.
* @animation drawer.hideBottom - The animation to use when hiding a drawer with `bottom` placement.
* @animation drawer.denyClose - The animation to use when a request to close the drawer is denied.
* @animation drawer.overlay.show - The animation to use when showing the drawer's overlay.
* @animation drawer.overlay.hide - The animation to use when hiding the drawer's overlay.
Expand Down Expand Up @@ -86,7 +92,7 @@ export default class SdDrawer extends SolidElement {
@property({ type: String, attribute: 'label', reflect: true }) label = '';

/** The direction from which the drawer will open. */
@property({ type: String, reflect: true }) placement: 'end' | 'start' = 'end';
@property({ type: String, reflect: true }) placement: 'end' | 'start' | 'top' | 'bottom' = 'end';

/**
* By default, the drawer slides out of its containing block (the viewport). Contained is a hidden feature used only for testing purposes. Please do not use it in production as it will likely change.
Expand Down Expand Up @@ -322,7 +328,9 @@ export default class SdDrawer extends SolidElement {
'absolute flex flex-col gap-4 z-10 max-w-full max-h-full bg-white shadow-lg overflow-auto pointer-events-auto focus:outline-none',
{
end: 'top-0 end-0 bottom-auto start-auto w-[var(--width)] h-full',
start: 'top-0 end-auto bottom-auto start-0 w-[var(--width)] h-full'
start: 'top-0 end-auto bottom-auto start-0 w-[var(--width)] h-full',
top: 'top-0 start-0 end-0 bottom-auto w-full h-[var(--height)]',
bottom: 'bottom-0 start-0 end-0 top-auto w-full h-[var(--height)]'
}[this.placement]
)}
role="dialog"
Expand Down Expand Up @@ -377,6 +385,7 @@ export default class SdDrawer extends SolidElement {
css`
:host {
--width: 25rem;
--height: 25rem;
@apply contents;
}

Expand Down Expand Up @@ -454,6 +463,40 @@ setDefaultAnimation('drawer.hideEnd', {
options: { duration: 'var(--sd-duration-fast, 150)', easing: 'ease-in-out' }
});

// Top
setDefaultAnimation('drawer.showTop', {
keyframes: [
{ opacity: 0, translate: '0 -100%' },
{ opacity: 1, translate: '0 0' }
],
options: { duration: 'var(--sd-duration-medium, 300)', easing: 'ease-in-out' }
});

setDefaultAnimation('drawer.hideTop', {
keyframes: [
{ opacity: 1, translate: '0 0' },
{ opacity: 0, translate: '0 -100%' }
],
options: { duration: 'var(--sd-duration-fast, 150)', easing: 'ease-in-out' }
});

// Bottom
setDefaultAnimation('drawer.showBottom', {
keyframes: [
{ opacity: 0, translate: '0 100%' },
{ opacity: 1, translate: '0 0' }
],
options: { duration: 'var(--sd-duration-medium, 300)', easing: 'ease-in-out' }
});

setDefaultAnimation('drawer.hideBottom', {
keyframes: [
{ opacity: 1, translate: '0 0' },
{ opacity: 0, translate: '0 100%' }
],
options: { duration: 'var(--sd-duration-fast, 150)', easing: 'ease-in-out' }
});

// Deny close
setDefaultAnimation('drawer.denyClose', {
keyframes: [{ scale: 1 }, { scale: 1.01 }, { scale: 1 }],
Expand Down
2 changes: 2 additions & 0 deletions packages/docs/src/stories/components/drawer.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ export const Open = {
*
* - `start`: The drawer will be positioned on the left side of the screen.
* - `end`: The drawer will be positioned on the right side of the screen.
* - `top`: The drawer will be positioned at the top of the screen.
* - `bottom`: The drawer will be positioned at the bottom of the screen.
*/
export const Placement = {
name: 'Placement',
Expand Down
Loading