Skip to content
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

feat: improve ActivityTimeline and TimelineMarker components #2100

Merged
merged 13 commits into from
Jan 16, 2021
Merged
4 changes: 4 additions & 0 deletions src/components/ActivityTimeline/context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import React from 'react';

export const ActivityTimelineContext = React.createContext();
export const { Provider, Consumer } = ActivityTimelineContext;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default function getChildTimelineMarkersNodes(ref) {
if (ref) {
return ref.querySelectorAll('li[data-id="timeline-marker-li"]');
}
return [];
}
2 changes: 2 additions & 0 deletions src/components/ActivityTimeline/helpers/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/* eslint-disable import/prefer-default-export */
export { default as getChildTimelineMarkersNodes } from './getChildTimelineMarkersNodes';
7 changes: 6 additions & 1 deletion src/components/ActivityTimeline/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { ReactNode } from 'react';
import { ReactNode, MouseEvent } from 'react';
import { BaseProps } from '../types';

type Names = string[] | string;

export interface ActivityTimelineProps extends BaseProps {
children?: ReactNode;
multiple?: boolean;
onToggleSection?: (event: MouseEvent<HTMLElement>, name: Names) => void;

Choose a reason for hiding this comment

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

Here we will pass one single argument, a object with the name:
{ name }

activeSectionNames?: Names;
}

export default function(props: ActivityTimelineProps): JSX.Element | null;
108 changes: 104 additions & 4 deletions src/components/ActivityTimeline/index.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,100 @@
import React from 'react';
import React, { useRef, useState, useCallback, useMemo, useEffect } from 'react';
import PropTypes from 'prop-types';
import isChildRegistered from '../InternalDropdown/helpers/isChildRegistered';
import insertChildOrderly from '../InternalDropdown/helpers/insertChildOrderly';
import { getChildTimelineMarkersNodes } from './helpers';
import { Provider } from './context';
import StyledUl from './styled/ul';

/**
* The ActivityTimeline displays each of any item upcoming, current, and past activities.
* @category Layout
*/
export default function ActivityTimeline(props) {
const { children, className, style } = props;
const {
wildergd marked this conversation as resolved.
Show resolved Hide resolved
id,
children,
className,
style,
variant,
multiple,
activeSectionNames,
onToggleSection,
} = props;
const registeredTimelineMarkers = useRef([]);
const [activeNames, setActiveNames] = useState(activeSectionNames);
const containerRef = useRef();

useEffect(() => {
if (
activeSectionNames &&
activeSectionNames !== activeNames &&
typeof onToggleSection === 'function'
) {
setActiveNames(activeSectionNames);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeSectionNames, onToggleSection]);

const privateRegisterMarker = useCallback((stepRef, stepProps) => {
if (isChildRegistered(stepProps.name, registeredTimelineMarkers.current)) return;
const [...nodes] = getChildTimelineMarkersNodes(containerRef.current);
const newStepsList = insertChildOrderly(
registeredTimelineMarkers.current,
{
ref: stepRef,
...stepProps,
},
nodes,
);
registeredTimelineMarkers.current = newStepsList;
}, []);

const privateUnregisterMarker = useCallback((stepRef, stepName) => {
if (!isChildRegistered(stepName, registeredTimelineMarkers.current)) return;
registeredTimelineMarkers.current = registeredTimelineMarkers.current.filter(
step => step.name !== stepName,
);
}, []);

const privateOnToggleMarker = useCallback(
(event, name) => {
if (typeof onToggleSection === 'function') {
return onToggleSection(event, name);
}
return setActiveNames(name);
},
[onToggleSection],
);

const context = useMemo(() => {
return {
activeNames,
multiple,
isVariantAccordion: variant === 'accordion',
privateRegisterMarker,
privateUnregisterMarker,
privateOnToggleMarker,
};
}, [
variant,
activeNames,
multiple,
privateRegisterMarker,
privateUnregisterMarker,
privateOnToggleMarker,
]);

return (
<StyledUl className={className} style={style}>
{children}
<StyledUl id={id} className={className} style={style} ref={containerRef} variant={variant}>
<Provider value={context}>{children}</Provider>
</StyledUl>

Choose a reason for hiding this comment

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

I think here, we should execute all this new code only if the variant is accordion:

if (variant === 'accordion') {
   return <NewAccordionComponent />;
}
return <OriginalComponent />;

);
}

ActivityTimeline.propTypes = {
/** The id of the outer element. */
id: PropTypes.string,
/**
* This prop that should not be visible in the documentation.
* @ignore
Expand All @@ -26,10 +104,32 @@ ActivityTimeline.propTypes = {
className: PropTypes.string,
/** An object with custom style applied to the outer element. */
style: PropTypes.object,
/** If true, expands multiples TimelineMarkers.
* This value defaults to false. */
multiple: PropTypes.bool,
/** The variant changes the appearance of the timeline. Accepted variants include
* default and accordion. */
variant: PropTypes.oneOf(['default', 'accordion']),
/** It contain the name of the TimelineMarker that is expanded.
* It is an array of string when multiple is true,
* or a string when when multiple is false.
* It must match the name of the TimelineMarker. */
activeSectionNames: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.string),
PropTypes.string,
]),
/** Action fired when a TimelineMarker is selected.
* The event params include the `name` of the selected TimelineMarker. */

