-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Refactor TimelineSeparator to shared-components #31797
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
a9b0abf
026a027
51a2480
fb786f5
8ac830f
cf8741f
1340a62
1149289
790ae5f
0b8b5e8
5f34279
021c699
5597f78
3b9a304
ea77d3b
bb0a19e
3b7868e
2d3908a
9274256
645838a
a06a41d
8b55a91
b4563c6
43f7f27
a558b16
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| /* | ||
| * Copyright 2026 Element 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. | ||
| */ | ||
|
|
||
| .timelineSeparator { | ||
| clear: both; | ||
| margin: var(--cpdSpace1X, var(--cpdSpace0X)); | ||
| display: flex; | ||
| align-items: center; | ||
| font: var(--cpd-font-body-md-regular); | ||
| color: var(--cpd-color-text-primary); | ||
| } | ||
|
|
||
| .timelineSeparator > hr { | ||
| flex: 1 1 0; | ||
| height: 0; | ||
| border: none; | ||
| border-bottom: 1px solid var(--cpd-color-gray-400); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| /* | ||
| * Copyright 2026 Element 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. | ||
| */ | ||
|
|
||
| import React, { type JSX } from "react"; | ||
|
|
||
| import type { Meta, StoryFn } from "@storybook/react-vite"; | ||
| import { TimelineSeparatorView, type TimelineSeparatorViewSnapshot } from "./TimelineSeparatorView"; | ||
| import { useMockedViewModel } from "../../useMockedViewModel"; | ||
| import styles from "./TimelineSeparatorView.module.css"; | ||
|
|
||
| type TimelineSeparatorProps = TimelineSeparatorViewSnapshot; | ||
| const TimelineSeparatorViewWrapper = (props: TimelineSeparatorProps): JSX.Element => { | ||
| // There is no action (undefined second param) | ||
| const vm = useMockedViewModel(props, undefined); | ||
| return <TimelineSeparatorView vm={vm} />; | ||
| }; | ||
|
|
||
| export default { | ||
| title: "MessageBody/TimelineSeparatorView", | ||
| component: TimelineSeparatorViewWrapper, | ||
| tags: ["autodocs"], | ||
| args: { | ||
| label: "Label Separator", | ||
| children: "Timeline Separator", | ||
| }, | ||
| } as Meta<typeof TimelineSeparatorViewWrapper>; | ||
|
|
||
| const Template: StoryFn<typeof TimelineSeparatorViewWrapper> = (args: any) => <TimelineSeparatorViewWrapper {...args} />; | ||
|
|
||
| export const Default = Template.bind({}); | ||
|
|
||
| export const WithHtmlChild = Template.bind({}); | ||
| WithHtmlChild.args = { | ||
| label: "Custom Label", | ||
| children: <h2 className={styles.timelineSeparator} aria-hidden="true">Thursday</h2>, | ||
| }; | ||
|
|
||
| export const WithDateEvent = Template.bind({}); | ||
| WithDateEvent.args = { | ||
| label: "Date Event Separator", | ||
| children: "Wednesday", | ||
| }; | ||
|
|
||
| export const WithLateEvent = Template.bind({}); | ||
| WithLateEvent.args = { | ||
| label: "Late Event Separator", | ||
| children: "Fri, Jan 9, 2026", | ||
| }; | ||
|
|
||
| export const WithoutChildren = Template.bind({}); | ||
| WithoutChildren.args = { | ||
| children: undefined, | ||
| label: "Separator without children", | ||
| }; | ||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| /* | ||
| * Copyright 2026 Element 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. | ||
| */ | ||
|
|
||
| import { render } from "jest-matrix-react"; | ||
| import { composeStories } from "@storybook/react-vite"; | ||
| import React from "react"; | ||
|
|
||
| import * as stories from "./TimelineSeparatorView.stories.tsx"; | ||
|
|
||
| const { Default, WithHtmlChild, WithoutChildren, WithDateEvent, WithLateEvent } = composeStories(stories); | ||
|
|
||
| describe("TimelineSeparatorView", () => { | ||
| afterEach(() => { | ||
| jest.clearAllMocks(); | ||
| }); | ||
|
|
||
| describe("Snapshot tests", () => { | ||
| it("renders the timeline separator in default state", () => { | ||
| const { container } = render(<Default />); | ||
| expect(container).toMatchSnapshot(); | ||
| }); | ||
|
|
||
| it("renders the timeline separator with HTML child", () => { | ||
| const { container } = render(<WithHtmlChild />); | ||
| expect(container).toMatchSnapshot(); | ||
| }); | ||
|
|
||
| it("renders the timeline separator without children", () => { | ||
| const { container } = render(<WithDateEvent />); | ||
| expect(container).toMatchSnapshot(); | ||
| }); | ||
|
|
||
| it("renders the timeline separator without children", () => { | ||
| const { container } = render(<WithLateEvent />); | ||
| expect(container).toMatchSnapshot(); | ||
| }); | ||
| it("renders the timeline separator without children", () => { | ||
| const { container } = render(<WithoutChildren />); | ||
| expect(container).toMatchSnapshot(); | ||
| }); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| /* | ||
| * Copyright 2026 Element 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. | ||
| */ | ||
|
|
||
| import { PropsWithChildren, type JSX } from "react"; | ||
| import React from "react"; | ||
| import classNames from "classnames"; | ||
|
|
||
| import { type ViewModel } from "../../viewmodel/ViewModel"; | ||
| import { useViewModel } from "../../useViewModel"; | ||
| import styles from "./TimelineSeparatorView.module.css"; | ||
|
|
||
|
|
||
| /** | ||
| * Snapshot interface for the timeline separator view model. | ||
| */ | ||
| export interface TimelineSeparatorViewSnapshot { | ||
| /** | ||
| * Accessible label for the separator (for example: "Today", "Yesterday", or a date). | ||
| */ | ||
| label: string; | ||
| /** | ||
| * Optional children to render inside the timeline separator | ||
| */ | ||
| children?: PropsWithChildren["children"]; | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it be fine using ["children"]; |
||
| } | ||
|
|
||
| /** | ||
| * The view model for the timeline separator. | ||
| */ | ||
| export type TimelineSeparatorViewModel = ViewModel<TimelineSeparatorViewSnapshot>; | ||
|
|
||
| interface TimelineSeparatorViewProps { | ||
| /** | ||
| * The view model for the timeline separator. | ||
| */ | ||
| vm: TimelineSeparatorViewModel; | ||
| } | ||
|
|
||
| /** | ||
| * TimelineSeparatorView component renders a visual separator inside the message timeline. | ||
| * It draws horizontal rules with an accessible label and optional children rendered between them. | ||
| * | ||
| * @param label the accessible label string describing the separator (used for `aria-label`) | ||
| * @param children optional React nodes to render between the separators | ||
| * | ||
| */ | ||
|
Comment on lines
+44
to
+50
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @florianduros Does this look better? If not, what do you suggest? |
||
| export function TimelineSeparatorView({ vm }: Readonly<TimelineSeparatorViewProps>): JSX.Element { | ||
| const { | ||
| label, children, | ||
| } = useViewModel(vm); | ||
|
|
||
| // Keep mx_TimelineSeparator to support the compatibility with existing timeline and the all the layout | ||
| return ( | ||
| <div className={classNames("mx_TimelineSeparator", styles.timelineSeparator)} role="separator" aria-label={label}> | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. {classNames("mx_TimelineSeparator", styles.timelineSeparator)} I would like to hear your opinion regarding this, this is like to comment says, to "Keep mx_TimelineSeparator to support the compatibility with existing timeline and the all the layout" |
||
| <hr role="none" /> | ||
| {children} | ||
| <hr role="none" /> | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing | ||
|
|
||
| exports[`TimelineSeparatorView Snapshot tests renders the timeline separator in default state 1`] = ` | ||
| <div> | ||
| <div | ||
| aria-label="Label Separator" | ||
| class="mx_TimelineSeparator timelineSeparator" | ||
| role="separator" | ||
| > | ||
| <hr | ||
| role="none" | ||
| /> | ||
| Timeline Separator | ||
| <hr | ||
| role="none" | ||
| /> | ||
| </div> | ||
| </div> | ||
| `; | ||
|
|
||
| exports[`TimelineSeparatorView Snapshot tests renders the timeline separator with HTML child 1`] = ` | ||
| <div> | ||
| <div | ||
| aria-label="Custom Label" | ||
| class="mx_TimelineSeparator timelineSeparator" | ||
| role="separator" | ||
| > | ||
| <hr | ||
| role="none" | ||
| /> | ||
| <h2 | ||
| aria-hidden="true" | ||
| class="timelineSeparator" | ||
| > | ||
| Thursday | ||
| </h2> | ||
| <hr | ||
| role="none" | ||
| /> | ||
| </div> | ||
| </div> | ||
| `; | ||
|
|
||
| exports[`TimelineSeparatorView Snapshot tests renders the timeline separator without children 1`] = ` | ||
| <div> | ||
| <div | ||
| aria-label="Date Event Separator" | ||
| class="mx_TimelineSeparator timelineSeparator" | ||
| role="separator" | ||
| > | ||
| <hr | ||
| role="none" | ||
| /> | ||
| Wednesday | ||
| <hr | ||
| role="none" | ||
| /> | ||
| </div> | ||
| </div> | ||
| `; | ||
|
|
||
| exports[`TimelineSeparatorView Snapshot tests renders the timeline separator without children 2`] = ` | ||
| <div> | ||
| <div | ||
| aria-label="Late Event Separator" | ||
| class="mx_TimelineSeparator timelineSeparator" | ||
| role="separator" | ||
| > | ||
| <hr | ||
| role="none" | ||
| /> | ||
| Fri, Jan 9, 2026 | ||
| <hr | ||
| role="none" | ||
| /> | ||
| </div> | ||
| </div> | ||
| `; | ||
|
|
||
| exports[`TimelineSeparatorView Snapshot tests renders the timeline separator without children 3`] = ` | ||
| <div> | ||
| <div | ||
| aria-label="Separator without children" | ||
| class="mx_TimelineSeparator timelineSeparator" | ||
| role="separator" | ||
| > | ||
| <hr | ||
| role="none" | ||
| /> | ||
| <hr | ||
| role="none" | ||
| /> | ||
| </div> | ||
| </div> | ||
| `; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| /* | ||
| * Copyright 2026 Element 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. | ||
| */ | ||
|
|
||
| export { TimelineSeparatorView, type TimelineSeparatorViewSnapshot, type TimelineSeparatorViewModel } from "./TimelineSeparatorView"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like this?