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(SimpleList): add simple list #3645

Merged
merged 12 commits into from Feb 14, 2020
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -0,0 +1,85 @@
import * as React from 'react';
import { css } from '@patternfly/react-styles';
import styles from '@patternfly/react-styles/css/components/SimpleList/simple-list';
import { SimpleListGroup } from './SimpleListGroup';
import { SimpleListItemProps } from './SimpleListItem';
import { Omit } from '../../helpers/typeUtils';

export interface SimpleListProps extends Omit<React.HTMLProps<HTMLDivElement>, 'onSelect'> {
/** Content rendered inside the SimpleList */
children?: React.ReactNode;
/** Additional classes added to the SimpleList container */
className?: string;
/** Callback when an item is selected */
onSelect?: (
ref: React.RefObject<HTMLButtonElement> | React.RefObject<HTMLAnchorElement>,
props: SimpleListItemProps
) => void;
}

export interface SimpleListState {
/** Ref of the current SimpleListItem */
currentRef: React.RefObject<HTMLButtonElement> | React.RefObject<HTMLAnchorElement>;
}

interface SimpleListContextProps {
currentRef: React.RefObject<HTMLButtonElement> | React.RefObject<HTMLAnchorElement>;
updateCurrentRef: (
id: React.RefObject<HTMLButtonElement> | React.RefObject<HTMLAnchorElement>,
props: SimpleListItemProps
) => void;
}

export const SimpleListContext = React.createContext<Partial<SimpleListContextProps>>({});

export class SimpleList extends React.Component<SimpleListProps, SimpleListState> {
static hasWarnBeta = false;
state = {
currentRef: null as React.RefObject<HTMLButtonElement> | React.RefObject<HTMLAnchorElement>
};

static defaultProps: SimpleListProps = {
children: null as React.ReactNode,
className: ''
};

componentDidMount() {
if (!SimpleList.hasWarnBeta && process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
console.warn('This component is in beta and subject to change.');
SimpleList.hasWarnBeta = true;
}
}

handleCurrentUpdate = (
newCurrentRef: React.RefObject<HTMLButtonElement> | React.RefObject<HTMLAnchorElement>,
itemProps: SimpleListItemProps
) => {
this.setState({ currentRef: newCurrentRef });
const { onSelect } = this.props;
onSelect && onSelect(newCurrentRef, itemProps);
};

render() {
const { children, className, onSelect, ...props } = this.props;

let isGrouped = false;
if (children) {
isGrouped = (React.Children.toArray(children)[0] as React.ReactElement).type === SimpleListGroup;
}

return (
<SimpleListContext.Provider
value={{
currentRef: this.state.currentRef,
updateCurrentRef: this.handleCurrentUpdate
}}
>
<div className={css(styles.simpleList, className)} {...props} {...(isGrouped && { role: 'list' })}>
{isGrouped && children}
{!isGrouped && <ul>{children}</ul>}
</div>
</SimpleListContext.Provider>
);
}
}
@@ -0,0 +1,35 @@
import * as React from 'react';
import { css } from '@patternfly/react-styles';
import styles from '@patternfly/react-styles/css/components/SimpleList/simple-list';
import { Omit } from '../../helpers/typeUtils';

export interface SimpleListGroupProps extends Omit<React.HTMLProps<HTMLTableSectionElement>, 'title'> {
/** Content rendered inside the SimpleList group */
children?: React.ReactNode;
/** Additional classes added to the SimpleList <ul> */
className?: string;
/** Additional classes added to the SimpleList group title */
titleClassName?: string;
/** Title of the SimpleList group */
title?: React.ReactNode;
/** ID of SimpleList group */
id?: string;
}

export const SimpleListGroup: React.FunctionComponent<SimpleListGroupProps> = ({
children = null,
className = '',
title = '',
titleClassName = '',
id = '',
...props
}: SimpleListGroupProps) => (
<section className={css(styles.simpleListSection)} {...props}>
<h2 id={id} className={css(styles.simpleListTitle, titleClassName)} aria-hidden="true">
{title}
</h2>
<ul className={css(className)} aria-labelledby={id}>
{children}
</ul>
</section>
);
@@ -0,0 +1,93 @@
import * as React from 'react';
import { css } from '@patternfly/react-styles';
import styles from '@patternfly/react-styles/css/components/SimpleList/simple-list';
import { SimpleListContext } from './SimpleList';

