Skip to content

Commit eaaaa29

Browse files
committed
Implement expanding GlobalSearch
1 parent 26c7bfc commit eaaaa29

File tree

6 files changed

+89
-82
lines changed

6 files changed

+89
-82
lines changed

website/src/views/layout/GlobalSearch.scss

Lines changed: 40 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,59 @@
11
@import '~styles/utils/modules-entry';
2+
23
$icon-size: $font-size-lg;
3-
$input-width: 2rem;
4-
$input-width-open: 21rem;
5-
$input-width-open-lg: 34rem;
4+
$input-width: 14rem;
5+
$input-width-lg: 18rem;
6+
$results-width-open: 21rem;
7+
$results-width-open-lg: 34rem;
8+
$container-vertical-margin: 0.3rem;
69
$container-padding: 0.5rem;
710
$dropdown-height: 40rem;
811

12+
$container-height: $navbar-height - $container-vertical-margin * 2;
13+
$internal-padding: ($container-height - $icon-size) / 2;
14+
915
.container {
1016
composes: btn-svg from global;
1117
position: relative;
12-
height: 100%;
13-
padding-left: $container-padding;
14-
margin-left: auto;
1518
font-size: 0.9rem;
1619

17-
&:hover .icon {
20+
background-color: var(--gray-lightest);
21+
border-radius: $container-height / 2;
22+
margin: $container-vertical-margin $container-padding;
23+
24+
border &:hover .icon {
1825
color: theme-color();
1926
}
2027
}
2128

22-
$icon-dist: $input-width-open - $input-width;
23-
$icon-dist-lg: $input-width-open-lg - $input-width;
24-
2529
.icon {
26-
// position: absolute;
27-
// right: $icon-dist + $container-padding;
28-
// position: relative;
29-
// left: 0;
3030
width: $icon-size;
3131
height: $icon-size;
32-
// transform: translate($icon-dist - 0.25rem);
32+
margin: 0 $internal-padding;
3333
transition: all $desktop-entering-duration $material-deceleration-curve;
3434

35-
@include media-breakpoint-up(lg) {
36-
// right: $icon-dist-lg + $container-padding;
37-
// transform: translate($icon-dist-lg - 0.25rem);
35+
&.iconOpen {
36+
color: theme-color();
3837
}
3938
}
4039

41-
.iconOpen {
42-
color: theme-color();
43-
// transform: translate(0);
44-
}
45-
4640
.input {
47-
$overhang: $icon-size/2; // Extend clickable area to the left of icon
48-
$internal-padding: $overhang; // Symmetrical padding around icon
49-
5041
position: relative;
51-
opacity: 0;
52-
left: -$icon-size - $overhang;
53-
padding-left: $icon-size + $overhang + $internal-padding;
54-
width: $input-width + $overhang + $internal-padding;
42+
width: 0;
5543
height: 100%;
44+
padding: 0;
5645

5746
border: 0;
5847
font-size: $font-size-s;
5948
color: var(--body-color);
6049
background: transparent;
6150
cursor: pointer;
62-
// transform: translate($input-width-open);
63-
transition: transform $desktop-entering-duration $material-deceleration-curve,
64-
opacity $desktop-entering-duration $material-deceleration-curve 75ms;
51+
52+
// Because .input is the "button" that opens the search, we extend input to
53+
// the left of icon so that it looks like the icon is clickable.
54+
$overhang: $icon-size + $internal-padding * 2;
55+
padding-left: $overhang;
56+
margin-left: -$overhang;
6557

6658
&:focus {
6759
outline: none;
@@ -70,23 +62,29 @@ $icon-dist-lg: $input-width-open-lg - $input-width;
7062
}
7163
}
7264

73-
.openWidth {
74-
width: $input-width-open;
65+
.inputOpen {
66+
cursor: auto;
67+
68+
width: $input-width;
7569

7670
@include media-breakpoint-up(lg) {
77-
width: $input-width-open-lg;
71+
width: $input-width-lg;
7872
}
7973
}
8074

81-
.inputOpen {
82-
composes: openWidth;
83-
opacity: 1;
84-
cursor: auto;
75+
.resultsWidth {
76+
margin-right: $internal-padding;
77+
width: $results-width-open;
78+
79+
@include media-breakpoint-up(lg) {
80+
width: $results-width-open-lg;
81+
}
8582
}
8683

8784
.selectListContainer {
8885
position: absolute;
8986
top: $navbar-height;
87+
left: 0;
9088
z-index: $module-select-z-index;
9189
border: $input-btn-border-width solid $input-border-color;
9290
border-width: 0 $input-btn-border-width $input-btn-border-width;
@@ -104,7 +102,7 @@ $icon-dist-lg: $input-width-open-lg - $input-width;
104102
}
105103

106104
.item {
107-
composes: openWidth;
105+
composes: resultsWidth;
108106
display: flex;
109107
justify-content: space-between;
110108
padding: 0.6rem $container-padding;
@@ -155,7 +153,7 @@ $icon-dist-lg: $input-width-open-lg - $input-width;
155153
}
156154

157155
.noResults {
158-
composes: openWidth;
156+
composes: resultsWidth;
159157
text-align: center;
160158

161159
p {

website/src/views/layout/GlobalSearch.tsx

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -21,46 +21,41 @@ import SemesterBadge from 'views/components/SemesterBadge';
2121
import styles from './GlobalSearch.scss';
2222

2323
type Props = {
24+
isOpen: boolean;
2425
getResults: (string: string | null) => SearchResult | null;
2526

2627
onSelectVenue: (venue: Venue) => void;
2728
onSelectModule: (moduleCondensed: ModuleCondensed) => void;
2829
onSearch: (resultType: ResultType, str: string) => void;
30+
open: () => void;
31+
close: () => void;
2932
};
3033

3134
type State = {
32-
isOpen: boolean;
3335
inputValue: string;
3436
};
3537

36-
const PLACEHOLDER = 'Search modules & venues. Try "GER" or "LT".';
38+
const PLACEHOLDER = 'Search modules & venues';
3739

3840
class GlobalSearch extends Component<Props, State> {
3941
input: HTMLInputElement | null = null;
4042

4143
state = {
42-
isOpen: false,
4344
inputValue: '',
4445
};
4546

46-
onOpen = () => {
47-
this.setState({ isOpen: true });
48-
};
49-
50-
onClose = () => {
51-
this.setState({
52-
isOpen: false,
53-
inputValue: '',
54-
});
47+
handleClose = () => {
48+
this.props.close();
49+
this.setState({ inputValue: '' });
5550

5651
if (this.input) this.input.blur();
5752
};
5853

59-
onOuterClick = () => {
54+
handleOuterClick = () => {
6055
// Preserve input value (if present) after user clicks outside.
6156
if (this.state.inputValue) {
57+
this.props.open();
6258
this.setState({
63-
isOpen: true,
6459
// Cannot use prevState as prevState.inputValue will be empty string
6560
// instead of the (non-empty) this.state.inputValue.
6661
// eslint-disable-next-line react/no-access-state-in-setstate
@@ -69,15 +64,15 @@ class GlobalSearch extends Component<Props, State> {
6964

7065
if (this.input) this.input.blur();
7166
} else {
72-
this.onClose();
67+
this.handleClose();
7368
}
7469
};
7570

76-
onInputValueChange = (newInputValue: string) => {
71+
handleInputValueChange = (newInputValue: string) => {
7772
this.setState({ inputValue: newInputValue });
7873
};
7974

80-
onChange = (item: SearchItem | null) => {
75+
handleChange = (item: SearchItem | null) => {
8176
if (item) {
8277
const { onSelectModule, onSelectVenue, onSearch } = this.props;
8378

@@ -96,7 +91,7 @@ class GlobalSearch extends Component<Props, State> {
9691
}
9792
}
9893

99-
this.onClose();
94+
this.handleClose();
10095
};
10196

10297
stateReducer = (state: DownshiftState<SearchItem>, changes: StateChangeOptions<SearchItem>) => {
@@ -134,7 +129,7 @@ class GlobalSearch extends Component<Props, State> {
134129
}}
135130
className={classnames(styles.input, { [styles.inputOpen]: isOpen })}
136131
{...getInputProps({ placeholder: PLACEHOLDER })}
137-
onFocus={this.onOpen}
132+
onFocus={this.props.open}
138133
/>
139134
</Fragment>
140135
);
@@ -286,14 +281,15 @@ class GlobalSearch extends Component<Props, State> {
286281
};
287282

288283
render() {
289-
const { isOpen, inputValue } = this.state;
284+
const { inputValue } = this.state;
285+
const { isOpen } = this.props;
290286

291287
return (
292288
<Downshift
293289
isOpen={isOpen}
294-
onOuterClick={this.onOuterClick}
295-
onChange={this.onChange}
296-
onInputValueChange={this.onInputValueChange}
290+
onOuterClick={this.handleOuterClick}
291+
onChange={this.handleChange}
292+
onInputValueChange={this.handleInputValueChange}
297293
inputValue={inputValue}
298294
stateReducer={this.stateReducer}
299295
/* Hack to force item selection to be empty */

website/src/views/layout/GlobalSearchContainer.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { useDispatch, useSelector } from 'react-redux';
77
import { useHistory } from 'react-router-dom';
88

99
import useMediaQuery from 'views/hooks/useMediaQuery';
10-
import GlobalSearch from 'views/layout/GlobalSearch';
10+
import GlobalSearch, { Props as GlobalSearchProps } from 'views/layout/GlobalSearch';
1111
import { modulePage, venuePage } from 'views/routes/paths';
1212

1313
import { ResultType, SearchResult, VENUE_RESULT } from 'types/views';
@@ -20,7 +20,9 @@ const RESULTS_LIMIT = 10;
2020
const LONG_LIST_LIMIT = 70;
2121
const MIN_INPUT_LENGTH = 2;
2222

23-
const GlobalSearchContainer: FC = () => {
23+
type Props = Pick<GlobalSearchProps, 'isOpen' | 'open' | 'close'>;
24+
25+
const GlobalSearchContainer: FC<Props> = ({ ...globalSearchPassthroughProps }) => {
2426
const dispatch = useDispatch();
2527
useEffect(() => {
2628
dispatch(fetchVenueList());
@@ -102,6 +104,7 @@ const GlobalSearchContainer: FC = () => {
102104
onSelectModule={onSelectModule}
103105
onSelectVenue={onSelectVenue}
104106
onSearch={onSearch}
107+
{...globalSearchPassthroughProps}
105108
/>
106109
);
107110
};

website/src/views/layout/Navbar.scss

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ $logo-vert-padding: 0.875rem;
4545
// Explicit width so the logo will not expand
4646
// to occupy the entire navbar
4747
width: 8.3rem;
48-
margin: $logo-vert-padding $grid-gutter-width;
48+
margin: $logo-vert-padding $grid-gutter-width/2 $logo-vert-padding $grid-gutter-width;
4949
}
5050

5151
.brandLogo {
@@ -63,8 +63,6 @@ $logo-vert-padding: 0.875rem;
6363

6464
.navRight {
6565
display: flex;
66-
align-items: center;
67-
padding: $vert-padding $grid-gutter-width/2;
6866
}
6967

7068
.weekText {
@@ -89,6 +87,7 @@ $logo-vert-padding: 0.875rem;
8987
}
9088

9189
.brand {
90+
// display: flex;
9291
flex: 1;
9392
margin: $logo-vert-padding 0 $logo-vert-padding $grid-gutter-width/2;
9493
}

website/src/views/layout/Navbar.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable arrow-body-style */
2-
import { FC } from 'react';
2+
import { FC, useCallback, useState } from 'react';
33
import { NavLink } from 'react-router-dom';
4-
import weekText from 'utils/weekText';
4+
55
import ErrorBoundary from 'views/errors/ErrorBoundary';
66
import Logo from 'img/nusmods-logo.svg';
77

@@ -11,14 +11,21 @@ import styles from './Navbar.scss';
1111
import Navtabs from './Navtabs';
1212

1313
const Navbar: FC = () => {
14+
const [isSearchOpen, setIsSearchOpen] = useState(false);
15+
const handleSearchOpen = useCallback(() => setIsSearchOpen(true), []);
16+
const handleSearchClose = useCallback(() => setIsSearchOpen(false), []);
1417
return (
1518
<div className={styles.navbarWrapper}>
1619
{/* Bottom bar must be above the top bar in HTML, so that top bar can be interacted with. */}
1720
<nav className={styles.topBar}>
1821
<div className={styles.navLeft}>
19-
<NavLogo />
22+
{!isSearchOpen && <NavLogo />}
2023
<ErrorBoundary>
21-
<GlobalSearchContainer />
24+
<GlobalSearchContainer
25+
isOpen={isSearchOpen}
26+
open={handleSearchOpen}
27+
close={handleSearchClose}
28+
/>
2229
</ErrorBoundary>
2330
</div>
2431
<div className={styles.navRight}>
@@ -31,7 +38,6 @@ const Navbar: FC = () => {
3138
</div>
3239
);
3340
};
34-
// <div className={styles.weekText}>{weekText}</div>
3541

3642
export default Navbar;
3743

website/src/views/layout/Navtabs.scss

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,13 @@ $divider-margin: 0.3rem;
8989
}
9090

9191
@include media-breakpoint-up(lg) {
92-
.link {
93-
min-width: 8em;
94-
}
92+
// .link {
93+
// min-width: 8em;
94+
// }
9595

96-
.title {
97-
display: block;
98-
}
96+
// .title {
97+
// display: block;
98+
// }
9999

100100
// .divider {
101101
// margin: $divider-margin 1rem $divider-margin 1.4rem;
@@ -104,6 +104,11 @@ $divider-margin: 0.3rem;
104104

105105
@include media-breakpoint-up(xl) {
106106
.link {
107-
min-width: 10em;
107+
// min-width: 10em;
108+
min-width: 7em;
109+
}
110+
111+
.title {
112+
display: block;
108113
}
109114
}

0 commit comments

Comments
 (0)