Skip to content

Commit

Permalink
feat(Timeline): add isItemActive prop
Browse files Browse the repository at this point in the history
  • Loading branch information
SevenOutman committed May 17, 2023
1 parent 20fbdee commit 9a6cc82
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 5 deletions.
16 changes: 16 additions & 0 deletions src/Timeline/Timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,21 @@ export interface TimelineProps extends WithAsProps {

/** Timeline endless **/
endless?: boolean;

/**
* Whether an item is active (with highlighted dot).
*
* @default
* The last item is marked active.
*/
isItemActive?: (index: number, totalItemsCount: number) => boolean;
}

interface TimelineComponent extends RsRefForwardingComponent<'div', TimelineProps> {
Item: typeof TimelineItem;

ACTIVE_FIRST: (index: number, totalItemsCount: number) => boolean;
ACTIVE_LAST: (index: number, totalItemsCount: number) => boolean;
}

const Timeline: TimelineComponent = React.forwardRef((props: TimelineProps, ref) => {
Expand All @@ -28,6 +39,7 @@ const Timeline: TimelineComponent = React.forwardRef((props: TimelineProps, ref)
className,
align = 'left',
endless,
isItemActive = Timeline.ACTIVE_LAST,
...rest
} = props;

Expand All @@ -44,12 +56,16 @@ const Timeline: TimelineComponent = React.forwardRef((props: TimelineProps, ref)
<Component {...rest} ref={ref} className={classes}>
{ReactChildren.mapCloneElement(children, (_child: any, index: number) => ({
last: index + 1 === count,
INTERNAL_active: isItemActive(index, count),
align
}))}
</Component>
);
}) as unknown as TimelineComponent;

Timeline.ACTIVE_FIRST = index => index === 0;
Timeline.ACTIVE_LAST = (index, totalItemsCount) => index === totalItemsCount - 1;

Timeline.Item = TimelineItem;

Timeline.displayName = 'Timeline';
Expand Down
29 changes: 25 additions & 4 deletions src/Timeline/TimelineItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,18 @@ import { useClassNames } from '../utils';
import { WithAsProps, RsRefForwardingComponent } from '../@types/common';

export interface TimelineItemProps extends WithAsProps {
/** Whether the last item */
/**
* Whether the last item
*
* @internal
* This props is supposed to be used only by Timeline component internally
* User should never rely on this prop
*
* @deprecated
* This prop was used to indicate whether an item is the last item so that it gets highlighted.
* Now we can specify whether an item should be highlighted individually.
* Use {@link INTERNAL_active} instead
*/
last?: boolean;

/** Customizing the Timeline item */
Expand All @@ -18,6 +29,13 @@ export interface TimelineItemProps extends WithAsProps {

/** Customized time of timeline **/
time?: React.ReactNode;

/**
* @internal
* This props is supposed to be used only by Timeline component internally
* User should never rely on this prop
*/
INTERNAL_active?: boolean;
}

const TimelineItem: RsRefForwardingComponent<'div', TimelineItemProps> = React.forwardRef(
Expand All @@ -26,15 +44,18 @@ const TimelineItem: RsRefForwardingComponent<'div', TimelineItemProps> = React.f
as: Component = 'li',
children,
classPrefix = 'timeline-item',
last,
last: DEPRECATED_last,
className,
dot,
time,
INTERNAL_active,
...rest
} = props;
const { merge, withClassPrefix, prefix } = useClassNames(classPrefix);
const classes = merge(className, withClassPrefix({ last }));

const classes = merge(
className,
withClassPrefix({ last: DEPRECATED_last, active: INTERNAL_active })
);
return (
<Component {...rest} ref={ref} className={classes}>
<span className={prefix('tail')} />
Expand Down
2 changes: 1 addition & 1 deletion src/Timeline/styles/index.less
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
}
}