Choose a reason for hiding this comment

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

we need to explain that this props (multiple, activeSectionNames and onToggleSection) are only used for accordion variant

onToggleSection: PropTypes.func,
};

ActivityTimeline.defaultProps = {
id: undefined,
children: null,
className: undefined,
style: undefined,
variant: 'default',
multiple: false,
onToggleSection: undefined,
activeSectionNames: undefined,
};
131 changes: 131 additions & 0 deletions src/components/ActivityTimeline/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,134 @@ const container = { width: 500, margin: 'auto', marginTop: 36, };
</ActivityTimeline>
</div>
```

# ActivityTimeline as accordion
##### ActivityTimeline can be used as accordion. You can do so by using the `variant` prop.

```js
import React, { useState, useMemo, useCallback, useEffect } from 'react';
import { ActivityTimeline, TimelineMarker } from 'react-rainbow-components';
import styled from 'styled-components';

const StyledContainer = styled.div`
margin: 36px 4rem 0;
`;

const StyledContentContainer = styled.div.attrs(props => {
return props.theme.rainbow.palette;
})`
border-radius: 1rem;
padding: 1rem 2rem;
background: ${props => props.background.highlight};
color: ${props => props.text.main};
`;

const StyledContentHeader = styled.h3`
font-weight: bold;
margin-bottom: 0.5rem;
`;

const StyledItemsContainer = styled.h3`
display: flex;
align-items: center;
`;

const StyledLabel = styled.span.attrs(props => {
return props.theme.rainbow.palette;
})`
color: ${props => props.text.label};
width: 100%;
max-width: 140px;
padding: 0.5rem 0;
`;

const StyledValue = styled.span.attrs(props => {
return props.theme.rainbow.palette;
})`
font-weight: bold;
color: ${props => props.text.main};
`;

const EventDetails = ({ uid, birthdate }) => {
return (
<StyledContentContainer>
<StyledContentHeader>Details</StyledContentHeader>
<StyledItemsContainer>
<StyledLabel>UID</StyledLabel>
<StyledValue>{uid}</StyledValue>
</StyledItemsContainer>
<StyledItemsContainer>
<StyledLabel>Date of Birthday</StyledLabel>
<StyledValue>{birthdate}</StyledValue>
</StyledItemsContainer>
</StyledContentContainer>
);
};

