mirror of
https://github.com/tabler/tabler.git
synced 2026-10-12 05:06:55 +04:00
Fix Datepicker popup snapping back on resize and an unformatted initial value (#3230)
This commit is contained in:
1 parent
4f471ddccf
commit
9fc77b9ac8
3 files changed
+104
-3
No files matched your search
@@ -47,6 +47,8 @@ const DATE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/
|
||||
// 3.3 exports none and gets no `extensions` option.
|
||||
const EXTENSION_NAMES = ['annotations', 'months', 'motion', 'time', 'weeks'] as const
|
||||
|
||||
const writtenSelections = new WeakMap<HTMLElement, { text: string; dates: string[] }>()
|
||||
|
||||
// The public API is typed with local copies of the Vanilla Calendar Pro types.
|
||||
// `vanilla-calendar-pro` is an optional peer dependency, so the published
|
||||
// `dist/types` must not import it: a project that never loads the datepicker
|
||||
@@ -178,6 +180,7 @@ class Datepicker extends BaseComponent {
|
||||
_displayElement: HTMLElement | false | null = null
|
||||
_themeObserver: MutationObserver | null = null
|
||||
_onFocusIn: ((event: Event) => void) | null = null
|
||||
_onResize = (): void => this._alignToPositionElement()
|
||||
// The plugin builds a popup lazily, so its context has no selection until
|
||||
// the first show; this mirror answers `getSelectedDates()` before that.
|
||||
_selectedDates: string[] = []
|
||||
@@ -280,6 +283,7 @@ class Datepicker extends BaseComponent {
|
||||
dispose(): void {
|
||||
window.clearTimeout(this._hideTimeout)
|
||||
this._themeObserver?.disconnect()
|
||||
window.removeEventListener('resize', this._onResize)
|
||||
|
||||
if (this._onFocusIn) {
|
||||
EventHandler.off(document, EVENT_FOCUSIN, this._onFocusIn)
|
||||
@@ -363,12 +367,11 @@ class Datepicker extends BaseComponent {
|
||||
}
|
||||
|
||||
_updateDisplayWithSelectedDates(): void {
|
||||
const { selectedDates } = this._config
|
||||
if (!selectedDates || selectedDates.length === 0) {
|
||||
if (this._selectedDates.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
this._writeSelection(selectedDates)
|
||||
this._writeSelection(this._selectedDates)
|
||||
}
|
||||
|
||||
_writeSelection(selectedDates: string[]): void {
|
||||
@@ -376,6 +379,7 @@ class Datepicker extends BaseComponent {
|
||||
|
||||
if (this._isInput) {
|
||||
this._element.value = formattedDate
|
||||
writtenSelections.set(this._element, { text: formattedDate, dates: [...selectedDates] })
|
||||
}
|
||||
|
||||
if (this._boundInput) {
|
||||
@@ -537,6 +541,8 @@ class Datepicker extends BaseComponent {
|
||||
this._isShown = true
|
||||
this._syncThemeAttribute(calendar.context.mainElement)
|
||||
this._alignToPositionElement()
|
||||
window.removeEventListener('resize', this._onResize)
|
||||
window.addEventListener('resize', this._onResize)
|
||||
|
||||
if (!wasShown) {
|
||||
EventHandler.trigger(this._element, EVENT_SHOWN)
|
||||
@@ -559,6 +565,7 @@ class Datepicker extends BaseComponent {
|
||||
}
|
||||
|
||||
this._isShown = false
|
||||
window.removeEventListener('resize', this._onResize)
|
||||
EventHandler.trigger(this._element, EVENT_HIDDEN)
|
||||
}
|
||||
|
||||
@@ -701,6 +708,13 @@ class Datepicker extends BaseComponent {
|
||||
return
|
||||
}
|
||||
|
||||
const written = writtenSelections.get(this._element)
|
||||
if (written && written.text.trim() === value) {
|
||||
this._selectedDates = [...written.dates]
|
||||
this._calendar?.set({ selectedDates: this._selectedDates, ...this._monthOf(this._selectedDates[0]) })
|
||||
return
|
||||
}
|
||||
|
||||
// `YYYY-MM-DD` is read as a local date: `new Date('2020-06-20')` would be
|
||||
// UTC midnight and land on the previous day west of Greenwich.
|
||||
const date = DATE_PATTERN.test(value) ? this._parseDate(value) : new Date(value)
|
||||
|
||||
@@ -402,4 +402,86 @@ describe('Datepicker', () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 150))
|
||||
})
|
||||
})
|
||||
|
||||
describe('resize and initial value', () => {
|
||||
const groupMarkup = '<div class="input-group" style="display: flex; margin-left: 40px"><span style="width: 120px"></span><input type="text"></div>'
|
||||
|
||||
it('keeps the popup aligned with the wrapper after a resize', async () => {
|
||||
fixtureEl.innerHTML = groupMarkup
|
||||
const wrapper = fixtureEl.querySelector<HTMLElement>('.input-group')!
|
||||
const instance = new Datepicker(input())
|
||||
await instance.show()
|
||||
|
||||
window.dispatchEvent(new Event('resize'))
|
||||
|
||||
const expected = wrapper.getBoundingClientRect().left + window.scrollX
|
||||
expect(instance.calendar!.context.mainElement.style.left).toBe(`${expected}px`)
|
||||
})
|
||||
|
||||
it('stops aligning on resize once hidden or disposed', async () => {
|
||||
fixtureEl.innerHTML = groupMarkup
|
||||
const instance = new Datepicker(input())
|
||||
const spy = vi.spyOn(instance, '_alignToPositionElement')
|
||||
await instance.show()
|
||||
await instance.hide()
|
||||
spy.mockClear()
|
||||
|
||||
window.dispatchEvent(new Event('resize'))
|
||||
expect(spy).not.toHaveBeenCalled()
|
||||
|
||||
await instance.show()
|
||||
instance.dispose()
|
||||
spy.mockClear()
|
||||
window.dispatchEvent(new Event('resize'))
|
||||
expect(spy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('formats an initial value like a picked date', () => {
|
||||
fixtureEl.innerHTML = '<input type="text" value="2024-06-20">'
|
||||
const dateFormat = { year: 'numeric', month: 'long', day: 'numeric' } as const
|
||||
|
||||
new Datepicker(input(), { dateFormat, locale: 'en-US' })
|
||||
|
||||
expect(input().value).toBe('June 20, 2024')
|
||||
})
|
||||
|
||||
it('writes the value, not selectedDates, when both are set', () => {
|
||||
fixtureEl.innerHTML = '<input type="text" value="2024-06-20">'
|
||||
|
||||
const instance = new Datepicker(input(), { selectedDates: ['2025-01-01'], locale: 'en-US' })
|
||||
|
||||
expect(instance.getSelectedDates()).toEqual(['2024-06-20'])
|
||||
expect(input().value).toBe(new Date(2024, 5, 20).toLocaleDateString('en-US'))
|
||||
})
|
||||
|
||||
it('keeps a formatted value selected when the input is initialised again', () => {
|
||||
fixtureEl.innerHTML = '<input type="text" value="2024-06-20">'
|
||||
new Datepicker(input(), { locale: 'pl-PL' }).dispose()
|
||||
|
||||
const instance = new Datepicker(input(), { locale: 'pl-PL' })
|
||||
|
||||
expect(input().value).toBe('20.06.2024')
|
||||
expect(instance.getSelectedDates()).toEqual(['2024-06-20'])
|
||||
})
|
||||
|
||||
it('keeps a picked range selected when the input is initialised again', () => {
|
||||
fixtureEl.innerHTML = '<input type="text">'
|
||||
const config = { locale: 'pl-PL', selectionMode: 'multiple-ranged' } as const
|
||||
const first = new Datepicker(input(), config)
|
||||
first.setSelectedDates(['2024-06-10', '2024-06-18'])
|
||||
first.dispose()
|
||||
|
||||
const instance = new Datepicker(input(), config)
|
||||
|
||||
expect(instance.getSelectedDates()).toEqual(['2024-06-10', '2024-06-18'])
|
||||
})
|
||||
|
||||
it('leaves an unparsable value as it is', () => {
|
||||
fixtureEl.innerHTML = '<input type="text" value="not a date">'
|
||||
|
||||
new Datepicker(input())
|
||||
|
||||
expect(input().value).toBe('not a date')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user