Skip to content

Commit

Permalink
Make ModulesSelect remain open after selection (#655)
Browse files Browse the repository at this point in the history
* Make ModulesSelect remains open after selection

* Fix cursor jumping and refactor ModulesSelect

- Fix cursor jumping to the end of the input
- Refactor `ModulesSelect`

* Fix modal opening by default

* Change selectedItem type to ModuleCode

* Remove unused ref and move Close button into Modal

* Fix incorrect isModalOpen state

* Fix ModulesSelect not close after Escape is pressed

* Fix position of close button

* Set default highlighted index

* Minor tweaks

* Revert template update and add comment on iOS bug

* Add background color and replace styles with list-unstyled from BS
  • Loading branch information
Eugene Lim authored and ZhangYiJiang committed Jan 9, 2018
1 parent bcd198c commit b15733c
Show file tree
Hide file tree
Showing 2 changed files with 79 additions and 42 deletions.
113 changes: 75 additions & 38 deletions www/src/js/views/timetable/ModulesSelect.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
// @flow
import React, { Component } from 'react';
import _ from 'lodash';
import { has } from 'lodash';
import Downshift from 'downshift';
import classnames from 'classnames';

import type { ModuleSelectList } from 'types/reducers';
import type { ModuleCode } from 'types/modules';
import { createSearchPredicate, sortModules } from 'utils/moduleSearch';
import { breakpointUp } from 'utils/css';
import makeResponsive from 'views/hocs/makeResponsive';
Expand All @@ -22,37 +23,68 @@ type Props = {
};

type State = {
isModalOpen: boolean,
isOpen: boolean,
isModalOpen: boolean,
inputValue: string,
selectedItem: ?ModuleCode,
};

const RESULTS_LIMIT = 500;

class ModulesSelect extends Component<Props, State> {
input: ?HTMLInputElement;
state = {
isOpen: false,
isModalOpen: false,
inputValue: '',
selectedItem: null,
};

onChange = (selection: any) => {
// Refocus after choosing a module
if (this.input) this.input.focus();
if (selection) this.props.onChange(selection);
onStateChange = (changes: any) => {
if (has(changes, 'selectedItem')) {
this.props.onChange(changes.selectedItem);
}
};

onFocus = () => this.setState({ isOpen: true });
onOuterClick = () => this.setState({ isOpen: false });
toggleModal = () => this.setState({ isModalOpen: !this.state.isModalOpen });
onBlur = () => {
if (!this.state.inputValue && this.state.isModalOpen) {
this.closeSelect();
}
};

getFilteredModules = (inputValue: string) => {
if (!inputValue) {
return [];
onInputChange = (event) => {
this.setState({ inputValue: event.target.value });
};

onFocus = () => this.openSelect();
onOuterClick = () => this.closeSelect();

onKeyDown = (event) => {
if (event.key === 'Escape') {
this.closeSelect();
event.target.blur();
}
};

closeSelect = () => {
this.setState({
isOpen: false,
isModalOpen: false,
inputValue: '',
selectedItem: null,
});
};

openSelect = () => {
this.setState({
isOpen: true,
isModalOpen: !this.props.matchBreakpoint,
});
};

getFilteredModules = (inputValue: string) => {
if (!inputValue) return [];
const predicate = createSearchPredicate(inputValue);
const results = this.props.moduleList.filter(predicate);

return sortModules(inputValue, results.slice(0, RESULTS_LIMIT));
};

Expand Down Expand Up @@ -80,20 +112,17 @@ class ModulesSelect extends Component<Props, State> {
{placeholder}
</label>
<input
ref={(input) => {
this.input = input;
}}
autoFocus={isModalOpen}
className={styles.input}
{...getInputProps({ placeholder })}
disabled={disabled}
onFocus={this.onFocus}
/* Also prevents iOS "Done" button from resetting input */
onBlur={() => {
if (!inputValue && isModalOpen) this.toggleModal();
}}
{...getInputProps({
className: styles.input,
autoFocus: isModalOpen,
placeholder,
disabled,
onFocus: this.onFocus,
onBlur: this.onBlur,
onChange: this.onInputChange,
onKeyDown: this.onKeyDown,
})}
/>
{isModalOpen && <CloseButton className={styles.close} onClick={this.toggleModal} />}
{showResults && (
<ol className={styles.selectList}>
{results.map(
Expand All @@ -105,6 +134,8 @@ class ModulesSelect extends Component<Props, State> {
[styles.optionSelected]: highlightedIndex === index,
})}
>
{/* Using interpolated string instead of JSX because of iOS Safari
bug that drops the whitespace between the module code and title */}
{`${module.ModuleCode} ${module.ModuleTitle}`}
<div>
<span className="badge badge-info">Added</span>
Expand All @@ -121,6 +152,8 @@ class ModulesSelect extends Component<Props, State> {
[styles.optionSelected]: highlightedIndex === index,
})}
>
{/* Using interpolated string instead of JSX because of iOS Safari
bug that drops the whitespace between the module code and title */}
{`${module.ModuleCode} ${module.ModuleTitle}`}
</li>
),
Expand All @@ -143,32 +176,36 @@ class ModulesSelect extends Component<Props, State> {
};

render() {
const { isModalOpen, isOpen } = this.state;
const { isModalOpen } = this.state;
const { matchBreakpoint, disabled } = this.props;

const downshiftComponent = (
<Downshift
isOpen={isModalOpen || isOpen}
onOuterClick={this.onOuterClick}
onChange={this.onChange}
render={this.renderDropdown}
onStateChange={this.onStateChange}
inputValue={this.state.inputValue}
isOpen={this.state.isOpen}
selectedItem={this.state.selectedItem}
defaultHighlightedIndex={0}
/* Hack to force item selection to be empty */
itemToString={_.stubString}
selectedItem=""
/>
);
return matchBreakpoint ? (
downshiftComponent
) : (

if (matchBreakpoint) {
return downshiftComponent;
}

return (
<div>
<button className={styles.input} onClick={this.toggleModal} disabled={disabled}>
<button className={styles.input} onClick={this.openSelect} disabled={disabled}>
{this.props.placeholder}
</button>
<Modal
isOpen={!disabled && isModalOpen}
onRequestClose={this.toggleModal}
onRequestClose={this.closeSelect}
className={styles.modal}
>
<CloseButton className={styles.close} onClick={this.closeSelect} />
{downshiftComponent}
</Modal>
</div>
Expand Down
8 changes: 4 additions & 4 deletions www/src/js/views/timetable/ModulesSelect.scss
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ $module-list-height: 13.5rem;
position: absolute;
top: 0;
right: 0;
z-index: 1;
width: $input-height;
height: $input-height;
}
Expand All @@ -53,11 +54,10 @@ div > .modal {
}

.selectList {
composes: scrollable-y from global;
composes: scrollable-y list-unstyled from global;
max-height: calc(100% - #{$input-height});
padding: 0;
margin: 0;
list-style: none;
// Background color so that elements behind this won't peek through for iOS overscroll
background: var(--body-bg);
}

@include media-breakpoint-up(md) {
Expand Down

0 comments on commit b15733c

Please sign in to comment.