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
32 changes: 23 additions & 9 deletions packages/demo/src/content/components/format-date.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ The <code>FormatDate</code> component renders a date as either **relative** time

When showing relative time, a tooltip reveals the absolute time on hover or keyboard focus.

If no date is provided (`null`, `undefined`, or an empty string), the component renders a `---` placeholder instead.

## Time zones

Absolute time is formatted in **UTC by default**. To render in a different zone, pass a `timeZone` (e.g. `"America/New_York"`). Relative time reads the same everywhere.
Expand Down Expand Up @@ -108,14 +110,26 @@ For a short numeric date like `2026-06-09`, use numeric options. Digit _order_ i
/>
```

## Empty state

When `date` is missing — `null`, `undefined`, or an empty string — the component renders a `---` placeholder with an accessible "No date" label. This is distinct from passing a present-but-unparseable value (e.g. `"not-a-date"`), which renders `Invalid date`.

<FormatDate date={null} />

```tsx
<FormatDate date={null} />
<FormatDate date={undefined} />
<FormatDate date="" />
```

## Props

| Name | Description | Type | Default | Required |
| ----------------- | ----------------------------------------------------------------------- | ---------------------------- | ---------- | -------- |
| `date` | The date to display | `string`, `number`, `Date` | — | ✅ |
| `displayAs` | Render relative or absolute time | `relative`, `absolute` | `absolute` | ❌ |
| `timeZone` | Time zone used for absolute formatting | `string` | `UTC` | ❌ |
| `locale` | BCP 47 locale used for formatting | `string` | `en-US` | ❌ |
| `tooltip` | When relative, show a tooltip with the absolute time on hover/focus | `boolean` | `true` | ❌ |
| `live` | When relative, re-render on an interval so the value stays current | `boolean` | `true` | ❌ |
| `absoluteOptions` | Override the `Intl.DateTimeFormat` options used for absolute formatting | `Intl.DateTimeFormatOptions` | — | ❌ |
| Name | Description | Type | Default | Required |
| ----------------- | ----------------------------------------------------------------------- | ---------------------------------- | ---------- | -------- |
| `date` | The date to display | `string`, `number`, `Date`, `null` | — | ✅ |
| `displayAs` | Render relative or absolute time | `relative`, `absolute` | `absolute` | ❌ |
| `timeZone` | Time zone used for absolute formatting | `string` | `UTC` | ❌ |
| `locale` | BCP 47 locale used for formatting | `string` | `en-US` | ❌ |
| `tooltip` | When relative, show a tooltip with the absolute time on hover/focus | `boolean` | `true` | ❌ |
| `live` | When relative, re-render on an interval so the value stays current | `boolean` | `true` | ❌ |
| `absoluteOptions` | Override the `Intl.DateTimeFormat` options used for absolute formatting | `Intl.DateTimeFormatOptions` | — | ❌ |
2 changes: 1 addition & 1 deletion packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@eqtylab/equality",
"description": "EQTYLab's component and token-based design system",
"homepage": "https://equality.eqtylab.io/",
"version": "2.1.2",
"version": "2.1.3",
"license": "Apache-2.0",
"keywords": [
"component library",
Expand Down
30 changes: 24 additions & 6 deletions packages/ui/src/components/format-date/format-date.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ export interface FormatDateProps extends Omit<
React.TimeHTMLAttributes<HTMLTimeElement>,
'dateTime' | 'children'
> {
/** The date to display. Accepts an ISO 8601 string, epoch milliseconds, or a Date. */
date: string | number | Date;
/** The date to display. Accepts an ISO 8601 string, epoch milliseconds, or a Date. A missing value (null, undefined, or empty string) renders a placeholder. */
date: string | number | Date | null | undefined;
/** Render relative ("2 weeks ago") or absolute ("Jun 9 2026, 18:42:03 UTC") time. */
displayAs?: FormatDateDisplayMode;
/** Time zone used for absolute formatting. Defaults to "UTC". */
Expand Down Expand Up @@ -58,7 +58,16 @@ const RELATIVE_DIVISIONS: { amount: number; unit: Intl.RelativeTimeFormatUnit }[
// Re-render relative time so values like "Just now" stay accurate without busy-looping
const LIVE_INTERVAL_MS = 30_000;

// Parse any date-like input into a Date, or null if missing/invalid
// Shown when no date is provided, as distinct from a date that fails to parse
const NO_DATE_PLACEHOLDER = '---';

// A missing value (null, undefined, or empty/whitespace string) is "no date",
// as opposed to a present-but-unparseable value, which is "Invalid date"
function isMissing(value: string | number | Date | null | undefined): value is null | undefined {
return value == null || (typeof value === 'string' && value.trim() === '');
}

// Parse any date-like input into a Date, or null if invalid
function toDate(value: string | number | Date): Date | null {
const date = value instanceof Date ? value : new Date(value);
return Number.isNaN(date.getTime()) ? null : date;
Expand Down Expand Up @@ -98,7 +107,8 @@ function FormatDate({
className,
...props
}: FormatDateProps) {
const parsed = React.useMemo(() => toDate(date), [date]);
const missing = isMissing(date);
const parsed = React.useMemo(() => (isMissing(date) ? null : toDate(date)), [date]);
const [now, setNow] = React.useState(() => new Date());
const [mounted, setMounted] = React.useState(false);

Expand All @@ -112,11 +122,19 @@ function FormatDate({
return () => clearInterval(id);
}, [isRelative, live]);

if (missing) {
return (
<span className={cn(styles['format-date'], className)} aria-label="No date" {...props}>
{NO_DATE_PLACEHOLDER}
</span>
);
}

if (!parsed) {
return (
<time className={cn(styles['format-date'], className)} {...props}>
<span className={cn(styles['format-date'], className)} {...props}>
Invalid date
</time>
</span>
);
}

Expand Down
Loading