Skip to content

Commit 4626e4a

Browse files
committed
better timepicker
1 parent ad6e7a4 commit 4626e4a

6 files changed

Lines changed: 876 additions & 23 deletions

File tree

.github/dependabot.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
version: 2
2+
updates:
3+
- package-ecosystem: npm
4+
directory: "/"
5+
schedule:
6+
interval: weekly
7+
open-pull-requests-limit: 5
8+
ignore:
9+
- dependency-name: form-data
10+
- dependency-name: postcss
11+
- dependency-name: uuid
12+
- dependency-name: js-yaml

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@ All notable changes to this project will be documented in this file.
77
### Changed
88
- Default interaction changed: focusing the input (for example via Tab) no longer opens the calendar automatically.
99
- Added option `focusOpens` (default `false`) to opt in to the previous focus-to-open behavior when needed.
10+
- Added `.github/dependabot.yml` with a temporary ignore list for problematic transitive advisories (`form-data`, `postcss`, `uuid`, `js-yaml`) to prevent repeated failing update runs until the dependency chain is modernized.
11+
- Time selection now always renders as an accessible wheel-style popover (including desktop) with clear selected states.
12+
- When `enableTime` is active, the custom picker is used on mobile too (native mobile input is bypassed) so the same time popover interaction is available everywhere.
1013

1114
### Fixed
1215
- Mobile date and datetime selection no longer forces input focus before opening the native picker, preventing the on-screen keyboard from appearing unnecessarily.
1316
- Time-only mobile inputs (`enableTime=true` and `noCalendar=true`) keep focus behavior so direct time entry still works.
17+
- In datetime mode, clicking/tapping a calendar day no longer auto-focuses the time field; this avoids repeatedly re-activating time input while selecting another date.
1418

1519
## [5.1.3] - 2026-05-22
1620

__tests__/src/index.spec.ts

Lines changed: 255 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1035,8 +1035,10 @@ describe("flatpickr", () => {
10351035
expect(document.activeElement).toBe(fp.nextMonthNav);
10361036
});
10371037

