Repository: laboratoriobridge/bold
Pull request #334 (2019-02-08 14:08:58)

[Range Calendar]


Patch:
diff --git a/src/components/elements/Calendar/Calendar.tsx b/src/components/elements/Calendar/Calendar.tsx
index 1b98e09b3..b3388f80f 100644
--- a/src/components/elements/Calendar/Calendar.tsx
+++ b/src/components/elements/Calendar/Calendar.tsx
@@ -1,5 +1,5 @@
 import { Interpolation } from 'emotion'
-import React, { CSSProperties, useState } from 'react'
+import React, { CSSProperties, MouseEvent, useState } from 'react'
 
 import { Theme, useStyles } from '../../../styles'
 import { Omit } from '../../../util/types'
@@ -26,10 +26,20 @@ export interface CalendarProps extends Omit<MonthViewProps, 'visibleDate'> {
    * Map of modifier styles to be applied to a date if the respective modifier predicate applies.
    */
   modifierStyles?: Partial<DayModifierStyleMap>
+
+  /**
+   *
+   */
+  onMouseLeave?(e: MouseEvent<HTMLDivElement>): void
+
+  /**
+   *
+   */
+  isDaySelected?(day: Date): boolean
 }
 
 export function Calendar(props: CalendarProps) {
-  const { initialVisibleDate, modifiers, modifierStyles, ...rest } = props
+  const { initialVisibleDate, modifiers, modifierStyles, isDaySelected, ...rest } = props
   const { classes, theme } = useStyles(createStyles)
 
   const [visibleDate, setVisibleDate] = useState(initialVisibleDate)
@@ -45,7 +55,7 @@ export function Calendar(props: CalendarProps) {
   }
 
   return (
-    <div className={classes.root}>
+    <div className={classes.root} onMouseLeave={props.onMouseLeave}>
       <HFlow hSpacing={0.5} justifyContent='space-around' style={classes.controls}>
         <MonthControl visibleDate={visibleDate} onChange={setVisibleDate} />
         <YearControl visibleDate={visibleDate} onChange={setVisibleDate} />
@@ -56,6 +66,7 @@ export function Calendar(props: CalendarProps) {
         {...rest}
         visibleDate={visibleDate}
         onDayClick={handleDayClick}
+        isDaySelected={isDaySelected}
       />
     </div>
   )
diff --git a/src/components/elements/Calendar/MonthView.tsx b/src/components/elements/Calendar/MonthView.tsx
index b4a4d255e..73c69bc40 100644
--- a/src/components/elements/Calendar/MonthView.tsx
+++ b/src/components/elements/Calendar/MonthView.tsx
@@ -13,6 +13,7 @@ export interface MonthViewProps {
   renderDay?(day: Date): React.ReactNode
   renderWeekName?(firstWeekDay: Date): React.ReactNode
   createDayStyles?(day: Date, props: MonthViewProps): Interpolation
+  isDaySelected?(day: Date): boolean
 }
 
 export function MonthView(props: MonthViewProps) {
@@ -42,6 +43,7 @@ export function MonthView(props: MonthViewProps) {
                   onClick={handleDayClick(day)}
                   onMouseOver={handleDayHover(day)}
                   role='button'
+                  aria-selected={props.isDaySelected && props.isDaySelected(day)}
                 >
                   {renderDay(day)}
                 </span>
diff --git a/src/components/elements/Calendar/RangeCalendar/ControlledRangeCalendar.stories.tsx b/src/components/elements/Calendar/RangeCalendar/ControlledRangeCalendar.stories.tsx
new file mode 100644
index 000000000..6e5b47023
--- /dev/null
+++ b/src/components/elements/Calendar/RangeCalendar/ControlledRangeCalendar.stories.tsx
@@ -0,0 +1,6 @@
+import { storiesOf } from '@storybook/react'
+import React from 'react'
+
+import { ControlledRangeCalendar } from './ControlledRangeCalendar'
+
+storiesOf('Components|Controlled Range Calendar', module).add('undefined interval', () => <ControlledRangeCalendar />)
diff --git a/src/components/elements/Calendar/RangeCalendar/ControlledRangeCalendar.tsx b/src/components/elements/Calendar/RangeCalendar/ControlledRangeCalendar.tsx
new file mode 100644
index 000000000..70ef3da3a
--- /dev/null
+++ b/src/components/elements/Calendar/RangeCalendar/ControlledRangeCalendar.tsx
@@ -0,0 +1,67 @@
+import { useEffect, useRef, useState } from 'react'
+import React from 'react'
+
+import { CalendarProps } from '../Calendar'
+
+import { RangeCalendar } from './RangeCalendar'
+
+export interface ControlledRangeCalendarProps extends CalendarProps {
+  initialValues?: {
+    initialDate: Date
+    finalDate: Date
+  }
+  onChange?(initialDate: Date, finalDate: Date): void
+}
+export const ControlledRangeCalendar = ({ onChange, initialValues, ...rest }: ControlledRangeCalendarProps) => {
+  const [initialDate, setInitialDate] = useState<Date>(initialValues ? initialValues.initialDate : undefined)
+  const [finalDate, setFinalDate] = useState<Date>(initialValues ? initialValues.finalDate : undefined)
+  const cmpMounted = useRef(false)
+
+  // Call to onChange prop only after component has been mounted
+  useEffect(() => {
+    if (cmpMounted.current && onChange) {
+      onChange(initialDate, finalDate)
+      return
+    }
+    cmpMounted.current = true
+  }, [initialDate, finalDate])
+
+  const setFinalVerified = (start: Date, end: Date) => {
+    if (!rest.modifiers || !rest.modifiers.disabled) {
+      setFinalDate(end)
+      return
+    }
+    const aux: Date = new Date(start)
+    for (; aux < end; aux.setDate(aux.getDate() + 1)) {
+      if (rest.modifiers.disabled(aux, rest)) {
+        aux.setDate(aux.getDate() - 1)
+        break
+      }
+    }
+    setFinalDate(aux)
+  }
+
+  const controllDayClick = (day: Date) => {
+    if (!initialDate) {
+      setInitialDate(day)
+      return
+    }
+    if (finalDate) {
+      setInitialDate(day)
+      setFinalDate(undefined)
+      return
+    }
+    if (initialDate.getTime() === day.getTime()) {
+      return
+    }
+    if (day < initialDate) {
+      setFinalVerified(day, initialDate)
+      setInitialDate(day)
+      return
+    }
+    setFinalVerified(initialDate, day)
+    return
+  }
+
+  return <RangeCalendar {...rest} initialDate={initialDate} finalDate={finalDate} onDayClick={controllDayClick} />
+}
diff --git a/src/components/elements/Calendar/RangeCalendar/RangeCalendar.stories.tsx b/src/components/elements/Calendar/RangeCalendar/RangeCalendar.stories.tsx
new file mode 100644
index 000000000..b43f9ca61
--- /dev/null
+++ b/src/components/elements/Calendar/RangeCalendar/RangeCalendar.stories.tsx
@@ -0,0 +1,34 @@
+import { storiesOf } from '@storybook/react'
+import React from 'react'
+
+import { Heading } from '../..'
+import { HFlow, VFlow } from '../../Flow'
+
+import { RangeCalendar } from './RangeCalendar'
+
+const day = new Date('2019-04-01')
+
+storiesOf('Components|Range Calendar', module)
+  .add('undefined interval', () => <RangeCalendar initialDate={undefined} finalDate={undefined} />)
+  .add('only one date', () => (
+    <HFlow>
+      <VFlow style={{ textAlign: 'center' }}>
+        <Heading level={3} color='normal'>
+          Only initial date defined
+        </Heading>
+        <RangeCalendar initialDate={day} finalDate={undefined} />
+      </VFlow>
+      <VFlow style={{ textAlign: 'center' }}>
+        <Heading level={3} color='normal'>
+          Only final date defined
+        </Heading>
+        <RangeCalendar initialDate={undefined} finalDate={day} />
+      </VFlow>
+    </HFlow>
+  ))
+  .add('interval defined', () => {
+    const nextDay = new Date(day)
+    nextDay.setDate(day.getDate() + 11)
+
+    return <RangeCalendar initialDate={day} finalDate={nextDay} />
+  })
diff --git a/src/components/elements/Calendar/RangeCalendar/RangeCalendar.tsx b/src/components/elements/Calendar/RangeCalendar/RangeCalendar.tsx
new file mode 100644
index 000000000..acaabdea8
--- /dev/null
+++ b/src/components/elements/Calendar/RangeCalendar/RangeCalendar.tsx
@@ -0,0 +1,61 @@
+import * as React from 'react'
+import { MouseEvent, useState } from 'react'
+
+import { Theme } from '../../../../styles'
+import { Calendar, CalendarProps, defaultModifierStyles } from '../Calendar'
+import { isSameDay } from '../util'
+
+export interface RangeCalendarProps extends CalendarProps {
+  initialDate: Date
+  finalDate: Date
+}
+export const RangeCalendar = ({ initialDate, finalDate, ...rest }: RangeCalendarProps) => {
+  if (initialDate && finalDate && finalDate < initialDate) {
+    if (process.env.NODE_ENV !== 'production') {
+      // tslint:disable-next-line no-console
+      console.warn(`RangeCalendar: finalDate should not be before initialDate`)
+    }
+    finalDate = undefined
+    initialDate = undefined
+  }
+
+  const [hoverFinalDate, setHoverFinalDate] = useState<Date>(undefined)
+
+  const isInTheRange = (day: Date): boolean => {
+    if (!initialDate) {
+      return false
+    }
+    if (!finalDate) {
+      return isSameDay(day, initialDate)
+    }
+    if (initialDate <= day && day <= finalDate) {
+      return true
+    }
+    return false
+  }
+
+  const isInTheHoverRange = (day: Date) =>
+    (!initialDate && hoverFinalDate && isSameDay(day, hoverFinalDate)) ||
+    ((hoverFinalDate <= day && day < initialDate) || (initialDate < day && day <= hoverFinalDate))
+
+  const onMouseLeave = (e: MouseEvent<HTMLDivElement>) => setHoverFinalDate(undefined)
+  return (
+    <Calendar
+      {...rest}
+      onMouseLeave={onMouseLeave}
+      onDayHover={setHoverFinalDate}
+      isDaySelected={isInTheRange}
+      modifiers={{
+        ...rest.modifiers,
+        inTheHoverRange: isInTheHoverRange,
+        inTheRange: isInTheRange,
+      }}
+      modifierStyles={{
+        ...rest.modifierStyles,
+        inTheHoverRange: dayHoverStyle,
+        inTheRange: defaultModifierStyles.selected,
+      }}
+    />
+  )
+}
+export const dayHoverStyle = (theme: Theme) => ({ background: theme.pallete.surface.background })


Test patch:
diff --git a/src/components/elements/Calendar/RangeCalendar/ControlledRangeCalendar.test.tsx b/src/components/elements/Calendar/RangeCalendar/ControlledRangeCalendar.test.tsx
new file mode 100644
index 000000000..6515daa98
--- /dev/null
+++ b/src/components/elements/Calendar/RangeCalendar/ControlledRangeCalendar.test.tsx
@@ -0,0 +1,236 @@
+import { matchers } from 'jest-emotion'
+import React from 'react'
+import { fireEvent, render, wait } from 'react-testing-library'
+
+import { ControlledRangeCalendar, ControlledRangeCalendarProps } from './ControlledRangeCalendar'
+
+expect.extend(matchers)
+
+const createComponent = (props: Partial<ControlledRangeCalendarProps> = {}) => (
+  <ControlledRangeCalendar
+    initialVisibleDate={new Date('2019-02-09')}
+    initialValues={{
+      initialDate: undefined,
+      finalDate: undefined,
+    }}
+    {...props}
+  />
+)
+
+describe('[Calendar][RangePicker]', () => {
+  it('With empty initialValues, should leave an empty interval', () => {
+    const { getAllByRole } = render(createComponent())
+
+    getAllByRole('button').forEach(item => expect(item.getAttribute('aria-selected')).toBe('false'))
+  })
+
+  it('Should select only the initialDate if the finalDate is null/undefined in initialValues', () => {
+    const { getByText } = render(
+      createComponent({
+        initialValues: {
+          initialDate: new Date('2019-02-11'),
+          finalDate: null,
+        },
+      })
+    )
+
+    expect(getByText('10').getAttribute('aria-selected')).toBe('false')
+    expect(getByText('11').getAttribute('aria-selected')).toBe('true')
+    expect(getByText('12').getAttribute('aria-selected')).toBe('false')
+  })
+
+  it('Should select nothing if only the finalDate is setted in initialValues', () => {
+    const { getByText } = render(
+      createComponent({
+        initialValues: {
+          initialDate: null,
+          finalDate: new Date('2019-02-11'),
+        },
+      })
+    )
+
+    expect(getByText('10').getAttribute('aria-selected')).toBe('false')
+    expect(getByText('11').getAttribute('aria-selected')).toBe('false')
+    expect(getByText('12').getAttribute('aria-selected')).toBe('false')
+  })
+
+  it('With initialValues should select the interval', () => {
+    const { getByText } = render(
+      createComponent({
+        initialValues: {
+          initialDate: new Date('2019-02-11'),
+          finalDate: new Date('2019-02-13'),
+        },
+      })
+    )
+
+    expect(getByText('10').getAttribute('aria-selected')).toBe('false')
+    expect(getByText('11').getAttribute('aria-selected')).toBe('true')
+    expect(getByText('12').getAttribute('aria-selected')).toBe('true')
+    expect(getByText('13').getAttribute('aria-selected')).toBe('true')
+    expect(getByText('14').getAttribute('aria-selected')).toBe('false')
+  })
+
+  it('Clicks on the same day, should not have effect', () => {
+    const { getByText } = render(createComponent())
+    expect(getByText('10').getAttribute('aria-selected')).toBe('false')
+    expect(getByText('11').getAttribute('aria-selected')).toBe('false')
+
+    fireEvent.click(getByText('10'))
+    expect(getByText('10').getAttribute('aria-selected')).toBe('true')
+    expect(getByText('11').getAttribute('aria-selected')).toBe('false')
+
+    fireEvent.click(getByText('10'))
+    expect(getByText('10').getAttribute('aria-selected')).toBe('true')
+    expect(getByText('11').getAttribute('aria-selected')).toBe('false')
+
+    fireEvent.click(getByText('11'))
+    expect(getByText('10').getAttribute('aria-selected')).toBe('true')
+    expect(getByText('11').getAttribute('aria-selected')).toBe('true')
+  })
+
+  it('Should select the finalDate correctly, with a predefined initialDate', () => {
+    const { getByText } = render(
+      createComponent({
+        initialValues: {
+          initialDate: new Date('2019-02-11'),
+          finalDate: null,
+        },
+      })
+    )
+    expect(getByText('11').getAttribute('aria-selected')).toBe('true')
+    expect(getByText('12').getAttribute('aria-selected')).toBe('false')
+
+    fireEvent.click(getByText('12'))
+    expect(getByText('11').getAttribute('aria-selected')).toBe('true')
+    expect(getByText('12').getAttribute('aria-selected')).toBe('true')
+  })
+
+  it('Should select correctly both initialDate and finalDate, without a predefined interval', () => {
+    const { getByText } = render(createComponent())
+    expect(getByText('10').getAttribute('aria-selected')).toBe('false')
+    expect(getByText('11').getAttribute('aria-selected')).toBe('false')
+
+    fireEvent.click(getByText('10'))
+    expect(getByText('10').getAttribute('aria-selected')).toBe('true')
+    expect(getByText('11').getAttribute('aria-selected')).toBe('false')
+
+    fireEvent.click(getByText('11'))
+    expect(getByText('10').getAttribute('aria-selected')).toBe('true')
+    expect(getByText('11').getAttribute('aria-selected')).toBe('true')
+  })
+
+  it('Should select correctly both initialDate and finalDate, with a predefined interval', () => {
+    const { getByText } = render(
+      createComponent({
+        initialValues: {
+          initialDate: new Date('2019-02-11'),
+          finalDate: new Date('2019-02-12'),
+        },
+      })
+    )
+    expect(getByText('11').getAttribute('aria-selected')).toBe('true')
+    expect(getByText('12').getAttribute('aria-selected')).toBe('true')
+
+    fireEvent.click(getByText('20'))
+    expect(getByText('11').getAttribute('aria-selected')).toBe('false')
+    expect(getByText('12').getAttribute('aria-selected')).toBe('false')
+    expect(getByText('20').getAttribute('aria-selected')).toBe('true')
+    expect(getByText('21').getAttribute('aria-selected')).toBe('false')
+
+    fireEvent.click(getByText('21'))
+    expect(getByText('11').getAttribute('aria-selected')).toBe('false')
+    expect(getByText('12').getAttribute('aria-selected')).toBe('false')
+    expect(getByText('20').getAttribute('aria-selected')).toBe('true')
+    expect(getByText('21').getAttribute('aria-selected')).toBe('true')
+  })
+
+  it('With interval properly defined, a click should define only the initialDate', () => {
+    const { getByText } = render(
+      createComponent({
+        initialValues: {
+          initialDate: new Date('2019-02-11'),
+          finalDate: new Date('2019-02-12'),
+        },
+      })
+    )
+    expect(getByText('11').getAttribute('aria-selected')).toBe('true')
+    expect(getByText('12').getAttribute('aria-selected')).toBe('true')
+
+    fireEvent.click(getByText('15'))
+    expect(getByText('11').getAttribute('aria-selected')).toBe('false')
+    expect(getByText('12').getAttribute('aria-selected')).toBe('false')
+    expect(getByText('15').getAttribute('aria-selected')).toBe('true')
+    expect(getByText('16').getAttribute('aria-selected')).toBe('false')
+  })
+
+  it('If the interval has a disabled day, use as finalDate the "disabled-day - 1day"', () => {
+    const { getByText } = render(
+      createComponent({
+        modifiers: {
+          disabled: (day: Date) => day.getDate() === new Date('2019-02-13').getDate(),
+        },
+      })
+    )
+    expect(getByText('11').getAttribute('aria-selected')).toBe('false')
+    expect(getByText('12').getAttribute('aria-selected')).toBe('false')
+    expect(getByText('13').getAttribute('aria-selected')).toBe('false')
+    expect(getByText('14').getAttribute('aria-selected')).toBe('false')
+
+    fireEvent.click(getByText('11'))
+    expect(getByText('11').getAttribute('aria-selected')).toBe('true')
+    expect(getByText('12').getAttribute('aria-selected')).toBe('false')
+    expect(getByText('13').getAttribute('aria-selected')).toBe('false')
+    expect(getByText('14').getAttribute('aria-selected')).toBe('false')
+
+    fireEvent.click(getByText('14'))
+    expect(getByText('11').getAttribute('aria-selected')).toBe('true')
+    expect(getByText('12').getAttribute('aria-selected')).toBe('true')
+    expect(getByText('13').getAttribute('aria-selected')).toBe('false')
+    expect(getByText('14').getAttribute('aria-selected')).toBe('false')
+  })
+
+  it('If the selected finalDate are earlier than initialDate, invert the dates', async () => {
+    let initial: Date
+    let final: Date
+    const { getByText } = render(
+      createComponent({
+        onChange: (i: Date, f: Date) => {
+          initial = i
+          final = f
+        },
+      })
+    )
+    expect(getByText('10').getAttribute('aria-selected')).toBe('false')
+    expect(initial).toBeUndefined()
+    expect(getByText('11').getAttribute('aria-selected')).toBe('false')
+    expect(final).toBeUndefined()
+
+    fireEvent.click(getByText('11'))
+    await wait()
+    expect(getByText('10').getAttribute('aria-selected')).toBe('false')
+    expect(initial.getTime()).toBe(new Date('2019-02-11').getTime())
+    expect(getByText('11').getAttribute('aria-selected')).toBe('true')
+    expect(final).toBeUndefined()
+
+    fireEvent.click(getByText('10'))
+    await wait()
+    expect(getByText('10').getAttribute('aria-selected')).toBe('true')
+    expect(initial.getTime()).toBe(new Date('2019-02-10').getTime())
+    expect(getByText('11').getAttribute('aria-selected')).toBe('true')
+    expect(final.getTime()).toBe(new Date('2019-02-11').getTime())
+  })
+
+  it('Component should call onChange on every update of the values', async () => {
+    const spy = jest.fn()
+    const { getByText } = render(createComponent({ onChange: spy }))
+
+    fireEvent.click(getByText('10'))
+    await wait()
+    expect(spy).toHaveBeenCalledTimes(1)
+
+    fireEvent.click(getByText('11'))
+    await wait()
+    expect(spy).toHaveBeenCalledTimes(2)
+  })
+})
diff --git a/src/components/elements/Calendar/RangeCalendar/RangeCalendar.test.tsx b/src/components/elements/Calendar/RangeCalendar/RangeCalendar.test.tsx
new file mode 100644
index 000000000..6a74336ac
--- /dev/null
+++ b/src/components/elements/Calendar/RangeCalendar/RangeCalendar.test.tsx
@@ -0,0 +1,192 @@
+import { Interpolation } from 'emotion'
+import { matchers } from 'jest-emotion'
+import React from 'react'
+import { fireEvent, render } from 'react-testing-library'
+
+import { createTheme } from '../../../../styles'
+import { defaultModifierStyles } from '../Calendar'
+
+import { dayHoverStyle, RangeCalendar, RangeCalendarProps } from './RangeCalendar'
+
+expect.extend(matchers)
+
+const createComponent = (props: Partial<RangeCalendarProps> = {}) => (
+  <RangeCalendar initialVisibleDate={new Date('2019-02-09')} initialDate={null} finalDate={null} {...props} />
+)
+
+/**
+ * iterates over the object executing an callback containing the desired tests
+ * @param obj
+ * @param testFn
+ */
+const iterateObjectFields = (obj: Object, testFn: (fieldName: string, fieldValue: any) => void) => {
+  for (const field in obj as any) {
+    if (obj.hasOwnProperty(field)) {
+      if (obj[field] === Object(obj[field])) {
+        iterateObjectFields(obj[field], testFn)
+      } else {
+        testFn(field, obj[field])
+      }
+    }
+  }
+}
+
+describe('[Calendar][RangePicker]', () => {
+  const theme = createTheme()
+  const normalizeCssClassNames = (str: string) => str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
+
+  it('Should initialize with null values if initialValues is null', () => {
+    const { getAllByRole } = render(createComponent())
+
+    getAllByRole('button').forEach(item => expect(item.getAttribute('aria-selected')).toBe('false'))
+  })
+
+  it('With nothing defined, hovered days should have the correct css', () => {
+    const { getByText } = render(createComponent())
+    const expectedStyle = dayHoverStyle(theme)
+
+    fireEvent.mouseOver(getByText('14'))
+    iterateObjectFields(expectedStyle, (fieldName: string, fieldValue: any) =>
+      expect(getByText('14')).toHaveStyleRule(normalizeCssClassNames(fieldName), fieldValue)
+    )
+  })
+
+  it('When finalDate is earlier than initialDate, both should be undefined', () => {
+    const { getByText } = render(
+      createComponent({ initialDate: new Date('2019-02-15'), finalDate: new Date('2019-02-14') })
+    )
+
+    expect(getByText('14').getAttribute('aria-selected')).toBe('false')
+    expect(getByText('15').getAttribute('aria-selected')).toBe('false')
+  })
+
+  it('With only the initialDate selected, just one day should have the "selectedStyle"', () => {
+    const { getByText } = render(createComponent({ initialDate: new Date('2019-02-15') }))
+    const expectedStyle: Interpolation = defaultModifierStyles.selected(theme)
+
+    iterateObjectFields(expectedStyle, (fieldName: string, fieldValue: any) => {
+      expect(getByText('14')).not.toHaveStyleRule(normalizeCssClassNames(fieldName), fieldValue)
+      expect(getByText('15')).toHaveStyleRule(normalizeCssClassNames(fieldName), fieldValue)
+      expect(getByText('16')).not.toHaveStyleRule(normalizeCssClassNames(fieldName), fieldValue)
+    })
+  })
+
+  it('With finalDate after than initialDate, the selected days must have been selected', () => {
+    const { getByText } = render(
+      createComponent({ initialDate: new Date('2019-02-11'), finalDate: new Date('2019-02-13') })
+    )
+
+    expect(getByText('10').getAttribute('aria-selected')).toBe('false')
+    expect(getByText('11').getAttribute('aria-selected')).toBe('true')
+    expect(getByText('12').getAttribute('aria-selected')).toBe('true')
+    expect(getByText('13').getAttribute('aria-selected')).toBe('true')
+    expect(getByText('14').getAttribute('aria-selected')).toBe('false')
+  })
+
+  it('With finalDate after than initialDate, the selected days should have the "selectedStyle"', () => {
+    const { getByText } = render(
+      createComponent({ initialDate: new Date('2019-02-11'), finalDate: new Date('2019-02-13') })
+    )
+    const expectedStyle: Interpolation = defaultModifierStyles.selected(theme)
+
+    iterateObjectFields(expectedStyle, (fieldName: string, fieldValue: any) => {
+      expect(getByText('10')).not.toHaveStyleRule(normalizeCssClassNames(fieldName), fieldValue)
+      expect(getByText('11')).toHaveStyleRule(normalizeCssClassNames(fieldName), fieldValue)
+      expect(getByText('12')).toHaveStyleRule(normalizeCssClassNames(fieldName), fieldValue)
+      expect(getByText('13')).toHaveStyleRule(normalizeCssClassNames(fieldName), fieldValue)
+      expect(getByText('14')).not.toHaveStyleRule(normalizeCssClassNames(fieldName), fieldValue)
+    })
+  })
+
+  it('With only initalDate defined, hover style must be applied on the interval between initialDate and mouse', () => {
+    const { getByText } = render(createComponent({ initialDate: new Date('2019-02-15') }))
+    const expectedStyle = dayHoverStyle(theme)
+
+    fireEvent.mouseOver(getByText('17'))
+    iterateObjectFields(expectedStyle, (fieldName: string, fieldValue: any) => {
+      expect(getByText('14')).not.toHaveStyleRule(normalizeCssClassNames(fieldName), fieldValue)
+      // Day 15 (the day selected in the interval) skipped to preserve the purpose of this test
+      expect(getByText('16')).toHaveStyleRule(normalizeCssClassNames(fieldName), fieldValue)
+      expect(getByText('17')).toHaveStyleRule(normalizeCssClassNames(fieldName), fieldValue)
+      expect(getByText('18')).not.toHaveStyleRule(normalizeCssClassNames(fieldName), fieldValue)
+    })
+
+    fireEvent.mouseOver(getByText('13'))
+    iterateObjectFields(expectedStyle, (fieldName: string, fieldValue: any) => {
+      expect(getByText('12')).not.toHaveStyleRule(normalizeCssClassNames(fieldName), fieldValue)
+      expect(getByText('13')).toHaveStyleRule(normalizeCssClassNames(fieldName), fieldValue)
+      expect(getByText('14')).toHaveStyleRule(normalizeCssClassNames(fieldName), fieldValue)
+      // Day 15 (the day selected in the interval) skipped to preserve the purpose of this test
+      expect(getByText('16')).not.toHaveStyleRule(normalizeCssClassNames(fieldName), fieldValue)
+    })
+  })
+
+  it('Hover style must be applied when initialDate and finalDate are correctly defined', () => {
+    const { getByText } = render(
+      createComponent({ initialDate: new Date('2019-02-11'), finalDate: new Date('2019-02-13') })
+    )
+    const expectedStyle = dayHoverStyle(theme)
+
+    fireEvent.mouseOver(getByText('15'))
+    iterateObjectFields(expectedStyle, (fieldName: string, fieldValue: any) => {
+      expect(getByText('10')).not.toHaveStyleRule(normalizeCssClassNames(fieldName), fieldValue)
+      // Day 11 to 13 (the day selected in the interval) skipped to preserve the purpose of this test
+      expect(getByText('14')).toHaveStyleRule(normalizeCssClassNames(fieldName), fieldValue)
+      expect(getByText('15')).toHaveStyleRule(normalizeCssClassNames(fieldName), fieldValue)
+      expect(getByText('16')).not.toHaveStyleRule(normalizeCssClassNames(fieldName), fieldValue)
+    })
+
+    fireEvent.mouseOver(getByText('09'))
+    iterateObjectFields(expectedStyle, (fieldName: string, fieldValue: any) => {
+      expect(getByText('08')).not.toHaveStyleRule(normalizeCssClassNames(fieldName), fieldValue)
+      expect(getByText('09')).toHaveStyleRule(normalizeCssClassNames(fieldName), fieldValue)
+      expect(getByText('10')).toHaveStyleRule(normalizeCssClassNames(fieldName), fieldValue)
+      // Day 11 to 13 (the day selected in the interval) skipped to preserve the purpose of this test
+      expect(getByText('14')).not.toHaveStyleRule(normalizeCssClassNames(fieldName), fieldValue)
+    })
+  })
+
+  it('Remove hover style when mouseLeave', () => {
+    const { getByText } = render(createComponent({ initialDate: new Date('2019-02-13') }))
+    const expectedStyle = dayHoverStyle(theme)
+
+    fireEvent.mouseOver(getByText('14'))
+    iterateObjectFields(expectedStyle, (fieldName: string, fieldValue: any) =>
+      expect(getByText('14')).toHaveStyleRule(normalizeCssClassNames(fieldName), fieldValue)
+    )
+
+    fireEvent.mouseLeave(getByText('14'))
+    iterateObjectFields(expectedStyle, (fieldName: string, fieldValue: any) =>
+      expect(getByText('14')).not.toHaveStyleRule(normalizeCssClassNames(fieldName), fieldValue)
+    )
+  })
+
+  it('The comparisons for which days belong to the selected range should be correct', () => {
+    const { getByText, rerender } = render(
+      createComponent({
+        initialVisibleDate: new Date('2019-01-15'),
+        initialDate: new Date('2019-02-15'),
+      })
+    )
+    // Showing the previous month, so nothing should be selected
+    expect(getByText('15').getAttribute('aria-selected')).toBe('false')
+
+    rerender(
+      createComponent({
+        initialVisibleDate: new Date('2018-02-15'),
+        initialDate: new Date('2019-02-15'),
+      })
+    )
+    // Showing the previous year, so nothing should be selected
+    expect(getByText('15').getAttribute('aria-selected')).toBe('false')
+
+    rerender(
+      createComponent({
+        initialVisibleDate: new Date('2018-01-15'),
+        initialDate: new Date('2019-02-15'),
+      })
+    )
+    // Showing the wrong year and month, so nothing should be selected
+    expect(getByText('15').getAttribute('aria-selected')).toBe('false')
+  })
+})