From cdd38de41b8e280aace18c03e1a2a89f3a4a7cc9 Mon Sep 17 00:00:00 2001 From: mariohamann Date: Fri, 8 May 2026 06:44:25 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20=E2=9C=A8=20Add=20top=20and=20bottom=20?= =?UTF-8?q?placement=20options=20to=20sd-drawer=20with=20corresponding=20a?= =?UTF-8?q?nimations=20and=20height=20property?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/add-drawer-top-bottom-placement.md | 8 +++ .../src/components/drawer/drawer.test.ts | 50 ++++++++++++++++++ .../src/components/drawer/drawer.ts | 51 +++++++++++++++++-- .../src/stories/components/drawer.stories.ts | 2 + 4 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 .changeset/add-drawer-top-bottom-placement.md diff --git a/.changeset/add-drawer-top-bottom-placement.md b/.changeset/add-drawer-top-bottom-placement.md new file mode 100644 index 0000000000..7bda32d096 --- /dev/null +++ b/.changeset/add-drawer-top-bottom-placement.md @@ -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`). diff --git a/packages/components/src/components/drawer/drawer.test.ts b/packages/components/src/components/drawer/drawer.test.ts index 40bc31fd3a..b31e364e9f 100644 --- a/packages/components/src/components/drawer/drawer.test.ts +++ b/packages/components/src/components/drawer/drawer.test.ts @@ -232,4 +232,54 @@ describe('', () => { 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(html` Lorem ipsum dolor sit amet. `); + const panel = el.shadowRoot!.querySelector('[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(html` + Lorem ipsum dolor sit amet. + `); + const panel = el.shadowRoot!.querySelector('[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(html` Lorem ipsum dolor sit amet. `); + 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(html` Lorem ipsum dolor sit amet. `); + 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; + }); }); diff --git a/packages/components/src/components/drawer/drawer.ts b/packages/components/src/components/drawer/drawer.ts index f018672198..6c6c1100ae 100644 --- a/packages/components/src/components/drawer/drawer.ts +++ b/packages/components/src/components/drawer/drawer.ts @@ -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. @@ -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. @@ -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" @@ -377,6 +385,7 @@ export default class SdDrawer extends SolidElement { css` :host { --width: 25rem; + --height: 25rem; @apply contents; } @@ -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 }], diff --git a/packages/docs/src/stories/components/drawer.stories.ts b/packages/docs/src/stories/components/drawer.stories.ts index 046535814a..afc08a325b 100644 --- a/packages/docs/src/stories/components/drawer.stories.ts +++ b/packages/docs/src/stories/components/drawer.stories.ts @@ -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',