1038-
it("tab from day moves focus forward to close button", () => {
1039-
createInstance();
1038+
it("tab from day moves focus forward to time trigger", () => {
1039+
createInstance({
1040+
enableTime: true,
1041+
});
10401042
fp.open();
10411043

10421044
const firstDay = fp.days.childNodes[0] as HTMLElement;
@@ -1046,7 +1048,10 @@ describe("flatpickr", () => {
10461048
keyCode: 9,
10471049
});
10481050

1049-
expect(document.activeElement).toBe(fp.closeButton);
1051+
const trigger = fp.timeContainer?.querySelector(
1052+
".flatpickr-time-wheel-trigger"
1053+
);
1054+
expect(document.activeElement).toBe(trigger);
10501055
});
10511056

10521057
it("pressing M focuses month control as keyboard fallback", () => {
@@ -1357,11 +1362,11 @@ describe("flatpickr", () => {
13571362
it("with value", () => {
13581363
mockAgent = "Android";
13591364
createInstance({
1360-
enableTime: true,
1365+
defaultDate: "2016-10-20",
13611366
});
13621367

13631368
const mobileInput = fp.mobileInput as HTMLInputElement;
1364-
mobileInput.value = "2016-10-20T02:30";
1369+
mobileInput.value = "2016-10-20";
13651370
simulate("change", mobileInput);
13661371

13671372
expect(fp.selectedDates.length).toBe(1);
@@ -1371,8 +1376,18 @@ describe("flatpickr", () => {
13711376
expect(fp.latestSelectedDateObj.getFullYear()).toBe(2016);
13721377
expect(fp.latestSelectedDateObj.getMonth()).toBe(9);
13731378
expect(fp.latestSelectedDateObj.getDate()).toBe(20);
1374-
expect(fp.latestSelectedDateObj.getHours()).toBe(2);
1375-
expect(fp.latestSelectedDateObj.getMinutes()).toBe(30);
1379+
expect(fp.latestSelectedDateObj.getHours()).toBe(0);
1380+
expect(fp.latestSelectedDateObj.getMinutes()).toBe(0);
1381+
});
1382+
1383+
it("enableTime uses custom picker on mobile", () => {
1384+
mockAgent = "Android";
1385+
createInstance({
1386+
enableTime: true,
1387+
});
1388+
1389+
expect(fp.isMobile).toBe(false);
1390+
expect(fp.mobileInput).toBeUndefined();
13761391
});
13771392

13781393
it("copy className and adds own", () => {
@@ -1452,21 +1467,15 @@ describe("flatpickr", () => {
14521467
expect(clickSpy).toHaveBeenCalled();
14531468
});
14541469

1455-
it("open() keeps focus for mobile time-only input", () => {
1470+
it("open() keeps custom picker for mobile time-only input", () => {
14561471
mockAgent = "Android";
14571472
createInstance({
14581473
enableTime: true,
14591474
noCalendar: true,
14601475
});
14611476

1462-
const mobileInput = fp.mobileInput as HTMLInputElement;
1463-
const focusSpy = jest.spyOn(mobileInput, "focus");
1464-
const clickSpy = jest.spyOn(mobileInput, "click");
1465-
1466-
fp.open();
1467-
1468-
expect(focusSpy).toHaveBeenCalled();
1469-
expect(clickSpy).toHaveBeenCalled();
1477+
expect(fp.isMobile).toBe(false);
1478+
expect(fp.mobileInput).toBeUndefined();
14701479
});
14711480
});
14721481
});
@@ -1547,6 +1556,219 @@ describe("flatpickr", () => {
15471556
expect(fp.hourElement.value).toEqual("09");
15481557
});
15491558

1559+
it("renders accessible time wheel popover", () => {
1560+
createInstance({
1561+
enableTime: true,
1562+
});
1563+
1564+
const trigger = fp.timeContainer?.querySelector(
1565+
".flatpickr-time-wheel-trigger"
1566+
) as HTMLElement | null;
1567+
const popover = fp.timeContainer?.querySelector(
1568+
".flatpickr-time-wheel-popover"
1569+
) as HTMLElement | null;
1570+
1571+
expect(trigger).toBeTruthy();
1572+
expect(popover).toBeTruthy();
1573+
expect(popover?.getAttribute("role")).toBe("dialog");
1574+
expect(popover?.hasAttribute("hidden")).toBe(true);
1575+
1576+
if (!trigger || !popover) return;
1577+
1578+
simulate("click", trigger, { which: 1 }, MouseEvent);
1579+
expect(popover.hasAttribute("hidden")).toBe(false);
1580+
1581+
const done = popover.querySelector(
1582+
".flatpickr-time-wheel-done"
1583+
) as HTMLElement | null;
1584+
expect(done).toBeTruthy();
1585+
if (!done) return;
1586+
1587+
simulate("click", done, { which: 1 }, MouseEvent);
1588+
expect(popover.hasAttribute("hidden")).toBe(true);
1589+
});
1590+
1591+
it("time wheel popover respects minuteIncrement", () => {
1592+
createInstance({
1593+
enableTime: true,
1594+
minuteIncrement: 15,
1595+
});
1596+
1597+
const minuteOptions = Array.from(
1598+
fp.timeContainer?.querySelectorAll(
1599+
".flatpickr-time-wheel-minutes .flatpickr-time-wheel-option"
1600+
) || []
1601+
).map((el) => (el as HTMLElement).textContent?.trim());
1602+
1603+
expect(minuteOptions).toEqual(["00", "15", "30", "45"]);
1604+
});
1605+
1606+
it("time wheel popover updates minute value on click", () => {
1607+
createInstance({
1608+
enableTime: true,
1609+
defaultDate: "2017-1-1 10:00",
1610+
});
1611+
1612+
const targetOption = fp.timeContainer?.querySelector(
1613+
".flatpickr-time-wheel-minutes .flatpickr-time-wheel-option[data-value='30']"
1614+
) as HTMLElement | null;
1615+
1616+
expect(targetOption).toBeTruthy();
1617+
if (!targetOption || !fp.minuteElement) return;
1618+
1619+
simulate("click", targetOption, { which: 1 }, MouseEvent);
1620+
expect(fp.minuteElement.value).toBe("30");
1621+
});
1622+
1623+
it("time wheel popover supports arrow navigation and escape", () => {
1624+
createInstance({
1625+
enableTime: true,
1626+
defaultDate: "2017-1-1 10:00",
1627+
});
1628+
1629+
const trigger = fp.timeContainer?.querySelector(
1630+
".flatpickr-time-wheel-trigger"
1631+
) as HTMLElement | null;
1632+
const popover = fp.timeContainer?.querySelector(
1633+
".flatpickr-time-wheel-popover"
1634+
) as HTMLElement | null;
1635+
1636+
expect(trigger).toBeTruthy();
1637+
expect(popover).toBeTruthy();
1638+
if (!trigger || !popover || !fp.hourElement || !fp.minuteElement) return;
1639+
1640+
simulate("click", trigger, { which: 1 }, MouseEvent);
1641+
1642+
const selectedHour = popover.querySelector(
1643+
".flatpickr-time-wheel-hours .flatpickr-time-wheel-option.is-selected"
1644+
) as HTMLElement | null;
1645+
expect(selectedHour).toBeTruthy();
1646+
if (!selectedHour) return;
1647+
1648+
simulate("keydown", selectedHour, { key: "ArrowDown" }, KeyboardEvent);
1649+
expect(fp.hourElement.value).toBe("11");
1650+
1651+
simulate("keydown", selectedHour, { key: "Tab" }, KeyboardEvent);
1652+
const selectedMinute = popover.querySelector(
1653+
".flatpickr-time-wheel-minutes .flatpickr-time-wheel-option.is-selected"
1654+
) as HTMLElement | null;
1655+
expect(document.activeElement).toBe(selectedMinute);
1656+
1657+
if (!selectedMinute) return;
1658+
simulate("keydown", selectedMinute, { key: "Escape" }, KeyboardEvent);
1659+
expect(popover.hasAttribute("hidden")).toBe(true);
1660+
expect(document.activeElement).toBe(trigger);
1661+
});
1662+
1663+
it("enter on time trigger opens popover instead of closing calendar", () => {
1664+
createInstance({
1665+
enableTime: true,
1666+
});
1667+
1668+
fp.open();
1669+
1670+
const trigger = fp.timeContainer?.querySelector(
1671+
".flatpickr-time-wheel-trigger"
1672+
) as HTMLElement | null;
1673+
const popover = fp.timeContainer?.querySelector(
1674+
".flatpickr-time-wheel-popover"
1675+
) as HTMLElement | null;
1676+
1677+
expect(trigger).toBeTruthy();
1678+
expect(popover).toBeTruthy();
1679+
if (!trigger || !popover) return;
1680+
1681+
trigger.focus();
1682+
simulate("keydown", trigger, { keyCode: 13 }, KeyboardEvent);
1683+
1684+
expect(fp.isOpen).toBe(true);
1685+
expect(popover.hasAttribute("hidden")).toBe(false);
1686+
});
1687+
1688+
it("enter on done closes only the popover", () => {
1689+
createInstance({
1690+
enableTime: true,
1691+
});
1692+
1693+
fp.open();
1694+
1695+
const trigger = fp.timeContainer?.querySelector(
1696+
".flatpickr-time-wheel-trigger"
1697+
) as HTMLElement | null;
1698+
const popover = fp.timeContainer?.querySelector(
1699+
".flatpickr-time-wheel-popover"
1700+
) as HTMLElement | null;
1701+
const done = fp.timeContainer?.querySelector(
1702+
".flatpickr-time-wheel-done"
1703+
) as HTMLElement | null;
1704+
1705+
expect(trigger).toBeTruthy();
1706+
expect(popover).toBeTruthy();
1707+
expect(done).toBeTruthy();
1708+
if (!trigger || !popover || !done) return;
1709+
1710+
simulate("click", trigger, { which: 1 }, MouseEvent);
1711+
done.focus();
1712+
simulate("keydown", done, { keyCode: 13 }, KeyboardEvent);
1713+
1714+
expect(fp.isOpen).toBe(true);
1715+
expect(popover.hasAttribute("hidden")).toBe(true);
1716+
expect(document.activeElement).toBe(trigger);
1717+
});
1718+
1719+
it("time only renders the wheel popover directly", () => {
1720+
createInstance({
1721+
enableTime: true,
1722+
noCalendar: true,
1723+
});
1724+
1725+
const trigger = fp.timeContainer?.querySelector(
1726+
".flatpickr-time-wheel-trigger"
1727+
) as HTMLElement | null;
1728+
const popover = fp.timeContainer?.querySelector(
1729+
".flatpickr-time-wheel-popover"
1730+
) as HTMLElement | null;
1731+
1732+
expect(trigger).toBeNull();
1733+
expect(popover).toBeTruthy();
1734+
expect(popover?.hasAttribute("hidden")).toBe(false);
1735+
});
1736+
1737+
it("time only reopens the wheel popover after close", () => {
1738+
createInstance({
1739+
enableTime: true,
1740+
noCalendar: true,
1741+
});
1742+
1743+
const popover = fp.timeContainer?.querySelector(
1744+
".flatpickr-time-wheel-popover"
1745+
) as HTMLElement | null;
1746+
expect(popover).toBeTruthy();
1747+
if (!popover) return;
1748+
1749+
fp.close();
1750+
expect(popover.hasAttribute("hidden")).toBe(true);
1751+
1752+
fp.open();
1753+
expect(popover.hasAttribute("hidden")).toBe(false);
1754+
});
1755+
1756+
it("time only tabs from input into the wheel", () => {
1757+
createInstance({
1758+
enableTime: true,
1759+
noCalendar: true,
1760+
});
1761+
1762+
fp._input.focus();
1763+
simulate("keydown", fp._input, { keyCode: 9 }, KeyboardEvent);
1764+
1765+
const selectedHour = fp.timeContainer?.querySelector(
1766+
".flatpickr-time-wheel-hours .flatpickr-time-wheel-option.is-selected"
1767+
) as HTMLElement | null;
1768+
1769+
expect(document.activeElement).toBe(selectedHour);
1770+
});
1771+
15501772
it("time input respects minDate", () => {
15511773
createInstance({
15521774
enableTime: true,
@@ -2117,6 +2339,23 @@ describe("flatpickr", () => {
21172339
expect(fp.currentMonth).toEqual(3);
21182340
expect(monthControl.value).toEqual("3");
21192341
});
2342+
2343+
it("datetime mode does not auto-focus time input after mouse date click", () => {
2344+
const fp = createInstance({
2345+
enableTime: true,
2346+
}) as Instance;
2347+
2348+
fp.open();
2349+
2350+
const hourFocusSpy = jest.spyOn(
2351+
fp.hourElement as HTMLInputElement,
2352+
"focus"
2353+
);
2354+
2355+
simulate("click", fp.days.childNodes[15], { which: 1 }, MouseEvent);
2356+
2357+
expect(hourFocusSpy).not.toHaveBeenCalled();
2358+
});
21202359
});
21212360

21222361
describe("Localization", () => {

0 commit comments

Comments
 (0)