&-item-last &-item-dot::before {
&-item-active &-item-dot::before {
background-color: var(--rs-timeline-indicator-active-bg);
}

Expand Down
13 changes: 13 additions & 0 deletions src/Timeline/test/TimelineItemSpec.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import { render } from '@testing-library/react';
import { getDOMNode } from '@test/testUtils';
import { testStandardProps } from '@test/commonCases';
import TimelineItem from '../TimelineItem';
Expand All @@ -9,13 +10,16 @@ describe('TimelineItem', () => {
it('Should output a TimelineItem', () => {
const instance = getDOMNode(<TimelineItem />);
assert.equal(instance.className, 'rs-timeline-item');
// eslint-disable-next-line testing-library/no-node-access
assert.ok(instance.querySelector('.rs-timeline-item-dot'));
// eslint-disable-next-line testing-library/no-node-access
assert.ok(instance.querySelector('.rs-timeline-item-tail'));
});

it('Should render a dot', () => {
const instance = getDOMNode(<TimelineItem dot={<i>test</i>} />);
assert.equal(
// eslint-disable-next-line testing-library/no-node-access
(instance.querySelector('.rs-timeline-item-custom-dot') as HTMLElement).textContent,
'test'
);
Expand All @@ -25,6 +29,7 @@ describe('TimelineItem', () => {
const time = '2019-10-21';
const instance = getDOMNode(<TimelineItem time={time} />);
assert.equal(
// eslint-disable-next-line testing-library/no-node-access
(instance.querySelector('.rs-timeline-item-time') as HTMLElement).textContent,
time
);
Expand All @@ -34,4 +39,12 @@ describe('TimelineItem', () => {
const instance = getDOMNode(<TimelineItem last />);
assert.ok(instance.className.match(/\brs-timeline-item-last\b/));
});

describe('Internal', () => {
it('Should have "rs-timeline-item-active" className when `INTERNAL_active=true`', () => {
const { container } = render(<TimelineItem INTERNAL_active />);

expect(container.firstChild).to.have.class('rs-timeline-item-active');
});
});
});
48 changes: 48 additions & 0 deletions src/Timeline/test/TimelineSpec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import { getDOMNode } from '@test/testUtils';
import { testStandardProps } from '@test/commonCases';
import Timeline from '../Timeline';
import { render, screen } from '@testing-library/react';

describe('Timeline', () => {
testStandardProps(<Timeline />);
Expand Down Expand Up @@ -34,4 +35,51 @@ describe('Timeline', () => {
const instance = getDOMNode(<Timeline align="left" />);
assert.ok(instance.className.match(/\brs-timeline-align-left\b/));
});

describe('Active item', () => {
it('Should mark the last item as active by default', () => {
render(
<Timeline>
<Timeline.Item>First item</Timeline.Item>
<Timeline.Item>Second item</Timeline.Item>
<Timeline.Item>Third item</Timeline.Item>
</Timeline>
);
expect(screen.getAllByRole('listitem')[2]).to.have.class('rs-timeline-item-active');
});

it('Should mark the item indicated by `isItemActive` as active', () => {
render(
<Timeline isItemActive={index => index === 1}>
<Timeline.Item>First item</Timeline.Item>
<Timeline.Item>Second item</Timeline.Item>
<Timeline.Item>Third item</Timeline.Item>
</Timeline>
);

expect(screen.getAllByRole('listitem')[1]).to.have.class('rs-timeline-item-active');
});

it('Should mark the first item as active with `Timeline.ACTIVE_FIRST`', () => {
render(
<Timeline isItemActive={Timeline.ACTIVE_FIRST}>
<Timeline.Item>First item</Timeline.Item>
<Timeline.Item>Second item</Timeline.Item>
<Timeline.Item>Third item</Timeline.Item>
</Timeline>
);
expect(screen.getAllByRole('listitem')[0]).to.have.class('rs-timeline-item-active');
});

it('Should mark the last item as active with `Timeline.ACTIVE_LAST`', () => {
render(
<Timeline isItemActive={Timeline.ACTIVE_LAST}>
<Timeline.Item>First item</Timeline.Item>
<Timeline.Item>Second item</Timeline.Item>
<Timeline.Item>Third item</Timeline.Item>
</Timeline>
);
expect(screen.getAllByRole('listitem')[2]).to.have.class('rs-timeline-item-active');
});
});
});

0 comments on commit 9a6cc82

Please sign in to comment.