-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
302e179
commit 1eda1dd
Showing
8 changed files
with
387 additions
and
7 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import { Args } from "@storybook/react"; | ||
import { DateDetails } from "./DateDetails"; | ||
|
||
export default { | ||
argTypes: { | ||
side: { | ||
control: { | ||
type: "select", | ||
}, | ||
options: ["top", "right", "left", "bottom"], | ||
}, | ||
date: { | ||
control: "date", | ||
}, | ||
systemTimeZone: { | ||
options: [ | ||
"America/Denver", | ||
"America/Los_Angeles", | ||
"America/New_York", | ||
"Asia/Shanghai", | ||
"Asia/Tokyo", | ||
"Europe/London", | ||
"Europe/Berlin", | ||
"Europe/Moscow", | ||
"Europe/Rome", | ||
], | ||
control: { | ||
type: "select", | ||
}, | ||
}, | ||
}, | ||
component: DateDetails, | ||
title: "Display/DateDetails", | ||
tags: ["autodocs"], | ||
}; | ||
|
||
export const Playground = { | ||
args: { | ||
date: new Date(), | ||
side: "top", | ||
systemTimeZone: "America/Los_Angeles", | ||
title: "DateDetails", | ||
}, | ||
render: (args: Args) => { | ||
const date = args.date ? new Date(args.date) : new Date(); | ||
return ( | ||
<DateDetails | ||
date={date} | ||
side={args.side} | ||
systemTimeZone={args.systemTimeZone} | ||
/> | ||
); | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
import { DateDetails } from "@/components/DateDetails/DateDetails"; | ||
import { renderCUI } from "@/utils/test-utils"; | ||
import { fireEvent } from "@testing-library/dom"; | ||
|
||
describe("DateDetails", () => { | ||
const actualTZ = process.env.TZ; | ||
|
||
beforeAll(() => { | ||
global.ResizeObserver = vi.fn(() => { | ||
return { | ||
observe: vi.fn(), | ||
unobserve: vi.fn(), | ||
disconnect: vi.fn(), | ||
}; | ||
}); | ||
|
||
process.env.TZ = "America/New_York"; | ||
}); | ||
|
||
afterAll(() => { | ||
process.env.TZ = actualTZ; | ||
}); | ||
|
||
it("renders the DateDetails component with relevant timezone information", () => { | ||
const baseDate = new Date("2024-12-24 11:45:00 AM"); | ||
const systemTimeZone = "America/Los_Angeles"; | ||
vi.setSystemTime(baseDate); | ||
|
||
const fiveMinutesAgo = new Date("2024-12-24 11:40:00 AM"); | ||
|
||
const { getByText } = renderCUI( | ||
<DateDetails | ||
date={fiveMinutesAgo} | ||
systemTimeZone={systemTimeZone} | ||
/> | ||
); | ||
|
||
const trigger = getByText("5 minutes ago"); | ||
expect(trigger).toBeInTheDocument(); | ||
|
||
fireEvent.click(trigger); | ||
expect( | ||
getByText(content => { | ||
return content.includes("EST"); | ||
}) | ||
).toBeInTheDocument(); | ||
expect( | ||
getByText(content => { | ||
return content.includes("PST"); | ||
}) | ||
).toBeInTheDocument(); | ||
expect(getByText("Dec 24, 4:40 p.m.")).toBeInTheDocument(); | ||
expect(getByText("Dec 24, 11:40 a.m. (EST)")).toBeInTheDocument(); | ||
expect(getByText("Dec 24, 8:40 a.m. (PST)")).toBeInTheDocument(); | ||
expect(getByText(fiveMinutesAgo.getTime() / 1000)).toBeInTheDocument(); | ||
}); | ||
|
||
it("only shows the date if the previous date isn't in this year", () => { | ||
const baseDate = new Date("2025-02-07 11:45:00 AM"); | ||
const systemTimeZone = "America/Los_Angeles"; | ||
vi.setSystemTime(baseDate); | ||
|
||
const oneYearAgo = new Date("2024-02-07 11:45:00 AM"); | ||
|
||
const { getByText } = renderCUI( | ||
<DateDetails | ||
date={oneYearAgo} | ||
systemTimeZone={systemTimeZone} | ||
/> | ||
); | ||
|
||
const trigger = getByText("1 year ago"); | ||
expect(trigger).toBeInTheDocument(); | ||
|
||
fireEvent.click(trigger); | ||
expect(getByText("Feb 7, 2024, 4:45 p.m.")).toBeInTheDocument(); | ||
expect(getByText("Feb 7, 2024, 11:45 a.m. (EST)")).toBeInTheDocument(); | ||
expect(getByText("Feb 7, 2024, 8:45 a.m. (PST)")).toBeInTheDocument(); | ||
expect(getByText(oneYearAgo.getTime() / 1000)).toBeInTheDocument(); | ||
}); | ||
|
||
it("handles Daylight Savings Time", () => { | ||
const baseDate = new Date("2024-07-04 11:45:00 AM"); | ||
const systemTimeZone = "America/Los_Angeles"; | ||
vi.setSystemTime(baseDate); | ||
|
||
const fiveMinutesAgo = new Date("2024-07-04 11:40:00 AM"); | ||
|
||
const { getByText } = renderCUI( | ||
<DateDetails | ||
date={fiveMinutesAgo} | ||
systemTimeZone={systemTimeZone} | ||
/> | ||
); | ||
|
||
const trigger = getByText("5 minutes ago"); | ||
expect(trigger).toBeInTheDocument(); | ||
|
||
fireEvent.click(trigger); | ||
expect( | ||
getByText(content => { | ||
return content.includes("EDT"); | ||
}) | ||
).toBeInTheDocument(); | ||
expect( | ||
getByText(content => { | ||
return content.includes("PDT"); | ||
}) | ||
).toBeInTheDocument(); | ||
expect(getByText("Jul 4, 3:40 p.m.")).toBeInTheDocument(); | ||
expect(getByText("Jul 4, 11:40 a.m. (EDT)")).toBeInTheDocument(); | ||
expect(getByText("Jul 4, 8:40 a.m. (PDT)")).toBeInTheDocument(); | ||
expect(getByText(fiveMinutesAgo.getTime() / 1000)).toBeInTheDocument(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,199 @@ | ||
import dayjs, { Dayjs } from "dayjs"; | ||
import advancedFormat from "dayjs/plugin/advancedFormat"; | ||
import duration from "dayjs/plugin/duration"; | ||
import localizedFormat from "dayjs/plugin/localizedFormat"; | ||
import relativeTime from "dayjs/plugin/relativeTime"; | ||
import timezone from "dayjs/plugin/timezone"; | ||
import updateLocale from "dayjs/plugin/updateLocale"; | ||
import utc from "dayjs/plugin/utc"; | ||
import { styled } from "styled-components"; | ||
|
||
import { Popover } from "@/components/Popover/Popover"; | ||
import { Text } from "@/components/Typography/Text/Text"; | ||
import { linkStyles, StyledLinkProps } from "@/components/Link/common"; | ||
import { GridContainer } from "@/components/GridContainer/GridContainer"; | ||
import { Container } from "@/components/Container/Container"; | ||
|
||
dayjs.extend(advancedFormat); | ||
dayjs.extend(duration); | ||
dayjs.extend(localizedFormat); | ||
dayjs.extend(updateLocale); | ||
dayjs.extend(utc); | ||
|
||
const thresholds = [ | ||
{ l: "s", r: 1, d: "second" }, | ||
{ l: "ss", r: 56, d: "second" }, | ||
{ l: "m", r: 90, d: "second" }, | ||
{ l: "mm", r: 55, d: "minute" }, | ||
{ l: "h", r: 90, d: "minute" }, | ||
{ l: "hh", r: 22, d: "hour" }, | ||
{ l: "d", r: 40, d: "hour" }, | ||
{ l: "dd", r: 31, d: "day" }, | ||
{ l: "M", r: 45, d: "day" }, | ||
{ l: "MM", r: 11, d: "month" }, | ||
{ l: "y", r: 17, d: "month" }, | ||
{ l: "yy", r: 2, d: "year" }, | ||
]; | ||
|
||
dayjs.extend(relativeTime, { thresholds }); | ||
|
||
dayjs.updateLocale("en", { | ||
relativeTime: { | ||
future: "In %s", | ||
past: "%s ago", | ||
s: "a few seconds", | ||
ss: "%d seconds", | ||
m: "1 minute", | ||
mm: "%d minutes", | ||
h: "1 hour", | ||
hh: "%d hours", | ||
d: "1 day", | ||
dd: "%d days", | ||
w: "1 week", | ||
ww: "%d weeks", | ||
M: "1 month", | ||
MM: "%d months", | ||
y: "1 year", | ||
yy: "%d years", | ||
}, | ||
}); | ||
|
||
const UnderlinedTrigger = styled(Popover.Trigger)<StyledLinkProps>` | ||
${linkStyles} | ||
`; | ||
|
||
const formatDateDetails = (date: Dayjs, timezone?: string): string => { | ||
const isCurrentYear = dayjs().year() === date.year(); | ||
const formatForCurrentYear = "MMM D, h:mm a"; | ||
const formatForPastYear = "MMM D, YYYY, h:mm a"; | ||
|
||
if (isCurrentYear) { | ||
if (timezone) { | ||
const dateWithTimezone = date.tz(timezone); | ||
return dateWithTimezone | ||
.format(formatForCurrentYear) | ||
.replace("am", "a.m.") | ||
.replace("pm", "p.m."); | ||
} | ||
|
||
return date.format(formatForCurrentYear).replace("am", "a.m.").replace("pm", "p.m."); | ||
} | ||
|
||
if (timezone) { | ||
const dateWithTimezone = date.tz(timezone); | ||
return dateWithTimezone | ||
.format(formatForPastYear) | ||
.replace("am", "a.m.") | ||
.replace("pm", "p.m."); | ||
} | ||
return date.format(formatForPastYear).replace("am", "a.m.").replace("pm", "p.m."); | ||
}; | ||
|
||
const formatTimezone = (date: Dayjs, timezone: string): string => { | ||
return ( | ||
new Intl.DateTimeFormat(undefined, { | ||
timeZone: timezone, | ||
timeZoneName: "short", | ||
}) | ||
.formatToParts(date.toDate()) | ||
.find(part => part.type === "timeZoneName")?.value ?? date.format("z") | ||
); | ||
}; | ||
|
||
export type ArrowPosition = "top" | "right" | "left" | "bottom"; | ||
|
||
export interface DateDetailsProps { | ||
date: Date; | ||
side?: ArrowPosition; | ||
systemTimeZone?: string; | ||
} | ||
|
||
export const DateDetails = ({ | ||
date, | ||
side = "top", | ||
systemTimeZone = "America/New_York", | ||
}: DateDetailsProps) => { | ||
const dayjsDate = dayjs(date); | ||
|
||
let systemTime; | ||
if (systemTimeZone) { | ||
dayjs.extend(timezone); | ||
try { | ||
systemTime = dayjsDate.tz(systemTimeZone); | ||
} catch { | ||
systemTime = dayjsDate.tz("America/New_York"); | ||
} | ||
} | ||
|
||
return ( | ||
<Popover> | ||
<UnderlinedTrigger | ||
$size="sm" | ||
$weight="medium" | ||
> | ||
<Text size="sm">{dayjs.utc(date).fromNow()}</Text> | ||
</UnderlinedTrigger> | ||
<Popover.Content | ||
side={side} | ||
showArrow | ||
> | ||
<GridContainer | ||
columnGap="xl" | ||
gridTemplateColumns="repeat(2, auto)" | ||
gap="sm" | ||
> | ||
<Text | ||
color="muted" | ||
size="sm" | ||
> | ||
Local | ||
</Text> | ||
<Container justifyContent="end"> | ||
<Text size="sm"> | ||
{formatDateDetails(dayjsDate)} ( | ||
{formatTimezone(dayjsDate, dayjs.tz.guess())}) | ||
</Text> | ||
</Container> | ||
|
||
{systemTime && ( | ||
<> | ||
<Text | ||
color="muted" | ||
size="sm" | ||
> | ||
System | ||
</Text> | ||
|
||
<Container justifyContent="end"> | ||
<Text size="sm"> | ||
{formatDateDetails(systemTime, systemTimeZone)} ( | ||
{formatTimezone(systemTime, systemTimeZone)}) | ||
</Text> | ||
</Container> | ||
</> | ||
)} | ||
|
||
<Text | ||
color="muted" | ||
size="sm" | ||
> | ||
UTC | ||
</Text> | ||
<Container justifyContent="end"> | ||
<Text size="sm">{formatDateDetails(dayjsDate.utc(), "UTC")}</Text> | ||
</Container> | ||
|
||
<Text | ||
color="muted" | ||
size="sm" | ||
> | ||
Unix | ||
</Text> | ||
<Container justifyContent="end"> | ||
<Text size="sm">{Math.round(date.getTime() / 1000)}</Text> | ||
</Container> | ||
</GridContainer> | ||
</Popover.Content> | ||
</Popover> | ||
); | ||
}; |
Oops, something went wrong.