Skip to content
This repository was archived by the owner on Jan 14, 2025. It is now read-only.

Commit 7e0f877

Browse files
author
Matt Goo
committed
feat(menu): add component (#786)
1 parent 509e93e commit 7e0f877

25 files changed

+2207
-500
lines changed

package-lock.json

Lines changed: 877 additions & 319 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"pretest": "npm stop",
1515
"test": "npm run lint && npm run test:unit && npm run test:screenshots",
1616
"posttest": "npm stop && istanbul report --root coverage text-summary && istanbul check-coverage --lines 95 --statements 95 --branches 95 --functions 95",
17-
"postinstall": "lerna bootstrap",
17+
"postinstall": "lerna bootstrap && rm node_modules/**/.babelrc -f && rm packages/**/node_modules/.babelrc -f",
1818
"test:watch": "karma start karma.local.js --auto-watch",
1919
"test:unit": "npm run clean && cross-env NODE_ENV=test karma start karma.local.js --single-run",
2020
"test:unit-ci": "karma start karma.ci.js --single-run",
@@ -77,6 +77,7 @@
7777
"@material/line-ripple": "^1.0.0",
7878
"@material/linear-progress": "^1.1.0",
7979
"@material/list": "^1.0.0",
80+
"@material/menu": "^1.1.0",
8081
"@material/menu-surface": "^1.0.1",
8182
"@material/notched-outline": "^1.1.1",
8283
"@material/radio": "^1.1.0",
@@ -122,8 +123,8 @@
122123
"cp-file": "^6.0.0",
123124
"cross-env": "^5.2.0",
124125
"css-loader": "^0.28.10",
125-
"enzyme": "^3.3.0",
126-
"enzyme-adapter-react-16": "^1.1.1",
126+
"enzyme": "^3.9.0",
127+
"enzyme-adapter-react-16": "^1.11.2",
127128
"eslint": "^5.9.0",
128129
"eslint-config-google": "^0.9.1",
129130
"eslint-plugin-react": "^7.7.0",

packages/list/ListItem.tsx

Lines changed: 120 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,24 +23,24 @@
2323
import React from 'react';
2424
import classnames from 'classnames';
2525
import {MDCListFoundation} from '@material/list/foundation';
26+
import {ListItemContext, ListItemContextShape} from './index';
27+
import {closest} from '@material/dom/ponyfill';
2628

27-
export interface ListItemProps<T> extends React.HTMLProps<T> {
29+
export interface ListItemProps<T extends HTMLElement = HTMLElement> extends React.HTMLProps<T>, ListItemContextShape {
2830
checkboxList?: boolean;
2931
radioList?: boolean;
30-
onKeyDown?: React.KeyboardEventHandler<T>;
31-
onClick?: React.MouseEventHandler<T>;
32-
onFocus?: React.FocusEventHandler<T>;
33-
onBlur?: React.FocusEventHandler<T>;
3432
tag?: string;
3533
activated?: boolean;
3634
selected?: boolean;
37-
onDestroy?: () => void;
35+
ref?: React.Ref<any>;
36+
};
37+
38+
export interface ListItemState {
39+
tabIndex?: number;
3840
}
3941

40-
export default class ListItem<T extends HTMLElement = HTMLElement> extends React.Component<
41-
ListItemProps<T>,
42-
{}
43-
> {
42+
export class ListItemBase<T extends HTMLElement = HTMLElement> extends React.Component<
43+
ListItemProps<T>, ListItemState> {
4444
private listItemElement = React.createRef<T>();
4545

4646
static defaultProps: Partial<ListItemProps<HTMLElement>> = {
@@ -54,15 +54,54 @@ export default class ListItem<T extends HTMLElement = HTMLElement> extends React
5454
onBlur: () => {},
5555
onDestroy: () => {},
5656
tag: 'li',
57+
handleClick: () => {},
58+
handleKeyDown: () => {},
59+
handleBlur: () => {},
60+
handleFocus: () => {},
61+
getListItemInitialTabIndex: () => -1,
62+
getClassNamesFromList: () => ({}),
63+
};
64+
65+
state = {
66+
tabIndex: this.props.tabIndex,
5767
};
5868

69+
get listElements(): Element[] {
70+
if (this.listItemElement.current) {
71+
const listElement = closest(this.listItemElement.current, `.${MDCListFoundation.cssClasses.ROOT}`);
72+
if (!listElement) return [];
73+
return [].slice.call(
74+
listElement.querySelectorAll(MDCListFoundation.strings.ENABLED_ITEMS_SELECTOR)
75+
);
76+
}
77+
return [];
78+
}
79+
80+
componentDidMount() {
81+
this.initializeTabIndex();
82+
}
83+
84+
componentDidUpdate(prevProps: ListItemProps) {
85+
if (prevProps.tabIndex !== this.props.tabIndex) {
86+
this.setState({tabIndex: this.props.tabIndex});
87+
}
88+
}
89+
5990
componentWillUnmount() {
60-
this.props.onDestroy!();
91+
if (this.listItemElement.current) {
92+
const index = this.getIndex(this.listItemElement.current);
93+
this.props.onDestroy!(index);
94+
}
6195
}
6296

6397
get classes() {
64-
const {className, activated, disabled, selected} = this.props;
65-
return classnames('mdc-list-item', className, {
98+
const {className, activated, disabled, selected, getClassNamesFromList} = this.props;
99+
let classesFromList = [''];
100+
if (this.listItemElement.current) {
101+
const index = this.getIndex(this.listItemElement.current);
102+
classesFromList = getClassNamesFromList!()[index];
103+
}
104+
return classnames('mdc-list-item', className, classesFromList, {
66105
[MDCListFoundation.cssClasses.LIST_ITEM_ACTIVATED_CLASS]: activated,
67106
[MDCListFoundation.cssClasses.LIST_ITEM_SELECTED_CLASS]: selected,
68107
'mdc-list-item--disabled': disabled,
@@ -81,6 +120,42 @@ export default class ListItem<T extends HTMLElement = HTMLElement> extends React
81120
return null;
82121
}
83122

123+
private initializeTabIndex = () => {
124+
if (this.listItemElement.current) {
125+
const index = this.getIndex(this.listItemElement.current);
126+
const tabIndex = this.props.getListItemInitialTabIndex!(index);
127+
this.setState({tabIndex});
128+
}
129+
}
130+
131+
getIndex = (listElement: Element) => {
132+
return this.listElements.indexOf(listElement);
133+
}
134+
135+
handleClick = (e: React.MouseEvent<any>) => {
136+
const {onClick} = this.props;
137+
onClick!(e);
138+
this.props.handleClick!(e, this.getIndex(e.currentTarget));
139+
}
140+
141+
handleKeyDown = (e: React.KeyboardEvent<any>) => {
142+
const {onKeyDown} = this.props;
143+
onKeyDown!(e);
144+
this.props.handleKeyDown!(e, this.getIndex(e.currentTarget));
145+
}
146+
147+
handleFocus = (e: React.FocusEvent<any>) => {
148+
const {onFocus} = this.props;
149+
onFocus!(e);
150+
this.props.handleFocus!(e, this.getIndex(e.currentTarget));
151+
}
152+
153+
handleBlur = (e: React.FocusEvent<any>) => {
154+
const {onBlur} = this.props;
155+
onBlur!(e);
156+
this.props.handleBlur!(e, this.getIndex(e.currentTarget));
157+
}
158+
84159
render() {
85160
const {
86161
/* eslint-disable no-unused-vars */
@@ -90,21 +165,51 @@ export default class ListItem<T extends HTMLElement = HTMLElement> extends React
90165
checkboxList,
91166
radioList,
92167
onDestroy,
168+
onClick,
169+
onKeyDown,
170+
onFocus,
171+
onBlur,
172+
handleClick,
173+
handleKeyDown,
174+
handleFocus,
175+
handleBlur,
176+
getListItemInitialTabIndex,
177+
getClassNamesFromList,
178+
tabIndex,
93179
/* eslint-enable no-unused-vars */
94180
tag: Tag,
95181
...otherProps
96182
} = this.props;
183+
97184
return (
98185
// https://github.com/Microsoft/TypeScript/issues/28892
99186
// @ts-ignore
100187
<Tag
188+
{...otherProps}
189+
{...this.context}
101190
role={this.role}
102191
className={this.classes}
103192
ref={this.listItemElement}
104-
{...otherProps}
193+
onClick={this.handleClick}
194+
onKeyDown={this.handleKeyDown}
195+
onFocus={this.handleFocus}
196+
onBlur={this.handleBlur}
197+
tabIndex={this.state.tabIndex}
105198
>
106-
{this.props.children}
199+
{children}
107200
</Tag>
108201
);
109202
}
110203
}
204+
205+
const ListItem: React.FunctionComponent<ListItemProps> = (props) => {
206+
return (
207+
<ListItemContext.Consumer>
208+
{(context) => (
209+
<ListItemBase {...context} {...props}/>
210+
)}
211+
</ListItemContext.Consumer>
212+
);
213+
};
214+
215+
export default ListItem;

0 commit comments

Comments
 (0)