zaman-backoffice 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.cjs +26 -0
- package/.husky/commit-msg +4 -0
- package/.husky/pre-commit +4 -0
- package/.idea/git_toolbox_blame.xml +6 -0
- package/.idea/git_toolbox_prj.xml +15 -0
- package/.idea/inspectionProfiles/Project_Default.xml +6 -0
- package/.idea/material_theme_project_new.xml +12 -0
- package/.idea/modules.xml +8 -0
- package/.idea/vcs.xml +6 -0
- package/.idea/zaman-backoffice2.iml +12 -0
- package/.prettierrc +9 -0
- package/.travis.yml +10 -0
- package/client/index.html +12 -0
- package/client/main.tsx +84 -0
- package/client/style.css +62 -0
- package/cypress/fixtures/example.json +5 -0
- package/cypress/support/commands.ts +38 -0
- package/cypress/support/component-index.html +12 -0
- package/cypress/support/component.ts +39 -0
- package/cypress/videos/Calendar/CalendarComponent.cy.tsx.mp4 +0 -0
- package/cypress/videos/DatePicker/DatePicker.cy.tsx.mp4 +0 -0
- package/cypress.config.ts +10 -0
- package/help/banner.png +0 -0
- package/jest.config.ts +47 -0
- package/package.json +1 -1
- package/src/components/CalendarItem/CalendarItem.styled.tsx +96 -0
- package/src/components/CalendarItem/CalendarItem.types.ts +7 -0
- package/src/components/CalendarItem/index.ts +1 -0
- package/src/components/CalendarWrapper/CalendarWrapper.styled.tsx +19 -0
- package/src/components/CalendarWrapper/index.ts +1 -0
- package/src/components/FloatingElement/FloatingElement.styled.tsx +8 -0
- package/src/components/FloatingElement/FloatingElement.tsx +83 -0
- package/src/components/FloatingElement/FloatingElement.types.ts +8 -0
- package/src/components/FloatingElement/index.tsx +1 -0
- package/src/components/Header/Header.styled.tsx +40 -0
- package/src/components/Header/Header.tsx +46 -0
- package/src/components/Header/Header.types.ts +6 -0
- package/src/components/Header/index.ts +1 -0
- package/src/components/IconButton/IconButton.styled.tsx +22 -0
- package/src/components/IconButton/IconButton.tsx +3 -0
- package/src/components/IconButton/index.tsx +1 -0
- package/src/components/Icons/ChevronLeft/index.tsx +22 -0
- package/src/components/Icons/ChevronRight/index.tsx +22 -0
- package/src/components/Modal/Modal.styled.tsx +23 -0
- package/src/components/Modal/Modal.tsx +29 -0
- package/src/components/Modal/index.tsx +1 -0
- package/src/components/Modal/types.ts +7 -0
- package/src/components/MonthPicker/Month.styled.tsx +11 -0
- package/src/components/MonthPicker/MonthPicker.tsx +35 -0
- package/src/components/MonthPicker/MonthPicker.types.ts +4 -0
- package/src/components/MonthPicker/index.ts +1 -0
- package/src/components/RenderCalendar/RenderCalendar.tsx +31 -0
- package/src/components/RenderCalendar/RenderCalendar.types.ts +10 -0
- package/src/components/RenderCalendar/index.ts +1 -0
- package/src/components/YearPicker/YearPicker.styled.tsx +14 -0
- package/src/components/YearPicker/YearPicker.tsx +49 -0
- package/src/components/YearPicker/YearPicker.types.ts +4 -0
- package/src/components/YearPicker/index.ts +1 -0
- package/src/constants.ts +6 -0
- package/src/hooks/__tests__/useClickOutside.test.tsx +32 -0
- package/src/hooks/useCalendarHandlers.tsx +113 -0
- package/src/hooks/useClickOutside.tsx +23 -0
- package/src/hooks/useSlideCalendar.tsx +95 -0
- package/src/hooks/useTimePicker.tsx +95 -0
- package/src/index.tsx +4 -0
- package/src/packages/Calendar/Calendar.styled.tsx +42 -0
- package/src/packages/Calendar/Calendar.tsx +159 -0
- package/src/packages/Calendar/Calendar.types.ts +31 -0
- package/src/packages/Calendar/CalendarComponent.cy.tsx +69 -0
- package/src/packages/Calendar/index.ts +2 -0
- package/src/packages/CalendarProvider/CalendarProvider.tsx +30 -0
- package/src/packages/CalendarProvider/CalendarProvider.types.ts +6 -0
- package/src/packages/CalendarProvider/index.ts +2 -0
- package/src/packages/DatePicker/DatePicker.cy.tsx +54 -0
- package/src/packages/DatePicker/DatePicker.tsx +127 -0
- package/src/packages/DatePicker/DatePicker.types.ts +26 -0
- package/src/packages/DatePicker/index.ts +2 -0
- package/src/packages/TimePicker/TimePicker.styled.tsx +77 -0
- package/src/packages/TimePicker/TimePicker.tsx +121 -0
- package/src/packages/TimePicker/TimePicker.types.ts +16 -0
- package/src/packages/TimePicker/components/Numbers/Numbers.styled.tsx +36 -0
- package/src/packages/TimePicker/components/Numbers/Numbers.tsx +58 -0
- package/src/packages/TimePicker/components/Numbers/Numbers.types.ts +14 -0
- package/src/packages/TimePicker/components/Numbers/index.ts +1 -0
- package/src/packages/TimePicker/index.ts +2 -0
- package/src/style/animation.ts +23 -0
- package/src/style/classNames.ts +8 -0
- package/src/style/colorPallete.ts +16 -0
- package/src/style/colors.ts +15 -0
- package/src/style/hexToHSL.ts +52 -0
- package/src/style/radius.ts +28 -0
- package/src/types.ts +75 -0
- package/src/utils/dateHelper/dateHelper.ts +67 -0
- package/src/utils/dateHelper/index.ts +1 -0
- package/src/utils/dateTimeFormat/dateTimeFormat.ts +43 -0
- package/src/utils/dateTimeFormat/index.ts +1 -0
- package/src/utils/format/format.test.ts +37 -0
- package/src/utils/format/format.ts +56 -0
- package/src/utils/format/format.types.ts +11 -0
- package/src/utils/format/index.ts +1 -0
- package/src/utils/index.ts +21 -0
- package/src/utils/locale.ts +13 -0
- package/src/utils/locales/en.ts +89 -0
- package/src/utils/locales/fa.ts +89 -0
- package/src/utils/locales/index.ts +10 -0
- package/src/utils/locales/locales.types.ts +11 -0
- package/src/utils/month/index.ts +1 -0
- package/src/utils/month/month.ts +54 -0
- package/src/utils/month/month.types.ts +11 -0
- package/src/utils/timePicker.ts +107 -0
- package/src/utils/type.ts +0 -0
- package/tsconfig.json +22 -0
- package/tsconfig.test.json +7 -0
- package/vite.config.ts +7 -0
@@ -0,0 +1,83 @@
|
|
1
|
+
import React, { useEffect, useRef } from 'react'
|
2
|
+
import { createPortal } from 'react-dom'
|
3
|
+
import debounce from 'lodash.debounce'
|
4
|
+
import type { FloatingElementProps } from './FloatingElement.types'
|
5
|
+
import type { Positions } from '../../types'
|
6
|
+
import { Wrapper } from './FloatingElement.styled'
|
7
|
+
|
8
|
+
const FloatingElement: React.FC<FloatingElementProps> = (props) => {
|
9
|
+
const { children, destinationRef, position } = props
|
10
|
+
const floatWrapperRef = useRef<HTMLDivElement>(null)
|
11
|
+
|
12
|
+
const calcPlacement = () => {
|
13
|
+
const gap = 4
|
14
|
+
let top = 0
|
15
|
+
|
16
|
+
if (destinationRef != null && floatWrapperRef !== null) {
|
17
|
+
const floatWrapper = floatWrapperRef.current
|
18
|
+
if (floatWrapper === null) {
|
19
|
+
return
|
20
|
+
}
|
21
|
+
const {
|
22
|
+
top: destTop,
|
23
|
+
bottom: destBottom,
|
24
|
+
right: destRight,
|
25
|
+
left: destLeft,
|
26
|
+
width: destWidth
|
27
|
+
} = destinationRef?.current?.getBoundingClientRect() as DOMRect
|
28
|
+
const { height: itemsHeight, width: floatWidth } =
|
29
|
+
floatWrapper?.getBoundingClientRect()
|
30
|
+
const isThereSpaceBelowDest =
|
31
|
+
window.innerHeight - destBottom > itemsHeight
|
32
|
+
|
33
|
+
if (isThereSpaceBelowDest) {
|
34
|
+
top = destBottom + window.scrollY
|
35
|
+
} else {
|
36
|
+
top = destTop + window.scrollY - itemsHeight - gap * 2
|
37
|
+
}
|
38
|
+
floatWrapper.style.top = `${top + gap}px`
|
39
|
+
|
40
|
+
const positionsCalc: Record<Positions, () => void> = {
|
41
|
+
right: () => {
|
42
|
+
floatWrapper.style.right = `${
|
43
|
+
Math.abs(document.body.clientWidth - destRight) + 16
|
44
|
+
}px`
|
45
|
+
},
|
46
|
+
left: () => {
|
47
|
+
floatWrapper.style.left = `${destLeft}px`
|
48
|
+
},
|
49
|
+
center: () => {
|
50
|
+
const isDestBiggerThanFloatElement = floatWidth > destWidth
|
51
|
+
const remainWidth = isDestBiggerThanFloatElement
|
52
|
+
? 0
|
53
|
+
: Math.abs(floatWidth - destWidth) / 2 + 16
|
54
|
+
floatWrapper.style.right = `${
|
55
|
+
Math.abs(document.body.clientWidth - destRight) + remainWidth
|
56
|
+
}px`
|
57
|
+
}
|
58
|
+
}
|
59
|
+
|
60
|
+
positionsCalc[position]()
|
61
|
+
}
|
62
|
+
}
|
63
|
+
|
64
|
+
useEffect(() => {
|
65
|
+
calcPlacement()
|
66
|
+
window.addEventListener('resize', debounce(calcPlacement, 500))
|
67
|
+
|
68
|
+
return () => {
|
69
|
+
window.removeEventListener('resize', calcPlacement)
|
70
|
+
}
|
71
|
+
}, [destinationRef])
|
72
|
+
|
73
|
+
return (
|
74
|
+
<>
|
75
|
+
{createPortal(
|
76
|
+
<Wrapper ref={floatWrapperRef}>{children}</Wrapper>,
|
77
|
+
document.body
|
78
|
+
)}
|
79
|
+
</>
|
80
|
+
)
|
81
|
+
}
|
82
|
+
|
83
|
+
export default FloatingElement
|
@@ -0,0 +1 @@
|
|
1
|
+
export { default } from './FloatingElement'
|
@@ -0,0 +1,40 @@
|
|
1
|
+
import styled from '@emotion/styled'
|
2
|
+
import { radius } from '../../style/radius'
|
3
|
+
|
4
|
+
export const Wrapper = styled.div`
|
5
|
+
display: flex;
|
6
|
+
justify-content: space-between;
|
7
|
+
align-items: center;
|
8
|
+
height: 56px;
|
9
|
+
padding-right: 8px;
|
10
|
+
padding-left: 8px;
|
11
|
+
background-color: ${(props) => props.theme.colors.primary[95]};
|
12
|
+
border-bottom: 2px solid ${(props) => props.theme.colors.primary[85]};
|
13
|
+
`
|
14
|
+
export const HeaderTitle = styled.button`
|
15
|
+
will-change: auto;
|
16
|
+
min-width: 100px;
|
17
|
+
outline: none;
|
18
|
+
border: 0;
|
19
|
+
font-family: inherit;
|
20
|
+
background-color: transparent;
|
21
|
+
cursor: pointer;
|
22
|
+
color: ${(props) => props.theme.colors.primary[50]};
|
23
|
+
padding: 8px 16px;
|
24
|
+
font-weight: 500;
|
25
|
+
transition: background-color 0.2s ease-in;
|
26
|
+
border-radius: ${(props) => radius[props.theme.round].calendarItem}px;
|
27
|
+
|
28
|
+
&:hover,
|
29
|
+
&:focus {
|
30
|
+
background-color: ${(props) => props.theme.colors.primary[90]};
|
31
|
+
}
|
32
|
+
`
|
33
|
+
export const DayName = styled.div`
|
34
|
+
display: flex;
|
35
|
+
justify-content: center;
|
36
|
+
align-items: center;
|
37
|
+
font-size: 14px;
|
38
|
+
width: 40px;
|
39
|
+
color: #8c8c8c;
|
40
|
+
`
|
@@ -0,0 +1,46 @@
|
|
1
|
+
import React from 'react'
|
2
|
+
import IconButton from '../IconButton'
|
3
|
+
import ChevronRight from '../Icons/ChevronRight'
|
4
|
+
import ChevronLeft from '../Icons/ChevronLeft'
|
5
|
+
import { Wrapper, HeaderTitle } from './Header.styled'
|
6
|
+
import type { HeaderProps } from './Header.types'
|
7
|
+
import {
|
8
|
+
HeaderClass,
|
9
|
+
IconNextButton,
|
10
|
+
IconPrevButton,
|
11
|
+
MonthYearButton
|
12
|
+
} from '../../style/classNames'
|
13
|
+
|
14
|
+
export const Header = (props: HeaderProps) => {
|
15
|
+
return (
|
16
|
+
<Wrapper className={HeaderClass}>
|
17
|
+
<IconButton
|
18
|
+
aria-label="Previous month"
|
19
|
+
onClick={props.onPrevClick}
|
20
|
+
className={IconPrevButton}
|
21
|
+
tabIndex={0}
|
22
|
+
>
|
23
|
+
<ChevronRight />
|
24
|
+
</IconButton>
|
25
|
+
<HeaderTitle
|
26
|
+
className={MonthYearButton}
|
27
|
+
role="presentation"
|
28
|
+
onClick={props.onClickOnTitle}
|
29
|
+
aria-label="calendar view is open, switch to year and month view"
|
30
|
+
tabIndex={0}
|
31
|
+
>
|
32
|
+
{props.monthName}
|
33
|
+
</HeaderTitle>
|
34
|
+
<IconButton
|
35
|
+
aria-label="Next month"
|
36
|
+
onClick={props.onNextClick}
|
37
|
+
className={IconNextButton}
|
38
|
+
tabIndex={0}
|
39
|
+
>
|
40
|
+
<ChevronLeft />
|
41
|
+
</IconButton>
|
42
|
+
</Wrapper>
|
43
|
+
)
|
44
|
+
}
|
45
|
+
|
46
|
+
export default Header
|
@@ -0,0 +1 @@
|
|
1
|
+
export { default } from './Header'
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import styled from '@emotion/styled'
|
2
|
+
import { radius } from '../../style/radius'
|
3
|
+
|
4
|
+
export const IconButton = styled.button`
|
5
|
+
cursor: pointer;
|
6
|
+
outline: none;
|
7
|
+
border: none;
|
8
|
+
display: flex;
|
9
|
+
justify-content: center;
|
10
|
+
align-items: center;
|
11
|
+
width: 40px;
|
12
|
+
height: 40px;
|
13
|
+
transition: background-color 0.2s ease-in;
|
14
|
+
color: ${(props) => props.theme.colors.primary[50]};
|
15
|
+
background-color: transparent;
|
16
|
+
border-radius: ${(props) => radius[props.theme.round].calendarItem}px;
|
17
|
+
|
18
|
+
&:hover,
|
19
|
+
&:focus {
|
20
|
+
background-color: ${(props) => props.theme.colors.primary[90]};
|
21
|
+
}
|
22
|
+
`
|
@@ -0,0 +1 @@
|
|
1
|
+
export { default } from './IconButton'
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import React from 'react'
|
2
|
+
import { isRtl } from '../../../utils'
|
3
|
+
|
4
|
+
export const ChevronLeft = () => (
|
5
|
+
<svg
|
6
|
+
xmlns="http://www.w3.org/2000/svg"
|
7
|
+
width="20"
|
8
|
+
height="20"
|
9
|
+
viewBox="0 0 24 24"
|
10
|
+
fill="none"
|
11
|
+
stroke="currentColor"
|
12
|
+
strokeWidth="2"
|
13
|
+
strokeLinecap="round"
|
14
|
+
strokeLinejoin="round"
|
15
|
+
className="feather feather-chevron-left"
|
16
|
+
style={{ transform: isRtl() ? 'unset' : 'rotate(180deg)' }}
|
17
|
+
>
|
18
|
+
<polyline points="15 18 9 12 15 6"></polyline>
|
19
|
+
</svg>
|
20
|
+
)
|
21
|
+
|
22
|
+
export default ChevronLeft
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import React from 'react'
|
2
|
+
import { isRtl } from '../../../utils'
|
3
|
+
|
4
|
+
export const ChevronRight = () => (
|
5
|
+
<svg
|
6
|
+
xmlns="http://www.w3.org/2000/svg"
|
7
|
+
width="20"
|
8
|
+
height="20"
|
9
|
+
viewBox="0 0 24 24"
|
10
|
+
fill="none"
|
11
|
+
stroke="currentColor"
|
12
|
+
strokeWidth="2"
|
13
|
+
strokeLinecap="round"
|
14
|
+
strokeLinejoin="round"
|
15
|
+
className="feather feather-chevron-right"
|
16
|
+
style={{ transform: isRtl() ? 'unset' : 'rotate(180deg)' }}
|
17
|
+
>
|
18
|
+
<polyline points="9 18 15 12 9 6"></polyline>
|
19
|
+
</svg>
|
20
|
+
)
|
21
|
+
|
22
|
+
export default ChevronRight
|
@@ -0,0 +1,23 @@
|
|
1
|
+
import styled from '@emotion/styled'
|
2
|
+
|
3
|
+
export const ModalDiv = styled.div`
|
4
|
+
position: fixed;
|
5
|
+
display: flex;
|
6
|
+
justify-content: center;
|
7
|
+
align-items: center;
|
8
|
+
top: 0;
|
9
|
+
right: 0;
|
10
|
+
width: 100%;
|
11
|
+
height: 100%;
|
12
|
+
z-index: 10;
|
13
|
+
|
14
|
+
.rdp__overlay {
|
15
|
+
position: absolute;
|
16
|
+
top: 0;
|
17
|
+
right: 0;
|
18
|
+
width: 100%;
|
19
|
+
height: 100%;
|
20
|
+
z-index: -10;
|
21
|
+
background-color: rgba(86, 86, 86, 0.4);
|
22
|
+
}
|
23
|
+
`
|
@@ -0,0 +1,29 @@
|
|
1
|
+
import React from 'react'
|
2
|
+
import type { IModalProps } from './types'
|
3
|
+
import { ModalDiv } from './Modal.styled'
|
4
|
+
import { createPortal } from 'react-dom'
|
5
|
+
|
6
|
+
export const Modal = (props: IModalProps) => {
|
7
|
+
const { open, toggleOpen, children } = props
|
8
|
+
|
9
|
+
if (open === false) {
|
10
|
+
return null
|
11
|
+
}
|
12
|
+
return (
|
13
|
+
<>
|
14
|
+
{createPortal(
|
15
|
+
<ModalDiv className="rdp__modal">
|
16
|
+
{children}
|
17
|
+
<div
|
18
|
+
data-testid="overlay"
|
19
|
+
className="rdp__overlay"
|
20
|
+
onClick={toggleOpen}
|
21
|
+
/>
|
22
|
+
</ModalDiv>,
|
23
|
+
document.body
|
24
|
+
)}
|
25
|
+
</>
|
26
|
+
)
|
27
|
+
}
|
28
|
+
|
29
|
+
export default Modal
|
@@ -0,0 +1 @@
|
|
1
|
+
export { default } from './Modal'
|
@@ -0,0 +1,11 @@
|
|
1
|
+
import styled from '@emotion/styled'
|
2
|
+
import { CALENDAR_WIDTH } from '../../constants'
|
3
|
+
|
4
|
+
export const Wrapper = styled.div`
|
5
|
+
display: flex;
|
6
|
+
justify-content: center;
|
7
|
+
align-items: center;
|
8
|
+
flex-wrap: wrap;
|
9
|
+
gap: 4px;
|
10
|
+
height: ${CALENDAR_WIDTH}px;
|
11
|
+
`
|
@@ -0,0 +1,35 @@
|
|
1
|
+
import React from 'react'
|
2
|
+
import localeCache from '../../utils/locale'
|
3
|
+
import locales from '../../utils/locales'
|
4
|
+
import CalendarItem from '../CalendarItem'
|
5
|
+
import { Wrapper } from './Month.styled'
|
6
|
+
import formatDate from '../../utils/format'
|
7
|
+
import type { MonthPickerProps } from './MonthPicker.types'
|
8
|
+
import { MonthPickerButton } from '../../style/classNames'
|
9
|
+
|
10
|
+
export const MonthPicker = (props: MonthPickerProps) => {
|
11
|
+
const { locale } = localeCache
|
12
|
+
const currentMonth = formatDate(props.value, 'MM', 'latn')
|
13
|
+
|
14
|
+
return (
|
15
|
+
<Wrapper>
|
16
|
+
{locales[locale].months.map((month) => (
|
17
|
+
<CalendarItem
|
18
|
+
key={month.key}
|
19
|
+
className={MonthPickerButton}
|
20
|
+
width={90}
|
21
|
+
height={48}
|
22
|
+
data-selected={month.key === parseInt(currentMonth, 10)}
|
23
|
+
onClick={() => props.onMonthSelect(month.key)}
|
24
|
+
aria-current="date"
|
25
|
+
type="button"
|
26
|
+
tabIndex={0}
|
27
|
+
>
|
28
|
+
{month.name}
|
29
|
+
</CalendarItem>
|
30
|
+
))}
|
31
|
+
</Wrapper>
|
32
|
+
)
|
33
|
+
}
|
34
|
+
|
35
|
+
export default MonthPicker
|
@@ -0,0 +1 @@
|
|
1
|
+
export { default } from './MonthPicker'
|
@@ -0,0 +1,31 @@
|
|
1
|
+
import React from 'react'
|
2
|
+
import { type RenderCalendarProps } from './RenderCalendar.types'
|
3
|
+
import FloatingElement from '../FloatingElement'
|
4
|
+
import Modal from '../Modal'
|
5
|
+
|
6
|
+
export const RenderCalendar = (props: RenderCalendarProps) => {
|
7
|
+
const { position = 'right' } = props
|
8
|
+
if (!props.showCalendar) {
|
9
|
+
return null
|
10
|
+
}
|
11
|
+
const { matches: isDesktop } = window.matchMedia('(min-width: 640px)')
|
12
|
+
|
13
|
+
if (isDesktop) {
|
14
|
+
return (
|
15
|
+
<FloatingElement
|
16
|
+
destinationRef={props.destinationRef}
|
17
|
+
position={position}
|
18
|
+
>
|
19
|
+
{props.children}
|
20
|
+
</FloatingElement>
|
21
|
+
)
|
22
|
+
}
|
23
|
+
|
24
|
+
return (
|
25
|
+
<Modal toggleOpen={props.toggleOpen} open={props.showCalendar}>
|
26
|
+
{props.children}
|
27
|
+
</Modal>
|
28
|
+
)
|
29
|
+
}
|
30
|
+
|
31
|
+
export default RenderCalendar
|
@@ -0,0 +1,10 @@
|
|
1
|
+
import type React from 'react'
|
2
|
+
import type { Positions } from '../../types'
|
3
|
+
|
4
|
+
export interface RenderCalendarProps {
|
5
|
+
showCalendar: boolean
|
6
|
+
toggleOpen: () => void
|
7
|
+
children: React.ReactNode
|
8
|
+
destinationRef: React.RefObject<HTMLInputElement>
|
9
|
+
position?: Positions
|
10
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
export { default } from './RenderCalendar'
|
@@ -0,0 +1,14 @@
|
|
1
|
+
import styled from '@emotion/styled'
|
2
|
+
import { CALENDAR_WIDTH } from '../../constants'
|
3
|
+
|
4
|
+
export const Wrapper = styled.div`
|
5
|
+
display: grid;
|
6
|
+
grid-template-columns: repeat(3, 92px);
|
7
|
+
grid-template-rows: auto;
|
8
|
+
justify-content: center;
|
9
|
+
gap: 4px;
|
10
|
+
max-height: ${CALENDAR_WIDTH}px;
|
11
|
+
overflow: auto;
|
12
|
+
padding-top: 8px;
|
13
|
+
padding-bottom: 8px;
|
14
|
+
`
|
@@ -0,0 +1,49 @@
|
|
1
|
+
import React, { useMemo } from 'react'
|
2
|
+
import CalendarItem from '../CalendarItem'
|
3
|
+
import { Wrapper } from './YearPicker.styled'
|
4
|
+
import { getYears } from '../../utils/dateHelper/dateHelper'
|
5
|
+
import formatDate from '../../utils/format'
|
6
|
+
import type { YearPickerProps } from './YearPicker.types'
|
7
|
+
import { localizeNumber } from '../../utils'
|
8
|
+
import { YearPickerButton } from '../../style/classNames'
|
9
|
+
|
10
|
+
export const YearPicker = (props: YearPickerProps) => {
|
11
|
+
const currentYear = parseInt(formatDate(props.value, 'YYYY', 'latn'), 10)
|
12
|
+
const years: number[] = useMemo(() => getYears(props.value), [])
|
13
|
+
|
14
|
+
const wrapperRef = React.useCallback((wrapper: HTMLDivElement) => {
|
15
|
+
if (wrapper === null) {
|
16
|
+
return
|
17
|
+
}
|
18
|
+
const qu = wrapper.querySelector('button[data-selected=true]')
|
19
|
+
if (qu != null) {
|
20
|
+
const { height: wrapperHeight, top: wrapperTop } =
|
21
|
+
wrapper.getBoundingClientRect()
|
22
|
+
const { top } = qu.getBoundingClientRect()
|
23
|
+
wrapper.scrollTop = Math.abs(wrapperTop - top) - wrapperHeight / 2
|
24
|
+
}
|
25
|
+
}, [])
|
26
|
+
|
27
|
+
return (
|
28
|
+
<Wrapper ref={wrapperRef}>
|
29
|
+
{years.map((year) => (
|
30
|
+
<CalendarItem
|
31
|
+
className={YearPickerButton}
|
32
|
+
key={year}
|
33
|
+
width={90}
|
34
|
+
height={48}
|
35
|
+
data-selected={currentYear === year}
|
36
|
+
aria-selected={currentYear === year}
|
37
|
+
aria-current="date"
|
38
|
+
type="button"
|
39
|
+
tabIndex={0}
|
40
|
+
onClick={() => props.onYearSelect(year)}
|
41
|
+
>
|
42
|
+
{localizeNumber(year)}
|
43
|
+
</CalendarItem>
|
44
|
+
))}
|
45
|
+
</Wrapper>
|
46
|
+
)
|
47
|
+
}
|
48
|
+
|
49
|
+
export default YearPicker
|
@@ -0,0 +1 @@
|
|
1
|
+
export { default } from './YearPicker'
|
package/src/constants.ts
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
import React, { useRef } from 'react'
|
2
|
+
import useClickOutside from '../useClickOutside'
|
3
|
+
import { fireEvent, render, screen } from '@testing-library/react'
|
4
|
+
|
5
|
+
interface MockComponentProps {
|
6
|
+
handler: () => void
|
7
|
+
}
|
8
|
+
const MockComponent = (props: MockComponentProps) => {
|
9
|
+
const insideRef = useRef(null)
|
10
|
+
useClickOutside(insideRef, props.handler)
|
11
|
+
return (
|
12
|
+
<div>
|
13
|
+
<div>click outside</div>
|
14
|
+
<div ref={insideRef}>Click inside</div>
|
15
|
+
</div>
|
16
|
+
)
|
17
|
+
}
|
18
|
+
test('test click on inside', () => {
|
19
|
+
const mockCallback = jest.fn(() => {})
|
20
|
+
render(<MockComponent handler={mockCallback} />)
|
21
|
+
fireEvent.mouseDown(screen.getByText(/click inside/i))
|
22
|
+
|
23
|
+
expect(mockCallback.mock.calls.length).toBe(0)
|
24
|
+
})
|
25
|
+
|
26
|
+
test('test click on outside', () => {
|
27
|
+
const mockCallback = jest.fn(() => {})
|
28
|
+
render(<MockComponent handler={mockCallback} />)
|
29
|
+
fireEvent.mouseDown(screen.getByText(/click outside/i))
|
30
|
+
|
31
|
+
expect(mockCallback.mock.calls.length).toBe(1)
|
32
|
+
})
|
@@ -0,0 +1,113 @@
|
|
1
|
+
import { type SyntheticEvent, useState } from 'react'
|
2
|
+
import dayjs from 'dayjs'
|
3
|
+
import type { DatePickerValue } from '../types'
|
4
|
+
import type {
|
5
|
+
CalendarDefaultProps,
|
6
|
+
CalendarRangeProps
|
7
|
+
} from 'src/packages/Calendar/Calendar.types'
|
8
|
+
|
9
|
+
type Event = SyntheticEvent<HTMLButtonElement>
|
10
|
+
|
11
|
+
interface BaseUseCalendarHandlersType {
|
12
|
+
from?: DatePickerValue
|
13
|
+
to?: DatePickerValue
|
14
|
+
}
|
15
|
+
|
16
|
+
type useCalendarHandlersType = BaseUseCalendarHandlersType &
|
17
|
+
(CalendarRangeProps | CalendarDefaultProps)
|
18
|
+
|
19
|
+
export const guardRange = (
|
20
|
+
value: useCalendarHandlersType
|
21
|
+
): value is CalendarRangeProps => {
|
22
|
+
return value.range === true
|
23
|
+
}
|
24
|
+
|
25
|
+
export const useCalendarHandlers = (props: useCalendarHandlersType) => {
|
26
|
+
const [selectingRange, setSelectingRange] = useState(false)
|
27
|
+
const [from, setFrom] = useState<Date | undefined>(
|
28
|
+
props.from !== undefined ? new Date(props.from) : undefined
|
29
|
+
)
|
30
|
+
const [to, setTo] = useState<Date | undefined | null>(
|
31
|
+
props.to !== undefined ? new Date(props.to) : undefined
|
32
|
+
)
|
33
|
+
|
34
|
+
const onClickCalendar = (e: Event) => {
|
35
|
+
const { value, disabled } = e.currentTarget.dataset
|
36
|
+
if (value === undefined) {
|
37
|
+
return
|
38
|
+
}
|
39
|
+
if (disabled === 'true') {
|
40
|
+
return
|
41
|
+
}
|
42
|
+
|
43
|
+
if (!guardRange(props) && typeof props.onChange === 'function') {
|
44
|
+
props.onChange({ value: new Date(value) })
|
45
|
+
}
|
46
|
+
|
47
|
+
return value
|
48
|
+
}
|
49
|
+
const onClickRange = (e: Event) => {
|
50
|
+
const { value } = e.currentTarget.dataset
|
51
|
+
// start selecting range
|
52
|
+
if (!selectingRange) {
|
53
|
+
if (value !== undefined) {
|
54
|
+
setFrom(new Date(value))
|
55
|
+
setTo(null)
|
56
|
+
}
|
57
|
+
setSelectingRange(true)
|
58
|
+
}
|
59
|
+
// submit select date in mobile
|
60
|
+
if (selectingRange && to === null) {
|
61
|
+
if (value !== undefined) {
|
62
|
+
setTo(new Date(value))
|
63
|
+
handleRangeOnChange(from, new Date(value))
|
64
|
+
}
|
65
|
+
setSelectingRange(false)
|
66
|
+
}
|
67
|
+
// finish selecting range in desktop
|
68
|
+
if (selectingRange && to !== undefined) {
|
69
|
+
handleRangeOnChange(from, to)
|
70
|
+
setSelectingRange(false)
|
71
|
+
}
|
72
|
+
}
|
73
|
+
const onMouseMove = (e: Event) => {
|
74
|
+
const { value } = e.currentTarget.dataset
|
75
|
+
if (!selectingRange) {
|
76
|
+
return
|
77
|
+
}
|
78
|
+
if (value !== undefined) {
|
79
|
+
if (dayjs(value).isAfter(dayjs(from))) {
|
80
|
+
setTo(new Date(value))
|
81
|
+
}
|
82
|
+
}
|
83
|
+
}
|
84
|
+
const handleClickEvent = (e: Event) => {
|
85
|
+
if (props.range === true) {
|
86
|
+
return onClickRange(e)
|
87
|
+
}
|
88
|
+
return onClickCalendar(e)
|
89
|
+
}
|
90
|
+
const handleRangeOnChange = (
|
91
|
+
from: Date | undefined,
|
92
|
+
to: Date | undefined | null
|
93
|
+
) => {
|
94
|
+
if (typeof props.onChange === 'function' && guardRange(props)) {
|
95
|
+
if (from != null && to != null) {
|
96
|
+
props.onChange({
|
97
|
+
from,
|
98
|
+
to
|
99
|
+
})
|
100
|
+
}
|
101
|
+
}
|
102
|
+
}
|
103
|
+
return {
|
104
|
+
handlers: {
|
105
|
+
onClick: handleClickEvent,
|
106
|
+
...(props.range === true && { onMouseMove })
|
107
|
+
},
|
108
|
+
from,
|
109
|
+
to
|
110
|
+
}
|
111
|
+
}
|
112
|
+
|
113
|
+
export default useCalendarHandlers
|