Skip to content
Open
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
49 changes: 27 additions & 22 deletions apps/www/src/app/examples/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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'
}}
/>

<RangePicker
Expand All @@ -297,18 +300,18 @@ const Page = () => {
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'
}
}}
Expand All @@ -323,8 +326,10 @@ const Page = () => {
/>

<DatePicker
calendarProps={{
captionLayout: 'dropdown'
slotProps={{
calendar: {
captionLayout: 'dropdown'
}
}}
/>

Expand Down
5 changes: 3 additions & 2 deletions apps/www/src/components/playground/calendar-examples.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<PlaygroundLayout title='Calendar'>
<Flex gap={5} direction='column'>
<Calendar numberOfMonths={2} />
<RangePicker inputsProps={{ startDate: { size: 'small' } }} />
<DatePicker inputProps={{ size: 'small' }} />
<RangePicker slotProps={{ startInput: { size: 'small' } }} />
<DatePicker slotProps={{ input: { size: 'small' } }} />
</Flex>
</PlaygroundLayout>
);
Expand Down
5 changes: 5 additions & 0 deletions apps/www/src/content/docs/components/filter-chip/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's expose a calendarProps similar to selectProps and expose and forward all props


/** Filterchip variant
* @default "default"
*/
Expand Down
84 changes: 84 additions & 0 deletions docs/V1-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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
<DatePicker
inputProps={{ size: "small" }}
calendarProps={{ captionLayout: "dropdown" }}
popoverProps={{ side: "bottom" }}
/>

// After
<DatePicker
slotProps={{
input: { size: "small" },
calendar: { captionLayout: "dropdown" },
popover: { side: "bottom" }
}}
/>
```

`RangePicker` splits its two inputs explicitly:

```tsx
// Before
<RangePicker inputsProps={{ startDate: { size: "small" }, endDate: { size: "small" } }} />

// After
<RangePicker slotProps={{ startInput: { size: "small" }, endInput: { size: "small" } }} />
```

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
<DatePicker onSelect={setDate} />

// After — opt in to the old behavior
<DatePicker defaultValue={new Date()} onSelect={setDate} />
```

`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
<DatePicker value={isoString} />

// After
<DatePicker value={isoString ? new Date(isoString) : undefined} />
```

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:
Expand Down Expand Up @@ -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
<FilterChip label="Created" columnType="date" value="2024-01-01" />

// After — pass a Date
<FilterChip label="Created" columnType="date" value={new Date("2024-01-01")} />
```

`onValueChange` for date columns receives a `Date` as before, and non-date column types are unaffected.

---

### Flex

```tsx
Expand Down
8 changes: 8 additions & 0 deletions packages/raystack/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(<FilterChip label='Created' columnType={FilterType.date} />)
).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(
<FilterChip
label='Created'
columnType={FilterType.date}
value='2026-05-27'
/>
)
).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(
<FilterChip
label='Created'
columnType={FilterType.date}
value={new Date(2026, 4, 27)}
/>
);
expect(screen.getByDisplayValue('27 May 2026')).toBeInTheDocument();
});

it('honors a custom dateFormat', () => {
render(
<FilterChip
label='Created'
columnType={FilterType.date}
value={new Date(2026, 4, 27)}
dateFormat='DD/MM/YYYY'
/>
);
expect(screen.getByDisplayValue('27/05/2026')).toBeInTheDocument();
});
});

describe('Content-fit width', () => {
it('sizes the string input container to its content', () => {
const { container } = render(
<FilterChip label='Name' columnType={FilterType.string} />
);
const inputContainer = container.querySelector(`.${styles.inputField}`);
expect(inputContainer).toHaveStyle({ width: 'fit-content' });
});

it('sizes the date field container to its content', () => {
const { container } = render(
<FilterChip label='Created' columnType={FilterType.date} />
);
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(
Expand Down
14 changes: 14 additions & 0 deletions packages/raystack/components/filter-chip/filter-chip.module.css
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The input field should also shrink on parent resize which was working before, but not working now.

Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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] {
Expand Down
Loading
Loading