Skip to content

Timeline interactive #4336

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

Merged
merged 8 commits into from
Jun 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/fuzzy-lobsters-wait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@twilio-paste/timeline": minor
"@twilio-paste/core": minor
---

[Timeline] add a new prop called `onClick` to `TimelineItem` that makes the title interactive
6 changes: 6 additions & 0 deletions packages/paste-core/components/timeline/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,17 @@
"@twilio-paste/detail-text": "^4.0.0",
"@twilio-paste/disclosure-primitive": "^3.0.0",
"@twilio-paste/icons": "^13.0.0",
"@twilio-paste/paragraph": "^11.0.1",
"@twilio-paste/reakit-library": "^3.0.0",
"@twilio-paste/skeleton-loader": "^7.0.1",
"@twilio-paste/spinner": "^15.0.0",
"@twilio-paste/stack": "^9.0.0",
"@twilio-paste/style-props": "^10.0.0",
"@twilio-paste/styling-library": "^4.0.0",
"@twilio-paste/summary-detail": "^2.0.0",
"@twilio-paste/text": "^11.0.0",
"@twilio-paste/theme": "^12.0.0",
"@twilio-paste/truncate": "^15.0.1",
"@twilio-paste/types": "^7.0.0",
"@twilio-paste/uid-library": "^3.0.0",
"@types/react": "^17.0.2 || ^18.0.27 || ^19.0.0",
Expand All @@ -63,14 +66,17 @@
"@twilio-paste/detail-text": "^4.0.1",
"@twilio-paste/disclosure-primitive": "^3.0.1",
"@twilio-paste/icons": "^13.0.1",
"@twilio-paste/paragraph": "^11.0.1",
"@twilio-paste/reakit-library": "^3.0.1",
"@twilio-paste/skeleton-loader": "^7.0.1",
"@twilio-paste/spinner": "^15.0.1",
"@twilio-paste/stack": "^9.0.1",
"@twilio-paste/style-props": "^10.0.1",
"@twilio-paste/styling-library": "^4.0.1",
"@twilio-paste/summary-detail": "^2.0.1",
"@twilio-paste/text": "^11.0.1",
"@twilio-paste/theme": "^12.0.1",
"@twilio-paste/truncate": "^15.0.1",
"@twilio-paste/types": "^7.0.1",
"@twilio-paste/uid-library": "^3.0.1",
"@types/react": "^19.0.8",
Expand Down
64 changes: 50 additions & 14 deletions packages/paste-core/components/timeline/src/TimelineItem.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Box } from "@twilio-paste/box";
import { Button } from "@twilio-paste/button";
import { css, styled } from "@twilio-paste/styling-library";
import { Truncate } from "@twilio-paste/truncate";
import React from "react";

import { TimelineGroupContext } from "./TimelineContext";
Expand All @@ -9,7 +11,18 @@ import type { TimelineItemProps } from "./types";

