Skip to content

Commit 4b5963f

Browse files
committed
refactor: behavior of marker/pointer positioning
1 parent 425145f commit 4b5963f

File tree

4 files changed

+62
-30
lines changed

4 files changed

+62
-30
lines changed

src/components/Timeline.css

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
--marker-size: 1rem;
55
--marker-color: var(--line-color);
66
--marker-radius: 50%;
7-
--marker-offset: 3rem;
87
--pointer-height: 2rem;
98
--pointer-width: 1rem;
109
--card-background: whitesmoke;
@@ -43,10 +42,6 @@
4342
right: 0;
4443
}
4544

46-
.timeline-item__marker {
47-
margin-top: var(--marker-offset);
48-
}
49-
5045
.timeline-item__marker:not(.timeline-item__marker--custom) {
5146
border-radius: var(--marker-radius);
5247
width: var(--marker-size);
@@ -86,7 +81,6 @@
8681
}
8782

8883
.timeline-card__pointer {
89-
top: var(--marker-offset);
9084
transform: translate(100%, -50%);
9185
position: absolute;
9286
}

src/components/Timeline.tsx

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import './Timeline.css';
22
import { Key, ReactElement, useEffect, useRef } from 'react';
3-
import { PropsWithKey, TimelineItem, TimelineItemProps } from './TimelineItem';
3+
import { PropsWithKey, TimelineItem, TimelineItemProps, TimelineItemRefs } from './TimelineItem';
44
import { OffsetConfig, resolveOffsets } from '../models/offset';
55
import { Positioning } from '../models/positioning';
66
import { convertToCssVariable, StyleConfig } from '../models/style';
@@ -11,6 +11,7 @@ export type TimelineProps = {
1111
gap?: number;
1212
offset?: OffsetConfig;
1313
minMarkerGap?: number;
14+
defaultPointerOffset?: number;
1415
dateFormat?: string;
1516
dateLocale?: Locale;
1617
customMarker?: ReactElement;
@@ -23,18 +24,32 @@ const defaultTimelineConfig: Partial<TimelineProps> = {
2324
positioning: 'alternating',
2425
gap: 50,
2526
offset: 50,
26-
minMarkerGap: 100,
27+
minMarkerGap: 50,
28+
defaultPointerOffset: 40,
2729
dateFormat: 'P',
2830
};
2931

3032
export function Timeline(props: TimelineProps) {
31-
const { items, positioning, gap, offset, minMarkerGap, className, dateFormat, dateLocale, customMarker, customPointer, styleConfig } = {
33+
const {
34+
items,
35+
positioning,
36+
gap,
37+
offset,
38+
minMarkerGap,
39+
defaultPointerOffset,
40+
className,
41+
dateFormat,
42+
dateLocale,
43+
customMarker,
44+
customPointer,
45+
styleConfig,
46+
} = {
3247
...defaultTimelineConfig,
3348
...props,
3449
};
3550

3651
const timelineRef = useRef<HTMLDivElement>(null);
37-
const itemsRef = useRef<Map<Key, HTMLElement>>();
52+
const itemsRef = useRef<Map<Key, TimelineItemRefs>>();
3853

3954
function getRefMap() {
4055
if (!itemsRef.current) {
@@ -61,26 +76,34 @@ export function Timeline(props: TimelineProps) {
6176
let leftHeight = left;
6277
let rightHeight = right;
6378

64-
elements.forEach((item) => {
65-
const element = item;
79+
let nextMarkerOffset = defaultPointerOffset ?? 0;
6680

67-
if ((positioning !== 'right' && leftHeight > rightHeight) || positioning === 'left') {
68-
leftHeight += getMinMarkerGapCompensation(leftHeight, rightHeight);
81+
elements.forEach((refs) => {
82+
const { item, pointer, marker } = refs;
83+
if (!item || !pointer || !marker) return;
6984

70-
element.style.top = `${rightHeight}px`;
71-
element.classList.add('timeline-item--right');
72-
element.classList.remove('timeline-item--left');
73-
rightHeight += element.offsetHeight + (gap ?? 0);
74-
} else {
75-
rightHeight += getMinMarkerGapCompensation(leftHeight, rightHeight);
85+
// offsets marker + pointer if it would get in the way of the last marker
86+
pointer.style.top = `${nextMarkerOffset}px`;
87+
marker.style.marginTop = `${nextMarkerOffset}px`;
88+
89+
const markerOffsetCompensation = getMinMarkerGapCompensation(leftHeight, rightHeight);
90+
nextMarkerOffset = markerOffsetCompensation + (defaultPointerOffset ?? 0);
7691

77-
element.style.top = `${leftHeight}px`;
78-
element.classList.add('timeline-item--left');
79-
element.classList.remove('timeline-item--right');
80-
leftHeight += element.offsetHeight + (gap ?? 0);
92+
// defines whether an item should be positioned left or right of the timeline
93+
if ((positioning !== 'right' && leftHeight > rightHeight) || positioning === 'left') {
94+
item.style.top = `${rightHeight}px`;
95+
item.classList.add('timeline-item--right');
96+
item.classList.remove('timeline-item--left');
97+
rightHeight += item.offsetHeight + (gap ?? 0);
98+
} else {
99+
item.style.top = `${leftHeight}px`;
100+
item.classList.add('timeline-item--left');
101+
item.classList.remove('timeline-item--right');
102+
leftHeight += item.offsetHeight + (gap ?? 0);
81103
}
82104
});
83105

106+
// update height of container element to match absolute positioned timeline
84107
const timelineElement = timelineRef.current;
85108
if (timelineElement) {
86109
timelineElement.style.height = `${Math.max(leftHeight, rightHeight)}px`;

src/components/TimelineItem.tsx

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import { forwardRef, Key, PropsWithChildren, ReactElement } from 'react';
1+
import { forwardRef, Key, PropsWithChildren, ReactElement, useRef, useImperativeHandle } from 'react';
22
import { format } from 'date-fns';
33

44
export type PropsWithKey<T> = T & {
55
key: Key;
66
};
77

8+
export type TimelineItemRefs = { pointer: HTMLDivElement | null; item: HTMLDivElement | null; marker: HTMLDivElement | null };
9+
810
export type TimelineItemProps = PropsWithChildren<{
911
className?: string;
1012
date: Date | string;
@@ -15,13 +17,27 @@ export type TimelineItemProps = PropsWithChildren<{
1517
customPointer?: ReactElement;
1618
}>;
1719

18-
export const TimelineItem = forwardRef<HTMLDivElement, TimelineItemProps>(
20+
export const TimelineItem = forwardRef<TimelineItemRefs, TimelineItemProps>(
1921
({ className, title, date, children, dateFormat, dateLocale, customMarker, customPointer }, ref) => {
22+
const itemRef = useRef<HTMLDivElement>(null);
23+
const pointerRef = useRef<HTMLDivElement>(null);
24+
const markerRef = useRef<HTMLDivElement>(null);
25+
26+
useImperativeHandle(ref, () => ({
27+
item: itemRef.current,
28+
pointer: pointerRef.current,
29+
marker: markerRef.current,
30+
}));
31+
2032
return (
21-
<div ref={ref} className={['timeline-item', className].join(' ')}>
22-
<div className={['timeline-item__marker', customMarker && 'timeline-item__marker--custom'].join(' ')}>{customMarker}</div>
33+
<div ref={itemRef} className={['timeline-item', className].join(' ')}>
34+
<div ref={markerRef} className={['timeline-item__marker', customMarker && 'timeline-item__marker--custom'].join(' ')}>
35+
{customMarker}
36+
</div>
2337
<div className="timeline-card">
24-
<div className={['timeline-card__pointer', customPointer && 'timeline-card__pointer--custom'].join(' ')}>{customPointer}</div>
38+
<div ref={pointerRef} className={['timeline-card__pointer', customPointer && 'timeline-card__pointer--custom'].join(' ')}>
39+
{customPointer}
40+
</div>
2541
<p className="timeline-card__date">{date instanceof Date ? format(date, dateFormat ?? 'P', { locale: dateLocale }) : date}</p>
2642
<p className="timeline-card__title">{title}</p>
2743
{children}

src/models/style.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ export type StyleConfig = {
99
size?: string;
1010
color?: string;
1111
radius?: string;
12-
offset?: string;
1312
};
1413
pointer?: {
1514
height?: string;

0 commit comments

Comments
 (0)