diff --git a/.changeset/fix-datepicker-stuck-after-error.md b/.changeset/fix-datepicker-stuck-after-error.md new file mode 100644 index 000000000..13c8cc218 --- /dev/null +++ b/.changeset/fix-datepicker-stuck-after-error.md @@ -0,0 +1,5 @@ +--- +"@tabler/core": patch +--- + +Fixed `Datepicker` getting stuck closed after the calendar plugin throws in `show()` or `hide()`. diff --git a/core/js/src/datepicker.ts b/core/js/src/datepicker.ts index c10a17f10..5b76a1680 100644 --- a/core/js/src/datepicker.ts +++ b/core/js/src/datepicker.ts @@ -245,18 +245,22 @@ class Datepicker extends BaseComponent { this._isShowing = true this._skipPluginShow = false - this._calendar.show() - // The first show builds the popup and opens it a tick later - if (!this._isShown) { - await new Promise((resolve) => { - this._resolveShown = resolve - afterPluginTimers(resolve) - }) + // Reset even if the plugin throws, or every later `show()` returns early + try { + this._calendar.show() + + // The first show builds the popup and opens it a tick later + if (!this._isShown) { + await new Promise((resolve) => { + this._resolveShown = resolve + afterPluginTimers(resolve) + }) + } + } finally { + this._resolveShown = null + this._isShowing = false } - - this._resolveShown = null - this._isShowing = false } // `async` like `show()`, so a plugin error reaches the caller as a rejection @@ -283,8 +287,11 @@ class Datepicker extends BaseComponent { }) this._isHiding = true - this._calendar.hide() - this._isHiding = false + try { + this._calendar.hide() + } finally { + this._isHiding = false + } } dispose(): void { diff --git a/core/js/tests/unit/datepicker.spec.ts b/core/js/tests/unit/datepicker.spec.ts index 4cb64ccb1..3191b4f81 100644 --- a/core/js/tests/unit/datepicker.spec.ts +++ b/core/js/tests/unit/datepicker.spec.ts @@ -235,6 +235,38 @@ describe('Datepicker', () => { expect(instance._isShown).toBe(false) }) + + it('should show again after the plugin throws while showing', async () => { + fixtureEl.innerHTML = '' + + const instance = new Datepicker(input()) + const pluginShow = vi.spyOn(instance.calendar!, 'show').mockImplementationOnce(() => { + throw new Error('plugin failed') + }) + + await expect(instance.show()).rejects.toThrow('plugin failed') + expect(instance._isShowing).toBe(false) + + await instance.show() + expect(pluginShow).toHaveBeenCalledTimes(2) + expect(instance._isShown).toBe(true) + }) + + it('should hide again after the plugin throws while hiding', async () => { + fixtureEl.innerHTML = '' + + const instance = new Datepicker(input()) + await instance.show() + vi.spyOn(instance.calendar!, 'hide').mockImplementationOnce(() => { + throw new Error('plugin failed') + }) + + await expect(instance.hide()).rejects.toThrow('plugin failed') + expect(instance._isHiding).toBe(false) + + await instance.hide() + expect(instance._isShown).toBe(false) + }) }) describe('setSelectedDates', () => { diff --git a/core/package.json b/core/package.json index 47ecf575f..7ab155ee3 100644 --- a/core/package.json +++ b/core/package.json @@ -212,7 +212,7 @@ }, { "path": "./dist/js/tabler.js", - "maxSize": "58 kB" + "maxSize": "59 kB" }, { "path": "./dist/js/tabler.min.js", @@ -220,7 +220,7 @@ }, { "path": "./dist/js/tabler.esm.js", - "maxSize": "58 kB" + "maxSize": "59 kB" }, { "path": "./dist/js/tabler.esm.min.js",