mirror of
https://github.com/vector-im/element-web.git
synced 2026-05-05 20:26:19 +02:00
Migrate DateSeperator to shared components
This commit is contained in:
parent
dcf3e536ab
commit
58bd72f4c9
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
* Copyright 2017 Vector Creations Ltd
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
.dateSeparator {
|
||||
clear: both;
|
||||
margin: 4px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font: var(--cpd-font-body-md-regular);
|
||||
color: var(--cpd-color-text-primary);
|
||||
}
|
||||
|
||||
.dateSeparator > hr {
|
||||
flex: 1 1 0;
|
||||
height: 0;
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--cpd-color-gray-400);
|
||||
}
|
||||
|
||||
.dateContent {
|
||||
padding: 0 25px;
|
||||
}
|
||||
|
||||
.dateHeading {
|
||||
flex: 0 0 auto;
|
||||
margin: 0;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
color: inherit;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import { DateSeparator } from "./DateSeparator";
|
||||
|
||||
const now = Date.now();
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
const meta: Meta<typeof DateSeparator> = {
|
||||
title: "Event Tiles/DateSeparator",
|
||||
component: DateSeparator,
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
locale: "en",
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof DateSeparator>;
|
||||
|
||||
export const Today: Story = {
|
||||
args: {
|
||||
ts: now,
|
||||
},
|
||||
};
|
||||
|
||||
export const Yesterday: Story = {
|
||||
args: {
|
||||
ts: now - DAY_MS,
|
||||
},
|
||||
};
|
||||
|
||||
export const LastWeek: Story = {
|
||||
args: {
|
||||
ts: now - 4 * DAY_MS,
|
||||
},
|
||||
};
|
||||
|
||||
export const LongAgo: Story = {
|
||||
args: {
|
||||
ts: now - 365 * DAY_MS,
|
||||
},
|
||||
};
|
||||
|
||||
export const DisableRelativeTimestamps: Story = {
|
||||
args: {
|
||||
ts: now,
|
||||
disableRelativeTimestamps: true,
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { render } from "jest-matrix-react";
|
||||
import React from "react";
|
||||
|
||||
import { DateSeparator } from "./DateSeparator";
|
||||
|
||||
describe("DateSeparator", () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
// Set a fixed "now" time for consistent testing
|
||||
jest.setSystemTime(new Date("2024-11-03T12:00:00Z"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("renders today's date", () => {
|
||||
const { container } = render(<DateSeparator ts={new Date("2024-11-03T10:00:00Z").getTime()} locale="en" />);
|
||||
expect(container).toMatchSnapshot();
|
||||
expect(container.textContent).toContain("today");
|
||||
});
|
||||
|
||||
it("renders yesterday's date", () => {
|
||||
const { container } = render(<DateSeparator ts={new Date("2024-11-02T10:00:00Z").getTime()} locale="en" />);
|
||||
expect(container).toMatchSnapshot();
|
||||
expect(container.textContent).toContain("yesterday");
|
||||
});
|
||||
|
||||
it("renders a weekday for dates within the last 6 days", () => {
|
||||
// 4 days ago
|
||||
const { container } = render(<DateSeparator ts={new Date("2024-10-30T10:00:00Z").getTime()} locale="en" />);
|
||||
expect(container).toMatchSnapshot();
|
||||
// Should show a day name like "Wednesday"
|
||||
expect(container.querySelector(".mx_DateSeparator_dateHeading")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders full date for dates older than 6 days", () => {
|
||||
const { container } = render(<DateSeparator ts={new Date("2024-10-01T10:00:00Z").getTime()} locale="en" />);
|
||||
expect(container).toMatchSnapshot();
|
||||
expect(container.textContent).toContain("Oct");
|
||||
});
|
||||
|
||||
it("renders full date when relative timestamps are disabled", () => {
|
||||
const { container } = render(
|
||||
<DateSeparator ts={new Date("2024-11-03T10:00:00Z").getTime()} locale="en" disableRelativeTimestamps />,
|
||||
);
|
||||
expect(container).toMatchSnapshot();
|
||||
// Should show full date even though it's today
|
||||
expect(container.textContent).toContain("Nov");
|
||||
});
|
||||
|
||||
it("applies custom className", () => {
|
||||
const { container } = render(
|
||||
<DateSeparator ts={Date.now()} locale="en" className="custom-class" />,
|
||||
);
|
||||
expect(container.querySelector(".mx_DateSeparator.custom-class")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("has correct ARIA attributes", () => {
|
||||
const { container } = render(<DateSeparator ts={Date.now()} locale="en" />);
|
||||
const separator = container.querySelector('[role="separator"]');
|
||||
expect(separator).toBeTruthy();
|
||||
expect(separator?.getAttribute("aria-label")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
* Copyright 2015-2021 The Matrix.org Foundation C.I.C.
|
||||
* Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { _t } from "../../utils/i18n";
|
||||
import { formatFullDateNoTime, getDaysArray, DAY_MS } from "../../utils/DateUtils";
|
||||
|
||||
import styles from "./DateSeparator.module.css";
|
||||
|
||||
export interface Props {
|
||||
/** The timestamp (in milliseconds) to display */
|
||||
ts: number;
|
||||
/** The locale to use for formatting. Defaults to "en" */
|
||||
locale?: string;
|
||||
/** Whether to disable relative timestamps (e.g., "Today", "Yesterday"). If true, always shows full date */
|
||||
disableRelativeTimestamps?: boolean;
|
||||
/** Additional CSS class name */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Timeline separator component to render within a MessagePanel bearing the date of the ts given
|
||||
*/
|
||||
export class DateSeparator extends React.Component<Props> {
|
||||
private get relativeTimeFormat(): Intl.RelativeTimeFormat {
|
||||
return new Intl.RelativeTimeFormat(this.props.locale ?? "en", { style: "long", numeric: "auto" });
|
||||
}
|
||||
|
||||
public getLabel(): string {
|
||||
try {
|
||||
const date = new Date(this.props.ts);
|
||||
const { disableRelativeTimestamps = false, locale = "en" } = this.props;
|
||||
|
||||
// If relative timestamps are disabled, return the full date
|
||||
if (disableRelativeTimestamps) return formatFullDateNoTime(date, locale);
|
||||
|
||||
const today = new Date();
|
||||
const yesterday = new Date();
|
||||
const days = getDaysArray("long", locale);
|
||||
yesterday.setDate(today.getDate() - 1);
|
||||
|
||||
if (date.toDateString() === today.toDateString()) {
|
||||
return this.relativeTimeFormat.format(0, "day"); // Today
|
||||
} else if (date.toDateString() === yesterday.toDateString()) {
|
||||
return this.relativeTimeFormat.format(-1, "day"); // Yesterday
|
||||
} else if (today.getTime() - date.getTime() < 6 * DAY_MS) {
|
||||
return days[date.getDay()]; // Sunday-Saturday
|
||||
} else {
|
||||
return formatFullDateNoTime(date, locale);
|
||||
}
|
||||
} catch {
|
||||
return _t("common|message_timestamp_invalid");
|
||||
}
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const label = this.getLabel();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles.dateSeparator, "mx_DateSeparator", this.props.className)}
|
||||
role="separator"
|
||||
aria-label={label}
|
||||
>
|
||||
<hr role="none" />
|
||||
<div className={classNames(styles.dateContent, "mx_DateSeparator_dateContent")}>
|
||||
<h2 className={classNames(styles.dateHeading, "mx_DateSeparator_dateHeading")} aria-hidden="true">
|
||||
{label}
|
||||
</h2>
|
||||
</div>
|
||||
<hr role="none" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,136 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`DateSeparator renders a weekday for dates within the last 6 days 1`] = `
|
||||
<div>
|
||||
<div
|
||||
aria-label="Wednesday"
|
||||
class="dateSeparator mx_DateSeparator"
|
||||
role="separator"
|
||||
>
|
||||
<hr
|
||||
role="none"
|
||||
/>
|
||||
<div
|
||||
class="dateContent mx_DateSeparator_dateContent"
|
||||
>
|
||||
<h2
|
||||
aria-hidden="true"
|
||||
class="dateHeading mx_DateSeparator_dateHeading"
|
||||
>
|
||||
Wednesday
|
||||
</h2>
|
||||
</div>
|
||||
<hr
|
||||
role="none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DateSeparator renders full date for dates older than 6 days 1`] = `
|
||||
<div>
|
||||
<div
|
||||
aria-label="Tue, Oct 1, 2024"
|
||||
class="dateSeparator mx_DateSeparator"
|
||||
role="separator"
|
||||
>
|
||||
<hr
|
||||
role="none"
|
||||
/>
|
||||
<div
|
||||
class="dateContent mx_DateSeparator_dateContent"
|
||||
>
|
||||
<h2
|
||||
aria-hidden="true"
|
||||
class="dateHeading mx_DateSeparator_dateHeading"
|
||||
>
|
||||
Tue, Oct 1, 2024
|
||||
</h2>
|
||||
</div>
|
||||
<hr
|
||||
role="none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DateSeparator renders full date when relative timestamps are disabled 1`] = `
|
||||
<div>
|
||||
<div
|
||||
aria-label="Sun, Nov 3, 2024"
|
||||
class="dateSeparator mx_DateSeparator"
|
||||
role="separator"
|
||||
>
|
||||
<hr
|
||||
role="none"
|
||||
/>
|
||||
<div
|
||||
class="dateContent mx_DateSeparator_dateContent"
|
||||
>
|
||||
<h2
|
||||
aria-hidden="true"
|
||||
class="dateHeading mx_DateSeparator_dateHeading"
|
||||
>
|
||||
Sun, Nov 3, 2024
|
||||
</h2>
|
||||
</div>
|
||||
<hr
|
||||
role="none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DateSeparator renders today's date 1`] = `
|
||||
<div>
|
||||
<div
|
||||
aria-label="today"
|
||||
class="dateSeparator mx_DateSeparator"
|
||||
role="separator"
|
||||
>
|
||||
<hr
|
||||
role="none"
|
||||
/>
|
||||
<div
|
||||
class="dateContent mx_DateSeparator_dateContent"
|
||||
>
|
||||
<h2
|
||||
aria-hidden="true"
|
||||
class="dateHeading mx_DateSeparator_dateHeading"
|
||||
>
|
||||
today
|
||||
</h2>
|
||||
</div>
|
||||
<hr
|
||||
role="none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DateSeparator renders yesterday's date 1`] = `
|
||||
<div>
|
||||
<div
|
||||
aria-label="yesterday"
|
||||
class="dateSeparator mx_DateSeparator"
|
||||
role="separator"
|
||||
>
|
||||
<hr
|
||||
role="none"
|
||||
/>
|
||||
<div
|
||||
class="dateContent mx_DateSeparator_dateContent"
|
||||
>
|
||||
<h2
|
||||
aria-hidden="true"
|
||||
class="dateHeading mx_DateSeparator_dateHeading"
|
||||
>
|
||||
yesterday
|
||||
</h2>
|
||||
</div>
|
||||
<hr
|
||||
role="none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -0,0 +1,9 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
export { DateSeparator } from "./DateSeparator";
|
||||
export type { Props as DateSeparatorProps } from "./DateSeparator";
|
||||
@ -11,6 +11,7 @@ export * from "./audio/Clock";
|
||||
export * from "./audio/PlayPauseButton";
|
||||
export * from "./audio/SeekBar";
|
||||
export * from "./avatar/AvatarWithDetails";
|
||||
export * from "./event-tiles/DateSeparator";
|
||||
export * from "./event-tiles/TextualEventView";
|
||||
export * from "./message-body/MediaBody";
|
||||
export * from "./pill-input/Pill";
|
||||
|
||||
@ -5,6 +5,34 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
export const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Returns array of 7 weekday names, from Sunday to Saturday, internationalised to the given locale.
|
||||
* @param weekday - format desired "long" | "short" | "narrow"
|
||||
* @param locale - the locale string to use, defaults to "en"
|
||||
*/
|
||||
export function getDaysArray(weekday: Intl.DateTimeFormatOptions["weekday"] = "short", locale = "en"): string[] {
|
||||
const sunday = 1672574400000; // 2023-01-01 12:00 UTC
|
||||
const { format } = new Intl.DateTimeFormat(locale, { weekday, timeZone: "UTC" });
|
||||
return [...Array(7).keys()].map((day) => format(sunday + day * DAY_MS));
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a given date to a human-friendly string with short weekday.
|
||||
* @example "Thu, 17 Nov 2022" in en-GB locale
|
||||
* @param date - date object to format
|
||||
* @param locale - the locale string to use, defaults to "en"
|
||||
*/
|
||||
export function formatFullDateNoTime(date: Date, locale = "en"): string {
|
||||
return new Intl.DateTimeFormat(locale, {
|
||||
weekday: "short",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a number of seconds into a human-readable string.
|
||||
* @param inSeconds
|
||||
|
||||
@ -6,6 +6,22 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
.mx_DateSeparator {
|
||||
clear: both;
|
||||
margin: 4px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font: var(--cpd-font-body-md-regular);
|
||||
color: var(--cpd-color-text-primary);
|
||||
}
|
||||
|
||||
.mx_DateSeparator > hr {
|
||||
flex: 1 1 0;
|
||||
height: 0;
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--cpd-color-gray-400);
|
||||
}
|
||||
|
||||
.mx_DateSeparator_dateContent {
|
||||
padding: 0 25px;
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import React, { type JSX } from "react";
|
||||
import { Direction, ConnectionError, MatrixError, HTTPError } from "matrix-js-sdk/src/matrix";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { capitalize } from "lodash";
|
||||
import { DateSeparator as SharedDateSeparator } from "@element-hq/web-shared-components";
|
||||
|
||||
import { _t, getUserLanguage } from "../../../languageHandler";
|
||||
import { formatFullDateNoDay, formatFullDateNoTime, getDaysArray } from "../../../DateUtils";
|
||||
@ -31,7 +32,6 @@ import IconizedContextMenu, {
|
||||
} from "../context_menus/IconizedContextMenu";
|
||||
import JumpToDatePicker from "./JumpToDatePicker";
|
||||
import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import TimelineSeparator from "./TimelineSeparator";
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
|
||||
interface IProps {
|
||||
@ -267,7 +267,7 @@ export default class DateSeparator extends React.Component<IProps, IState> {
|
||||
this.closeMenu();
|
||||
};
|
||||
|
||||
private renderJumpToDateMenu(): React.ReactElement {
|
||||
private renderJumpToDateMenu(label: string): React.ReactElement {
|
||||
let contextMenu: JSX.Element | undefined;
|
||||
if (this.state.contextMenuPosition) {
|
||||
const relativeTimeFormat = this.relativeTimeFormat;
|
||||
@ -310,7 +310,7 @@ export default class DateSeparator extends React.Component<IProps, IState> {
|
||||
title={_t("room|jump_to_date")}
|
||||
>
|
||||
<h2 className="mx_DateSeparator_dateHeading" aria-hidden="true">
|
||||
{this.getLabel()}
|
||||
{label}
|
||||
</h2>
|
||||
<div className="mx_DateSeparator_chevron" />
|
||||
{contextMenu}
|
||||
@ -319,21 +319,29 @@ export default class DateSeparator extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const label = this.getLabel();
|
||||
const disableRelativeTimestamps = !SettingsStore.getValue(UIFeature.TimelineEnableRelativeDates);
|
||||
|
||||
let dateHeaderContent: JSX.Element;
|
||||
// If jump to date is enabled and we're not exporting, we need to wrap the content
|
||||
// in our custom jump-to-date menu button
|
||||
if (this.state.jumpToDateEnabled && !this.props.forExport) {
|
||||
dateHeaderContent = this.renderJumpToDateMenu();
|
||||
} else {
|
||||
dateHeaderContent = (
|
||||
<div className="mx_DateSeparator_dateContent">
|
||||
<h2 className="mx_DateSeparator_dateHeading" aria-hidden="true">
|
||||
{label}
|
||||
</h2>
|
||||
const label = this.getLabel();
|
||||
|
||||
return (
|
||||
<div className="mx_DateSeparator" role="separator" aria-label={label}>
|
||||
<hr role="none" />
|
||||
{this.renderJumpToDateMenu(label)}
|
||||
<hr role="none" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <TimelineSeparator label={label}>{dateHeaderContent}</TimelineSeparator>;
|
||||
// Otherwise, just use the shared component directly
|
||||
return (
|
||||
<SharedDateSeparator
|
||||
ts={this.props.ts}
|
||||
locale={getUserLanguage()}
|
||||
disableRelativeTimestamps={this.props.forExport || disableRelativeTimestamps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,21 +1,21 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DateSeparator renders invalid date separator correctly 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
aria-label="Invalid timestamp"
|
||||
class="mx_TimelineSeparator"
|
||||
class="_dateSeparator_15xjf_9 mx_DateSeparator"
|
||||
role="separator"
|
||||
>
|
||||
<hr
|
||||
role="none"
|
||||
/>
|
||||
<div
|
||||
class="mx_DateSeparator_dateContent"
|
||||
class="_dateContent_15xjf_25 mx_DateSeparator_dateContent"
|
||||
>
|
||||
<h2
|
||||
aria-hidden="true"
|
||||
class="mx_DateSeparator_dateHeading"
|
||||
class="_dateHeading_15xjf_29 mx_DateSeparator_dateHeading"
|
||||
>
|
||||
Invalid timestamp
|
||||
</h2>
|
||||
@ -31,18 +31,18 @@ exports[`DateSeparator renders the date separator correctly 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
aria-label="today"
|
||||
class="mx_TimelineSeparator"
|
||||
class="_dateSeparator_15xjf_9 mx_DateSeparator"
|
||||
role="separator"
|
||||
>
|
||||
<hr
|
||||
role="none"
|
||||
/>
|
||||
<div
|
||||
class="mx_DateSeparator_dateContent"
|
||||
class="_dateContent_15xjf_25 mx_DateSeparator_dateContent"
|
||||
>
|
||||
<h2
|
||||
aria-hidden="true"
|
||||
class="mx_DateSeparator_dateHeading"
|
||||
class="_dateHeading_15xjf_29 mx_DateSeparator_dateHeading"
|
||||
>
|
||||
today
|
||||
</h2>
|
||||
@ -58,7 +58,7 @@ exports[`DateSeparator when feature_jump_to_date is enabled renders the date sep
|
||||
<DocumentFragment>
|
||||
<div
|
||||
aria-label="Fri, Dec 17, 2021"
|
||||
class="mx_TimelineSeparator"
|
||||
class="mx_DateSeparator"
|
||||
role="separator"
|
||||
>
|
||||
<hr
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user