Repository: uber-web/baseui
Pull request #708 (2019-01-04 18:38:01)

feat(datepicker): add date range select

chasestarr: not sure if you're still working on this pr, but it looks like the non-range date picker throws an error. also seeing that keyboard nav still works, but i cannot use enter/space to select a date
nadiia: > Code changes lgtm, but keyboard nav seems to be broken when selecting a date.

Thanks for catching it, was a result of a prop removal - fixed!

Patch:
diff --git a/documentation-site/routes.js b/documentation-site/routes.js
index bcd82e8bf..15c028015 100644
--- a/documentation-site/routes.js
+++ b/documentation-site/routes.js
@@ -74,7 +74,7 @@ const routes = [
           },
           {
             text: 'Unstable Datepicker',
-            path: getPath('/components/datepicker'),
+            path: '/components/datepicker',
           },
         ],
       },
diff --git a/src/datepicker/calendar.js b/src/datepicker/calendar.js
index de28765bb..789927dd4 100644
--- a/src/datepicker/calendar.js
+++ b/src/datepicker/calendar.js
@@ -37,9 +37,9 @@ export default class Calendar extends React.Component<
   static defaultProps = {
     excludeDates: null,
     filterDate: null,
-    highlightDates: null,
     highlightedDate: null,
     includeDates: null,
+    isRange: false,
     locale: null,
     maxDate: null,
     minDate: null,
@@ -74,12 +74,21 @@ export default class Calendar extends React.Component<
     }
   }
 
