From 6e96c468c617cc142b9de509374eb8e569e5b1f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81rgio=20Fonseca?= Date: Wed, 28 Jan 2026 16:09:22 +0000 Subject: [PATCH 01/11] fix: adjust calendar view placement --- .../src/components/datepicker/datepicker.ts | 46 +++++++++++++++---- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/packages/components/src/components/datepicker/datepicker.ts b/packages/components/src/components/datepicker/datepicker.ts index a9ab33a52a..d00b769d6e 100644 --- a/packages/components/src/components/datepicker/datepicker.ts +++ b/packages/components/src/components/datepicker/datepicker.ts @@ -12,6 +12,7 @@ import { watch } from '../../internal/watch'; import cx from 'classix'; import SolidElement from '../../internal/solid-element'; import type { SolidFormControl } from '../../internal/solid-element'; +import type SdPopup from '../popup/popup'; /** * @summary Used to enter or select a date or a range of dates using a calendar view. @@ -274,6 +275,8 @@ export default class SdDatepicker extends SolidElement implements SolidFormContr /** The text value shown in the input, synchronized with selection. */ @state() private inputValue = ''; + @query('sd-popup') popup: SdPopup; + @query('#invalid-message') invalidMessage: HTMLDivElement; @query('#input') input: HTMLInputElement; @@ -326,6 +329,13 @@ export default class SdDatepicker extends SolidElement implements SolidFormContr this.formControlController.updateValidity(); } + @watch('open', { waitUntilFirstUpdate: true }) + handleOpenChange() { + if (this.popup) { + this.popup.active = this.open && !this.disabled && !this.visuallyDisabled; + } + } + disconnectedCallback() { super.disconnectedCallback(); this.removeEventListener('focusin', this.onFocusIn); @@ -1127,8 +1137,12 @@ export default class SdDatepicker extends SolidElement implements SolidFormContr this.handleBlur(); }; - private handleCurrentPlacement = (ev: CustomEvent<{ placement: 'top' | 'bottom' }>) => { - this.currentPlacement = ev.detail.placement; + private handleCurrentPlacement = (e: CustomEvent<'top' | 'bottom'>) => { + const incomingPlacement = e.detail; + + if (incomingPlacement) { + this.currentPlacement = incomingPlacement; + } }; private setMonth(offset: number) { @@ -1622,9 +1636,14 @@ export default class SdDatepicker extends SolidElement implements SolidFormContr return html`
@@ -1936,7 +1955,11 @@ export default class SdDatepicker extends SolidElement implements SolidFormContr 'absolute top-0 w-full h-full pointer-events-none border rounded-default z-10 transition-[border] duration-medium ease-in-out', borderColor, this.open && this.alignment === 'left' ? 'rounded-bl-none' : '', - this.open && this.alignment === 'right' ? 'rounded-br-none' : '' + this.open && this.alignment === 'right' ? 'rounded-br-none' : '', + this.open && + (this.currentPlacement === 'bottom' + ? 'rounded-bl-none rounded-br-none' + : 'rounded-tl-none rounded-tr-none') )} > ${hasLabel && this.floatingLabel @@ -1973,8 +1996,14 @@ export default class SdDatepicker extends SolidElement implements SolidFormContr
- - ${this.renderCalendar()}
+ ${this.renderCalendar()}
Date: Wed, 28 Jan 2026 16:10:34 +0000 Subject: [PATCH 02/11] chore: update stories and screenshot tests --- .../docs/src/stories/components/datepicker.stories.ts | 2 +- .../src/stories/components/datepicker.test.stories.ts | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/docs/src/stories/components/datepicker.stories.ts b/packages/docs/src/stories/components/datepicker.stories.ts index 9ad79d0e97..250e1d46d6 100644 --- a/packages/docs/src/stories/components/datepicker.stories.ts +++ b/packages/docs/src/stories/components/datepicker.stories.ts @@ -23,7 +23,7 @@ export default { export const Default = { render: (args: any) => { - return html`
${generateTemplate({ args })}
`; + return html`
${generateTemplate({ args })}
`; } }; diff --git a/packages/docs/src/stories/components/datepicker.test.stories.ts b/packages/docs/src/stories/components/datepicker.test.stories.ts index b8e1e0d037..2ef0a1fa2a 100644 --- a/packages/docs/src/stories/components/datepicker.test.stories.ts +++ b/packages/docs/src/stories/components/datepicker.test.stories.ts @@ -161,6 +161,15 @@ export const MinAndMax = { } }; +export const Placement = { + name: 'Placement', + render: () => { + return html`
+ +
`; + } +}; + export const Mouseless = { name: 'Mouseless', render: (args: any) => { From f96f21bb47798a89a96c947c2faf66cd1cccdffd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81rgio=20Fonseca?= Date: Wed, 28 Jan 2026 16:11:28 +0000 Subject: [PATCH 03/11] chore: add changeset --- .changeset/silver-buttons-fly.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/silver-buttons-fly.md diff --git a/.changeset/silver-buttons-fly.md b/.changeset/silver-buttons-fly.md new file mode 100644 index 0000000000..26a547844c --- /dev/null +++ b/.changeset/silver-buttons-fly.md @@ -0,0 +1,6 @@ +--- +'@solid-design-system/components': patch +'@solid-design-system/docs': patch +--- + +Fixed `sd-datepicker` `placement` attribute to properly render the calendar view on top or bottom of the input element and included screenshot test `Placement`. From 77ed6ca6e32d857b1499784cfbe75bf8e09afe7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81rgio=20Fonseca?= Date: Wed, 28 Jan 2026 16:28:17 +0000 Subject: [PATCH 04/11] chore: update combination content --- packages/docs/src/stories/components/datepicker.test.stories.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/docs/src/stories/components/datepicker.test.stories.ts b/packages/docs/src/stories/components/datepicker.test.stories.ts index 2ef0a1fa2a..d837fb66cc 100644 --- a/packages/docs/src/stories/components/datepicker.test.stories.ts +++ b/packages/docs/src/stories/components/datepicker.test.stories.ts @@ -204,6 +204,7 @@ export const Combination = generateScreenshotStory([ DisabledWeekends, DisabledDates, MinAndMax, + Placement, Mouseless, LocaleAware ]); From a9dda8d88121735a077c046ec90ca8176095b3e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81rgio=20Fonseca?= Date: Wed, 28 Jan 2026 17:27:47 +0000 Subject: [PATCH 05/11] fix: sync alignment and placement logic --- .../src/components/datepicker/datepicker.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/components/src/components/datepicker/datepicker.ts b/packages/components/src/components/datepicker/datepicker.ts index d00b769d6e..6e26e69c5e 100644 --- a/packages/components/src/components/datepicker/datepicker.ts +++ b/packages/components/src/components/datepicker/datepicker.ts @@ -1137,7 +1137,7 @@ export default class SdDatepicker extends SolidElement implements SolidFormContr this.handleBlur(); }; - private handleCurrentPlacement = (e: CustomEvent<'top' | 'bottom'>) => { + private handleCurrentPlacement = (e: CustomEvent) => { const incomingPlacement = e.detail; if (incomingPlacement) { @@ -1639,7 +1639,7 @@ export default class SdDatepicker extends SolidElement implements SolidFormContr class=${cx( 'w-[284px] z-50 bg-white py-3 px-4', this.open ? 'block' : 'hidden', - this.currentPlacement === 'bottom' + this.currentPlacement?.startsWith('bottom') ? 'border-r-2 border-b-2 border-l-2 rounded-br-default rounded-bl-default' : 'border-r-2 border-t-2 border-l-2 rounded-tr-default rounded-tl-default', 'border-primary' @@ -1957,7 +1957,7 @@ export default class SdDatepicker extends SolidElement implements SolidFormContr this.open && this.alignment === 'left' ? 'rounded-bl-none' : '', this.open && this.alignment === 'right' ? 'rounded-br-none' : '', this.open && - (this.currentPlacement === 'bottom' + (this.currentPlacement?.startsWith('bottom') ? 'rounded-bl-none rounded-br-none' : 'rounded-tl-none rounded-tr-none') )} @@ -1998,10 +1998,9 @@ export default class SdDatepicker extends SolidElement implements SolidFormContr @sd-current-placement=${this.handleCurrentPlacement} class=${cx( 'inline-flex relative w-full', - this.currentPlacement === 'bottom' ? 'origin-top' : 'origin-bottom' + this.currentPlacement?.startsWith('bottom') ? 'origin-top' : 'origin-bottom' )} - sync="width" - placement=${this.placement} + placement=${this.alignment === 'left' ? `${this.placement}-start` : `${this.placement}-end`} flip shift auto-size="vertical" From 16c6d1741d40c364ac4980dab26c624380587a5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81rgio=20Fonseca?= Date: Wed, 28 Jan 2026 17:29:11 +0000 Subject: [PATCH 06/11] test: fixed datepicker failing tests --- .../components/datepicker/datepicker.test.ts | 64 ++++++++++++------- 1 file changed, 41 insertions(+), 23 deletions(-) diff --git a/packages/components/src/components/datepicker/datepicker.test.ts b/packages/components/src/components/datepicker/datepicker.test.ts index 66464bd455..7e7ae40d7e 100644 --- a/packages/components/src/components/datepicker/datepicker.test.ts +++ b/packages/components/src/components/datepicker/datepicker.test.ts @@ -120,10 +120,14 @@ describe('', () => { el.show(); await el.updateComplete; - const dayButton = el.shadowRoot!.querySelector('button.day:not(.out-month):not(.disabled)')!; - await clickOnElement(dayButton); - await el.updateComplete; - + await waitUntil( + () => !!el.shadowRoot!.querySelector('button.day:not(.out-month):not(.disabled)'), + 'No enabled day rendered', + { timeout: 2000 } + ); + const dayButton = el.shadowRoot!.querySelector('button.day:not(.out-month):not(.disabled)')!; + dayButton.click(); + await waitUntil(() => el.value !== null, 'Value not set after click', { timeout: 2000 }); expect(el.value).to.not.be.null; }); @@ -138,9 +142,14 @@ describe('', () => { el.show(); await el.updateComplete; - const dayButton = el.shadowRoot!.querySelector('button.day:not(.out-month):not(.disabled)')!; - await clickOnElement(dayButton); - await el.updateComplete; + await waitUntil( + () => !!el.shadowRoot!.querySelector('button.day:not(.out-month):not(.disabled)'), + 'No enabled day rendered', + { timeout: 2000 } + ); + const dayButton = el.shadowRoot!.querySelector('button.day:not(.out-month):not(.disabled)')!; + dayButton.click(); + await waitUntil(() => el.value !== null, 'Value not set after click', { timeout: 2000 }); expect(changeHandler).to.have.been.calledOnce; expect(selectHandler).to.have.been.calledOnce; @@ -326,18 +335,28 @@ describe('', () => { }); describe('calendar alignment and placement', () => { - it('applies alignment classes to calendar', async () => { - const elLeft = await fixture(html``); - elLeft.show(); - await elLeft.updateComplete; - const calLeft = elLeft.shadowRoot!.querySelector('[part~="datepicker"]')!; - expect(calLeft.className).to.include('left-0'); - - const elRight = await fixture(html``); - elRight.show(); - await elRight.updateComplete; - const calRight = elRight.shadowRoot!.querySelector('[part~="datepicker"]')!; - expect(calRight.className).to.include('right-0'); + it('forwards placement to sd-popup (with alignment)', async () => { + const elBottom = await fixture( + html`` + ); + elBottom.show(); + await elBottom.updateComplete; + const popupBottom = elBottom.shadowRoot!.querySelector('sd-popup')!; + expect(popupBottom.getAttribute('placement')).to.equal('bottom-start'); + + const elTop = await fixture(html``); + elTop.show(); + await elTop.updateComplete; + const popupTop = elTop.shadowRoot!.querySelector('sd-popup')!; + expect(popupTop.getAttribute('placement')).to.equal('top-start'); + + const elBottomRight = await fixture( + html`` + ); + elBottomRight.show(); + await elBottomRight.updateComplete; + const popupBottomRight = elBottomRight.shadowRoot!.querySelector('sd-popup')!; + expect(popupBottomRight.getAttribute('placement')).to.equal('bottom-end'); }); it('updates currentPlacement from sd-popup', async () => { @@ -345,12 +364,11 @@ describe('', () => { el.show(); await el.updateComplete; - el.dispatchEvent( - new CustomEvent('sd-current-placement', { bubbles: true, composed: true, detail: { placement: 'top' } }) - ); + const popup = el.shadowRoot!.querySelector('sd-popup')!; + popup.dispatchEvent(new CustomEvent('sd-current-placement', { bubbles: true, composed: true, detail: 'top' })); await el.updateComplete; - expect(el['currentPlacement']).to.equal('bottom'); + expect(el['currentPlacement']).to.equal('top'); }); }); From 92348fd9d49e5f6f166ad0d6222cc226e294a795 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81rgio=20Fonseca?= Date: Wed, 28 Jan 2026 22:32:57 +0000 Subject: [PATCH 07/11] fix: custom event typing --- packages/components/src/components/datepicker/datepicker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/src/components/datepicker/datepicker.ts b/packages/components/src/components/datepicker/datepicker.ts index 6e26e69c5e..d4b3f109a7 100644 --- a/packages/components/src/components/datepicker/datepicker.ts +++ b/packages/components/src/components/datepicker/datepicker.ts @@ -1137,7 +1137,7 @@ export default class SdDatepicker extends SolidElement implements SolidFormContr this.handleBlur(); }; - private handleCurrentPlacement = (e: CustomEvent) => { + private handleCurrentPlacement = (e: CustomEvent<'top' | 'bottom'>) => { const incomingPlacement = e.detail; if (incomingPlacement) { From 9cb7afd84897e80a2564cf00a98bd0b19466bd18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81rgio=20Fonseca?= Date: Wed, 28 Jan 2026 22:59:10 +0000 Subject: [PATCH 08/11] chore: remove unneeded timeout --- .../src/components/datepicker/datepicker.test.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/components/src/components/datepicker/datepicker.test.ts b/packages/components/src/components/datepicker/datepicker.test.ts index 7e7ae40d7e..3c74f0adec 100644 --- a/packages/components/src/components/datepicker/datepicker.test.ts +++ b/packages/components/src/components/datepicker/datepicker.test.ts @@ -123,11 +123,11 @@ describe('', () => { await waitUntil( () => !!el.shadowRoot!.querySelector('button.day:not(.out-month):not(.disabled)'), 'No enabled day rendered', - { timeout: 2000 } + { timeout: 0 } ); const dayButton = el.shadowRoot!.querySelector('button.day:not(.out-month):not(.disabled)')!; dayButton.click(); - await waitUntil(() => el.value !== null, 'Value not set after click', { timeout: 2000 }); + await waitUntil(() => el.value !== null, 'Value not set after click'); expect(el.value).to.not.be.null; }); @@ -145,12 +145,11 @@ describe('', () => { await waitUntil( () => !!el.shadowRoot!.querySelector('button.day:not(.out-month):not(.disabled)'), 'No enabled day rendered', - { timeout: 2000 } + { timeout: 0 } ); const dayButton = el.shadowRoot!.querySelector('button.day:not(.out-month):not(.disabled)')!; dayButton.click(); - await waitUntil(() => el.value !== null, 'Value not set after click', { timeout: 2000 }); - + await waitUntil(() => el.value !== null, 'Value not set after click'); expect(changeHandler).to.have.been.calledOnce; expect(selectHandler).to.have.been.calledOnce; }); From 1816cee621a1835c3af0d6bb90e37d0a9911ac2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81rgio=20Fonseca?= Date: Wed, 4 Feb 2026 14:04:26 +0000 Subject: [PATCH 09/11] fix: improve readonly handling --- .../src/components/datepicker/datepicker.ts | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/components/src/components/datepicker/datepicker.ts b/packages/components/src/components/datepicker/datepicker.ts index d4b3f109a7..3d48d5fa9b 100644 --- a/packages/components/src/components/datepicker/datepicker.ts +++ b/packages/components/src/components/datepicker/datepicker.ts @@ -833,7 +833,7 @@ export default class SdDatepicker extends SolidElement implements SolidFormContr }; show() { - if (this.open || this.disabled || this.visuallyDisabled) { + if (this.open || this.disabled || this.visuallyDisabled || this.readonly) { this.open = false; return; } @@ -861,7 +861,7 @@ export default class SdDatepicker extends SolidElement implements SolidFormContr }; private handleMouseDown(event: MouseEvent) { - if (this.visuallyDisabled || this.disabled) { + if (this.visuallyDisabled || this.disabled || this.readonly) { event.preventDefault(); event.stopPropagation(); return; @@ -879,14 +879,14 @@ export default class SdDatepicker extends SolidElement implements SolidFormContr private handleFocus() { this.hasFocus = true; - if (!this.open && !this.disabled && !this.visuallyDisabled) { + if (!this.open && !this.disabled && !this.visuallyDisabled && !this.readonly) { this.show(); } this.emit('sd-focus'); } private handleInput = (ev: Event) => { - if (this.disabled || this.visuallyDisabled) { + if (this.disabled || this.visuallyDisabled || this.readonly) { ev.preventDefault?.(); ev.stopPropagation?.(); return; @@ -1230,6 +1230,9 @@ export default class SdDatepicker extends SolidElement implements SolidFormContr direction: -1 | 1, isLastHeaderControl: boolean ) => { + if (this.disabled || this.visuallyDisabled || this.readonly) { + return; + } // Only the last header control sends focus into the grid on Tab if (ev.key === 'Tab' && !ev.shiftKey) { if (isLastHeaderControl) { @@ -1250,6 +1253,7 @@ export default class SdDatepicker extends SolidElement implements SolidFormContr }; private selectSingleDate(d: Date) { + if (this.readonly) return; if (this.isDisabled(d)) return; const localMidnight = DateUtils.startOfDayLocal(d); @@ -1277,6 +1281,7 @@ export default class SdDatepicker extends SolidElement implements SolidFormContr } private selectRangeDate(d: Date) { + if (this.readonly) return; const day = DateUtils.startOfDayLocal(d); const rs = this.rangeStart ? DateUtils.parseLocalISO(this.rangeStart) : null; const re = this.rangeEnd ? DateUtils.parseLocalISO(this.rangeEnd) : null; @@ -1416,12 +1421,13 @@ export default class SdDatepicker extends SolidElement implements SolidFormContr } private selectDate(d: Date) { + if (this.readonly) return; if (this.range) this.selectRangeDate(d); else this.selectSingleDate(d); } private onKeyDown = (ev: KeyboardEvent) => { - if (this.disabled || this.visuallyDisabled) { + if (this.disabled || this.visuallyDisabled || this.readonly) { ev.preventDefault(); ev.stopPropagation(); return; @@ -1501,7 +1507,7 @@ export default class SdDatepicker extends SolidElement implements SolidFormContr }; private handleInputKeyDown = (ev: KeyboardEvent) => { - if ((this.disabled || this.visuallyDisabled) && ev.key !== 'Tab') { + if ((this.disabled || this.visuallyDisabled || this.readonly) && ev.key !== 'Tab') { ev.preventDefault(); ev.stopPropagation(); return; @@ -1527,7 +1533,7 @@ export default class SdDatepicker extends SolidElement implements SolidFormContr return; } - if (ev.key === 'Enter' && !this.open && !this.disabled && !this.visuallyDisabled) { + if (ev.key === 'Enter' && !this.open && !this.disabled && !this.visuallyDisabled && !this.readonly) { ev.preventDefault(); this.show(); } @@ -1599,7 +1605,7 @@ export default class SdDatepicker extends SolidElement implements SolidFormContr /** Mouse enter on a day: updates preview end when selecting a range. */ private onDayMouseEnter(day: Date) { - if (!this.range) return; + if (!this.range || this.readonly) return; const rs = this.rangeStart ? DateUtils.parseLocalISO(this.rangeStart) : null; const re = this.rangeEnd ? DateUtils.parseLocalISO(this.rangeEnd) : null; if (rs && !re) { @@ -1833,8 +1839,10 @@ export default class SdDatepicker extends SolidElement implements SolidFormContr aria-colindex=${colIndex + 1} aria-labelledby=${'col-' + (colIndex + 1)} .tabIndex=${tabIndex} - ?disabled=${disabled || this.disabled} - aria-disabled=${disabled || this.visuallyDisabled || this.disabled ? 'true' : 'false'} + ?disabled=${disabled || this.disabled || this.readonly} + aria-disabled=${disabled || this.visuallyDisabled || this.disabled || this.readonly + ? 'true' + : 'false'} aria-selected=${isSelectedSingle || inSelectedRange || isRangeStart || isRangeEnd ? 'true' : 'false'} From a05d99b42bd9b8b40277fb17feaeb8a8337d86d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81rgio=20Fonseca?= Date: Wed, 4 Feb 2026 14:05:28 +0000 Subject: [PATCH 10/11] fix: updated readonly test --- .../components/src/components/datepicker/datepicker.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/components/src/components/datepicker/datepicker.test.ts b/packages/components/src/components/datepicker/datepicker.test.ts index 3c74f0adec..3b710aa9a5 100644 --- a/packages/components/src/components/datepicker/datepicker.test.ts +++ b/packages/components/src/components/datepicker/datepicker.test.ts @@ -277,13 +277,13 @@ describe('', () => { }); describe('form integration', () => { - it('readonly prevents typing but allows calendar open', async () => { + it('readonly prevents typing and calendar open', async () => { const el = await fixture(html``); const input = el.shadowRoot!.querySelector('#input')!; input.focus(); await el.updateComplete; - expect(input.ariaExpanded).to.equal('true'); + expect(input.ariaExpanded).to.equal('false'); const before = input.value; await sendKeys({ type: '15012024' }); await el.updateComplete; From 520fce68bfe9ee146f8b3d328337b40b78afc1fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81rgio=20Fonseca?= Date: Wed, 4 Feb 2026 23:28:27 +0000 Subject: [PATCH 11/11] chore: update changeset --- .changeset/silver-buttons-fly.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.changeset/silver-buttons-fly.md b/.changeset/silver-buttons-fly.md index 26a547844c..5c4239c0a0 100644 --- a/.changeset/silver-buttons-fly.md +++ b/.changeset/silver-buttons-fly.md @@ -3,4 +3,8 @@ '@solid-design-system/docs': patch --- -Fixed `sd-datepicker` `placement` attribute to properly render the calendar view on top or bottom of the input element and included screenshot test `Placement`. +Fixed `sd-datepicker`: + +- `placement` attribute properly renders the calendar view on top or bottom of the input element. +- Included screenshot test `Placement`. +- Improved `readonly` attribute handling.