Skip to content

Commit dc0f4bb

Browse files
interacseanclaude
andcommitted
feat(data-table): locale-format the date value in filter chips
Date filter chips now render the value as a locale-appropriate medium date (e.g. "15 Jun 2026" / "Jun 15, 2026") via @internationalized/date's DateFormatter with the resolved AppShell locale, instead of the raw ISO "2026-06-15". Formatted in UTC so the calendar date never shifts across zones. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 86213e1 commit dc0f4bb

2 files changed

Lines changed: 48 additions & 13 deletions

File tree

packages/core/src/components/data-table/toolbar.test.tsx

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,7 @@ describe("DateFilterEditor", () => {
502502
wrapper,
503503
});
504504

505-
await user.click(screen.getByRole("button", { name: /Created At exact date 2025-01-01/ }));
505+
await user.click(screen.getByRole("button", { name: /Created At exact date/ }));
506506

507507
expect(await screen.findByRole("button", { name: "Apply" })).toBeDefined();
508508
});
@@ -514,7 +514,7 @@ describe("DateFilterEditor", () => {
514514
});
515515
render(<TestFilters control={control} columns={[dateColumn]} />, { wrapper });
516516

517-
await user.click(screen.getByRole("button", { name: /Created At exact date 2025-01-01/ }));
517+
await user.click(screen.getByRole("button", { name: /Created At exact date/ }));
518518

519519
// The editor uses the app-shell DatePicker: a labelled group of spinbuttons.
520520
expect(
@@ -529,7 +529,7 @@ describe("DateFilterEditor", () => {
529529
});
530530
render(<TestFilters control={control} columns={[dateColumn]} />, { wrapper });
531531

532-
await user.click(screen.getByRole("button", { name: /Created At exact date 2025-01-01/ }));
532+
await user.click(screen.getByRole("button", { name: /Created At exact date/ }));
533533
await screen.findByRole("button", { name: "Apply" });
534534

535535
await typeDateInto(user, 0, "2026-06-15");
@@ -545,7 +545,7 @@ describe("DateFilterEditor", () => {
545545
});
546546
render(<TestFilters control={control} columns={[dateColumn]} />, { wrapper });
547547

548-
await user.click(screen.getByRole("button", { name: /Created At exact date 2025-01-01/ }));
548+
await user.click(screen.getByRole("button", { name: /Created At exact date/ }));
549549
await screen.findByRole("button", { name: "Apply" });
550550

551551
await clearDateIn(user, 0);
@@ -571,15 +571,17 @@ describe("DateFilterEditor", () => {
571571
/>,
572572
{ wrapper },
573573
);
574-
expect(screen.getByText(/Created At exact date 2025-01-01/)).toBeDefined();
574+
// Operator label is friendly, and the value is locale-formatted (not raw ISO).
575+
expect(screen.getByText(/Created At exact date Jan 1, 2025/)).toBeDefined();
576+
expect(screen.queryByText(/2025-01-01/)).toBeNull();
575577

576578
rerender(
577579
<TestFilters
578580
control={makeControl({ filters: [control.filters[1]] })}
579581
columns={[dateColumn]}
580582
/>,
581583
);
582-
expect(screen.getByText(/Created At after 2025-02-01/)).toBeDefined();
584+
expect(screen.getByText(/Created At after Feb 1, 2025/)).toBeDefined();
583585
expect(screen.queryByText(/greater than/)).toBeNull();
584586

585587
rerender(
@@ -588,7 +590,7 @@ describe("DateFilterEditor", () => {
588590
columns={[dateColumn]}
589591
/>,
590592
);
591-
expect(screen.getByText(/Created At before 2025-03-01/)).toBeDefined();
593+
expect(screen.getByText(/Created At before Mar 1, 2025/)).toBeDefined();
592594
});
593595
});
594596

