Skip to content

Commit

Permalink
feat(TimeAgo): add TimeAgo component to the core package
Browse files Browse the repository at this point in the history
  • Loading branch information
Paulo Lagoá committed May 4, 2023
1 parent 6a771d6 commit 54d547b
Show file tree
Hide file tree
Showing 11 changed files with 536 additions and 1 deletion.
8 changes: 7 additions & 1 deletion .config/applitools.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
const isExcludedPath = (kind) => {
const excludePaths = ["Overview/", "Foundation/", "Guides/", "Templates/"];
const excludePaths = [
"Overview/",
"Foundation/",
"Guides/",
"Templates/",
"Components/Time Ago",
];

return excludePaths.some((p) => kind.includes(p));
};
Expand Down
4 changes: 4 additions & 0 deletions docs/overview/status/data.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@
"name": "Text Area",
"status": ""
},
{
"name": "Time Ago",
"status": ""
},
{
"name": "Time Picker",
"status": "in progress"
Expand Down
162 changes: 162 additions & 0 deletions packages/core/src/components/TimeAgo/TimeAgo.stories.tsx
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)",
},
},
};
75 changes: 75 additions & 0 deletions packages/core/src/components/TimeAgo/TimeAgo.test.tsx
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();
});
});
74 changes: 74 additions & 0 deletions packages/core/src/components/TimeAgo/TimeAgo.tsx
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>
);
};

0 comments on commit 54d547b

Please sign in to comment.