Skip to content

Commit e693313

Browse files
authored
Adds no-value rendering to format-date (#100)
1 parent 4cf1538 commit e693313

3 files changed

Lines changed: 48 additions & 16 deletions

File tree

packages/demo/src/content/components/format-date.mdx

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ The <code>FormatDate</code> component renders a date as either **relative** time
1818

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

21+
If no date is provided (`null`, `undefined`, or an empty string), the component renders a `---` placeholder instead.
22+
2123
## Time zones
2224

2325
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.
@@ -108,14 +110,26 @@ For a short numeric date like `2026-06-09`, use numeric options. Digit _order_ i
108110
/>
109111
```
110112

113+
## Empty state
114+
115+
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`.
116+
117+
<FormatDate date={null} />
118+
119+
```tsx
120+
<FormatDate date={null} />
121+
<FormatDate date={undefined} />
122+
<FormatDate date="" />
123+
```
124+
111125
## Props
112126

113-
| Name | Description | Type | Default | Required |
114-
| ----------------- | ----------------------------------------------------------------------- | ---------------------------- | ---------- | -------- |
115-
| `date` | The date to display | `string`, `number`, `Date` |||
116-
| `displayAs` | Render relative or absolute time | `relative`, `absolute` | `absolute` ||
117-
| `timeZone` | Time zone used for absolute formatting | `string` | `UTC` ||
118-
| `locale` | BCP 47 locale used for formatting | `string` | `en-US` ||
119-
| `tooltip` | When relative, show a tooltip with the absolute time on hover/focus | `boolean` | `true` ||
120-
| `live` | When relative, re-render on an interval so the value stays current | `boolean` | `true` ||
121-
| `absoluteOptions` | Override the `Intl.DateTimeFormat` options used for absolute formatting | `Intl.DateTimeFormatOptions` |||
127+
| Name | Description | Type | Default | Required |
128+
| ----------------- | ----------------------------------------------------------------------- | ---------------------------------- | ---------- | -------- |
129+
| `date` | The date to display | `string`, `number`, `Date`, `null` |||
130+
| `displayAs` | Render relative or absolute time | `relative`, `absolute` | `absolute` ||
131+
| `timeZone` | Time zone used for absolute formatting | `string` | `UTC` ||
132+
| `locale` | BCP 47 locale used for formatting | `string` | `en-US` ||
133+
| `tooltip` | When relative, show a tooltip with the absolute time on hover/focus | `boolean` | `true` ||
134+
| `live` | When relative, re-render on an interval so the value stays current | `boolean` | `true` ||
135+
| `absoluteOptions` | Override the `Intl.DateTimeFormat` options used for absolute formatting | `Intl.DateTimeFormatOptions` |||

packages/ui/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "@eqtylab/equality",
33
"description": "EQTYLab's component and token-based design system",
44
"homepage": "https://equality.eqtylab.io/",
5-
"version": "2.1.2",
5+
"version": "2.1.3",
66
"license": "Apache-2.0",
77
"keywords": [
88
"component library",

packages/ui/src/components/format-date/format-date.tsx

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ export interface FormatDateProps extends Omit<
1515
React.TimeHTMLAttributes<HTMLTimeElement>,
1616
'dateTime' | 'children'
1717
> {
18-
/** The date to display. Accepts an ISO 8601 string, epoch milliseconds, or a Date. */
19-
date: string | number | Date;
18+
/** 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. */
19+
date: string | number | Date | null | undefined;
2020
/** Render relative ("2 weeks ago") or absolute ("Jun 9 2026, 18:42:03 UTC") time. */
2121
displayAs?: FormatDateDisplayMode;
2222
/** Time zone used for absolute formatting. Defaults to "UTC". */
@@ -58,7 +58,16 @@ const RELATIVE_DIVISIONS: { amount: number; unit: Intl.RelativeTimeFormatUnit }[
5858
// Re-render relative time so values like "Just now" stay accurate without busy-looping
5959
const LIVE_INTERVAL_MS = 30_000;
6060

61-
// Parse any date-like input into a Date, or null if missing/invalid
61+
// Shown when no date is provided, as distinct from a date that fails to parse
62+
const NO_DATE_PLACEHOLDER = '---';
63+
64+
// A missing value (null, undefined, or empty/whitespace string) is "no date",
65+
// as opposed to a present-but-unparseable value, which is "Invalid date"
66+
function isMissing(value: string | number | Date | null | undefined): value is null | undefined {
67+
return value == null || (typeof value === 'string' && value.trim() === '');
68+
}
69+
70+
// Parse any date-like input into a Date, or null if invalid
6271
function toDate(value: string | number | Date): Date | null {
6372
const date = value instanceof Date ? value : new Date(value);
6473
return Number.isNaN(date.getTime()) ? null : date;
@@ -98,7 +107,8 @@ function FormatDate({
98107
className,
99108
...props
100109
}: FormatDateProps) {
101-
const parsed = React.useMemo(() => toDate(date), [date]);
110+
const missing = isMissing(date);
111+
const parsed = React.useMemo(() => (isMissing(date) ? null : toDate(date)), [date]);
102112
const [now, setNow] = React.useState(() => new Date());
103113
const [mounted, setMounted] = React.useState(false);
104114

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

125+
if (missing) {
126+
return (
127+
<span className={cn(styles['format-date'], className)} aria-label="No date" {...props}>
128+
{NO_DATE_PLACEHOLDER}
129+
</span>
130+
);
131+
}
132+
115133
if (!parsed) {
116134
return (
117-
<time className={cn(styles['format-date'], className)} {...props}>
135+
<span className={cn(styles['format-date'], className)} {...props}>
118136
Invalid date
119-
</time>
137+
</span>
120138
);
121139
}
122140

0 commit comments

Comments
 (0)