From b15733c7507a98f273814051f03ae3a61447d97b Mon Sep 17 00:00:00 2001 From: Eugene Lim Date: Tue, 9 Jan 2018 20:00:43 +0800 Subject: [PATCH] Make ModulesSelect remain open after selection (#655) * 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 --- www/src/js/views/timetable/ModulesSelect.jsx | 113 ++++++++++++------ www/src/js/views/timetable/ModulesSelect.scss | 8 +- 2 files changed, 79 insertions(+), 42 deletions(-) diff --git a/www/src/js/views/timetable/ModulesSelect.jsx b/www/src/js/views/timetable/ModulesSelect.jsx index fa4222af55..0b2b59aee7 100644 --- a/www/src/js/views/timetable/ModulesSelect.jsx +++ b/www/src/js/views/timetable/ModulesSelect.jsx @@ -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'; @@ -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 { - 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)); }; @@ -80,20 +112,17 @@ class ModulesSelect extends Component { {placeholder} { - 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 && } {showResults && (
    {results.map( @@ -105,6 +134,8 @@ class ModulesSelect extends Component { [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}`}
    Added @@ -121,6 +152,8 @@ class ModulesSelect extends Component { [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}`} ), @@ -143,32 +176,36 @@ class ModulesSelect extends Component { }; render() { - const { isModalOpen, isOpen } = this.state; + const { isModalOpen } = this.state; const { matchBreakpoint, disabled } = this.props; + const downshiftComponent = ( ); - return matchBreakpoint ? ( - downshiftComponent - ) : ( + + if (matchBreakpoint) { + return downshiftComponent; + } + + return (
    - + {downshiftComponent}
    diff --git a/www/src/js/views/timetable/ModulesSelect.scss b/www/src/js/views/timetable/ModulesSelect.scss index 66793cdd21..3afda1e306 100644 --- a/www/src/js/views/timetable/ModulesSelect.scss +++ b/www/src/js/views/timetable/ModulesSelect.scss @@ -33,6 +33,7 @@ $module-list-height: 13.5rem; position: absolute; top: 0; right: 0; + z-index: 1; width: $input-height; height: $input-height; } @@ -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) {