const AccordionActivityTimeline = ({ eventsList }) => {
const [activeEvents, setActiveEvents] = useState([]);
const [loading, setLoading] = useState(true);
const markers = useMemo(() => eventsList.map(event => {
const { name, user, datetime, description, details } = event;
const iconStyles = { width: 32, height: 32 };
return (
<TimelineMarker
key={name}
name={name}
label={user}
isLoading={loading}
icon={<UserSignUpIcon style={iconStyles} />}
datetime={datetime}
description={description}
>
<EventDetails uid={details.uid} birthdate={details.birdate} />
</TimelineMarker>
);
}), [eventsList, loading]);

useEffect(() => {
setTimeout(() => setLoading(false), 3000);
}, []);

const handleToggleEvent = useCallback((event, name) => {
setActiveEvents(name);
}, []);

return (
<StyledContainer>
<ActivityTimeline
variant="accordion"
multiple
activeSectionNames={activeEvents}
onToggleSection={handleToggleEvent}
>
{markers}
</ActivityTimeline>
</StyledContainer>
);
};

const events = [
{
name: 'event1',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing.',
user: 'Tahimi Leon',
datetime: '11:00 AM, Today',
details: {
UID: '1610482374420',
birthdate: 'January 12, 2021',
},
},
{
name: 'event2',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing.',
user: 'Tahimi Leon',
datetime: '11:00 AM, Today',
details: {
UID: '1610482374420',
birthdate: 'January 12, 2021',
},
}
];
<AccordionActivityTimeline eventsList={events} />
```
6 changes: 6 additions & 0 deletions src/components/ActivityTimeline/styled/ul.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ const StyledUl = styled.ul`
padding: 0;
list-style: none;
box-sizing: border-box;

${props =>
props.variant === 'accordion' &&
`
padding-left: 1.25rem;
`};
`;

export default StyledUl;
37 changes: 29 additions & 8 deletions src/components/TimelineMarker/__test__/timelineMarker.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import { mount } from 'enzyme';
import ActivityTimeline from '../../ActivityTimeline';
import TimelineMarker from '../index';
import Card from '../../Card';
import Avatar from '../../Avatar';
Expand All @@ -10,31 +11,51 @@ import StyledDescription from '../styled/description';
describe('<TimelineMarker/>', () => {
it('should render the children passed', () => {
const component = mount(
<TimelineMarker>
<Card title="TimelineMarker--children" />
</TimelineMarker>,
<ActivityTimeline>
<TimelineMarker>
<Card title="TimelineMarker--children" />
</TimelineMarker>
</ActivityTimeline>,
);
expect(component.find(Card).exists()).toBe(true);
});
it('should render the icon passed', () => {
const component = mount(<TimelineMarker icon={<Avatar />} />);
const component = mount(
<ActivityTimeline>
<TimelineMarker icon={<Avatar />} />
</ActivityTimeline>,
);
expect(component.find(Avatar).exists()).toBe(true);
});
it('should render the calendar icon by default', () => {
const component = mount(<TimelineMarker />);
const component = mount(
<ActivityTimeline>
<TimelineMarker />
</ActivityTimeline>,
);
expect(component.find('CalendarIcon').exists()).toBe(true);
});
it('should render the label passed', () => {
const component = mount(<TimelineMarker label="testing label on TimelineMarker" />);
const component = mount(
<ActivityTimeline>
<TimelineMarker label="testing label on TimelineMarker" />
</ActivityTimeline>,
);
expect(component.find(StyledLabel).text()).toBe('testing label on TimelineMarker');
});
it('should render the datetime passed', () => {
const component = mount(<TimelineMarker datetime="Yesterday" />);
const component = mount(
<ActivityTimeline>
<TimelineMarker datetime="Yesterday" />
</ActivityTimeline>,
);
expect(component.find(StyledDatetime).text()).toBe('Yesterday');
});
it('should render the description passed', () => {
const component = mount(
<TimelineMarker description="testing description on TimelineMarker" />,
<ActivityTimeline>
<TimelineMarker description="testing description on TimelineMarker" />,
</ActivityTimeline>,
);
expect(component.find(StyledDescription).text()).toBe(
'testing description on TimelineMarker',
Expand Down
Loading