+  getSingleDate(value: ?Date | Array<Date>): ?Date {
+    // need to check this.props.isRange but flow would complain
+    // at the return value in the else clause
+    if (Array.isArray(value)) {
+      return value[0] || null;
+    }
+    return value;
+  }
+
   getDateInView = (): Date => {
     const {highlightedDate, selected} = this.props;
     const minDate = getEffectiveMinDate(this.props);
     const maxDate = getEffectiveMaxDate(this.props);
     const current = new Date();
-    const initialDate = selected || highlightedDate;
+    const initialDate = this.getSingleDate(selected) || highlightedDate;
     if (initialDate) {
       return initialDate;
     } else {
@@ -92,10 +101,6 @@ export default class Calendar extends React.Component<
     return current;
   };
 
-  handleDayClick = ({date, event}: {date: Date, event: Event}) => {
-    this.props.onSelect({date});
-  };
-
   handleMonthChange = (date: Date) => {
     if (this.props.onMonthChange) {
       this.props.onMonthChange({date});
@@ -172,9 +177,9 @@ export default class Calendar extends React.Component<
             date={monthDate}
             excludeDates={this.props.excludeDates}
             filterDate={this.props.filterDate}
-            highlightDates={this.props.highlightDates}
             highlightedDate={this.props.highlightedDate}
             includeDates={this.props.includeDates}
+            isRange={this.props.isRange}
             locale={this.props.locale}
             maxDate={this.props.maxDate}
             minDate={this.props.minDate}
diff --git a/src/datepicker/day.js b/src/datepicker/day.js
index 8d738ebf1..97c75472f 100644
--- a/src/datepicker/day.js
+++ b/src/datepicker/day.js
@@ -7,7 +7,14 @@ LICENSE file in the root directory of this source tree.
 // @flow
 import * as React from 'react';
 import {StyledDay} from './styled-components.js';
-import {getDay, getMonth, getDate, isSameDay} from './utils/index.js';
+import {
+  getDay,
+  getMonth,
+  getDate,
+  isSameDay,
+  isDayInRange,
+  isAfter,
+} from './utils/index.js';
 import {getOverrides} from '../helpers/overrides.js';
 import type {DayPropsT, DayStateT} from './types.js';
 
@@ -16,6 +23,7 @@ export default class Day extends React.Component<DayPropsT, DayStateT> {
     disabled: false,
     date: new Date(),
     isHighlighted: false,
+    isRange: false,
     month: new Date().getMonth(),
     onClick: () => {},
     onSelect: () => {},
@@ -23,8 +31,7 @@ export default class Day extends React.Component<DayPropsT, DayStateT> {
     onMouseLeave: () => {},
     overrides: {},
     peekNextMonth: true,
-    selected: false,
-    // pseudoSelected: false,
+    value: null,
   };
 
   state = {
@@ -63,11 +70,28 @@ export default class Day extends React.Component<DayPropsT, DayStateT> {
     }
   }
 
+  onSelect(selectedDate: Date) {
+    const {isRange, value} = this.props;
+    let date;
+    if (Array.isArray(value) && isRange) {
+      if (!value.length || value.length > 1) {
+        date = [selectedDate];
+      } else if (isAfter(selectedDate, value[0])) {
+        date = [value[0], selectedDate];
+      } else {
+        date = [selectedDate, value[0]];
+      }
+    } else {
+      date = selectedDate;
+    }
+    this.props.onSelect({date});
+  }
+
   onKeyDown = (event: KeyboardEvent) => {
-    const {isHighlighted, date, disabled, onSelect} = this.props;
+    const {isHighlighted, date, disabled} = this.props;
     if (event.key === 'Enter' && isHighlighted && !disabled) {
       event.preventDefault();
-      onSelect({date});
+      this.onSelect(date);
     }
   };
 
@@ -75,7 +99,7 @@ export default class Day extends React.Component<DayPropsT, DayStateT> {
     const {date, disabled} = this.props;
     if (!disabled) {
       this.props.onClick({event, date});
-      this.props.onSelect({date});
+      this.onSelect(date);
     }
   };
 
@@ -101,15 +125,81 @@ export default class Day extends React.Component<DayPropsT, DayStateT> {
     );
   };
 
+  isSelected() {
+    const {value, date} = this.props;
+    if (Array.isArray(value)) {
+      return isSameDay(date, value[0]) || isSameDay(date, value[1]);
+    } else {
+      return isSameDay(date, value);
+    }
+  }
+
+  // calculated for range case only
+  isPseudoSelected() {
+    const {date, value} = this.props;
+    // fix flow by passing a specific arg type and remove 'Array.isArray(value)'
+    if (Array.isArray(value) && value.length > 1) {
+      return isDayInRange(date, value[0], value[1]);
+    }
+  }
+
+  // calculated for range case only
+  isPseudoHighlighted() {
+    const {date, value, highlightedDate} = this.props;
+    // fix flow by passing a specific arg type and remove 'Array.isArray(value)'
+    if (Array.isArray(value) && highlightedDate && value[0] && !value[1]) {
+      if (isAfter(highlightedDate, value[0])) {
+        return isDayInRange(date, value[0], highlightedDate);
+      } else {
+        return isDayInRange(date, highlightedDate, value[0]);
+      }
+    }
+  }
+
   getSharedProps() {
+    const {date, value, highlightedDate, isRange, isHighlighted} = this.props;
+    const $isHighlighted = isHighlighted;
+    const $selected = this.isSelected();
+    const $hasRangeHighlighted =
+      Array.isArray(value) &&
+      isRange &&
+      value.length === 1 &&
+      highlightedDate &&
+      !isSameDay(value[0], highlightedDate)
+        ? true
+        : false;
     return {
+      $date: date,
       $disabled: this.props.disabled,
+      $endDate:
+        (Array.isArray(value) &&
+          this.props.isRange &&
+          $selected &&
+          isSameDay(date, value[1])) ||
+        false,
       $isHovered: this.state.isHovered,
-      $isHighlighted: this.props.isHighlighted,
-      // $pseudoSelected: this.props.pseudoSelected,
-      $selected: this.props.selected,
-      $today: isSameDay(this.props.date, new Date()),
-      $weekend: this.isWeekend(),
+      $isHighlighted,
+      $isRange: this.props.isRange,
+      $hasRangeHighlighted,
+      $hasRangeOnRight:
+        Array.isArray(value) &&
+        $hasRangeHighlighted &&
+        (highlightedDate && value[0]) &&
+        isAfter(highlightedDate, value[0]),
+      $hasRangeSelected: Array.isArray(value) ? value.length === 2 : false,
+      $highlightedDate: highlightedDate,
+      $peekNextMonth: this.props.peekNextMonth,
+      $pseudoHighlighted:
+        this.props.isRange && !$isHighlighted && !$selected
+          ? this.isPseudoHighlighted()
+          : false,
+      $pseudoSelected:
+        this.props.isRange && !$selected ? this.isPseudoSelected() : false,
+      $selected,
+      $startDate:
+        Array.isArray(this.props.value) && this.props.isRange && $selected
+          ? isSameDay(date, this.props.value[0])
+          : false,
       $outsideMonth: this.isOutsideMonth(),
     };
   }
@@ -125,7 +215,7 @@ export default class Day extends React.Component<DayPropsT, DayStateT> {
       <Day
         aria-label={`day-${getDate(date)}`}
         role="option"
-        tabIndex={this.props.selected ? '0' : '-1'}
+        tabIndex={sharedProps.$selected ? '0' : '-1'}
         {...sharedProps}
         {...dayProps}
         // Adding event handlers after customers overrides in order to
diff --git a/src/datepicker/examples-list.js b/src/datepicker/examples-list.js
index e2cf63e49..775edce23 100644
--- a/src/datepicker/examples-list.js
+++ b/src/datepicker/examples-list.js
@@ -11,5 +11,6 @@ module.exports = {
   SIMPLE_EXAMPLE: 'Stateless datepicker',
   STATEFUL_EXAMPLE: 'Stateful datepicker',
   STATEFUL_IN_POPOVER: 'Stateful datepicker in a popover',
+  STATEFUL_RANGE_IN_POPOVER: 'Stateful range datepicker in a popover',
   WITH_OVERRIDES: 'Datepicker with overrides',
 };
diff --git a/src/datepicker/examples.js b/src/datepicker/examples.js
index d1dc37cd8..db68b4ba3 100644
--- a/src/datepicker/examples.js
+++ b/src/datepicker/examples.js
@@ -17,25 +17,48 @@ import tests from './examples-list.js';
 
 export const suite = 'Component Test Suite';
 
+type ExamplePropsT = {
+  isRange: boolean,
+  value: ?Date | Array<Date>,
+};
+
+type ExampleStateT = {
+  isOpen: boolean,
+  value: ?Date | Array<Date>,
+  formattedValue: string,
+};
+
 class DatepickerWithInput extends React.Component<
-  {},
-  {
-    isOpen: boolean,
-    value: ?Date,
-    formattedValue: string,
-  },
+  ExamplePropsT,
+  ExampleStateT,
 > {
+  static defaultProps = {
+    isRange: false,
+    value: null,
+  };
   state = {
     isOpen: false,
-    value: null,
+    value: this.props.value || null,
     formattedValue: '',
   };
 
+  formatDate(date) {
+    if (this.props.isRange) {
+      // $FlowFixMe
+      return date.map(day => formatDate(day, 'YYYY/MM/dd')).join(' - ');
+    }
+    return formatDate(date, 'YYYY/MM/dd');
+  }
+
   onChange = ({date}) => {
+    let isOpen = false;
+    if (Array.isArray(date) && this.props.isRange && date.length < 2) {
+      isOpen = true;
+    }
     this.setState({
       value: date,
-      formattedValue: formatDate(date, 'YYYY/MM/dd'),
-      isOpen: false,
+      formattedValue: this.formatDate(date),
+      isOpen,
     });
   };
 
@@ -62,6 +85,7 @@ class DatepickerWithInput extends React.Component<
         content={
           <StatefulDatepicker
             initialState={{value: this.state.value}}
+            isRange={this.props.isRange}
             onSelect={this.onChange}
           />
         }
@@ -69,7 +93,7 @@ class DatepickerWithInput extends React.Component<
         <Input
           value={this.state.formattedValue}
           onFocus={this.open}
-          onBlur={this.close}
+          // onBlur={this.close}
           // Change the on key event types in Input
           // $FlowFixMe
           onKeyDown={this.handleKeyDown}
@@ -90,7 +114,9 @@ export default {
   [tests.STATEFUL_IN_POPOVER]: function Story3() {
     return <DatepickerWithInput />;
   },
-
+  [tests.STATEFUL_RANGE_IN_POPOVER]: function Story3() {
+    return <DatepickerWithInput isRange value={[]} />;
+  },
   [tests.WITH_OVERRIDES]: function Story3() {
     const selectOverrides = {
       ControlContainer: {
diff --git a/src/datepicker/month.js b/src/datepicker/month.js
index c983b3b49..4faf8cb1e 100644
--- a/src/datepicker/month.js
+++ b/src/datepicker/month.js
@@ -63,8 +63,8 @@ export default class CalendarMonth extends React.Component<MonthPropsT> {
           excludeDates={this.props.excludeDates}
           filterDate={this.props.filterDate}
           highlightedDate={this.props.highlightedDate}
-          highlightDates={this.props.highlightDates}
           includeDates={this.props.includeDates}
+          isRange={this.props.isRange}
           key={i}
           locale={this.props.locale}
           minDate={this.props.minDate}
diff --git a/src/datepicker/navigation-container.js b/src/datepicker/navigation-container.js
index 314b52f86..996bbed7f 100644
--- a/src/datepicker/navigation-container.js
+++ b/src/datepicker/navigation-container.js
@@ -40,10 +40,14 @@ class NavigationContainer extends React.Component<
 
   state = {
     highlightedDate:
-      this.props.highlightedDate || this.props.selected || new Date(),
+      this.props.highlightedDate ||
+      this.getSingleDate(this.props.selected) ||
+      new Date(),
     isActive: true,
     lastHighlightedDate:
-      this.props.highlightedDate || this.props.selected || new Date(),
+      this.props.highlightedDate ||
+      this.getSingleDate(this.props.selected) ||
+      new Date(),
   };
 
   componentDidMount() {
@@ -71,7 +75,15 @@ class NavigationContainer extends React.Component<
     }
   };
 
-  onSelect = (data: {date: Date}) => {
+  getSingleDate(value: ?Date | Array<Date>): ?Date {
+    if (Array.isArray(value)) {
+      return value[0] || null;
+    } else {
+      return value;
+    }
+  }
+
+  onSelect = (data: {date: Date | Array<Date>}) => {
     if (this.state.isActive) {
       this.props.onSelect(data);
     }
@@ -105,7 +117,8 @@ class NavigationContainer extends React.Component<
 
   onDayMouseLeave = (data: {event: Event, date: Date}) => {
     const {date} = data;
-    const {stateReducer, selected} = this.props;
+    const {stateReducer, selected: value} = this.props;
+    const selected = this.getSingleDate(value);
     const inSameMonth = isSameMonth(selected, date);
     const inSameYear = isSameYear(selected, date);
     let nextState;
diff --git a/src/datepicker/styled-components.js b/src/datepicker/styled-components.js
index d100abb21..c4c9cbde2 100644
--- a/src/datepicker/styled-components.js
+++ b/src/datepicker/styled-components.js
@@ -72,23 +72,108 @@ export const StyledMonth = styled('div', (props: SharedStylePropsT) => {
 });
 
 export const StyledWeek = styled('div', (props: SharedStylePropsT) => {
+  const {
+    $theme: {sizing},
+  } = props;
   return {
     whiteSpace: 'no-wrap',
     display: 'flex',
+    marginBottom: sizing.scale100,
   };
 });
 
+type BorderRadiusT = {
+  borderTopLeftRadius: string | number,
+  borderBottomLeftRadius: string | number,
+  borderTopRightRadius: string | number,
+  borderBottomRightRadius: string | number,
+};
+
+function getBorderRadius(left, right): BorderRadiusT {
+  return {
+    borderTopLeftRadius: left,
+    borderBottomLeftRadius: left,
+    borderTopRightRadius: right,
+    borderBottomRightRadius: right,
+  };
+}
+
+export function calculateBorderRadius(
+  props: SharedStylePropsT,
+): ?BorderRadiusT {
+  const {
+    $isHighlighted,
+    $pseudoHighlighted,
+    $pseudoSelected,
+    $selected,
+    $startDate,
+    $isRange,
+    $hasRangeHighlighted,
+    $hasRangeOnRight,
+    $hasRangeSelected,
+    $theme: {borders},
+  } = props;
+  if (borders.useRoundedCorners) {
+    if ($selected) {
+      if (!$isRange) {
+        return getBorderRadius(borders.radius200, borders.radius200);
+      } else {
+        if ($hasRangeSelected) {
+          return $startDate
+            ? getBorderRadius(borders.radius200, 0)
+            : getBorderRadius(0, borders.radius200);
+        } else {
+          if ($hasRangeHighlighted) {
+            return $hasRangeOnRight
+              ? getBorderRadius(borders.radius200, 0)
+              : getBorderRadius(0, borders.radius200);
+          } else {
+            return getBorderRadius(borders.radius200, borders.radius200);
+          }
+        }
+      }
+    } else {
+      if (!$isHighlighted && ($pseudoHighlighted || $pseudoSelected)) {
+        return getBorderRadius(0, 0);
+      } else {
+        if ($isHighlighted) {
+          if (!$isRange) {
+            return getBorderRadius(borders.radius200, borders.radius200);
+          } else if ($hasRangeHighlighted) {
+            return $hasRangeOnRight
+              ? getBorderRadius(0, borders.radius200)
+              : getBorderRadius(borders.radius200, 0);
+          } else {
+            return $pseudoSelected
+              ? getBorderRadius(0, 0)
+              : getBorderRadius(borders.radius200, borders.radius200);
+          }
+        } else {
+          return !$pseudoSelected
+            ? getBorderRadius(borders.radius200, borders.radius200)
+            : null;
+        }
+      }
+    }
+  } else {
+    return getBorderRadius(0, 0);
+  }
+}
+
 export const StyledDay = styled('div', (props: SharedStylePropsT) => {
   const {
+    $disabled,
     $isHovered,
     $isHighlighted,
     $outsideMonth,
-    $disabled,
+    $pseudoHighlighted,
+    $pseudoSelected,
     $selected,
     $theme: {colors, sizing, borders},
   } = props;
   return {
     boxSizing: 'border-box',
+    position: 'relative',
     cursor: $disabled ? 'default' : 'pointer',
     display: 'inline-block',
     width: sizing.scale1000,
@@ -110,9 +195,25 @@ export const StyledDay = styled('div', (props: SharedStylePropsT) => {
         : 'inherit',
     backgroundColor: $selected
       ? colors.primary
-      : $isHovered || $isHighlighted
+      : $isHovered || $isHighlighted || $pseudoHighlighted || $pseudoSelected
         ? colors.primary100
         : 'transparent',
-    borderRadius: borders.useRoundedCorners ? borders.radius200 : '0px',
+    ...calculateBorderRadius(props),
+    ':first-child': {
+      ...(borders.useRoundedCorners
+        ? {
+            borderTopLeftRadius: borders.radius200,
+            borderBottomLeftRadius: borders.radius200,
+          }
+        : {}),
+    },
+    ':last-child': {
+      ...(borders.useRoundedCorners
+        ? {
+            borderTopRightRadius: borders.radius200,
+            borderBottomRightRadius: borders.radius200,
+          }
+        : {}),
+    },
   };
 });
diff --git a/src/datepicker/types.js b/src/datepicker/types.js
index ffcb62689..991866b1f 100644
--- a/src/datepicker/types.js
+++ b/src/datepicker/types.js
@@ -32,18 +32,21 @@ export type DatepickerOverridesT<T> = {
 export type DayPropsT = {
   disabled: boolean,
   date: Date,
-  // highlightDates: Array<Date>,
+  filterDate: ?(day: Date) => boolean,
+  highlightedDate: ?Date,
+  includeDates: ?Array<Date>,
   isHighlighted: boolean,
+  isRange: boolean,
   maxDate: ?Date,
   minDate: ?Date,
   month: ?number,
-  onSelect: ({date: Date}) => void,
-  onClick: ({event: Event, date: Date}) => void,
-  onMouseOver: ({event: Event, date: Date}) => void,
-  onMouseLeave: ({event: Event, date: Date}) => void,
+  onSelect: ({date: Date | Array<Date>}) => mixed,
+  onClick: ({event: Event, date: Date}) => mixed,
+  onMouseOver: ({event: Event, date: Date}) => mixed,
+  onMouseLeave: ({event: Event, date: Date}) => mixed,
   overrides?: DatepickerOverridesT<{}>,
   peekNextMonth: boolean,
-  selected: boolean,
+  value: ?Date | Array<Date>,
 };
 
 export type DayStateT = {
@@ -54,21 +57,21 @@ export type WeekPropsT = {
   date: Date,
   excludeDates: ?Array<Date>,
   filterDate: ?(day: Date) => boolean,
-  highlightDates: ?Array<Date>,
   // highlighted while keyboard navigating or hovered
   highlightedDate: ?Date,
   includeDates: ?Array<Date>,
+  isRange: boolean,
   locale: ?LocaleT,
   maxDate: ?Date,
   minDate: ?Date,
   month: ?number,
-  onDayClick: ({date: Date, event: Event}) => void,
-  onDayMouseOver: ({date: Date, event: Event}) => void,
-  onDayMouseLeave: ({date: Date, event: Event}) => void,
-  onSelect: ({date: Date}) => void,
+  onDayClick: ({date: Date, event: Event}) => mixed,
+  onDayMouseOver: ({date: Date, event: Event}) => mixed,
+  onDayMouseLeave: ({date: Date, event: Event}) => mixed,
+  onSelect: ({date: Date | Array<Date>}) => mixed,
   overrides?: DatepickerOverridesT<{}>,
   peekNextMonth: boolean,
-  selected: ?Date,
+  selected: ?Date | Array<Date>,
 };
 
 export type MonthPropsT = WeekPropsT;
@@ -78,12 +81,12 @@ export type CalendarPropsT = {
   excludeDates: ?Array<Date>,
   /** A filter function that is called to check the disabled state of a day. If `false` is returned the day is considered to be disabled. */
   filterDate: ?(day: Date) => boolean,
-  /** A date to highlight on an initial render when the selected value is not set. */
-  highlightDates: ?Array<Date>,
   /** Indicates a highlighted date on hover and keyboard navigation */
   highlightedDate: ?Date,
   /** A list of selectable dates. */
   includeDates: ?Array<Date>,
+  /** Defines if a range of dates can be selected. */
+  isRange: boolean,
   /** A locale object. See `date-fns` for more details https://github.com/date-fns/date-fns/tree/master/src/locale. */
   locale: ?LocaleT,
   /** A max date that is selectable. */
@@ -93,24 +96,24 @@ export type CalendarPropsT = {
   /** A number of months rendered in the calendar. */
   monthsShown: number,
   /** Day's `click` event handler. */
-  onDayClick: ({date: Date, event: Event}) => void,
+  onDayClick: ({date: Date, event: Event}) => mixed,
   /** Day's `mouseover` event handler. */
-  onDayMouseOver: ({date: Date, event: Event}) => void,
+  onDayMouseOver: ({date: Date, event: Event}) => mixed,
   /** Day's `mouseleave` event handler. */
-  onDayMouseLeave: ({date: Date, event: Event}) => void,
+  onDayMouseLeave: ({date: Date, event: Event}) => mixed,
   /** Event handler that is called when the current rendered month is changed. */
-  onMonthChange: ({date: Date}) => void,
+  onMonthChange: ({date: Date}) => mixed,
   /** Event handler that is called when the current rendered month's year is changed. */
-  onYearChange: ({date: Date}) => void,
+  onYearChange: ({date: Date}) => mixed,
   /** Event handler that is called when a new date is selected. */
-  onSelect: ({date: Date}) => void,
+  onSelect: ({date: Date | Array<Date>}) => mixed,
   overrides?: DatepickerOverridesT<{}>,
   /** Defines if dates outside of the range of the current month are displayed. */
   peekNextMonth: boolean,
   /** Currently selected date. */
-  selected: ?Date,
+  selected: ?Date | Array<Date>,
   /** A helper handler for disabling a keyboard navigation and keyboard selection through the calendar dates while navigation through the month or year select controls. */
-  setActiveState: boolean => void,
+  setActiveState: boolean => mixed,
 };
 
 export type HeaderPropsT = CalendarPropsT & {
@@ -118,11 +121,19 @@ export type HeaderPropsT = CalendarPropsT & {
 };
 
 export type SharedStylePropsT = {
+  $date: Date,
   $disabled: boolean,
   $isHighlighted: boolean,
   $isHovered: boolean,
   $outsideMonth: boolean,
+  $pseudoHighlighted: boolean,
+  $pseudoSelected: boolean,
   $selected: boolean,
+  $startDate: boolean,
+  $isRange: boolean,
+  $hasRangeHighlighted: boolean,
+  $hasRangeOnRight: boolean,
+  $hasRangeSelected: boolean,
   $theme: ThemeT,
 };
 
@@ -130,7 +141,7 @@ export type StateChangeTypeT = ?$Values<typeof STATE_CHANGE_TYPE>;
 
 export type CalendarStateT = {
   /** Selected `Date`. If `isRange` is set, `value` is an array of 2 values. */
-  value?: ?Date,
+  value?: ?Date | Array<Date>,
 };
 
 export type NavigationContainerStateT = {
@@ -164,20 +175,23 @@ export type StatefulContainerPropsT = {
   /** A state change handler. */
   stateReducer: StateReducerT,
   /** Event handler that is called when a new date is selected. */
-  onSelect: ({date: Date}) => void,
+  onSelect: ({date: Date}) => mixed,
 };
 
 export type NavigationContainerPropsT = {
   children: CalendarPropsT => React.Node,
+  isRange?: boolean,
   highlightedDate?: ?Date,
   /** Day's `mouseover` event handler. */
-  onDayMouseOver: (params: {date: Date, event: Event}) => void,
+  onDayMouseOver: (params: {date: Date, event: Event}) => mixed,
   /** Day's `mouseleave` event handler. */
-  onDayMouseLeave: (params: {date: Date, event: Event}) => void,
+  onDayMouseLeave: (params: {date: Date, event: Event}) => mixed,
+  onSelect: ({date: Date}) => mixed,
+  selected?: ?Date,
   /** Event handler that is called when a new date is selected. */
-  onSelect: ({date: Date}) => void,
+  onSelect: ({date: Date | Array<Date>}) => mixed,
   /** Selected `Date`. If `isRange` is set, `value` is an array of 2 values. */
-  selected?: ?Date,
+  selected?: ?Date | Array<Date>,
   stateReducer: NavigationContainerStateReducerT,
 };
 
diff --git a/src/datepicker/utils/index.js b/src/datepicker/utils/index.js
index dfde07c77..654198ca4 100644
--- a/src/datepicker/utils/index.js
+++ b/src/datepicker/utils/index.js
@@ -15,6 +15,7 @@ import differenceInCalendarMonths from 'date-fns/differenceInCalendarMonths';
 import dfIsSameDay from 'date-fns/isSameDay';
 import dfIsSameMonth from 'date-fns/isSameMonth';
 import dfIsSameYear from 'date-fns/isSameYear';
+import endOfMonth from 'date-fns/endOfMonth';
 import format from 'date-fns/format';
 import getDay from 'date-fns/getDay';
 import getDate from 'date-fns/getDate';
@@ -64,6 +65,10 @@ export function getStartOfMonth(date: Date) {
   return startOfMonth(date);
 }
 
+export function getEndOfMonth(date: Date) {
+  return endOfMonth(date);
+}
+
 // ** Date Math **
 
 // *** Addition ***
@@ -106,6 +111,14 @@ export function isDayInRange(day: Date, startDate: Date, endDate: Date) {
   return isWithinInterval(day, {start: startDate, end: endDate});
 }
 
+export function isStartOfMonth(date: Date) {
+  return isSameDay(date, getStartOfMonth(date));
+}
+
+export function isEndOfMonth(date: Date) {
+  return isSameDay(date, getEndOfMonth(date));
+}
+
 // ** Date Localization **
 // eslint-disable-next-line flowtype/no-weak-types
 export function getWeekdayMinInLocale(date: Date, locale: any) {
diff --git a/src/datepicker/week.js b/src/datepicker/week.js
index 2d828148a..bf2337bf8 100644
--- a/src/datepicker/week.js
+++ b/src/datepicker/week.js
@@ -44,9 +44,10 @@ export default class Week extends React.Component<WeekPropsT> {
             disabled={isDayDisabled(day, this.props)}
             excludeDates={this.props.excludeDates}
             filterDate={this.props.filterDate}
-            highlightDates={this.props.highlightDates}
-            includeDates={this.props.includeDates}
+            highlightedDate={this.props.highlightedDate}
             isHighlighted={isSameDay(day, this.props.highlightedDate)}
+            includeDates={this.props.includeDates}
+            isRange={this.props.isRange}
             key={offset}
             locale={this.props.locale}
             minDate={this.props.minDate}
@@ -58,7 +59,7 @@ export default class Week extends React.Component<WeekPropsT> {
             onMouseLeave={this.props.onDayMouseLeave}
             overrides={this.props.overrides}
             peekNextMonth={this.props.peekNextMonth}
-            selected={isSameDay(day, this.props.selected)}
+            value={this.props.selected}
           />
         );
       }),


Test patch:
diff --git a/src/datepicker/__tests__/__snapshots__/styled-components.test.js.snap b/src/datepicker/__tests__/__snapshots__/styled-components.test.js.snap
index 80cb08573..6e82581be 100644
--- a/src/datepicker/__tests__/__snapshots__/styled-components.test.js.snap
+++ b/src/datepicker/__tests__/__snapshots__/styled-components.test.js.snap
@@ -11,8 +11,19 @@ Object {
 
 exports[`Component styled components Basic render: StyledDay has correct styles 1`] = `
 Object {
+  ":first-child": Object {
+    "borderBottomLeftRadius": "$theme.borders.radius200",
+    "borderTopLeftRadius": "$theme.borders.radius200",
+  },
+  ":last-child": Object {
+    "borderBottomRightRadius": "$theme.borders.radius200",
+    "borderTopRightRadius": "$theme.borders.radius200",
+  },
   "backgroundColor": "transparent",
-  "borderRadius": "$theme.borders.radius200",
+  "borderBottomLeftRadius": "$theme.borders.radius200",
+  "borderBottomRightRadius": "$theme.borders.radius200",
+  "borderTopLeftRadius": "$theme.borders.radius200",
+  "borderTopRightRadius": "$theme.borders.radius200",
   "boxSizing": "border-box",
   "color": "inherit",
   "cursor": "pointer",
@@ -27,6 +38,7 @@ Object {
   "paddingLeft": "$theme.sizing.scale200",
   "paddingRight": "$theme.sizing.scale200",
   "paddingTop": "$theme.sizing.scale300",
+  "position": "relative",
   "textAlign": "center",
   "width": "$theme.sizing.scale1000",
 }
@@ -76,14 +88,26 @@ Object {
 exports[`Component styled components Basic render: StyledWeek has correct styles 1`] = `
 Object {
   "display": "flex",
+  "marginBottom": "$theme.sizing.scale100",
   "whiteSpace": "no-wrap",
 }
 `;
 
 exports[`Component styled components Prop based styles: StyledDay has correct styles when $disabled is set to true 1`] = `
 Object {
+  ":first-child": Object {
+    "borderBottomLeftRadius": "$theme.borders.radius200",
+    "borderTopLeftRadius": "$theme.borders.radius200",
+  },
+  ":last-child": Object {
+    "borderBottomRightRadius": "$theme.borders.radius200",
+    "borderTopRightRadius": "$theme.borders.radius200",
+  },
   "backgroundColor": "transparent",
-  "borderRadius": "$theme.borders.radius200",
+  "borderBottomLeftRadius": "$theme.borders.radius200",
+  "borderBottomRightRadius": "$theme.borders.radius200",
+  "borderTopLeftRadius": "$theme.borders.radius200",
+  "borderTopRightRadius": "$theme.borders.radius200",
   "boxSizing": "border-box",
   "color": "$theme.colors.mono600",
   "cursor": "default",
@@ -98,6 +122,7 @@ Object {
   "paddingLeft": "$theme.sizing.scale200",
   "paddingRight": "$theme.sizing.scale200",
   "paddingTop": "$theme.sizing.scale300",
+  "position": "relative",
   "textAlign": "center",
   "width": "$theme.sizing.scale1000",
 }
@@ -105,8 +130,19 @@ Object {
 
 exports[`Component styled components Prop based styles: StyledDay has correct styles when $isHighlighted is set to true 1`] = `
 Object {
+  ":first-child": Object {
+    "borderBottomLeftRadius": "$theme.borders.radius200",
+    "borderTopLeftRadius": "$theme.borders.radius200",
+  },
+  ":last-child": Object {
+    "borderBottomRightRadius": "$theme.borders.radius200",
+    "borderTopRightRadius": "$theme.borders.radius200",
+  },
   "backgroundColor": "$theme.colors.primary100",
-  "borderRadius": "$theme.borders.radius200",
+  "borderBottomLeftRadius": "$theme.borders.radius200",
+  "borderBottomRightRadius": "$theme.borders.radius200",
+  "borderTopLeftRadius": "$theme.borders.radius200",
+  "borderTopRightRadius": "$theme.borders.radius200",
   "boxSizing": "border-box",
   "color": "inherit",
   "cursor": "pointer",
@@ -121,6 +157,7 @@ Object {
   "paddingLeft": "$theme.sizing.scale200",
   "paddingRight": "$theme.sizing.scale200",
   "paddingTop": "$theme.sizing.scale300",
+  "position": "relative",
   "textAlign": "center",
   "width": "$theme.sizing.scale1000",
 }
@@ -128,8 +165,19 @@ Object {
 
 exports[`Component styled components Prop based styles: StyledDay has correct styles when $isHovered is set to true 1`] = `
 Object {
+  ":first-child": Object {
+    "borderBottomLeftRadius": "$theme.borders.radius200",
+    "borderTopLeftRadius": "$theme.borders.radius200",
+  },
+  ":last-child": Object {
+    "borderBottomRightRadius": "$theme.borders.radius200",
+    "borderTopRightRadius": "$theme.borders.radius200",
+  },
   "backgroundColor": "$theme.colors.primary100",
-  "borderRadius": "$theme.borders.radius200",
+  "borderBottomLeftRadius": "$theme.borders.radius200",
+  "borderBottomRightRadius": "$theme.borders.radius200",
+  "borderTopLeftRadius": "$theme.borders.radius200",
+  "borderTopRightRadius": "$theme.borders.radius200",
   "boxSizing": "border-box",
   "color": "inherit",
   "cursor": "pointer",
@@ -144,6 +192,7 @@ Object {
   "paddingLeft": "$theme.sizing.scale200",
   "paddingRight": "$theme.sizing.scale200",
   "paddingTop": "$theme.sizing.scale300",
+  "position": "relative",
   "textAlign": "center",
   "width": "$theme.sizing.scale1000",
 }
@@ -151,8 +200,19 @@ Object {
 
 exports[`Component styled components Prop based styles: StyledDay has correct styles when $outsideMonth is set to true 1`] = `
 Object {
+  ":first-child": Object {
+    "borderBottomLeftRadius": "$theme.borders.radius200",
+    "borderTopLeftRadius": "$theme.borders.radius200",
+  },
+  ":last-child": Object {
+    "borderBottomRightRadius": "$theme.borders.radius200",
+    "borderTopRightRadius": "$theme.borders.radius200",
+  },
   "backgroundColor": "transparent",
-  "borderRadius": "$theme.borders.radius200",
+  "borderBottomLeftRadius": "$theme.borders.radius200",
+  "borderBottomRightRadius": "$theme.borders.radius200",
+  "borderTopLeftRadius": "$theme.borders.radius200",
+  "borderTopRightRadius": "$theme.borders.radius200",
   "boxSizing": "border-box",
   "color": "$theme.colors.mono600",
   "cursor": "pointer",
@@ -167,6 +227,7 @@ Object {
   "paddingLeft": "$theme.sizing.scale200",
   "paddingRight": "$theme.sizing.scale200",
   "paddingTop": "$theme.sizing.scale300",
+  "position": "relative",
   "textAlign": "center",
   "width": "$theme.sizing.scale1000",
 }
@@ -174,8 +235,19 @@ Object {
 
 exports[`Component styled components Prop based styles: StyledDay has correct styles when $selected is set to true 1`] = `
 Object {
+  ":first-child": Object {
+    "borderBottomLeftRadius": "$theme.borders.radius200",
+    "borderTopLeftRadius": "$theme.borders.radius200",
+  },
+  ":last-child": Object {
+    "borderBottomRightRadius": "$theme.borders.radius200",
+    "borderTopRightRadius": "$theme.borders.radius200",
+  },
   "backgroundColor": "$theme.colors.primary",
-  "borderRadius": "$theme.borders.radius200",
+  "borderBottomLeftRadius": "$theme.borders.radius200",
+  "borderBottomRightRadius": "$theme.borders.radius200",
+  "borderTopLeftRadius": "$theme.borders.radius200",
+  "borderTopRightRadius": "$theme.borders.radius200",
   "boxSizing": "border-box",
   "color": "$theme.colors.white",
   "cursor": "pointer",
@@ -190,6 +262,7 @@ Object {
   "paddingLeft": "$theme.sizing.scale200",
   "paddingRight": "$theme.sizing.scale200",
   "paddingTop": "$theme.sizing.scale300",
+  "position": "relative",
   "textAlign": "center",
   "width": "$theme.sizing.scale1000",
 }
diff --git a/src/datepicker/__tests__/styled-components.test.js b/src/datepicker/__tests__/styled-components.test.js
index 26499fb34..bf39936ed 100644
--- a/src/datepicker/__tests__/styled-components.test.js
+++ b/src/datepicker/__tests__/styled-components.test.js
@@ -16,6 +16,7 @@ import {
   StyledWeek,
   StyledDay,
 } from '../index.js';
+import {calculateBorderRadius as calcRadius} from '../styled-components.js';
 
 const styledComponents = [
   [StyledRoot, 'StyledRoot'],
@@ -50,3 +51,186 @@ describe('Component styled components', () => {
     );
   });
 });
+
+describe('Border radius calculation', () => {
+  function getProps(overrides: any) {
+    return {
+      $date: new Date(),
+      $isHighlighted: false,
+      $pseudoHighlighted: false,
+      $pseudoSelected: false,
+      $selected: false,
+      $startDate: false,
+      $isRange: true,
+      $hasRangeHighlighted: false,
+      $hasRangeOnRight: false,
+      $hasRangeSelected: false,
+      $theme: {
+        borders: {
+          useRoundedCorners: true,
+          radius200: '4px',
+        },
+      },
+      ...overrides,
+    };
+  }
+
+  type BorderRadiusT = {
+    borderTopLeftRadius: string | number,
+    borderBottomLeftRadius: string | number,
+    borderTopRightRadius: string | number,
+    borderBottomRightRadius: string | number,
+  };
+
+  function toStrVal(obj: ?BorderRadiusT) {
+    return obj
+      ? `${obj.borderTopLeftRadius} ${obj.borderTopRightRadius} ${
+          obj.borderBottomRightRadius
+        } ${obj.borderBottomLeftRadius}`
+      : null;
+  }
+
+  test('isRange and useRoundedCorners', () => {
+    expect(toStrVal(calcRadius(getProps()))).toEqual('4px 4px 4px 4px');
+  });
+  test('!useRoundedCorners', () => {
+    // !useRoundedCorners
+    expect(
+      toStrVal(
+        calcRadius(
+          getProps({
+            $theme: {
+              borders: {useRoundedCorners: false, radius200: '4px'},
+            },
+          }),
+        ),
+      ),
+    ).toEqual('0 0 0 0');
+  });
+  test('$selected', () => {
+    // $selected
+    expect(toStrVal(calcRadius(getProps({$selected: true})))).toEqual(
+      '4px 4px 4px 4px',
+    );
+  });
+  test('$selected and !isRange', () => {
+    // $selected/!$isRange
+    expect(
+      toStrVal(calcRadius(getProps({$selected: true, $isRange: false}))),
+    ).toEqual('4px 4px 4px 4px');
+  });
+  test('$selected, $hasRangeSelected, and $startDate', () => {
+    // $selected/$hasRangeSelected/$startDate
+    expect(
+      toStrVal(
+        calcRadius(
+          getProps({
+            $selected: true,
+            $hasRangeSelected: true,
+            $startDate: true,
+          }),
+        ),
+      ),
+    ).toEqual('4px 0 0 4px');
+  });
+  test('$selected, $hasRangeSelected, and !$startDate (endDate)', () => {
+    // $selected/$hasRangeSelected/!$startDate(means endDate)
+    expect(
+      toStrVal(
+        calcRadius(
+          getProps({
+            $selected: true,
+            $hasRangeSelected: true,
+          }),
+        ),
+      ),
+    ).toEqual('0 4px 4px 0');
+  });
+  test('$selected, $hasRangeHighlighted, and $hasRangeOnRight', () => {
+    // $selected/$hasRangeHighlighted/$hasRangeOnRight
+    expect(
+      toStrVal(
+        calcRadius(
+          getProps({
+            $selected: true,
+            $hasRangeHighlighted: true,
+            $hasRangeOnRight: true,
+          }),
+        ),
+      ),
+    ).toEqual('4px 0 0 4px');
+  });
+  test('$selected, $hasRangeHighlighted, and !$hasRangeOnRight(heighlighted range is on the left)', () => {
+    // $selected/$hasRangeHighlighted/!$hasRangeOnRight(means heighlighted range is on the left)
+    expect(
+      toStrVal(
+        calcRadius(
+          getProps({
+            $selected: true,
+            $hasRangeHighlighted: true,
+          }),
+        ),
+      ),
+    ).toEqual('0 4px 4px 0');
+  });
+  test('$pseudoHighlighted', () => {
+    expect(toStrVal(calcRadius(getProps({$pseudoHighlighted: true})))).toEqual(
+      '0 0 0 0',
+    );
+  });
+  test('$pseudoSelected', () => {
+    expect(toStrVal(calcRadius(getProps({$pseudoSelected: true})))).toEqual(
+      '0 0 0 0',
+    );
+  });
+  test('$isHighlighted and !$isRange', () => {
+    expect(
+      toStrVal(calcRadius(getProps({$isHighlighted: true, $isRange: false}))),
+    ).toEqual('4px 4px 4px 4px');
+  });
+  test('$isHighlighted and $hasRangeHighlighted', () => {
+    expect(
+      toStrVal(
+        calcRadius(
+          getProps({$isHighlighted: true, $hasRangeHighlighted: true}),
+        ),
+      ),
+    ).toEqual('4px 0 0 4px');
+  });
+  test('$isHighlighted, $hasRangeHighlighted, and $hasRangeOnRight', () => {
+    expect(
+      toStrVal(
+        calcRadius(
+          getProps({
+            $isHighlighted: true,
+            $hasRangeHighlighted: true,
+            $hasRangeOnRight: true,
+          }),
+        ),
+      ),
+    ).toEqual('0 4px 4px 0');
+  });
+  test('$isHighlighted and $pseudoSelected', () => {
+    expect(
+      toStrVal(
+        calcRadius(
+          getProps({
+            $isHighlighted: true,
+            $pseudoSelected: true,
+          }),
+        ),
+      ),
+    ).toEqual('0 0 0 0');
+  });
+  test('$isHighlighted', () => {
+    expect(
+      toStrVal(
+        calcRadius(
+          getProps({
+            $isHighlighted: true,
+          }),
+        ),
+      ),
+    ).toEqual('4px 4px 4px 4px');
+  });
+});