Skip to content

Commit

Permalink
feat(menu): add component (#786)
Browse files Browse the repository at this point in the history
  • Loading branch information
Matt Goo committed Apr 30, 2019
1 parent 509e93e commit 7e0f877
Show file tree
Hide file tree
Showing 25 changed files with 2,207 additions and 500 deletions.
1,196 changes: 877 additions & 319 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"pretest": "npm stop",
"test": "npm run lint && npm run test:unit && npm run test:screenshots",
"posttest": "npm stop && istanbul report --root coverage text-summary && istanbul check-coverage --lines 95 --statements 95 --branches 95 --functions 95",
"postinstall": "lerna bootstrap",
"postinstall": "lerna bootstrap && rm node_modules/**/.babelrc -f && rm packages/**/node_modules/.babelrc -f",
"test:watch": "karma start karma.local.js --auto-watch",
"test:unit": "npm run clean && cross-env NODE_ENV=test karma start karma.local.js --single-run",
"test:unit-ci": "karma start karma.ci.js --single-run",
Expand Down Expand Up @@ -77,6 +77,7 @@
"@material/line-ripple": "^1.0.0",
"@material/linear-progress": "^1.1.0",
"@material/list": "^1.0.0",
"@material/menu": "^1.1.0",
"@material/menu-surface": "^1.0.1",
"@material/notched-outline": "^1.1.1",
"@material/radio": "^1.1.0",
Expand Down Expand Up @@ -122,8 +123,8 @@
"cp-file": "^6.0.0",
"cross-env": "^5.2.0",
"css-loader": "^0.28.10",
"enzyme": "^3.3.0",
"enzyme-adapter-react-16": "^1.1.1",
"enzyme": "^3.9.0",
"enzyme-adapter-react-16": "^1.11.2",
"eslint": "^5.9.0",
"eslint-config-google": "^0.9.1",
"eslint-plugin-react": "^7.7.0",
Expand Down
135 changes: 120 additions & 15 deletions packages/list/ListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,24 +23,24 @@
import React from 'react';
import classnames from 'classnames';
import {MDCListFoundation} from '@material/list/foundation';
import {ListItemContext, ListItemContextShape} from './index';
import {closest} from '@material/dom/ponyfill';

export interface ListItemProps<T> extends React.HTMLProps<T> {
export interface ListItemProps<T extends HTMLElement = HTMLElement> extends React.HTMLProps<T>, ListItemContextShape {
checkboxList?: boolean;
radioList?: boolean;
onKeyDown?: React.KeyboardEventHandler<T>;
onClick?: React.MouseEventHandler<T>;
onFocus?: React.FocusEventHandler<T>;
onBlur?: React.FocusEventHandler<T>;
tag?: string;
activated?: boolean;
selected?: boolean;
onDestroy?: () => void;
ref?: React.Ref<any>;
};

export interface ListItemState {
tabIndex?: number;
}

export default class ListItem<T extends HTMLElement = HTMLElement> extends React.Component<
ListItemProps<T>,
{}
> {
export class ListItemBase<T extends HTMLElement = HTMLElement> extends React.Component<
ListItemProps<T>, ListItemState> {
private listItemElement = React.createRef<T>();

static defaultProps: Partial<ListItemProps<HTMLElement>> = {
Expand All @@ -54,15 +54,54 @@ export default class ListItem<T extends HTMLElement = HTMLElement> extends React
onBlur: () => {},
onDestroy: () => {},
tag: 'li',
handleClick: () => {},
handleKeyDown: () => {},
handleBlur: () => {},
handleFocus: () => {},
getListItemInitialTabIndex: () => -1,
getClassNamesFromList: () => ({}),
};

state = {
tabIndex: this.props.tabIndex,
};

get listElements(): Element[] {
if (this.listItemElement.current) {
const listElement = closest(this.listItemElement.current, `.${MDCListFoundation.cssClasses.ROOT}`);
if (!listElement) return [];
return [].slice.call(
listElement.querySelectorAll(MDCListFoundation.strings.ENABLED_ITEMS_SELECTOR)
);
}
return [];
}

componentDidMount() {
this.initializeTabIndex();
}

componentDidUpdate(prevProps: ListItemProps) {
if (prevProps.tabIndex !== this.props.tabIndex) {
this.setState({tabIndex: this.props.tabIndex});
}
}

componentWillUnmount() {
this.props.onDestroy!();
if (this.listItemElement.current) {
const index = this.getIndex(this.listItemElement.current);
this.props.onDestroy!(index);
}
}

get classes() {
const {className, activated, disabled, selected} = this.props;
return classnames('mdc-list-item', className, {
const {className, activated, disabled, selected, getClassNamesFromList} = this.props;
let classesFromList = [''];
if (this.listItemElement.current) {
const index = this.getIndex(this.listItemElement.current);
classesFromList = getClassNamesFromList!()[index];
}
return classnames('mdc-list-item', className, classesFromList, {
[MDCListFoundation.cssClasses.LIST_ITEM_ACTIVATED_CLASS]: activated,
[MDCListFoundation.cssClasses.LIST_ITEM_SELECTED_CLASS]: selected,
'mdc-list-item--disabled': disabled,
Expand All @@ -81,6 +120,42 @@ export default class ListItem<T extends HTMLElement = HTMLElement> extends React
return null;
}

private initializeTabIndex = () => {
if (this.listItemElement.current) {
const index = this.getIndex(this.listItemElement.current);
const tabIndex = this.props.getListItemInitialTabIndex!(index);
this.setState({tabIndex});
}
}

getIndex = (listElement: Element) => {
return this.listElements.indexOf(listElement);
}

handleClick = (e: React.MouseEvent<any>) => {
const {onClick} = this.props;
onClick!(e);
this.props.handleClick!(e, this.getIndex(e.currentTarget));
}

handleKeyDown = (e: React.KeyboardEvent<any>) => {
const {onKeyDown} = this.props;
onKeyDown!(e);
this.props.handleKeyDown!(e, this.getIndex(e.currentTarget));
}

handleFocus = (e: React.FocusEvent<any>) => {
const {onFocus} = this.props;
onFocus!(e);
this.props.handleFocus!(e, this.getIndex(e.currentTarget));
}

handleBlur = (e: React.FocusEvent<any>) => {
const {onBlur} = this.props;
onBlur!(e);
this.props.handleBlur!(e, this.getIndex(e.currentTarget));
}

render() {
const {
/* eslint-disable no-unused-vars */
Expand All @@ -90,21 +165,51 @@ export default class ListItem<T extends HTMLElement = HTMLElement> extends React
checkboxList,
radioList,
onDestroy,
onClick,
onKeyDown,
onFocus,
onBlur,
handleClick,
handleKeyDown,
handleFocus,
handleBlur,
getListItemInitialTabIndex,
getClassNamesFromList,
tabIndex,
/* eslint-enable no-unused-vars */
tag: Tag,
...otherProps
} = this.props;

return (
// https://github.com/Microsoft/TypeScript/issues/28892
// @ts-ignore
<Tag
{...otherProps}
{...this.context}
role={this.role}
className={this.classes}
ref={this.listItemElement}
{...otherProps}
onClick={this.handleClick}
onKeyDown={this.handleKeyDown}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
tabIndex={this.state.tabIndex}
>
{this.props.children}
{children}
</Tag>
);
}
}

const ListItem: React.FunctionComponent<ListItemProps> = (props) => {
return (
<ListItemContext.Consumer>
{(context) => (
<ListItemBase {...context} {...props}/>
)}
</ListItemContext.Consumer>
);
};

export default ListItem;
Loading

0 comments on commit 7e0f877

Please sign in to comment.