Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/date-input-locale-aware-numerals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@zag-js/date-utils": patch
"@zag-js/date-picker": patch
"@zag-js/date-input": patch
---

Allow typing dates using the locale's native numerals (e.g. Arabic-Indic `٠-٩`, Devanagari `०-९`)
in addition to ASCII digits. Latin-locale behavior is unchanged.
5 changes: 5 additions & 0 deletions .changeset/date-picker-normalize-range-on-blur.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@zag-js/date-picker": patch
---

Adds date reordering on blur for range selection to match other selection paths.
10 changes: 5 additions & 5 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7

- name: Install
uses: ./.github/composite-actions/install
Expand All @@ -40,7 +40,7 @@ jobs:
shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7

- name: Install
uses: ./.github/composite-actions/install
Expand All @@ -66,7 +66,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7

- name: Install
uses: ./.github/composite-actions/install
Expand All @@ -81,7 +81,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7

- name: Install
uses: ./.github/composite-actions/install
Expand All @@ -94,7 +94,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7

- name: Install
uses: ./.github/composite-actions/install
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
token: ${{ secrets.SAGE_PAT }}
Expand Down
32 changes: 31 additions & 1 deletion e2e/date-picker.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { test } from "@playwright/test"
import { expect, test } from "@playwright/test"
import { DatePickerModel } from "./models/datepicker.model"

let I: DatePickerModel
Expand Down Expand Up @@ -292,4 +292,34 @@ test.describe("datepicker [range]", () => {
// Should not crash and the new value should be set
await I.seeInputHasValue("06/15/2025", 1)
})

test("normalizes reversed range dates entered via input on blur", async () => {
// Regression test for issue #3186
await I.focusInput(0)
await I.type("06/20/2024", 0)
await I.clickOutsideToBlur()

await I.focusInput(1)
await I.type("06/10/2024", 1)
await I.clickOutsideToBlur()

// dates entered in reverse order should be swapped, matching the Enter/click path
await I.seeInputHasValue("06/10/2024", 0)
await I.seeInputHasValue("06/20/2024", 1)
})
})