const TimelineItem = React.forwardRef<HTMLLIElement, TimelineItemProps>(
(
{ children, icon, timestamp, title, collapsible = false, collapsibleHeading, element = "TIMELINE_ITEM", ...props },
{
children,
icon,
timestamp,
title,
collapsible = false,
collapsibleHeading,
element = "TIMELINE_ITEM",
onClick,
disabled,
...props
},
ref,
) => {
const isGrouped = React.useContext(TimelineGroupContext);
Expand Down Expand Up @@ -62,19 +75,42 @@ const TimelineItem = React.forwardRef<HTMLLIElement, TimelineItemProps>(
flexDirection="column"
columnGap="space10"
paddingBottom="space60"
width="100%"
>
<Box
element={`${element}_TITLE`}
as="span"
color="colorText"
paddingY="space10"
fontWeight="fontWeightSemibold"
lineHeight="lineHeight20"
fontSize="fontSize30"
letterSpacing="-0.28px"
>
{title}
</Box>
{onClick ? (
<Button
element={`${element}_TITLE_INTERACTIVE`}
variant="reset"
size="reset"
onClick={onClick}
textDecoration="underline"
paddingY="space10"
fontWeight="fontWeightSemibold"
lineHeight="lineHeight20"
fontSize="fontSize30"
letterSpacing="-0.28px"
_hover={{ color: "colorTextLink" }}
_focus={{ color: "colorTextLink", boxShadow: "shadowFocus" }}
_disabled={{ color: "colorTextWeaker" }}
disabled={disabled}
display="block"
>
<Truncate title={title}>{title}</Truncate>
</Button>
) : (
<Box
element={`${element}_TITLE`}
as="span"
color="colorText"
paddingY="space10"
fontWeight="fontWeightSemibold"
lineHeight="lineHeight20"
fontSize="fontSize30"
letterSpacing="-0.28px"
>
{title}
</Box>
)}

{collapsible ? (
<TimelineItemCollapsible element={element} timestamp={timestamp ? timestamp : collapsibleHeading}>
Expand All @@ -94,7 +130,7 @@ const TimelineItem = React.forwardRef<HTMLLIElement, TimelineItemProps>(
{timestamp}
</Box>
) : null}
<Box element={`${element}_CONTENT`}>{children}</Box>
{children ? <Box element={`${element}_CONTENT`}>{children}</Box> : null}
</>
)}
</Box>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const TimelineItemGroup = React.forwardRef<HTMLLIElement, TimelineItemGro
<TimelineItemIcon icon={icon} />
</Box>

<Box element={`${element}_CONTENT_WRAPPER`}>
<Box element={`${element}_CONTENT_WRAPPER`} width="100%">
<Box element={`${element}_TIMESTAMP`} marginTop="space10" marginBottom="space30">
<DetailText element={`${element}_TIMESTAMP_DETAIL_TEXT`} marginTop="space0">
{timestamp}
Expand Down
18 changes: 18 additions & 0 deletions packages/paste-core/components/timeline/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,24 @@ export type TimelineItemProps = {
* @memberof TimelineItemProps
*/
element?: BoxProps["element"];

/**
* Add an action to the timeline item.
*
* @default undefined
* @type {React.MouseEventHandler<HTMLButtonElement>}
* @memberof TimelineItemProps
*/
onClick?: React.MouseEventHandler<HTMLButtonElement>;

/**
* If true, the timeline item will be disabled
*
* @type {boolean}
* @memberof TimelineItemProps
* @default false
*/
disabled?: boolean;
} & HTMLPasteProps<"li">;

export type TimelineItemIconProps = {
Expand Down
32 changes: 32 additions & 0 deletions packages/paste-core/components/timeline/stories/index.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,38 @@ export const TimelineGrouped = (): React.ReactNode => {
);
};

export const TimelineInteractive = (): React.ReactNode => {
return (
<Box maxWidth="300px">
<Timeline>
<TimelineItemGroup timestamp="Today – Wednesday, April 9, 2025">
<TimelineItem title="Invoice billing issue for March cycle resolved" timestamp="9:12 AM" onClick={() => {}} />
<TimelineItem
title="Clarification on service tier limits for standard accounts"
timestamp="11:26 AM"
onClick={() => {}}
/>
<TimelineItem title="New user onboarding questions and setup steps" timestamp="1:45 PM" onClick={() => {}} />
</TimelineItemGroup>
<TimelineItemGroup timestamp="Yesterday – Tuesday, April 8, 2025">
<TimelineItem
title="Account verification delay and next steps discussed"
timestamp="10:15 AM"
onClick={() => {}}
/>
<TimelineItem title="Custom domain setup assistance provided" timestamp="4:38 PM" onClick={() => {}} />
<TimelineItem
title="Request to add additional admins to workspace"
timestamp="6:09 PM"
onClick={() => {}}
disabled
/>
</TimelineItemGroup>
</Timeline>
</Box>
);
};

const allItems = [
{ date: "August 10, 2024", text: "Event title" },
{ date: "August 11, 2024", text: "Event title" },
Expand Down
28 changes: 22 additions & 6 deletions packages/paste-core/components/timeline/type-docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -2194,6 +2194,13 @@
"required": false,
"externalProp": true
},
"disabled": {
"type": "boolean",
"defaultValue": false,
"required": false,
"externalProp": false,
"description": "If true, the timeline item will be disabled"
},
"draggable": {
"type": "Booleanish",
"defaultValue": null,
Expand Down Expand Up @@ -2433,10 +2440,11 @@
"externalProp": true
},
"onClick": {
"type": "MouseEventHandler<HTMLLIElement>",
"defaultValue": null,
"type": "MouseEventHandler<HTMLButtonElement> &\n MouseEventHandler<HTMLLIElement>",
"defaultValue": "undefined",
"required": false,
"externalProp": true
"externalProp": false,
"description": "Add an action to the timeline item."
},
"onClickCapture": {
"type": "MouseEventHandler<HTMLLIElement>",
Expand Down Expand Up @@ -3923,6 +3931,13 @@
"required": false,
"externalProp": true
},
"disabled": {
"type": "boolean",
"defaultValue": false,
"required": false,
"externalProp": false,
"description": "If true, the timeline item will be disabled"
},
"draggable": {
"type": "Booleanish",
"defaultValue": null,
Expand Down Expand Up @@ -4162,10 +4177,11 @@
"externalProp": true
},
"onClick": {
"type": "MouseEventHandler<HTMLLIElement>",
"defaultValue": null,
"type": "MouseEventHandler<HTMLButtonElement> &\n MouseEventHandler<HTMLLIElement>",
"defaultValue": "undefined",
"required": false,
"externalProp": true
"externalProp": false,
"description": "Add an action to the timeline item."
},
"onClickCapture": {
"type": "MouseEventHandler<HTMLLIElement>",
Expand Down
35 changes: 35 additions & 0 deletions packages/paste-website/src/component-examples/TimelineExamples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,41 @@ const TimelineGrouped = () => {
render(<TimelineGrouped/>)
`.trim();

export const TimelineInteractiveTitle = `
const TimelineInteractiveTitle = () => {
return (
<Box maxWidth="300px">
<Timeline>
<TimelineItemGroup timestamp="Today – Wednesday, April 9, 2025">
<TimelineItem title="Invoice billing issue for March cycle resolved" timestamp="9:12 AM" onClick={() => {}} />
<TimelineItem
title="Clarification on service tier limits for standard accounts"
timestamp="11:26 AM"
onClick={() => {}}
/>
<TimelineItem title="New user onboarding questions and setup steps" timestamp="1:45 PM" onClick={() => {}} />
</TimelineItemGroup>
<TimelineItemGroup timestamp="Yesterday – Tuesday, April 8, 2025">
<TimelineItem
title="Account verification delay and next steps discussed"
timestamp="10:15 AM"
onClick={() => {}}
/>
<TimelineItem title="Custom domain setup assistance provided" timestamp="4:38 PM" onClick={() => {}} />
<TimelineItem
title="Request to add additional admins to workspace"
timestamp="6:09 PM"
onClick={() => {}}
disabled
/>
</TimelineItemGroup>
</Timeline>
</Box>
);
};
render(<TimelineInteractiveTitle/>)
`.trim();

export const TimelineComposition = `
const TimelineComposition = () => {
return (
Expand Down
25 changes: 25 additions & 0 deletions packages/paste-website/src/pages/components/timeline/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
TimelineComposition,
TimelineGrouped,
TimelineIcon,
TimelineInteractiveTitle,
TimelineStart,
TimelineTimestamp,
} from "../../../component-examples/TimelineExamples.ts";
Expand Down Expand Up @@ -89,6 +90,10 @@ Timelines can be used in different use cases, such as tracking milestones, monit
### Accessibility

<UnorderedList>
<ListItem>
The toggle button when using the `collapsible` prop: `collapsible` TimelineItems use the <Anchor href="/components/summary-detail">Summary Detail</Anchor> component. The toggle button is labeled by the value of the `timestamp` prop. If the value is too verbose for a screen reader or not a clear label for the collapsed content, provide a helpful label for the button using the collapsibleLabelText prop on TimelineItem.
</ListItem>
Comment on lines +93 to +95
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👏


<ListItem>
Timeline is an ordered list (&lt;ol&gt;) and TimelineItem is a list item (&lt;li&gt;).
</ListItem>
Expand Down Expand Up @@ -134,6 +139,26 @@ Use a basic Timeline as the default option to display a series of events that ne
{TimelineBasic}
</LivePreview>

### Timeline with interactive title

Use the `onClick` prop on the `TimelineItem` to make the title interactive. Use interactive titles only when the Timeline Event links to a specific moment in another view or surface.

<Callout variant="warning" marginY="space70">
<CalloutText>These titles should be used only when the event exists elsewhere and the user needs to be redirected to that exact point in time. If the event doesn't have a corresponding view, use a non-interactive title instead.</CalloutText>
</Callout>

<LivePreview
scope={{
Timeline,
TimelineItem,
TimelineItemGroup,
Box,
}}
noInline
>
{TimelineInteractiveTitle}
</LivePreview>

### Timeline with icons

Use a Timeline with icons to highlight events that would benefit from a visual cue to make the event more noticeable.
Expand Down
6 changes: 6 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -15269,14 +15269,17 @@ __metadata:
"@twilio-paste/detail-text": ^4.0.1
"@twilio-paste/disclosure-primitive": ^3.0.1
"@twilio-paste/icons": ^13.0.1
"@twilio-paste/paragraph": ^11.0.1
"@twilio-paste/reakit-library": ^3.0.1
"@twilio-paste/skeleton-loader": ^7.0.1
"@twilio-paste/spinner": ^15.0.1
"@twilio-paste/stack": ^9.0.1
"@twilio-paste/style-props": ^10.0.1
"@twilio-paste/styling-library": ^4.0.1
"@twilio-paste/summary-detail": ^2.0.1
"@twilio-paste/text": ^11.0.1
"@twilio-paste/theme": ^12.0.1
"@twilio-paste/truncate": ^15.0.1
"@twilio-paste/types": ^7.0.1
"@twilio-paste/uid-library": ^3.0.1
"@types/react": ^19.0.8
Expand All @@ -15297,14 +15300,17 @@ __metadata:
"@twilio-paste/detail-text": ^4.0.0
"@twilio-paste/disclosure-primitive": ^3.0.0
"@twilio-paste/icons": ^13.0.0
"@twilio-paste/paragraph": ^11.0.1
"@twilio-paste/reakit-library": ^3.0.0
"@twilio-paste/skeleton-loader": ^7.0.1
"@twilio-paste/spinner": ^15.0.0
"@twilio-paste/stack": ^9.0.0
"@twilio-paste/style-props": ^10.0.0
"@twilio-paste/styling-library": ^4.0.0
"@twilio-paste/summary-detail": ^2.0.0
"@twilio-paste/text": ^11.0.0
"@twilio-paste/theme": ^12.0.0
"@twilio-paste/truncate": ^15.0.1
"@twilio-paste/types": ^7.0.0
"@twilio-paste/uid-library": ^3.0.0
"@types/react": ^17.0.2 || ^18.0.27 || ^19.0.0
Expand Down
Loading