Skip to content
This repository was archived by the owner on Jul 16, 2020. It is now read-only.

Commit b56d3a1

Browse files
author
ricokahler
committed
improved search interactions
1 parent 555fdc1 commit b56d3a1

File tree

4 files changed

+83
-4
lines changed

4 files changed

+83
-4
lines changed

src/client/routes/catalog/catalog.tsx

+34-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { Loading } from 'components/loading';
1616
import { CourseDetail } from './components/course-detail';
1717
import { Course } from './components/course';
1818

19-
const INPUT_DEBOUNCE_TIME = 250;
19+
const INPUT_DEBOUNCE_TIME = 100;
2020

2121
const Container = styled(View)`
2222
flex: 1 1 auto;
@@ -117,6 +117,7 @@ export class Catalog extends React.PureComponent<CatalogProps, CatalogState> {
117117
search$ = new Subject<string>();
118118
searchSubscription: Subscription | undefined;
119119
searching = false;
120+
bigSearchRef = React.createRef<HTMLInputElement>();
120121

121122
constructor(props: CatalogProps) {
122123
super(props);
@@ -138,6 +139,9 @@ export class Catalog extends React.PureComponent<CatalogProps, CatalogState> {
138139
}),
139140
)
140141
.subscribe(this.props.onSearch);
142+
const bigSearchElement = this.bigSearchRef.current;
143+
if (!bigSearchElement) return;
144+
bigSearchElement.focus();
141145
}
142146

143147
componentWillUnmount() {
@@ -146,6 +150,18 @@ export class Catalog extends React.PureComponent<CatalogProps, CatalogState> {
146150
}
147151
}
148152

153+
componentDidUpdate(previousProps: CatalogProps) {
154+
const lastPath = previousProps.location.pathname;
155+
const currentPath = this.props.location.pathname;
156+
157+
if (lastPath !== '/catalog' && currentPath === '/catalog') {
158+
const bigSearchElement = this.bigSearchRef.current;
159+
if (!bigSearchElement) return;
160+
bigSearchElement.focus();
161+
bigSearchElement.select();
162+
}
163+
}
164+
149165
get noSearch() {
150166
return this.props.totalMatches <= 0 && this.state.searchValue === '';
151167
}
@@ -188,7 +204,21 @@ export class Catalog extends React.PureComponent<CatalogProps, CatalogState> {
188204
if (!subjectCode) return null;
189205
if (!courseNumber) return null;
190206

191-
return <CourseDetail subjectCode={subjectCode} courseNumber={courseNumber} />;
207+
return (
208+
<CourseDetail
209+
subjectCode={subjectCode}
210+
courseNumber={courseNumber}
211+
open={this.courseSelected}
212+
/>
213+
);
214+
};
215+
216+
handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
217+
if (e.key !== 'Enter') return;
218+
const firstResult = this.props.searchResults[0];
219+
if (!firstResult) return;
220+
this.handleCourseClick(firstResult);
221+
e.currentTarget.blur();
192222
};
193223

194224
render() {
@@ -202,6 +232,8 @@ export class Catalog extends React.PureComponent<CatalogProps, CatalogState> {
202232
placeholder="Search for a course..."
203233
value={this.state.searchValue}
204234
onChange={this.handleSearch}
235+
innerRef={this.bigSearchRef}
236+
onKeyPress={this.handleKeyPress}
205237
/>
206238
<ClearSearch onClick={this.handleClearSearch}>Clear Search</ClearSearch>
207239
</SearchRow>

src/client/routes/catalog/components/course-detail/course-detail.tsx

+14
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ const EditPrerequisites = styled(ActionableText)`
6363
`;
6464

6565
interface CourseDetailProps {
66+
open: boolean;
6667
course: Model.Course.Model;
6768
overrideApplied: boolean;
6869
prerequisitesContainConcurrent: boolean;
@@ -80,6 +81,19 @@ export class CourseDetail extends React.PureComponent<CourseDetailProps, CourseD
8081
};
8182
}
8283

84+
componentDidMount() {
85+
document.addEventListener('keydown', this.handleKeyDown);
86+
}
87+
componentWillUnmount() {
88+
document.removeEventListener('keydown', this.handleKeyDown);
89+
}
90+
91+
handleKeyDown = (e: KeyboardEvent) => {
92+
if (e.key !== 'Backspace') return;
93+
if (!this.props.open) return;
94+
history.goBack();
95+
};
96+
8397
handleBackClick = () => {
8498
history.goBack();
8599
};

src/client/routes/catalog/components/course-detail/index.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import { CourseDetail } from './course-detail';
44
import { history } from 'client/history';
55

66
interface CourseDetailContainerProps {
7+
open: boolean;
78
subjectCode: string;
89
courseNumber: string;
910
}
1011

1112
const Container = Model.store.connect({
12-
mapStateToProps: (state, { subjectCode, courseNumber }: CourseDetailContainerProps) => {
13+
mapStateToProps: (state, { subjectCode, courseNumber, open }: CourseDetailContainerProps) => {
1314
const { user, prerequisiteOverrides, catalog } = state;
1415
if (!user) throw new Error('user required');
1516

@@ -19,6 +20,7 @@ const Container = Model.store.connect({
1920
history.replace('/catalog');
2021

2122
return {
23+
open: false,
2224
course: {
2325
id: '',
2426
name: '',
@@ -33,6 +35,7 @@ const Container = Model.store.connect({
3335
}
3436

3537
return {
38+
open,
3639
course,
3740
overrideApplied: !!Model.Course.getPrerequisiteOverrideType(
3841
course,

src/client/routes/catalog/components/course.tsx

+31-1
Original file line numberDiff line numberDiff line change
@@ -52,16 +52,26 @@ interface CourseProps {
5252
}
5353
interface CourseState {
5454
showMore: boolean;
55+
focused: boolean;
5556
}
5657

5758
export class Course extends React.PureComponent<CourseProps, CourseState> {
5859
constructor(props: CourseProps) {
5960
super(props);
6061
this.state = {
6162
showMore: false,
63+
focused: false,
6264
};
6365
}
6466

67+
componentDidMount() {
68+
document.addEventListener('keypress', this.handleKeyPress);
69+
}
70+
71+
componentWillUnmount() {
72+
document.removeEventListener('keypress', this.handleKeyPress);
73+
}
74+
6575
get description() {
6676
return this.props.course.description || '';
6777
}
@@ -99,6 +109,21 @@ export class Course extends React.PureComponent<CourseProps, CourseState> {
99109
}));
100110
};
101111

112+
handleKeyPress = (e: KeyboardEvent) => {
113+
const key = e.key;
114+
if (key !== 'Enter') return;
115+
if (!this.state.focused) return;
116+
this.props.onClick();
117+
};
118+
119+
handleFocus = () => {
120+
this.setState({ focused: true });
121+
};
122+
123+
handleBlur = () => {
124+
this.setState({ focused: false });
125+
};
126+
102127
handleClick = (e: React.MouseEvent<Element>) => {
103128
const target = e.target;
104129
if (!target) return;
@@ -112,7 +137,12 @@ export class Course extends React.PureComponent<CourseProps, CourseState> {
112137
render() {
113138
const { course } = this.props;
114139
return (
115-
<Root onClick={this.handleClick}>
140+
<Root
141+
onClick={this.handleClick}
142+
onFocus={this.handleFocus}
143+
onBlur={this.handleBlur}
144+
tabIndex={0}
145+
>
116146
<Summary>
117147
<SimpleName>{Model.Course.getSimpleName(course)}</SimpleName>
118148
<FullName>{course.name}</FullName>

0 commit comments

Comments
 (0)