export interface SimpleListItemProps {
/** Content rendered inside the SimpleList item */
children?: React.ReactNode;
/** Additional classes added to the SimpleList <li> */
className?: string;
/** Component type of the SimpleList item */
component?: 'button' | 'a';
/** Additional classes added to the SimpleList <a> or <button> */
componentClassName?: string;
/** Additional props added to the SimpleList <a> or <button> */
componentProps?: any;
/** Indicates if the link is current/highlighted */
isCurrent?: boolean;
/** OnClick callback for the SimpleList item */
onClick?: (event: React.MouseEvent | React.ChangeEvent) => void;
/** Type of button SimpleList item */
type?: 'button' | 'submit' | 'reset';
/** Default hyperlink location */
href?: string;
}

export class SimpleListItem extends React.Component<SimpleListItemProps> {
ref = React.createRef<any>();
static defaultProps: SimpleListItemProps = {
children: null,
className: '',
isCurrent: false,
component: 'button',
componentClassName: '',
type: 'button',
href: '',
onClick: () => {}
};

render() {
const {
children,
isCurrent,
className,
component: Component,
componentClassName,
componentProps,
onClick,
type,
href,
...props
} = this.props;

return (
<SimpleListContext.Consumer>
{({ currentRef, updateCurrentRef }) => {
const isButton = Component === 'button';
const isCurrentItem = this.ref && currentRef ? currentRef.current === this.ref.current : isCurrent;

const additionalComponentProps = isButton
? {
type
Copy link
Collaborator

Choose a reason for hiding this comment

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

just making sure this is intended, but type can be submit or reset as well if that's what's passed in as prop

Copy link
Contributor Author

Choose a reason for hiding this comment

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

we want to make sure the default type is "button", this line lets the user change that if they wish. otherwise adding a type property manually would be caught by the component and not actually set.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure either way if we want to allow this

@mcarrano

}
: {
tabIndex: 0,
href
};

return (
<li className={css(className)} {...props}>
<Component
className={css(
styles.simpleListItemLink,
isCurrentItem && styles.modifiers.current,
componentClassName
)}
onClick={(evt: React.MouseEvent) => {
onClick(evt);
updateCurrentRef(this.ref, this.props);
}}
ref={this.ref}
{...componentProps}
{...additionalComponentProps}
>
{children}
</Component>
</li>
);
}}
</SimpleListContext.Consumer>
);
}
}
@@ -0,0 +1,87 @@
import * as React from 'react';
import { mount } from 'enzyme';
import { SimpleList } from '../SimpleList';
import { SimpleListGroup } from '../SimpleListGroup';
import { SimpleListItem } from '../SimpleListItem';

const items = [
<SimpleListItem key="i1">Item 1</SimpleListItem>,
<SimpleListItem key="i2">Item 2</SimpleListItem>,
<SimpleListItem key="i3">Item 3</SimpleListItem>
];

const anchors = [
<SimpleListItem key="i1" component="a">
Item 1
</SimpleListItem>,
<SimpleListItem key="i2" component="a">
Item 2
</SimpleListItem>,
<SimpleListItem key="i3" component="a">
Item 3
</SimpleListItem>
];

describe('SimpleList', () => {
test('renders content', () => {
const view = mount(<SimpleList>{items}</SimpleList>);
expect(view).toMatchSnapshot();
});

test('renders grouped content', () => {
const view = mount(
<SimpleList>
<SimpleListGroup title="Group 1">{items}</SimpleListGroup>
</SimpleList>
);
expect(view).toMatchSnapshot();
});

test('onSelect is called when item is selected', () => {
const selectSpy = jest.fn();
const view = mount(<SimpleList onSelect={selectSpy}>{items}</SimpleList>);
view
.find('button')
.first()
.simulate('click', { target: { value: 'r' } });
view.update();
expect(selectSpy).toBeCalled();
expect(view).toMatchSnapshot();
});

test('renders anchor content', () => {
const view = mount(<SimpleList>{anchors}</SimpleList>);
expect(view).toMatchSnapshot();
});

test('onSelect is called when anchor item is selected', () => {
const selectSpy = jest.fn();
const view = mount(<SimpleList onSelect={selectSpy}>{anchors}</SimpleList>);
view
.find('a')
.first()
.simulate('click', { target: { value: 'r' } });
view.update();
expect(selectSpy).toBeCalled();
expect(view).toMatchSnapshot();
});
});

describe('SimpleListGroup', () => {
test('renders content', () => {
const view = mount(<SimpleListGroup title="Group 1">{items}</SimpleListGroup>);
expect(view).toMatchSnapshot();
});
});

describe('SimpleListItem', () => {
test('renders content', () => {
const view = mount(<SimpleListItem>Item 1</SimpleListItem>);
expect(view).toMatchSnapshot();
});

test('renders anchor', () => {
const view = mount(<SimpleListItem component="a">Item 1</SimpleListItem>);
expect(view).toMatchSnapshot();
});
});