@@ -897,7 +899,7 @@ describe("TemporalFilterEditor (between)", () => {
897899

898900
await user.click(
899901
screen.getByRole("button", {
900-
name: /Created At between 2025-01-01 - 2025-12-31/,
902+
name: /Created At between/,
901903
}),
902904
);
903905

@@ -920,7 +922,7 @@ describe("TemporalFilterEditor (between)", () => {
920922

921923
await user.click(
922924
screen.getByRole("button", {
923-
name: /Created At between 2025-01-01 - 2025-12-31/,
925+
name: /Created At between/,
924926
}),
925927
);
926928
await screen.findByRole("button", { name: "Apply" });
@@ -950,7 +952,7 @@ describe("TemporalFilterEditor (between)", () => {
950952

951953
await user.click(
952954
screen.getByRole("button", {
953-
name: /Created At between 2025-01-01 - 2025-12-31/,
955+
name: /Created At between/,
954956
}),
955957
);
956958
await screen.findByRole("button", { name: "Apply" });

packages/core/src/components/data-table/toolbar.tsx

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import { Button } from "@/components/button";
88
import { Input } from "@/components/input";
99
import { Select } from "@/components/select-standalone";
1010
import { DatePicker } from "@/components/date-picker-standalone";
11-
import { parseDate } from "@internationalized/date";
11+
import { parseDate, DateFormatter } from "@internationalized/date";
12+
import { useResolvedLocale } from "@/contexts/appshell-context";
1213
import { useDataTableContext } from "./data-table-context";
1314
import { useDataTableT } from "./i18n";
1415
import type { CollectionControl, Filter, FilterConfig, FilterOperator } from "@/types/collection";
@@ -548,6 +549,7 @@ function FilterChip({
548549
control: CollectionControl;
549550
}) {
550551
const t = useDataTableT();
552+
const { locale } = useResolvedLocale();
551553
const config = column.filter;
552554
const label = column.label ?? config.field;
553555

@@ -565,7 +567,7 @@ function FilterChip({
565567
control.removeFilter(config.field);
566568
}, [control, config.field]);
567569

568-
const chipLabel = getChipDisplayLabel(label, filter, config, t);
570+
const chipLabel = getChipDisplayLabel(label, filter, config, t, locale);
569571

570572
return (
571573
<div data-slot="data-table-filter-chip" className="astw:flex astw:items-center astw:gap-0">
@@ -1370,10 +1372,28 @@ function getOperatorLabel(
13701372
}
13711373
}
13721374

1375+
/** Format an ISO "YYYY-MM-DD" value as a locale-appropriate medium date. */
1376+
function formatDateValue(iso: string, locale: string): string {
1377+
if (!/^\d{4}-\d{2}-\d{2}$/.test(iso)) return iso;
1378+
try {
1379+
// Format in UTC against a UTC-anchored instant so the calendar date never
1380+
// shifts across timezones.
1381+
return new DateFormatter(locale, {
1382+
year: "numeric",
1383+
month: "short",
1384+
day: "numeric",
1385+
timeZone: "UTC",
1386+
}).format(parseDate(iso).toDate("UTC"));
1387+
} catch {
1388+
return iso;
1389+
}
1390+
}
1391+
13731392
function formatFilterValue(
13741393
filter: Filter,
13751394
config: FilterConfig,
13761395
t: ReturnType<typeof useDataTableT>,
1396+
locale: string,
13771397
): string {
13781398
if (config.type === "enum" && Array.isArray(filter.value)) {
13791399
const labels = filter.value
@@ -1394,6 +1414,18 @@ function formatFilterValue(
13941414
return [min, max].filter(Boolean).join(" - ");
13951415
}
13961416

1417+
if (config.type === "date") {
1418+
if (filter.operator === "between") {
1419+
const range = filter.value as { min?: unknown; max?: unknown } | null;
1420+
if (!range || typeof range !== "object") return "";
1421+
const min = range.min != null ? formatDateValue(String(range.min), locale) : "";
1422+
const max = range.max != null ? formatDateValue(String(range.max), locale) : "";
1423+
return [min, max].filter(Boolean).join(" - ");
1424+
}
1425+
if (filter.value == null || filter.value === "") return "";
1426+
return formatDateValue(String(filter.value), locale);
1427+
}
1428+
13971429
if (isTemporalFilterType(config.type) && filter.operator === "between") {
13981430
const range = filter.value as { min?: unknown; max?: unknown } | null;
13991431
if (!range || typeof range !== "object") return "";
@@ -1415,8 +1447,9 @@ function getChipDisplayLabel(
14151447
filter: Filter,
14161448
config: FilterConfig,
14171449
t: ReturnType<typeof useDataTableT>,
1450+
locale: string,
14181451
): string {
1419-
const valueLabel = formatFilterValue(filter, config, t);
1452+
const valueLabel = formatFilterValue(filter, config, t, locale);
14201453
if (!valueLabel) return columnLabel;
14211454

14221455
const operatorLabel = getOperatorLabel(filter.operator, t, config.type);

0 commit comments

Comments
 (0)