-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(TimeAgo): add TimeAgo component to the core package
- Loading branch information
Paulo Lagoá
committed
May 4, 2023
1 parent
6a771d6
commit 54d547b
Showing
11 changed files
with
536 additions
and
1 deletion.
There are no files selected for viewing
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
162 changes: 162 additions & 0 deletions
162
packages/core/src/components/TimeAgo/TimeAgo.stories.tsx
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,162 @@ | ||
import { Meta, StoryObj } from "@storybook/react"; | ||
import { | ||
HvRadio, | ||
HvRadioGroup, | ||
HvTimeAgo, | ||
HvTimeAgoProps, | ||
HvTypography, | ||
} from "@core/components"; | ||
import { useMemo, useState } from "react"; | ||
import dayjs from "dayjs"; | ||
import "dayjs/locale/fr"; | ||
import "dayjs/locale/de"; | ||
import "dayjs/locale/pt"; | ||
import { css } from "@emotion/css"; | ||
import { theme } from "@hitachivantara/uikit-styles"; | ||
|
||
const styles = { | ||
root: css({ | ||
display: "flex", | ||
alignItems: "center", | ||
}), | ||
container: css({ | ||
minHeight: 300, | ||
"& > div": { | ||
padding: theme.spacing("xs"), | ||
}, | ||
}), | ||
table: css({ | ||
border: `1px solid ${theme.colors.secondary}`, | ||
borderCollapse: "collapse", | ||
"& th, td": { | ||
border: `1px solid ${theme.colors.secondary}`, | ||
padding: theme.spacing(["5px", "sm"]), | ||
}, | ||
}), | ||
}; | ||
|
||
const meta: Meta<typeof HvTimeAgo> = { | ||
title: "Components/Time Ago", | ||
component: HvTimeAgo, | ||
}; | ||
export default meta; | ||
|
||
export const Main: StoryObj<HvTimeAgoProps> = { | ||
args: { | ||
timestamp: dayjs().valueOf(), | ||
locale: "en", | ||
disableRefresh: false, | ||
showSeconds: false, | ||
justText: false, | ||
}, | ||
argTypes: { | ||
classes: { control: { disable: true } }, | ||
component: { control: { disable: true } }, | ||
emptyElement: { control: { disable: true } }, | ||
locale: { control: "select", options: ["en", "fr", "de", "pt"] }, | ||
}, | ||
render: (args) => { | ||
return <HvTimeAgo {...args} />; | ||
}, | ||
}; | ||
|
||
export const Samples = () => { | ||
const dates = useMemo( | ||
() => | ||
[ | ||
dayjs(), | ||
dayjs().subtract(1, "minutes"), | ||
dayjs().subtract(10, "minutes"), | ||
dayjs().subtract(59, "minutes"), | ||
dayjs().hour(0), | ||
dayjs().day(0), | ||
dayjs().date(0), | ||
dayjs().month(-2), | ||
dayjs().month(-4), | ||
].map((date) => date.valueOf()), | ||
[] | ||
); | ||
|
||
return ( | ||
<table className={css(styles.table)}> | ||
<thead> | ||
<tr> | ||
<th>ISO Date</th> | ||
<th>{"<TimeAgo />"}</th> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
{dates.map((dateTs, idx) => ( | ||
<tr key={`${dateTs}-${idx}`}> | ||
<td>{new Date(dateTs).toISOString()}</td> | ||
<td> | ||
<HvTimeAgo timestamp={dateTs} /> | ||
</td> | ||
</tr> | ||
))} | ||
</tbody> | ||
</table> | ||
); | ||
}; | ||
|
||
export const LocaleOverride = () => { | ||
const [locale, setLocale] = useState("en"); | ||
const [time /*, setTime*/] = useState(Date.now()); | ||
|
||
// const handleTimeChange = ({ hours, minutes, seconds }) => { | ||
// const newDate = new Date(); | ||
// newDate.setHours(hours); | ||
// newDate.setMinutes(minutes); | ||
// newDate.setSeconds(seconds); | ||
// setTime(newDate.getTime()); | ||
// }; | ||
|
||
// dynamically import locales | ||
// if the supported locales are known beforehand, its preferable | ||
// to import them statically, to avoid bundling unnecessary locales | ||
const handleLocaleChange = async (event, newLocale) => { | ||
setLocale(newLocale); | ||
dayjs.updateLocale("fr", {}); | ||
}; | ||
|
||
return ( | ||
<div className={css(styles.container)}> | ||
<div> | ||
<HvRadioGroup | ||
orientation="horizontal" | ||
value={locale} | ||
onChange={handleLocaleChange} | ||
> | ||
<HvRadio label="🇬🇧 English" value="en" /> | ||
<HvRadio label="🇫🇷 French" value="fr" /> | ||
<HvRadio label="🇩🇪 German" value="de" /> | ||
<HvRadio label="🇵🇹 Portuguese" value="pt" /> | ||
</HvRadioGroup> | ||
</div> | ||
<div> | ||
<HvTypography variant="sTitle"> | ||
<HvTimeAgo timestamp={time} locale={locale} /> | ||
</HvTypography> | ||
<span>{new Date(time).toISOString()}</span> | ||
</div> | ||
{/* <div style={{ width: 300, minHeight: 300 }}> | ||
<HvTimePicker | ||
label="Time" | ||
description="Pick a time" | ||
onChange={handleTimeChange} | ||
/> | ||
</div> */} | ||
</div> | ||
); | ||
}; | ||
|
||
LocaleOverride.parameters = { | ||
docs: { | ||
description: { | ||
story: | ||
"Sample dates and locale controlled externally.<br /> \ | ||
`HvTimeAgo` leverages `dayjs` for locales and custom dates. To use a locale, import it with `dayjs/locale/{locale}`<br />\ | ||
Locale strings can be overridden using [dayjs.updateLocale](https://day.js.org/docs/en/plugin/update-locale)", | ||
}, | ||
}, | ||
}; |
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,75 @@ | ||
import { describe, expect, it, vi } from "vitest"; | ||
import { render } from "@testing-library/react"; | ||
import { HvButton } from "@hitachivantara/uikit-react-core"; | ||
import { HvTimeAgo } from "./TimeAgo"; | ||
|
||
const EM_DASH = "—"; | ||
|
||
const MOCK_TIME_AGO = "MOCK_TIME_AGO"; | ||
const MOCK_DELAY = 120; | ||
|
||
vi.mock("./formatUtils", () => ({ | ||
formatTimeAgo: vi.fn(() => ({ timeAgo: MOCK_TIME_AGO, delay: MOCK_DELAY })), | ||
})); | ||
|
||
describe("TimeAgo without timestamp", () => { | ||
let wrapper; | ||
|
||
it("should be defined", () => { | ||
wrapper = render(<HvTimeAgo />); | ||
expect(wrapper).toBeDefined(); | ||
}); | ||
|
||
it("should render the emptyElement", () => { | ||
wrapper = render(<HvTimeAgo />); | ||
|
||
const component = wrapper.getByText(EM_DASH); | ||
expect(component).toBeVisible(); | ||
}); | ||
|
||
it("should render the custom emptyElement", () => { | ||
const MOCK_EMPTY = "EMPTY"; | ||
wrapper = render(<HvTimeAgo emptyElement={MOCK_EMPTY} />); | ||
|
||
const component = wrapper.getByText(MOCK_EMPTY); | ||
expect(component).toBeVisible(); | ||
}); | ||
}); | ||
|
||
describe("TimeAgo with timestamp", () => { | ||
const timestamp = Date.now(); | ||
|
||
it("should be defined", () => { | ||
const { container } = render(<HvTimeAgo timestamp={timestamp} />); | ||
|
||
expect(container).toBeDefined(); | ||
}); | ||
|
||
it("should contain the relative time", () => { | ||
const { getByText } = render(<HvTimeAgo timestamp={timestamp} />); | ||
|
||
const component = getByText("MOCK_TIME_AGO"); | ||
expect(component).toBeVisible(); | ||
}); | ||
}); | ||
|
||
describe("TimeAgo with custom Button element", () => { | ||
const timestamp = Date.now(); | ||
|
||
it("should be defined", () => { | ||
const { container } = render( | ||
<HvTimeAgo timestamp={timestamp} component={HvButton} /> | ||
); | ||
|
||
expect(container).toBeDefined(); | ||
}); | ||
|
||
it("should render the Button", () => { | ||
const { getByRole } = render( | ||
<HvTimeAgo timestamp={timestamp} component={HvButton} /> | ||
); | ||
|
||
const component = getByRole("button"); | ||
expect(component).toBeVisible(); | ||
}); | ||
}); |
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,74 @@ | ||
import { HvTypography } from "@core/components"; | ||
import { HvBaseProps } from "@core/types"; | ||
import { clsx } from "clsx"; | ||
import isEmpty from "lodash/isEmpty"; | ||
import timeAgoClasses, { HvTimeAgoClasses } from "./timeAgoClasses"; | ||
import useTimeAgo from "./useTimeAgo"; | ||
|
||
export interface HvTimeAgoProps extends HvBaseProps<HTMLElement, { children }> { | ||
/** | ||
* The timestamp to format, in seconds or milliseconds. | ||
* Defaults to `emptyElement` if value is null or 0 | ||
*/ | ||
timestamp?: number; | ||
/** | ||
* The locale to be used. Should be on of the dayjs supported locales and explicitly imported | ||
* @see https://day.js.org/docs/en/i18n/i18n | ||
*/ | ||
locale?: string; | ||
/** | ||
* The component used for the root node. Either a string to use a HTML element or a component. | ||
* Defaults to `HvTypography`. | ||
*/ | ||
component?: React.ElementType<React.HTMLAttributes<HTMLElement>>; | ||
/** | ||
* The element to render when the timestamp is null or 0 | ||
* Defaults to `—` (Em Dash) | ||
*/ | ||
emptyElement?: React.ReactNode; | ||
/** | ||
* Disables periodic date refreshes | ||
*/ | ||
disableRefresh?: boolean; | ||
/** | ||
* Whether to show seconds in the rendered time | ||
*/ | ||
showSeconds?: boolean; | ||
/** | ||
* Whether the component should render just the string | ||
* Consider using `useTimeAgo` instead | ||
*/ | ||
justText?: boolean; | ||
/** A Jss Object used to override or extend the styles applied to the component. */ | ||
classes?: HvTimeAgoClasses; | ||
} | ||
|
||
/** | ||
* The HvTimeAgo component implements the Design System relative time format guidelines. | ||
*/ | ||
export const HvTimeAgo = ({ | ||
classes, | ||
timestamp, | ||
locale: localeProp = "en", | ||
component: Component = HvTypography, | ||
emptyElement = "—", | ||
disableRefresh = false, | ||
showSeconds = false, | ||
justText = false, | ||
...others | ||
}: HvTimeAgoProps) => { | ||
const locale = isEmpty(localeProp) ? "en" : localeProp; | ||
const timeAgo = useTimeAgo(timestamp, { | ||
locale, | ||
disableRefresh, | ||
showSeconds, | ||
}); | ||
|
||
if (justText && timestamp) return <>{timeAgo}</>; | ||
|
||
return ( | ||
<Component className={clsx(classes?.root, timeAgoClasses.root)} {...others}> | ||
{!timestamp ? emptyElement : timeAgo} | ||
</Component> | ||
); | ||
}; |
Oops, something went wrong.