test.describe("datepicker [locale numerals]", () => {
test.beforeEach(async ({ page }) => {
I = new DatePickerModel(page)
// custom-calendar example uses locale "fa-IR" (Persian numerals)
await I.goto("/date-picker/custom-calendar")
})

test("accepts the locale's native numerals typed into the input", async () => {
// Regression test for issue #3165 (non-ASCII numbering systems were rejected)
await I.focusInput()
await I.type("۱۴۰۳") // Persian digits
await expect(I.getInput()).toHaveValue(/[۰-۹]/)
})
})
2 changes: 1 addition & 1 deletion examples/preact-ts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,6 @@
"eslint": "10.4.0",
"eslint-config-preact": "2.0.0",
"typescript": "6.0.3",
"vite": "8.0.13"
"vite": "8.0.16"
}
}
2 changes: 1 addition & 1 deletion examples/svelte-ts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@
"svelte-check": "4.4.6",
"tslib": "2.8.1",
"typescript": "6.0.3",
"vite": "8.0.13",
"vite": "8.0.16",
"vite-tsconfig-paths": "6.1.1"
}
}
2 changes: 1 addition & 1 deletion examples/vanilla-ts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
},
"devDependencies": {
"typescript": "^6.0.2",
"vite": "^8.0.3"
"vite": "8.0.16"
},
"dependencies": {
"@internationalized/date": "3.12.0",
Expand Down
30 changes: 15 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,41 +75,41 @@
"@axe-core/playwright": "4.11.3",
"@changesets/changelog-github": "0.7.0",
"@changesets/cli": "2.31.0",
"@commitlint/cli": "21.0.1",
"@commitlint/config-conventional": "21.0.1",
"@commitlint/cli": "21.0.2",
"@commitlint/config-conventional": "21.0.2",
"@eslint/js": "^10.0.1",
"@internationalized/date": "3.12.1",
"@playwright/test": "1.60.0",
"@swc/core": "1.15.40",
"@internationalized/date": "3.12.2",
"@playwright/test": "1.61.0",
"@swc/core": "1.15.41",
"@types/jsdom": "^28.0.1",
"@types/node": "25.9.1",
"@types/node": "25.9.4",
"@types/signale": "1.4.7",
"commitlint": "21.0.1",
"commitlint": "21.0.2",
"cross-env": "^10.1.0",
"dotenv": "17.4.2",
"esbuild-plugin-file-path-extensions": "^2.1.4",
"eslint": "10.4.0",
"eslint": "10.5.0",
"fast-glob": "3.3.3",
"find-packages": "10.0.4",
"globals": "^17.4.0",
"husky": "9.1.7",
"lint-staged": "17.0.5",
"lint-staged": "17.0.8",
"node-fetch": "3.3.2",
"plop": "4.0.5",
"prettier": "3.8.3",
"prettier-plugin-svelte": "3.5.2",
"prettier": "3.8.4",
"prettier-plugin-svelte": "4.1.1",
"scule": "1.3.0",
"signale": "1.4.0",
"ts-morph": "28.0.0",
"tsup": "8.5.1",
"tsx": "4.22.3",
"tsx": "4.22.4",
"typescript": "6.0.3",
"typescript-eslint": "^8.58.0",
"vite": "8.0.14",
"vitest": "4.1.7"
"vite": "8.0.16",
"vitest": "4.1.9"
},
"engines": {
"node": ">=18.0.0"
},
"packageManager": "pnpm@11.3.0"
"packageManager": "pnpm@11.8.0"
}
2 changes: 1 addition & 1 deletion packages/anatomy-icons/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"devDependencies": {
"@types/react": "^19",
"clean-package": "2.2.0",
"react": "19.2.6"
"react": "19.2.7"
},
"dependencies": {
"color2k": "2.0.3",
Expand Down
4 changes: 2 additions & 2 deletions packages/frameworks/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/jsdom": "^28.0.1",
"react": "19.2.6",
"react-dom": "19.2.6",
"react": "19.2.7",
"react-dom": "19.2.7",
"clean-package": "2.2.0",
"@testing-library/react": "^16.3.2",
"@testing-library/jest-dom": "^6.9.1",
Expand Down
6 changes: 3 additions & 3 deletions packages/frameworks/svelte/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@
"@zag-js/utils": "workspace:*"
},
"devDependencies": {
"@sveltejs/package": "2.5.7",
"@sveltejs/package": "2.5.8",
"@sveltejs/vite-plugin-svelte": "7.1.2",
"clean-package": "2.2.0",
"clsx": "^2.1.1",
"svelte": "5.55.9",
"vitest": "4.1.7"
"svelte": "5.56.3",
"vitest": "4.1.9"
},
"peerDependencies": {
"svelte": ">=5"
Expand Down
2 changes: 1 addition & 1 deletion packages/frameworks/vue/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
},
"devDependencies": {
"clean-package": "2.2.0",
"vue": "3.5.34"
"vue": "3.5.38"
},
"peerDependencies": {
"vue": ">=3.0.0"
Expand Down
2 changes: 1 addition & 1 deletion packages/machines/date-input/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"@internationalized/date": ">=3.0.0"
},
"devDependencies": {
"@internationalized/date": "3.12.1",
"@internationalized/date": "3.12.2",
"clean-package": "2.2.0"
},
"clean-package": "../../../clean-package.config.json"
Expand Down
2 changes: 1 addition & 1 deletion packages/machines/date-input/src/date-input.connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ export function connect<T extends PropTypes>(service: DateInputService, normaliz
// For dayPeriod and era segments, accept letter input (a/p for AM/PM, etc.)
const isTextSegment = segment.type === "dayPeriod" || segment.type === "era"

if (data && (isTextSegment || isValidCharacter(data, separator))) {
if (data && (isTextSegment || isValidCharacter(data, separator, locale))) {
event.preventDefault()
send({ type: "SEGMENT.INPUT", segment, input: data })
} else {
Expand Down
2 changes: 1 addition & 1 deletion packages/machines/date-picker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"@internationalized/date": ">=3.0.0"
},
"devDependencies": {
"@internationalized/date": "3.12.1",
"@internationalized/date": "3.12.2",
"clean-package": "2.2.0"
},
"clean-package": "./clean-package.config.json"
Expand Down
4 changes: 2 additions & 2 deletions packages/machines/date-picker/src/date-picker.connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -933,7 +933,7 @@ export function connect<T extends PropTypes>(
defaultValue: computed("valueAsString")[index],
onBeforeInput(event) {
const { data } = getNativeEvent(event)
if (!isValidCharacter(data, separator)) {
if (!isValidCharacter(data, separator, locale)) {
event.preventDefault()
}
},
Expand Down Expand Up @@ -971,7 +971,7 @@ export function connect<T extends PropTypes>(
},
onInput(event) {
const value = event.currentTarget.value
send({ type: "INPUT.CHANGE", value: ensureValidCharacters(value, separator), index })
send({ type: "INPUT.CHANGE", value: ensureValidCharacters(value, separator, locale), index })
},
})
},
Expand Down
7 changes: 4 additions & 3 deletions packages/machines/date-picker/src/date-picker.machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1203,13 +1203,14 @@ export const machine = createMachine<DatePickerSchema>({
date = constrainValue(date, prop("min"), prop("max"))

const values = Array.from(context.get("value"))
values[event.index] = date
values[event.index] = preserveTime(values[event.index], date)
const adjustedValues = adjustStartAndEndDate(values)

context.set("value", values)
context.set("value", adjustedValues)

// always sync the input value, even if the selecteddate is not changed
// e.g. selected value is 02/28/2024, and the input value changed to 02/28
const valueAsString = getValueAsString(values, prop)
const valueAsString = getValueAsString(adjustedValues, prop)
context.set("inputValue", valueAsString[event.index])
},

Expand Down
2 changes: 1 addition & 1 deletion packages/machines/number-input/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"url": "https://github.com/chakra-ui/zag/issues"
},
"dependencies": {
"@internationalized/number": "3.6.6",
"@internationalized/number": "3.6.7",
"@zag-js/anatomy": "workspace:*",
"@zag-js/core": "workspace:*",
"@zag-js/dom-query": "workspace:*",
Expand Down
2 changes: 1 addition & 1 deletion packages/utilities/date-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"@internationalized/date": ">=3.0.0"
},
"devDependencies": {
"@internationalized/date": "3.12.1",
"@internationalized/date": "3.12.2",
"clean-package": "2.2.0"
}
}
25 changes: 21 additions & 4 deletions packages/utilities/date-utils/src/locale.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,29 @@
export const isValidCharacter = (char: string | null, separator: string) => {
const digitsCache = new Map<string, string>()

// ASCII digits plus the locale's native numerals (e.g. Arabic-Indic ٠-٩, Devanagari ०-९)
function getLocaleDigits(locale: string) {
let digits = digitsCache.get(locale)
if (digits != null) return digits
const localeDigits = new Intl.NumberFormat(locale, { useGrouping: false }).format(1234567890)
digits = "0123456789" + localeDigits
digitsCache.set(locale, digits)
return digits
}

const isDigit = (char: string, locale: string | undefined) => {
return locale ? getLocaleDigits(locale).includes(char) : /\d/.test(char)
}

export const isValidCharacter = (char: string | null, separator: string, locale?: string) => {
if (!char) return true
return /\d/.test(char) || separator.includes(char) || char.length !== 1
if (char.length !== 1) return true // paste / IME passthrough
return isDigit(char, locale) || separator.includes(char)
}

export const ensureValidCharacters = (value: string, separator: string) => {
export const ensureValidCharacters = (value: string, separator: string, locale?: string) => {
return value
.split("")
.filter((char) => isValidCharacter(char, separator))
.filter((char) => isValidCharacter(char, separator, locale))
.join("")
}

Expand Down
Loading
Loading