From 4cade3fa2e55acc5a33845320f501d5ff1c0ad05 Mon Sep 17 00:00:00 2001 From: yqrashawn Date: Wed, 14 Oct 2020 19:49:44 +0800 Subject: [PATCH 1/3] feat(tooltip): support hoverable tooltip --- components/tooltip/__test__/index.test.tsx | 104 +++++++++++++++++++++ components/tooltip/tooltip.tsx | 67 +++++++------ pages/en-us/components/tooltip.mdx | 38 +++++--- pages/zh-cn/components/tooltip.mdx | 38 +++++--- 4 files changed, 186 insertions(+), 61 deletions(-) diff --git a/components/tooltip/__test__/index.test.tsx b/components/tooltip/__test__/index.test.tsx index 965e7532a..10dfaa37b 100644 --- a/components/tooltip/__test__/index.test.tsx +++ b/components/tooltip/__test__/index.test.tsx @@ -49,6 +49,7 @@ describe('Tooltip', () => { wrapper.find('.tooltip').simulate('mouseLeave', nativeEvent) await updateWrapper(wrapper, 400) expectTooltipIsHidden(wrapper) + expect(() => wrapper.unmount()).not.toThrow() }) it('should call custom mouse event handler when is controlled', async () => { @@ -82,6 +83,7 @@ describe('Tooltip', () => { await clickAway(wrapper) expect(onClickAway).toBeCalledTimes(1) + expect(() => wrapper.unmount()).not.toThrow() }) it('should render text when hover it', async () => { @@ -97,6 +99,7 @@ describe('Tooltip', () => { wrapper.find('.tooltip').simulate('mouseLeave', nativeEvent) await updateWrapper(wrapper, 400) expectTooltipIsHidden(wrapper) + expect(() => wrapper.unmount()).not.toThrow() }) it('should render react-node when click it', async () => { @@ -124,6 +127,7 @@ describe('Tooltip', () => { await updateWrapper(wrapper, 400) expectTooltipIsHidden(wrapper) + expect(() => wrapper.unmount()).not.toThrow() }) it('should render inner components', async () => { @@ -135,6 +139,7 @@ describe('Tooltip', () => { , ) expect(wrapper.find('#test').length).not.toBe(0) + expect(() => wrapper.unmount()).not.toThrow() }) it('should render correctly by visible', async () => { @@ -148,6 +153,7 @@ describe('Tooltip', () => { await updateWrapper(wrapper, 150) expect(wrapper.find('#visible').length).toBe(1) + expect(() => wrapper.unmount()).not.toThrow() }) it('should render correctly by using wrong placement', async () => { @@ -162,5 +168,103 @@ describe('Tooltip', () => { , ) expect(wrapper.find('#default-visible').length).toBe(1) + expect(() => wrapper.unmount()).not.toThrow() + }) + + it('should have the hoverable effect', async () => { + const onVisibleChange = jest.fn() + const onMouseLeave = jest.fn() + + const wrapper = mount( + + tooltip + , + ) + + // normal show hide + // on + await act(async () => { + wrapper.find('.tooltip').simulate('mouseEnter', nativeEvent) + await updateWrapper(wrapper, 0) + }) + expectTooltipIsShow(wrapper) + + // off + await act(async () => { + wrapper.find('.tooltip').simulate('mouseLeave', nativeEvent) + await updateWrapper(wrapper, 0) + }) + expectTooltipIsShow(wrapper) + await updateWrapper(wrapper, 250) + expectTooltipIsHidden(wrapper) + + wrapper.setProps({ onVisibleChange }) + + // hover tooltip -> leave t and hover content -> leave content + // on + await act(async () => { + wrapper.find('.tooltip').simulate('mouseEnter', nativeEvent) + await updateWrapper(wrapper, 0) + }) + expectTooltipIsShow(wrapper) + + // off + await act(async () => { + wrapper.find('.tooltip').simulate('mouseLeave', nativeEvent) + await updateWrapper(wrapper, 0) + }) + expectTooltipIsShow(wrapper) + + // on content + await act(async () => { + wrapper.find('.tooltip-content').simulate('mouseEnter', nativeEvent) + await updateWrapper(wrapper, 0) + }) + expectTooltipIsShow(wrapper) + + // off content + await act(async () => { + wrapper.find('.tooltip-content').simulate('mouseLeave', nativeEvent) + await updateWrapper(wrapper, 0) + }) + expectTooltipIsHidden(wrapper) + + // hover t -> hover c and leave t -> leave c + // on + await act(async () => { + wrapper.find('.tooltip').simulate('mouseEnter', nativeEvent) + await updateWrapper(wrapper, 0) + }) + expectTooltipIsShow(wrapper) + + // on content + await act(async () => { + wrapper.find('.tooltip-content').simulate('mouseEnter', nativeEvent) + await updateWrapper(wrapper, 0) + }) + expectTooltipIsShow(wrapper) + + // off + await act(async () => { + wrapper.find('.tooltip').simulate('mouseLeave', nativeEvent) + await updateWrapper(wrapper, 0) + }) + await updateWrapper(wrapper, 250) + expectTooltipIsShow(wrapper) + + // off content + await act(async () => { + wrapper.find('.tooltip-content').simulate('mouseLeave', nativeEvent) + await updateWrapper(wrapper, 0) + }) + expectTooltipIsHidden(wrapper) + + expect(() => wrapper.unmount()).not.toThrow() }) }) diff --git a/components/tooltip/tooltip.tsx b/components/tooltip/tooltip.tsx index fa0e6d216..2ffdcc1b8 100644 --- a/components/tooltip/tooltip.tsx +++ b/components/tooltip/tooltip.tsx @@ -1,10 +1,9 @@ -import React, { useMemo, useEffect, useRef, useState, MouseEvent } from 'react' +import React, { useMemo, useEffect, useRef, useState, MouseEvent, useCallback } from 'react' import withDefaults from '../utils/with-defaults' import { usePopper } from 'react-popper' import useTheme from '../styles/use-theme' import { getColors } from './styles' import { TriggerTypes, Placement, SnippetColors } from '../utils/prop-types' -// import CSSTransition from '../shared/css-transition' import useClickAway from '../utils/use-click-away' import type { Options } from '@popperjs/core/lib/modifiers/offset' @@ -21,14 +20,16 @@ export const defaultProps = { color: 'default' as SnippetColors, trigger: 'hover' as TriggerTypes, placement: 'auto' as Placement, - // enterDelay: 100, - // leaveDelay: 200, offset: [0, 10] as Options['offset'], className: '', contentClassName: '', + hoverable: false, + hoverableTimeout: 200, } export interface Props extends Omit, 'onMouseEnter' | 'onMouseLeave'> { + hoverable?: boolean + hoverableTimeout?: number text: string | React.ReactNode color?: SnippetColors placement?: Placement @@ -36,8 +37,6 @@ export interface Props extends Omit, 'onMouseEnter' | defaultVisible?: boolean hideArrow?: boolean trigger?: TriggerTypes - // enterDelay?: number - // leaveDelay?: number offset?: Options['offset'] contentClassName?: string onVisibleChange?: TooltipOnVisibleChange @@ -50,14 +49,14 @@ export interface Props extends Omit, 'onMouseEnter' | export type TooltipProps = React.PropsWithChildren const Tooltip: React.FC = ({ + hoverable, + hoverableTimeout, children, defaultVisible, text, offset, placement, contentClassName, - // enterDelay, - // leaveDelay, trigger, color, className, @@ -70,7 +69,8 @@ const Tooltip: React.FC = ({ onClickAway, ...props }: TooltipProps & typeof defaultProps) => { - // const timer = useRef() + const hoverableTimer = useRef() + const contentHovering = useRef() const theme = useTheme() const parentRef = useRef(null) const contentRef = useRef(null) @@ -96,29 +96,27 @@ const Tooltip: React.FC = ({ const colors = useMemo(() => getColors(color, theme.palette), [color, theme.palette]) - const changeVisible = (visible: boolean) => { - if (typeof onVisibleChange === 'function') onVisibleChange(visible) - setVisible(visible) + const changeVisible = (newVisible: boolean) => { + if (typeof onVisibleChange === 'function') onVisibleChange(newVisible) + setVisible(newVisible) } - // const changeVisible = (nextState: boolean) => { - // const clear = () => { - // clearTimeout(timer.current) - // timer.current = undefined - // } - // const handler = (nextState: boolean) => { - // setVisible(nextState) - // onVisibleChange(nextState) - // clear() - // } - // clear() - // if (nextState) { - // timer.current = window.setTimeout(() => handler(true), enterDelay) - // return - // } - // timer.current = window.setTimeout(() => handler(false), leaveDelay) - // } - - const mouseEventHandler = (e: MouseEvent, next: boolean) => { + + const realHoverMouseLeaveHandler = useCallback((e: MouseEvent) => { + if (contentHovering?.current) return + customVisible === undefined && changeVisible(false) + onMouseLeave && onMouseLeave(e, changeVisible) + }, []) + + const mouseEventHandler = (e: MouseEvent, next: boolean, fromContent?: boolean) => { + if (hoverable && trigger === 'hover' && !next && !fromContent) { + if (hoverableTimer.current) clearTimeout(hoverableTimer.current) + hoverableTimer.current = window.setTimeout( + () => realHoverMouseLeaveHandler(e), + hoverableTimeout, + ) + return + } + if (customVisible === undefined) trigger === 'hover' && changeVisible(next) if (next) onMouseEnter && onMouseEnter(e, changeVisible) else onMouseLeave && onMouseLeave(e, changeVisible) @@ -153,6 +151,13 @@ const Tooltip: React.FC = ({ className={`tooltip-content ${contentClassName}`} ref={contentRef} style={styles.popper} + onMouseEnter={() => { + contentHovering.current = true + }} + onMouseLeave={e => { + contentHovering.current = false + mouseEventHandler(e, false, true) + }} {...attributes.popper}> {/* @ts-ignore*/} {!hideArrow &&
} diff --git a/pages/en-us/components/tooltip.mdx b/pages/en-us/components/tooltip.mdx index 0ab6b53ef..d11fe8224 100644 --- a/pages/en-us/components/tooltip.mdx +++ b/pages/en-us/components/tooltip.mdx @@ -31,6 +31,14 @@ Displays additional information on hover. `} /> +Tooltip +`} +/> + Tooltip.Props -| Attribute | Description | Type | Accepted values | Default | -| -------------------- | ---------------------------------------------- | ---------------------------------------------------------------- | -------------------------------- | --------- | -| **text** | text of pop-up | `string` `React.ReactNode` | - | - | -| **visible** | visible or not | `boolean` | - | `false` | -| **defaultVisible** | visible on initial | `boolean` | - | `false` | -| **hideArrow** | hide arrow icon | `boolean` | - | `false` | -| **color** | preset style color | [TooltipColors](#tooltipcolors) | - | `default` | -| **placement** | position of the tooltip relative to the target | [Placement](#placement) | - | `top` | -| **trigger** | tooltip trigger mode | `'click' / 'hover'` | - | `hover` | -| **enterDelay**(ms) | delay before tooltip is shown | `number` | - | `100` | -| **leaveDelay**(ms) | delay before tooltip is hidden | `number` | - | `0` | -| **offset**(px) | skidding and distance, | check the doc at https://popper.js.org/docs/v2/modifiers/offset/ | - | `[0, 10]` | -| **contentClassName** | className of pop-up box | `string` | - | - | -| **onVisibleChange** | call when visibility of the tooltip is changed | `(visible: boolean) => void` | - | - | -| ... | native props | `HTMLAttributes` | `'id', 'name', 'className', ...` | - | +| Attribute | Description | Type | Accepted values | Default | +| ------------------------ | ----------------------------------------------------------------------- | ---------------------------------------------------------------- | -------------------------------- | --------- | +| **text** | text of pop-up | `string` `React.ReactNode` | - | - | +| **visible** | visible or not | `boolean` | - | `false` | +| **defaultVisible** | visible on initial | `boolean` | - | `false` | +| **hideArrow** | hide arrow icon | `boolean` | - | `false` | +| **color** | preset style color | [TooltipColors](#tooltipcolors) | - | `default` | +| **placement** | position of the tooltip relative to the target | [Placement](#placement) | - | `top` | +| **trigger** | tooltip trigger mode | `'click' / 'hover'` | - | `hover` | +| **hoverable** | make the tooltip content hoverable, only works when `trigger==='hover'` | `boolean` | - | `false` | +| **hoverableTimeout**(ms) | timeout after which tooltip content get removed | `number` | - | `200` | +| **offset**(px) | skidding and distance, | check the doc at https://popper.js.org/docs/v2/modifiers/offset/ | - | `[0, 10]` | +| **contentClassName** | className of pop-up box | `string` | - | - | +| **onVisibleChange** | call when visibility of the tooltip is changed | `(visible: boolean) => void` | - | - | +| ... | native props | `HTMLAttributes` | `'id', 'name', 'className', ...` | - | TooltipColors diff --git a/pages/zh-cn/components/tooltip.mdx b/pages/zh-cn/components/tooltip.mdx index 0a45303ce..2bef4f30b 100644 --- a/pages/zh-cn/components/tooltip.mdx +++ b/pages/zh-cn/components/tooltip.mdx @@ -31,6 +31,14 @@ export const meta = { `} /> +文字提示 +`} +/> + Tooltip.Props -| 属性 | 描述 | 类型 | 推荐值 | 默认 | -| -------------------- | -------------------------- | ---------------------------------------------------- | -------------------------------- | --------- | -| **text** | 弹出框文字 | `string` `React.ReactNode` | - | - | -| **visible** | 手动控制提示框的显示与隐藏 | `boolean` | - | `false` | -| **defaultVisible** | 初始是否可见 | `boolean` | - | `false` | -| **hideArrow** | 隐藏箭头 | `boolean` | - | `false` | -| **color** | 不同的文字提示颜色 | [TooltipColors](#tooltipcolors) | - | `default` | -| **placement** | 提示框与目标的对齐方式 | [Placement](#placement) | - | `top` | -| **trigger** | 触发提示框的方式 | `'click' / 'hover'` | - | `hover` | -| **enterDelay**(ms) | 在提示显示前的延迟 | `number` | - | `100` | -| **leaveDelay**(ms) | 关闭提示前的延迟 | `number` | - | `0` | -| **offset**(px) | Skidding 和 Distance | 参考 https://popper.js.org/docs/v2/modifiers/offset/ | - | `[0,10]` | -| **contentClassName** | 弹出框的类名 | `string` | - | - | -| **onVisibleChange** | 当提示框状态改变时触发 | `(visible: boolean) => void` | - | - | -| ... | 原生属性 | `HTMLAttributes` | `'id', 'name', 'className', ...` | - | +| 属性 | 描述 | 类型 | 推荐值 | 默认 | +| ------------------------ | -------------------------------------------------------------- | ---------------------------------------------------- | -------------------------------- | --------- | +| **text** | 弹出框文字 | `string` `React.ReactNode` | - | - | +| **visible** | 手动控制提示框的显示与隐藏 | `boolean` | - | `false` | +| **defaultVisible** | 初始是否可见 | `boolean` | - | `false` | +| **hideArrow** | 隐藏箭头 | `boolean` | - | `false` | +| **color** | 不同的文字提示颜色 | [TooltipColors](#tooltipcolors) | - | `default` | +| **placement** | 提示框与目标的对齐方式 | [Placement](#placement) | - | `top` | +| **trigger** | 触发提示框的方式 | `'click' / 'hover'` | - | `hover` | +| **hoverable** | 使 tooltip 的内容可以 hover, 只在 `trigger==='hover'` 有效 | `boolean` | - | `false` | +| **hoverableTimeout**(ms) | 当 `hoverable` 为 `true` 时, 会在该时间后再删除 tooltip 的内容 | `number` | - | `200` | +| **offset**(px) | Skidding 和 Distance | 参考 https://popper.js.org/docs/v2/modifiers/offset/ | - | `[0,10]` | +| **contentClassName** | 弹出框的类名 | `string` | - | - | +| **onVisibleChange** | 当提示框状态改变时触发 | `(visible: boolean) => void` | - | - | +| ... | 原生属性 | `HTMLAttributes` | `'id', 'name', 'className', ...` | - | TooltipColors From 2346fc97f9f50ba562f23d3a4dda60d394a54857 Mon Sep 17 00:00:00 2001 From: yqrashawn Date: Thu, 15 Oct 2020 11:23:18 +0800 Subject: [PATCH 2/3] feat(tooltip): default tooltip wrapper display to inline-block --- components/tooltip/tooltip.tsx | 9 ++++++++- pages/en-us/components/tooltip.mdx | 1 + pages/zh-cn/components/tooltip.mdx | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/components/tooltip/tooltip.tsx b/components/tooltip/tooltip.tsx index 2ffdcc1b8..1f78cb96a 100644 --- a/components/tooltip/tooltip.tsx +++ b/components/tooltip/tooltip.tsx @@ -25,9 +25,11 @@ export const defaultProps = { contentClassName: '', hoverable: false, hoverableTimeout: 200, + displayBlock: false, } export interface Props extends Omit, 'onMouseEnter' | 'onMouseLeave'> { + displayBlock?: boolean hoverable?: boolean hoverableTimeout?: number text: string | React.ReactNode @@ -49,6 +51,7 @@ export interface Props extends Omit, 'onMouseEnter' | export type TooltipProps = React.PropsWithChildren const Tooltip: React.FC = ({ + displayBlock, hoverable, hoverableTimeout, children, @@ -136,7 +139,7 @@ const Tooltip: React.FC = ({ }, [customVisible]) return ( -
+
= ({
)}
SuMoTuWeThFrSa
26
27
28
29
30
31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
1
2
3
4
5
SuMoTuWeThFrSa
26
27
28
29
30
31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
1
2
3
4
5
SuMoTuWeThFrSa
27
28
29
30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
1
2
3
4
5
6
7
 
  • 00
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 00
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 00
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
SuMoTuWeThFrSa
28
29
30
31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
1
2
3
4
5
6
7
 
  • 00
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 00
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 00
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
SuMoTuWeThFrSa
27
28
29
30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
1
2
3
4
5
6
7
 
  • 00
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 00
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 00
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
SuMoTuWeThFrSa
28
29
30
31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
1
2
3
4
5
6
7
 
  • 00
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 00
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 00
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
SuMoTuWeThFrSa
28
29
30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
1
2
3
4
5
6
7
8
SuMoTuWeThFrSa
26
27
28
29
30
31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
1
2
3
4
5
SuMoTuWeThFrSa
28
29
30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
1
2
3
4
5
6
7
8
SuMoTuWeThFrSa
26
27
28
29
30
31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
1
2
3
4
5
SuMoTuWeThFrSa
27
28
29
30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
1
2
3
4
5
6
7
 
  • 12
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
  • 11
  • 00
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 00
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • AM
  • PM
SuMoTuWeThFrSa
28
29
30
31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
1
2
3
4
5
6
7
 
  • 12
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
  • 11
  • 00
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 00
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • AM
  • PM