diff --git a/apps/www/src/app/examples/page.tsx b/apps/www/src/app/examples/page.tsx index 18e9e9c79..59a142675 100644 --- a/apps/www/src/app/examples/page.tsx +++ b/apps/www/src/app/examples/page.tsx @@ -46,6 +46,7 @@ import { import dayjs from 'dayjs'; import React, { useState } from 'react'; +/** Kitchen-sink examples route rendering Apsara components for manual QA. */ const Page = () => { const [dialogOpen, setDialogOpen] = useState(false); const [nestedDialogOpen, setNestedDialogOpen] = useState(false); @@ -274,18 +275,20 @@ const Page = () => { dateFormat='D MMM YYYY' value={dayjs().add(16, 'year').toDate()} onSelect={(value: Date) => console.log(value)} - calendarProps={{ - captionLayout: 'dropdown', - startMonth: dayjs().add(3, 'month').toDate(), - endMonth: dayjs().add(4, 'year').toDate(), - disabled: { - before: dayjs().add(3, 'month').toDate(), - after: dayjs().add(3, 'year').toDate() + slotProps={{ + calendar: { + captionLayout: 'dropdown', + startMonth: dayjs().add(3, 'month').toDate(), + endMonth: dayjs().add(4, 'year').toDate(), + disabled: { + before: dayjs().add(3, 'month').toDate(), + after: dayjs().add(3, 'year').toDate() + } + }, + input: { + size: 'small' } }} - inputProps={{ - size: 'small' - }} /> { to: range.to ?? new Date() }) } - calendarProps={{ - captionLayout: 'dropdown', - numberOfMonths: 2, - startMonth: dayjs('2024-01-01').toDate(), - endMonth: dayjs('2027-12-01').toDate(), - defaultMonth: dayjs('2027-11-01').toDate() - }} - inputsProps={{ - startDate: { + slotProps={{ + calendar: { + captionLayout: 'dropdown', + numberOfMonths: 2, + startMonth: dayjs('2024-01-01').toDate(), + endMonth: dayjs('2027-12-01').toDate(), + defaultMonth: dayjs('2027-11-01').toDate() + }, + startInput: { size: 'small' }, - endDate: { + endInput: { size: 'small' } }} @@ -323,8 +326,10 @@ const Page = () => { /> diff --git a/apps/www/src/components/playground/calendar-examples.tsx b/apps/www/src/components/playground/calendar-examples.tsx index 3f59619fb..76d42a74c 100644 --- a/apps/www/src/components/playground/calendar-examples.tsx +++ b/apps/www/src/components/playground/calendar-examples.tsx @@ -3,13 +3,14 @@ import { Calendar, DatePicker, Flex, RangePicker } from '@raystack/apsara'; import PlaygroundLayout from './playground-layout'; +/** Playground showcase for `Calendar`, `DatePicker`, and `RangePicker` using the `slotProps` API. */ export function CalendarExamples() { return ( - - + + ); diff --git a/apps/www/src/content/docs/components/filter-chip/props.ts b/apps/www/src/content/docs/components/filter-chip/props.ts index a8f36ad60..2bcc80e23 100644 --- a/apps/www/src/content/docs/components/filter-chip/props.ts +++ b/apps/www/src/content/docs/components/filter-chip/props.ts @@ -10,6 +10,11 @@ export interface FilterChipProps { */ columnType?: 'select' | 'date' | 'string' | 'number'; + /** Date display/parse format for `columnType="date"`, forwarded to the underlying DatePicker. + * @default "DD MMM YYYY" + */ + dateFormat?: string; + /** Filterchip variant * @default "default" */ diff --git a/docs/V1-migration.md b/docs/V1-migration.md index efd8e2052..f6f5ef707 100644 --- a/docs/V1-migration.md +++ b/docs/V1-migration.md @@ -22,6 +22,7 @@ This guide covers all breaking changes when upgrading from the last stable Radix - [Avatar](#avatar) - [Breadcrumb](#breadcrumb) - [Button](#button) + - [Calendar, DatePicker & RangePicker](#calendar-datepicker--rangepicker) - [Chip](#chip) - [Checkbox](#checkbox) - [New: `Checkbox.Group`](#new-checkboxgroup) @@ -35,6 +36,7 @@ This guide covers all breaking changes when upgrading from the last stable Radix - [New Features](#new-features-3) - [DropdownMenu -\> Menu](#dropdownmenu---menu) - [New Features](#new-features-4) + - [FilterChip](#filterchip) - [Flex](#flex) - [Grid](#grid) - [Headline](#headline) @@ -437,6 +439,72 @@ Unchanged: `size`, `radius`, `variant`, `color`, `fallback`, `src`, `alt`, `clas --- +### Calendar, DatePicker & RangePicker + +The three calendar surfaces were overhauled (see the package CHANGELOG, PR #819). The consumer-facing migration items: + +1. **`slotProps` replaces the per-slot props.** `inputProps`, `calendarProps`, `popoverProps` (and `RangePicker`'s `inputsProps`) are now `@deprecated` — they still work, but `slotProps` wins when both are set. Migrate to the consolidated shape: + +```tsx +// Before + + +// After + +``` + +`RangePicker` splits its two inputs explicitly: + +```tsx +// Before + + +// After + +``` + +2. **`DatePicker` no longer defaults to today.** Previously `value` defaulted to `new Date()`, so the picker always rendered with today selected. It now starts **unselected** when neither `value` nor `defaultValue` is passed, and honors the "Select date" placeholder. If you relied on the today-default, opt in explicitly: + +```tsx +// Before — implicitly selected today + + +// After — opt in to the old behavior + +``` + +`RangePicker` likewise drops its `{ from: today, to: today }` default and starts empty. + +3. **`value` requires a real `Date` (or `undefined`).** Both pickers are now strict about their controlled value and sync on its timestamp — passing a string or other non-`Date` will throw. Coerce before passing: + +```tsx +// Before — a string happened to coerce via dayjs + + +// After + +``` + +4. **`onSelect` only fires with a defined date.** It stays typed `(date: Date) => void` and no longer fires with `undefined` (e.g. on deselect). Use `defaultValue` for uncontrolled initialization instead of relying on `onSelect` firing on mount. + +5. **`value` is now reactive.** Controlled changes — form resets, preset buttons, URL-driven updates — propagate to the input on both pickers (previously the input could go stale). + +6. **Calendar date-bound props renamed.** `fromYear` / `toYear` / `fromMonth` / `toMonth` / `fromDate` / `toDate` are superseded by `startMonth` / `endMonth` (bounds) and `hidden` (disable specific days). See the Calendar docs. + +7. **New public types.** `CalendarProps`, `CalendarPropsExtended`, and `DateRange` are now re-exported from `@raystack/apsara`. + +--- + ### Chip **`ariaLabel` prop removed** -- use the standard `aria-label` HTML attribute: @@ -1021,6 +1089,22 @@ import { Menu } from '@raystack/apsara'; --- +### FilterChip + +**Date columns now require a real `Date` value.** `FilterChip` forwards its value to the overhauled `DatePicker` (see [Calendar, DatePicker & RangePicker](#calendar-datepicker--rangepicker)), which is now strict about its `value` type. Previously a string was loosely coerced; now a non-`Date` value (including the empty-string default) starts the date field **unselected** instead of erroring. If you drive a `columnType="date"` chip with a controlled value, pass a `Date`: + +```tsx +// Before — string value + + +// After — pass a Date + +``` + +`onValueChange` for date columns receives a `Date` as before, and non-date column types are unaffected. + +--- + ### Flex ```tsx diff --git a/packages/raystack/CHANGELOG.md b/packages/raystack/CHANGELOG.md index 2750c464e..c16970556 100644 --- a/packages/raystack/CHANGELOG.md +++ b/packages/raystack/CHANGELOG.md @@ -78,6 +78,14 @@ API added, and the three legacy prop names (`inputProps`, `fill="none"` on stroke-based icons (lucide) and filling the outline paths solid. `color` alone now carries the selected style via `currentColor` for both stroke- and fill-based icon libraries. +- **FilterChip date column no longer crashes** — the stricter + `DatePicker` `value?: Date` contract above surfaced a latent bug: + `FilterChip` seeded its value with `''` and forwarded that string + straight to the picker, so the new controlled-sync effect's + `valueProp?.getTime()` threw `TypeError`. `FilterChip` now coerces + non-`Date` values to `undefined` (the date field starts unselected) + and uses the new `slotProps.input` API instead of the deprecated + `inputProps`. #### Code-review and audit follow-ups diff --git a/packages/raystack/components/filter-chip/__tests__/filter-chip.test.tsx b/packages/raystack/components/filter-chip/__tests__/filter-chip.test.tsx index bb40414a2..ac03407dc 100644 --- a/packages/raystack/components/filter-chip/__tests__/filter-chip.test.tsx +++ b/packages/raystack/components/filter-chip/__tests__/filter-chip.test.tsx @@ -125,6 +125,75 @@ describe('FilterChip', () => { }); }); + describe('Date Filter Type', () => { + it('renders the date picker without crashing when no value is set', () => { + // Regression: an unset date chip seeds its value with '' and forwarded + // that string to DatePicker, whose controlled-sync effect ran + // `valueProp?.getTime()` → "getTime is not a function". + expect(() => + render() + ).not.toThrow(); + expect(screen.getByPlaceholderText('Select date')).toBeInTheDocument(); + }); + + it('coerces a non-Date value to unselected instead of crashing', () => { + // FilterChipValue allows string, but a date column needs a real Date — + // strings must start the field unselected, not throw. + expect(() => + render( + + ) + ).not.toThrow(); + expect(screen.getByPlaceholderText('Select date')).toHaveValue(''); + }); + + it('formats a Date value with the default month-as-text format', () => { + // Local-component Date so the formatted string is timezone-stable. + render( + + ); + expect(screen.getByDisplayValue('27 May 2026')).toBeInTheDocument(); + }); + + it('honors a custom dateFormat', () => { + render( + + ); + expect(screen.getByDisplayValue('27/05/2026')).toBeInTheDocument(); + }); + }); + + describe('Content-fit width', () => { + it('sizes the string input container to its content', () => { + const { container } = render( + + ); + const inputContainer = container.querySelector(`.${styles.inputField}`); + expect(inputContainer).toHaveStyle({ width: 'fit-content' }); + }); + + it('sizes the date field container to its content', () => { + const { container } = render( + + ); + const dateContainer = container.querySelector(`.${styles.dateField}`); + expect(dateContainer).toHaveStyle({ width: 'fit-content' }); + }); + }); + describe('Forwarded HTML attributes', () => { it('forwards arbitrary HTML attributes onto the root div', () => { render( diff --git a/packages/raystack/components/filter-chip/filter-chip.module.css b/packages/raystack/components/filter-chip/filter-chip.module.css index 299d2f38a..aabd60005 100644 --- a/packages/raystack/components/filter-chip/filter-chip.module.css +++ b/packages/raystack/components/filter-chip/filter-chip.module.css @@ -156,9 +156,18 @@ button.selectValue:hover { min-height: var(--rs-space-7); } +/* +* Hug the content via `field-sizing` (Chromium/Safari); elsewhere fall back +* to the intrinsic `width: auto`. Bounds are guardrails: clickable floor + +* runaway ceiling. +*/ .inputFieldWrapper input { padding-left: var(--rs-space-3); padding-right: var(--rs-space-3); + field-sizing: content; + width: auto; + min-width: var(--rs-space-10); + max-width: 200px; } .dateFieldWrapper { @@ -189,6 +198,11 @@ button.selectValue:hover { text-align: left; padding-left: var(--rs-space-3); padding-right: var(--rs-space-3); + /* Hug the formatted date string — see .inputFieldWrapper input. */ + field-sizing: content; + width: auto; + min-width: var(--rs-space-10); + max-width: 200px; } .dateField [data-trailing] { diff --git a/packages/raystack/components/filter-chip/filter-chip.tsx b/packages/raystack/components/filter-chip/filter-chip.tsx index 3f72046c6..d4733df97 100644 --- a/packages/raystack/components/filter-chip/filter-chip.tsx +++ b/packages/raystack/components/filter-chip/filter-chip.tsx @@ -47,8 +47,22 @@ export interface FilterChipProps leadingIcon?: ReactElement; operations?: FilterOperator[]; selectProps?: BaseSelectProps; + /** + * Date display/parse format for `columnType="date"`, forwarded to the + * underlying DatePicker. Defaults to a month-as-text format so the + * month reads as e.g. "27 May 2026" instead of "27/05/2026". + */ + dateFormat?: string; } +/** + * A compact, removable filter pill that pairs a label and operator with a + * value control chosen by `columnType`: a `Select` (`select`/`multiselect`), + * a `DatePicker` (`date`), or a text `Input` (`string`/`number`). The value + * control sizes to its content so the chip hugs the active filter. Emits + * `onValueChange`/`onOperationChange` and renders a remove button when + * `onRemove` is provided. + */ export const FilterChip = ({ label, value, @@ -63,6 +77,7 @@ export const FilterChip = ({ variant, operations, selectProps, + dateFormat = 'DD MMM YYYY', ...props }: FilterChipProps) => { const computedOperations = operations?.length @@ -138,10 +153,16 @@ export const FilterChip = ({ return (
handleFilterValueChange(date)} + dateFormat={dateFormat} showCalendarIcon={false} - inputProps={{ classNames: { container: styles.dateField } }} + slotProps={{ + input: { + width: 'fit-content', + classNames: { container: styles.dateField } + } + }} />
); @@ -153,6 +174,7 @@ export const FilterChip = ({ classNames={{ container: styles.inputField }} value={filterValue} onChange={e => handleFilterValueChange(e.target.value)} + width='fit-content' /> );