Repository: uber-web/baseui
Pull request #667 (2018-12-18 20:44:43)

fix(datepicker): add overrides functionality


Patch:
diff --git a/package.json b/package.json
index bfd7c0e2c..7dd21f7c8 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "baseui",
-  "version": "5.4.0",
+  "version": "5.4.1",
   "description": "React implementation of uber design components platform.",
   "keywords": [
     "react",
@@ -128,7 +128,7 @@
   },
   "dependencies": {
     "date-fns": "2.0.0-alpha.26",
-    "memoize-one": "^4.0.2",
+    "memoize-one": "^5.0.0",
     "popper.js": "^1.14.3",
     "react-dropzone": "^8.0.0",
     "react-input-mask": "^2.0.4",
diff --git a/src/datepicker/calendar-header.js b/src/datepicker/calendar-header.js
index 4f1006dde..b38e09dfb 100644
--- a/src/datepicker/calendar-header.js
+++ b/src/datepicker/calendar-header.js
@@ -20,7 +20,9 @@ import {
   setYear,
   subMonths,
 } from './utils/index.js';
+import {getOverrides, mergeOverrides} from '../helpers/overrides.js';
 import type {HeaderPropsT} from './types.js';
+import type {SharedStylePropsT} from '../select/types.js';
 
 export default class CalendarHeader extends React.Component<HeaderPropsT> {
   static defaultProps = {
@@ -30,6 +32,7 @@ export default class CalendarHeader extends React.Component<HeaderPropsT> {
     minDate: null,
     onMonthChange: () => {},
     onYearChange: () => {},
+    overrides: {},
     setActiveState: () => {},
   };
 
@@ -49,41 +52,79 @@ export default class CalendarHeader extends React.Component<HeaderPropsT> {
     this.props.onMonthChange({date: subMonths(this.props.date, 1)});
   };
 
+  setActive = () => {
+    this.props.setActiveState(true);
+  };
+
+  setInactive = () => {
+    this.props.setActiveState(false);
+  };
+
   renderPreviousMonthButton = () => {
-    const allPrevDaysDisabled = monthDisabledBefore(
-      this.props.date,
-      this.props,
-    );
+    const {date, overrides = {}} = this.props;
+    const allPrevDaysDisabled = monthDisabledBefore(date, this.props);
+
     if (allPrevDaysDisabled) {
       return;
     }
+
+    const [PrevButton, prevButtonProps] = getOverrides(
+      overrides.PrevButton,
+      ArrowLeft,
+    );
     let clickHandler = this.decreaseMonth;
     if (allPrevDaysDisabled) {
       clickHandler = null;
     }
     return (
-      <ArrowLeft
+      <PrevButton
         role="button"
-        onClick={clickHandler}
         overrides={{Svg: {style: {cursor: 'pointer'}}}}
+        {...prevButtonProps}
+        // Adding internal click handler last that means no custom click handler
+        // can be added throught props overrides unless passing a component
+        // replacement that handles internal handler an then calls its custom one
+        // What we can do here is extract an onClick handler from `nextButtonProps`
+        // and call it along with `this.increaseMonth` withing a handler that is created
+        // on render
+        onClick={clickHandler}
       />
     );
   };
 
   renderNextMonthButton = () => {
-    const allNextDaysDisabled = monthDisabledAfter(this.props.date, this.props);
+    const {date, overrides = {}} = this.props;
+    const allNextDaysDisabled = monthDisabledAfter(date, this.props);
+
     if (allNextDaysDisabled) {
       return;
     }
+
+    const [NextButton, nextButtonProps] = getOverrides(
+      overrides.NextButton,
+      ArrowRight,
+    );
     let clickHandler = this.increaseMonth;
+    // The other option is to always provide a click handler and let customers
+    // override its functionality based on the `$allPrevDaysDisabled` prop
+    // in a custom NextButton component override
+    // Their options would be to render `null` or not apply the components handler
+    // on click or do nothing
     if (allNextDaysDisabled) {
       clickHandler = null;
     }
     return (
-      <ArrowRight
+      <NextButton
         role="button"
-        onClick={clickHandler}
         overrides={{Svg: {style: {cursor: 'pointer'}}}}
+        {...nextButtonProps}
+        // Adding internal click handler last that means no custom click handler
+        // can be added throught props overrides unless passing a component
+        // replacement that handles internal handler an then calls its custom one
+        // What we can do here is extract an onClick handler from `nextButtonProps`
+        // and call it along with `this.increaseMonth` withing a handler that is created
+        // on render
+        onClick={clickHandler}
       />
     );
   };
@@ -91,7 +132,7 @@ export default class CalendarHeader extends React.Component<HeaderPropsT> {
   getSelectOverrides({width}: {width: string}) {
     return {
       ControlContainer: {
-        style: props => {
+        style: (props: SharedStylePropsT) => {
           const {
             $isFocused,
             $isPseudoFocused,
@@ -99,10 +140,12 @@ export default class CalendarHeader extends React.Component<HeaderPropsT> {
           } = props;
           return {
             width,
-            backgroundColor: colors.primary,
+            backgroundColor:
+              $isFocused || $isPseudoFocused
+                ? colors.primary500
+                : colors.primary,
             color: colors.white,
-            borderColor:
-              $isFocused || $isPseudoFocused ? colors.white : colors.primary,
+            borderColor: 'transparent',
           };
         },
       },
@@ -112,12 +155,12 @@ export default class CalendarHeader extends React.Component<HeaderPropsT> {
         },
       },
       SelectArrow: {
-        style: ({$theme: {colors}}) => ({
+        style: ({$theme: {colors}}: SharedStylePropsT) => ({
           color: 'inherit',
         }),
       },
       ValueContainer: {
-        style: ({$theme: {sizing}}) => ({
+        style: ({$theme: {sizing}}: SharedStylePropsT) => ({
           paddingTop: '0',
           paddingBottom: '0',
           paddingLeft: sizing.scale200,
@@ -125,7 +168,7 @@ export default class CalendarHeader extends React.Component<HeaderPropsT> {
         }),
       },
       SingleValue: {
-        style: ({$theme: {sizing}}) => ({
+        style: ({$theme: {sizing}}: SharedStylePropsT) => ({
           paddingTop: '0',
           paddingBottom: '0',
           paddingLeft: sizing.scale200,
@@ -135,66 +178,93 @@ export default class CalendarHeader extends React.Component<HeaderPropsT> {
     };
   }
 
-  setActive = () => {
-    this.props.setActiveState(true);
-  };
-
-  setInactive = () => {
-    this.props.setActiveState(false);
-  };
-
   renderMonthDropdown() {
+    const {date, locale, overrides = {}} = this.props;
+    const [MonthSelect, monthSelectProps] = getOverrides(
+      overrides.MonthSelect,
+      Select,
+    );
+    const selectOverrides = mergeOverrides(
+      this.getSelectOverrides({width: '115px'}),
+      // $FlowFixMe
+      monthSelectProps && monthSelectProps.overrides,
+    );
     const monthOptions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].map(M => ({
       id: M,
-      label: getMonthInLocale(M, this.props.locale),
+      label: getMonthInLocale(M, locale),
     }));
     return (
-      <Select
-        onOpen={this.setInactive}
-        onClose={this.setActive}
-        overrides={this.getSelectOverrides({width: '115px'})}
+      <MonthSelect
         clearable={false}
-        value={[{id: getMonth(this.props.date)}]}
+        value={[{id: getMonth(date)}]}
         maxDropdownHeight={'300px'}
-        // $FlowFixMe
-        onChange={this.handleMonthChange}
         options={monthOptions}
         searchable={false}
+        {...monthSelectProps}
+        // Adding event handlers after customers overrides in order to
+        // make sure the components functions as expected
+        // We can extract the handlers from props overrides
+        // and call it along with internal handlers by creating an inline handler
+        onOpen={this.setInactive}
+        onClose={this.setActive}
+        onChange={this.handleMonthChange}
+        // internal and incoming overrides are merged above
+        overrides={selectOverrides}
       />
     );
   }
 
   renderYearDropdown() {
-    const minYear = this.props.minDate ? getYear(this.props.minDate) : 1900;
-    const maxYear = this.props.maxDate ? getYear(this.props.maxDate) : 2100;
+    const {date, minDate, maxDate, overrides = {}} = this.props;
+    const [YearSelect, yearSelectProps] = getOverrides(
+      overrides.YearSelect,
+      Select,
+    );
+    const selectOverrides = mergeOverrides(
+      this.getSelectOverrides({width: '80px'}),
+      // $FlowFixMe
+      yearSelectProps && yearSelectProps.overrides,
+    );
+    const maxYear = maxDate ? getYear(maxDate) : 2100;
+    const minYear = minDate ? getYear(minDate) : 1900;
     const yearOptions = [];
     for (let i = minYear; i <= maxYear; i++) {
       yearOptions.push({id: i, label: i});
     }
     return (
-      <Select
-        onOpen={this.setInactive}
-        onClose={this.setActive}
-        overrides={this.getSelectOverrides({width: '80px'})}
+      <YearSelect
         clearable={false}
-        value={[{id: getYear(this.props.date)}]}
+        value={[{id: getYear(date)}]}
         maxDropdownHeight={'300px'}
-        // $FlowFixMe
-        onChange={this.handleYearChange}
         options={yearOptions}
         searchable={false}
+        {...yearSelectProps}
+        // Adding event handlers after customers overrides in order to
+        // make sure the components functions as expected
+        // We can extract the handlers from props overrides
+        // and call it along with internal handlers by creating an inline handler
+        onOpen={this.setInactive}
+        onClose={this.setActive}
+        onChange={this.handleYearChange}
+        // internal and incoming overrides are merged above
+        overrides={selectOverrides}
       />
     );
   }
 
   render() {
+    const {overrides = {}} = this.props;
+    const [CalendarHeader, calendarHeaderProps] = getOverrides(
+      overrides.CalendarHeader,
+      StyledHeader,
+    );
     return (
-      <StyledHeader>
+      <CalendarHeader {...calendarHeaderProps}>
         {this.renderPreviousMonthButton()}
         {this.renderMonthDropdown()}
         {this.renderYearDropdown()}
         {this.renderNextMonthButton()}
-      </StyledHeader>
+      </CalendarHeader>
     );
   }
 }
diff --git a/src/datepicker/calendar.js b/src/datepicker/calendar.js
index f39b1d498..de28765bb 100644
--- a/src/datepicker/calendar.js
+++ b/src/datepicker/calendar.js
@@ -27,6 +27,7 @@ import {
   getEffectiveMaxDate,
 } from './utils/index.js';
 import {WEEKDAYS} from './constants.js';
+import {getOverrides} from '../helpers/overrides.js';
 import type {CalendarPropsT} from './types.js';
 
 export default class Calendar extends React.Component<
@@ -49,6 +50,7 @@ export default class Calendar extends React.Component<
     onMonthChange: () => {},
     onYearChange: () => {},
     onSelect: () => {},
+    overrides: {},
     peekNextMonth: false,
     selected: null,
     setActiveState: () => {},
@@ -128,30 +130,43 @@ export default class Calendar extends React.Component<
   };
 
   renderMonthHeader = (date: Date = this.state.date, order: number) => {
-    const {locale} = this.props;
+    const {locale, overrides = {}} = this.props;
     const startOfWeek = getStartOfWeek(date, locale);
+    const [MonthHeader, monthHeaderProps] = getOverrides(
+      overrides.MonthHeader,
+      StyledMonthHeader,
+    );
+    const [WeekdayHeader, weekdayHeaderProps] = getOverrides(
+      overrides.WeekdayHeader,
+      StyledDay,
+    );
     return (
-      <StyledMonthHeader>
+      <MonthHeader {...monthHeaderProps}>
         {WEEKDAYS.map(offset => {
           const day = addDays(startOfWeek, offset);
           return (
-            <StyledDay $disabled key={offset}>
+            <WeekdayHeader $disabled key={offset} {...weekdayHeaderProps}>
               {getWeekdayMinInLocale(day, locale)}
-            </StyledDay>
+            </WeekdayHeader>
           );
         })}
-      </StyledMonthHeader>
+      </MonthHeader>
     );
   };
 
   renderMonths = () => {
-    var monthList = [];
-    for (var i = 0; i < this.props.monthsShown; ++i) {
-      var monthDate = addMonths(this.state.date, i);
-      var monthKey = `month-${i}`;
+    const {overrides = {}} = this.props;
+    const monthList = [];
+    const [CalendarContainer, calendarContainerProps] = getOverrides(
+      overrides.CalendarContainer,
+      StyledCalendarContainer,
+    );
+    for (let i = 0; i < this.props.monthsShown; ++i) {
+      const monthDate = addMonths(this.state.date, i);
+      const monthKey = `month-${i}`;
       monthList.push(this.renderCalendarHeader(monthDate, i));
       monthList.push(
-        <StyledCalendarContainer key={monthKey}>
+        <CalendarContainer key={monthKey} {...calendarContainerProps}>
           {this.renderMonthHeader(monthDate, i)}
           <Month
             date={monthDate}
@@ -168,16 +183,19 @@ export default class Calendar extends React.Component<
             onDayMouseOver={this.props.onDayMouseOver}
             onDayMouseLeave={this.props.onDayMouseLeave}
             onSelect={this.props.onSelect}
+            overrides={overrides}
             selected={this.props.selected}
             peekNextMonth={this.props.peekNextMonth}
           />
-        </StyledCalendarContainer>,
+        </CalendarContainer>,
       );
     }
     return monthList;
   };
 
   render() {
-    return <StyledRoot>{this.renderMonths()}</StyledRoot>;
+    const {overrides = {}} = this.props;
+    const [Root, rootProps] = getOverrides(overrides.Root, StyledRoot);
+    return <Root {...rootProps}>{this.renderMonths()}</Root>;
   }
 }
diff --git a/src/datepicker/day.js b/src/datepicker/day.js
index 68ef2372c..04ed13a20 100644
--- a/src/datepicker/day.js
+++ b/src/datepicker/day.js
@@ -8,6 +8,7 @@ LICENSE file in the root directory of this source tree.
 import * as React from 'react';
 import {StyledDay} from './styled-components.js';
 import {getDay, getMonth, getDate, isSameDay} from './utils/index.js';
+import {getOverrides} from '../helpers/overrides.js';
 import type {DayPropsT, DayStateT} from './types.js';
 
 export default class Day extends React.Component<DayPropsT, DayStateT> {
@@ -20,6 +21,7 @@ export default class Day extends React.Component<DayPropsT, DayStateT> {
     onSelect: () => {},
     onMouseOver: () => {},
     onMouseLeave: () => {},
+    overrides: {},
     peekNextMonth: true,
     selected: false,
     // pseudoSelected: false,
@@ -113,23 +115,29 @@ export default class Day extends React.Component<DayPropsT, DayStateT> {
   }
 
   render() {
-    const {date, peekNextMonth} = this.props;
+    const {date, peekNextMonth, overrides = {}} = this.props;
     const sharedProps = this.getSharedProps();
+    const [Day, dayProps] = getOverrides(overrides.Day, StyledDay);
     return !peekNextMonth && sharedProps.$outsideMonth ? (
-      <StyledDay />
+      <Day />
     ) : (
       // eslint-disable-next-line jsx-a11y/mouse-events-have-key-events
-      <StyledDay
-        onClick={this.onClick}
-        onMouseOver={this.onMouseOver}
-        onMouseLeave={this.onMouseLeave}
+      <Day
         aria-label={`day-${getDate(date)}`}
         role="option"
         tabIndex={this.props.selected ? '0' : '-1'}
         {...sharedProps}
+        {...dayProps}
+        // Adding event handlers after customers overrides in order to
+        // make sure the components functions as expected
+        // We can extract the handlers from props overrides
+        // and call it along with internal handlers by creating an inline handler
+        onClick={this.onClick}
+        onMouseOver={this.onMouseOver}
+        onMouseLeave={this.onMouseLeave}
       >
         {getDate(date)}
-      </StyledDay>
+      </Day>
     );
   }
 }
diff --git a/src/datepicker/examples-list.js b/src/datepicker/examples-list.js
index f826c93b8..e2cf63e49 100644
--- a/src/datepicker/examples-list.js
+++ b/src/datepicker/examples-list.js
@@ -11,6 +11,5 @@ module.exports = {
   SIMPLE_EXAMPLE: 'Stateless datepicker',
   STATEFUL_EXAMPLE: 'Stateful datepicker',
   STATEFUL_IN_POPOVER: 'Stateful datepicker in a popover',
-  STYLE_PROPS_OVERRIDES: 'Style and props overrides',
-  STYLED_COMPONENT_OVERRIDES: 'Styled components overrides',
+  WITH_OVERRIDES: 'Datepicker with overrides',
 };
diff --git a/src/datepicker/examples.js b/src/datepicker/examples.js
index 6f6b5bffe..a5761f799 100644
--- a/src/datepicker/examples.js
+++ b/src/datepicker/examples.js
@@ -84,4 +84,51 @@ export default {
   [tests.STATEFUL_IN_POPOVER]: function Story3() {
     return <DatepickerWithInput />;
   },
+
+  [tests.WITH_OVERRIDES]: function Story3() {
+    const selectOverrides = {
+      ControlContainer: {
+        style: ({$theme, $isFocused, $isPseudoFocused}) => ({
+          backgroundColor:
+            $isFocused || $isPseudoFocused
+              ? $theme.colors.positive500
+              : $theme.colors.positive,
+        }),
+      },
+      OptionContent: {
+        style: ({$theme, $isHighlighted}) => ({
+          color: $isHighlighted
+            ? $theme.colors.positive
+            : $theme.colors.foreground,
+        }),
+      },
+    };
+    return (
+      <StatefulDatepicker
+        initialState={{value: new Date()}}
+        overrides={{
+          CalendarHeader: {
+            style: ({$theme}) => ({
+              backgroundColor: $theme.colors.positive,
+            }),
+          },
+          MonthSelect: {
+            props: {overrides: selectOverrides},
+          },
+          YearSelect: {
+            props: {overrides: selectOverrides},
+          },
+          Day: {
+            style: ({$theme, $selected, $isHovered, $isHighlighted}) => ({
+              backgroundColor: $selected
+                ? $theme.colors.positive
+                : $isHovered || $isHighlighted
+                  ? $theme.colors.positive100
+                  : 'transparent',
+            }),
+          },
+        }}
+      />
+    );
+  },
 };
diff --git a/src/datepicker/index.js b/src/datepicker/index.js
index 40c81fe47..8fa66ce6b 100644
--- a/src/datepicker/index.js
+++ b/src/datepicker/index.js
@@ -8,7 +8,7 @@ LICENSE file in the root directory of this source tree.
 export {default as Unstable_StatefulCalendar} from './stateful-calendar.js';
 export {default as StatefulContainer} from './stateful-container.js';
 export {default as Unstable_Calendar} from './calendar.js';
-// Utils functions
+// Util functions
 export {formatDate} from './utils/index.js';
 // Constants
 export {STATE_CHANGE_TYPE} from './constants.js';
diff --git a/src/datepicker/month.js b/src/datepicker/month.js
index 0cf4dea7c..c983b3b49 100644
--- a/src/datepicker/month.js
+++ b/src/datepicker/month.js
@@ -17,6 +17,7 @@ import {
   getMonth,
   addWeeks,
 } from './utils/index.js';
+import {getOverrides} from '../helpers/overrides.js';
 import type {MonthPropsT} from './types.js';
 
 const defaultProps = {
@@ -32,6 +33,7 @@ const defaultProps = {
   onDayClick: () => {},
   onDayMouseOver: () => {},
   onDayMouseLeave: () => {},
+  overrides: {},
   peekNextMonth: false,
   selected: null,
 };
@@ -72,6 +74,7 @@ export default class CalendarMonth extends React.Component<MonthPropsT> {
           onDayMouseOver={this.props.onDayMouseOver}
           onDayMouseLeave={this.props.onDayMouseLeave}
           onSelect={this.props.onSelect}
+          overrides={this.props.overrides}
           peekNextMonth={this.props.peekNextMonth}
           selected={this.props.selected}
         />,
@@ -85,13 +88,16 @@ export default class CalendarMonth extends React.Component<MonthPropsT> {
   };
 
   render() {
+    const {overrides = {}} = this.props;
+    const [Month, monthProps] = getOverrides(overrides.Month, StyledMonth);
     return (
-      <StyledMonth
+      <Month
         role="listbox"
         aria-label={`month-${formatDate(this.props.date, 'YYYY-MM')}`}
+        {...monthProps}
       >
         {this.renderWeeks()}
-      </StyledMonth>
+      </Month>
     );
   }
 }
diff --git a/src/datepicker/types.js b/src/datepicker/types.js
index 133b0cc0b..59280a38c 100644
--- a/src/datepicker/types.js
+++ b/src/datepicker/types.js
@@ -8,11 +8,27 @@ LICENSE file in the root directory of this source tree.
 /* eslint-disable flowtype/generic-spacing */
 import * as React from 'react';
 import type {ThemeT} from '../styles/types.js';
+import type {OverrideT} from '../helpers/overrides.js';
 import {STATE_CHANGE_TYPE} from './constants.js';
 
 // eslint-disable-next-line flowtype/no-weak-types
 type LocaleT = any; // see https://github.com/date-fns/date-fns/blob/master/src/locale/index.js.flow
 
+export type DatepickerOverridesT<T> = {
+  Root?: OverrideT<T>,
+  CalendarContainer?: OverrideT<T>,
+  CalendarHeader?: OverrideT<T>,
+  PrevButton?: OverrideT<T>,
+  NextButton?: OverrideT<T>,
+  MonthSelect?: OverrideT<T>,
+  YearSelect?: OverrideT<T>,
+  MonthHeader?: OverrideT<T>,
+  WeekdayHeader?: OverrideT<T>,
+  Month?: OverrideT<T>,
+  Week?: OverrideT<T>,
+  Day?: OverrideT<T>,
+};
+
 export type DayPropsT = {
   disabled: boolean,
   date: Date,
@@ -25,6 +41,7 @@ export type DayPropsT = {
   onClick: ({event: Event, date: Date}) => void,
   onMouseOver: ({event: Event, date: Date}) => void,
   onMouseLeave: ({event: Event, date: Date}) => void,
+  overrides?: DatepickerOverridesT<{}>,
   peekNextMonth: boolean,
   selected: boolean,
 };
@@ -49,6 +66,7 @@ export type WeekPropsT = {
   onDayMouseOver: ({date: Date, event: Event}) => void,
   onDayMouseLeave: ({date: Date, event: Event}) => void,
   onSelect: ({date: Date}) => void,
+  overrides?: DatepickerOverridesT<{}>,
   peekNextMonth: boolean,
   selected: ?Date,
 };
@@ -71,6 +89,7 @@ export type CalendarPropsT = {
   onMonthChange: ({date: Date}) => void,
   onYearChange: ({date: Date}) => void,
   onSelect: ({date: Date}) => void,
+  overrides?: DatepickerOverridesT<{}>,
   peekNextMonth: boolean,
   selected: ?Date,
   setActiveState: boolean => void,
diff --git a/src/datepicker/week.js b/src/datepicker/week.js
index 558652565..2d828148a 100644
--- a/src/datepicker/week.js
+++ b/src/datepicker/week.js
@@ -15,6 +15,7 @@ import {
   isSameDay,
 } from './utils/index.js';
 import {WEEKDAYS} from './constants.js';
+import {getOverrides} from '../helpers/overrides.js';
 import type {WeekPropsT} from './types.js';
 
 export default class Week extends React.Component<WeekPropsT> {
@@ -25,6 +26,7 @@ export default class Week extends React.Component<WeekPropsT> {
     onDayMouseOver: () => {},
     onDayMouseLeave: () => {},
     onSelect: () => {},
+    overrides: {},
     peekNextMonth: false,
   };
 
@@ -54,6 +56,7 @@ export default class Week extends React.Component<WeekPropsT> {
             onClick={this.props.onDayClick}
             onMouseOver={this.props.onDayMouseOver}
             onMouseLeave={this.props.onDayMouseLeave}
+            overrides={this.props.overrides}
             peekNextMonth={this.props.peekNextMonth}
             selected={isSameDay(day, this.props.selected)}
           />
@@ -63,6 +66,8 @@ export default class Week extends React.Component<WeekPropsT> {
   };
 
   render() {
-    return <StyledWeek>{this.renderDays()}</StyledWeek>;
+    const {overrides = {}} = this.props;
+    const [Week, weekProps] = getOverrides(overrides.Week, StyledWeek);
+    return <Week {...weekProps}>{this.renderDays()}</Week>;
   }
 }
diff --git a/src/select/select.js b/src/select/select.js
index eafb47b4c..0c1e20893 100644
--- a/src/select/select.js
+++ b/src/select/select.js
@@ -53,6 +53,7 @@ class Select extends React.Component<PropsT, SelectStateT> {
   dragging: boolean;
   focusAfterClear: boolean;
   openAfterFocus: boolean;
+  isValueJustSelected: boolean;
 
   state = {
     inputValue: '',
@@ -66,7 +67,6 @@ class Select extends React.Component<PropsT, SelectStateT> {
       this.focus();
     }
   }
-
   componentDidUpdate(prevProps: PropsT, prevState: SelectStateT) {
     if (prevState.isOpen !== this.state.isOpen) {
       this.toggleTouchOutsideEvent(this.state.isOpen);
@@ -75,6 +75,11 @@ class Select extends React.Component<PropsT, SelectStateT> {
         : this.props.onClose;
       handler && handler();
     }
+    if (prevState.isFocused !== this.state.isFocused && this.state.isFocused) {
+      if (__BROWSER__) {
+        document.addEventListener('click', this.handleClickOutside);
+      }
+    }
   }
 
   componentWillUnmount() {
@@ -130,6 +135,18 @@ class Select extends React.Component<PropsT, SelectStateT> {
     this.clearValue(event);
   };
 
+  handleClickOutside = (event: Event) => {
+    if (this.isValueJustSelected) {
+      this.isValueJustSelected = false;
+      return;
+    }
+    const isFocused = this.state.isFocused || this.state.isPseudoFocused;
+    // $FlowFixMe
+    if (isFocused && this.wrapper && !this.wrapper.contains(event.target)) {
+      this.handleInputBlur(event);
+    }
+  };
+
   handleClick = (event: Event) => {
     // If the event was triggered by a mouse click and not the primary
     // button, or if the component is disabled, ignore it
@@ -170,7 +187,7 @@ class Select extends React.Component<PropsT, SelectStateT> {
       // calls to input.focus() that weren't triggered by a click event.
       // Call focus() again here to be safe.
       this.focus();
-      let toOpen = true;
+      let toOpen = !this.state.isOpen;
       // clears the value so that the cursor will be at the end of input when the component re-renders
       if (this.input) this.input.value = '';
       if (this.focusAfterClear) {
@@ -247,7 +264,7 @@ class Select extends React.Component<PropsT, SelectStateT> {
     this.openAfterFocus = false;
   };
 
-  handleInputBlur = (event: SyntheticEvent<HTMLElement>) => {
+  handleInputBlur = (event: Event) => {
     if (this.props.onBlur) {
       this.props.onBlur(event);
     }
@@ -261,6 +278,9 @@ class Select extends React.Component<PropsT, SelectStateT> {
       onBlurredState.inputValue = '';
     }
     this.setState(onBlurredState);
+    if (__BROWSER__) {
+      document.removeEventListener('click', this.handleClickOutside);
+    }
   };
 
   handleInputChange = (event: SyntheticInputEvent<HTMLInputElement>) => {
@@ -291,6 +311,14 @@ class Select extends React.Component<PropsT, SelectStateT> {
           this.setState({isOpen: true});
         }
         break;
+      case 9: // tab
+        this.setState(prevState => ({
+          isPseudoFocused: false,
+          isFocused: false,
+          isOpen: false,
+          inputValue: this.props.onCloseResetsInput ? '' : prevState.inputValue,
+        }));
+        break;
       case 27: // escape
         event.preventDefault();
         if (this.state.isOpen) {
@@ -387,6 +415,9 @@ class Select extends React.Component<PropsT, SelectStateT> {
   }
 
   selectValue = ({item}: {item: OptionT}) => {
+    // NOTE: we check this is in handleClickOutside to not count
+    // menu clicks as outside clicks
+    this.isValueJustSelected = true;
     if (item.disabled) {
       return;
     }
@@ -581,7 +612,6 @@ class Select extends React.Component<PropsT, SelectStateT> {
       'aria-disabled': this.props.disabled || null,
       disabled: this.props.disabled || null,
       inputRef: ref => (this.input = ref),
-      onBlur: this.handleInputBlur,
       onChange: this.handleInputChange,
       onFocus: this.handleInputFocus,
       overrides: {Input: overrides.Input},
@@ -596,7 +626,6 @@ class Select extends React.Component<PropsT, SelectStateT> {
           aria-disabled={this.props.disabled}
           aria-label={this.props['aria-label']}
           aria-labelledby={this.props['aria-labelledby']}
-          onBlur={this.handleInputBlur}
           onFocus={this.handleInputFocus}
           $ref={ref => (this.input = ref)}
           role="combobox"
diff --git a/src/select/types.js b/src/select/types.js
index fe05370c1..719140cc5 100644
--- a/src/select/types.js
+++ b/src/select/types.js
@@ -85,7 +85,7 @@ export type PropsT = {
   maxDropdownHeight: string,
   multi: boolean,
   noResultsMsg: React.Node,
-  onBlur: (e: SyntheticEvent<HTMLElement>) => void,
+  onBlur: (e: Event) => void,
   onBlurResetsInput: boolean,
   onChange: (params: OnChangeParamsT) => void,
   onFocus: (e: SyntheticEvent<HTMLElement>) => void,
diff --git a/yarn.lock b/yarn.lock
index 7f8546e69..9b260e530 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9392,10 +9392,10 @@ mem@^1.1.0:
   dependencies:
     mimic-fn "^1.0.0"
 
-memoize-one@^4.0.2:
-  version "4.0.2"
-  resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-4.0.2.tgz#3fb8db695aa14ab9c0f1644e1585a8806adc1aee"
-  integrity sha512-ucx2DmXTeZTsS4GPPUZCbULAN7kdPT1G+H49Y34JjbQ5ESc6OGhVxKvb1iKhr9v19ZB9OtnHwNnhUnNR/7Wteg==
+memoize-one@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.0.0.tgz#d55007dffefb8de7546659a1722a5d42e128286e"
+  integrity sha512-7g0+ejkOaI9w5x6LvQwmj68kUj6rxROywPSCqmclG/HBacmFnZqhVscQ8kovkn9FBCNJmOz6SY42+jnvZzDWdw==
 
 memory-fs@^0.3.0:
   version "0.3.0"


Test patch:
diff --git a/src/select/__tests__/__snapshots__/select.test.js.snap b/src/select/__tests__/__snapshots__/select.test.js.snap
index 2d7a45eae..2b87e0c3f 100644
--- a/src/select/__tests__/__snapshots__/select.test.js.snap
+++ b/src/select/__tests__/__snapshots__/select.test.js.snap
@@ -259,7 +259,6 @@ exports[`Select component renders component in search mode and false for multipl
                     aria-labelledby={null}
                     disabled={null}
                     inputRef={[Function]}
-                    onBlur={[Function]}
                     onChange={[Function]}
                     onFocus={[Function]}
                     overrides={
@@ -294,7 +293,6 @@ exports[`Select component renders component in search mode and false for multipl
                       aria-label={null}
                       aria-labelledby={null}
                       disabled={null}
-                      onBlur={[Function]}
                       onChange={[Function]}
                       onFocus={[Function]}
                       required={false}
@@ -310,7 +308,6 @@ exports[`Select component renders component in search mode and false for multipl
                         aria-label={null}
                         aria-labelledby={null}
                         disabled={null}
-                        onBlur={[Function]}
                         onChange={[Function]}
                         onFocus={[Function]}
                         required={false}
@@ -615,7 +612,6 @@ exports[`Select component renders component in search mode and true for multiple
                     aria-labelledby={null}
                     disabled={null}
                     inputRef={[Function]}
-                    onBlur={[Function]}
                     onChange={[Function]}
                     onFocus={[Function]}
                     overrides={
@@ -650,7 +646,6 @@ exports[`Select component renders component in search mode and true for multiple
                       aria-label={null}
                       aria-labelledby={null}
                       disabled={null}
-                      onBlur={[Function]}
                       onChange={[Function]}
                       onFocus={[Function]}
                       required={false}
@@ -666,7 +661,6 @@ exports[`Select component renders component in search mode and true for multiple
                         aria-label={null}
                         aria-labelledby={null}
                         disabled={null}
-                        onBlur={[Function]}
                         onChange={[Function]}
                         onFocus={[Function]}
                         required={false}
@@ -893,7 +887,6 @@ exports[`Select component renders component in select mode and false for multipl
                     aria-labelledby={null}
                     disabled={null}
                     inputRef={[Function]}
-                    onBlur={[Function]}
                     onChange={[Function]}
                     onFocus={[Function]}
                     overrides={
@@ -928,7 +921,6 @@ exports[`Select component renders component in select mode and false for multipl
                       aria-label={null}
                       aria-labelledby={null}
                       disabled={null}
-                      onBlur={[Function]}
                       onChange={[Function]}
                       onFocus={[Function]}
                       required={false}
@@ -944,7 +936,6 @@ exports[`Select component renders component in select mode and false for multipl
                         aria-label={null}
                         aria-labelledby={null}
                         disabled={null}
-                        onBlur={[Function]}
                         onChange={[Function]}
                         onFocus={[Function]}
                         required={false}
@@ -1248,7 +1239,6 @@ exports[`Select component renders component in select mode and true for multiple
                     aria-labelledby={null}
                     disabled={null}
                     inputRef={[Function]}
-                    onBlur={[Function]}
                     onChange={[Function]}
                     onFocus={[Function]}
                     overrides={
@@ -1283,7 +1273,6 @@ exports[`Select component renders component in select mode and true for multiple
                       aria-label={null}
                       aria-labelledby={null}
                       disabled={null}
-                      onBlur={[Function]}
                       onChange={[Function]}
                       onFocus={[Function]}
                       required={false}
@@ -1299,7 +1288,6 @@ exports[`Select component renders component in select mode and true for multiple
                         aria-label={null}
                         aria-labelledby={null}
                         disabled={null}
-                        onBlur={[Function]}
                         onChange={[Function]}
                         onFocus={[Function]}
                         required={false}