From 93341e006967479acdae3a1d9f112b5ee42989c2 Mon Sep 17 00:00:00 2001 From: Erzhan Torokulov Date: Wed, 24 Aug 2022 21:25:12 +0600 Subject: [PATCH] Add wildcard/calendar component based on 'react-calendar' npm package (#40679) --- .../components/Calendar/Calendar.module.scss | 75 ++++++++++++ .../components/Calendar/Calendar.story.tsx | 109 ++++++++++++++++++ .../src/components/Calendar/Calendar.tsx | 62 ++++++++++ .../wildcard/src/components/Calendar/index.ts | 1 + client/wildcard/src/components/index.ts | 1 + package.json | 2 + yarn.lock | 36 +++++- 7 files changed, 285 insertions(+), 1 deletion(-) create mode 100644 client/wildcard/src/components/Calendar/Calendar.module.scss create mode 100644 client/wildcard/src/components/Calendar/Calendar.story.tsx create mode 100644 client/wildcard/src/components/Calendar/Calendar.tsx create mode 100644 client/wildcard/src/components/Calendar/index.ts diff --git a/client/wildcard/src/components/Calendar/Calendar.module.scss b/client/wildcard/src/components/Calendar/Calendar.module.scss new file mode 100644 index 00000000000..fac4aa84801 --- /dev/null +++ b/client/wildcard/src/components/Calendar/Calendar.module.scss @@ -0,0 +1,75 @@ +// stylelint-disable selector-class-pattern + +/* +* The 'react-calendar' is imported without styles but with predefined BEM-classes. +* This file styles the calendar to follow Sourcegraph designs styles. +*/ +.container { + width: 21rem; + display: inline-block; + + :global { + .react-calendar { + &__navigation__arrow, + &__navigation__label, + &__tile { + background-color: inherit; + border: none; + padding: 0.25rem; + } + + &__navigation { + display: flex; + margin-bottom: 0.5rem; + + &__arrow { + border: 1px solid var(--secondary); + margin: 0 0.25rem; + border-radius: 0.25rem; + padding-left: 0.5rem; + padding-right: 0.5rem; + } + + &__label { + font-weight: bold; + pointer-events: none; + } + } + + &__month-view__weekdays { + &__weekday { + text-align: center; + font-weight: bold; + } + } + + &__tile { + &--rangeStart { + border-top-left-radius: 0.25rem; + } + &--rangeEnd { + border-bottom-right-radius: 0.25rem; + } + &--rangeBothEnds { + border-radius: 0.25rem; + } + &--active { + background-color: var(--primary-2); + } + &:disabled { + color: var(--secondary); + } + } + + &__month-view__days__day--neighboringMonth { + color: var(--text-muted); + } + } + } +} + +.highlight-today :global(.react-calendar__tile--now) { + font-weight: 500; + text-decoration: underline; + text-decoration-thickness: 0.1rem; +} diff --git a/client/wildcard/src/components/Calendar/Calendar.story.tsx b/client/wildcard/src/components/Calendar/Calendar.story.tsx new file mode 100644 index 00000000000..dde17ed895e --- /dev/null +++ b/client/wildcard/src/components/Calendar/Calendar.story.tsx @@ -0,0 +1,109 @@ +import { useState } from 'react' + +import { DecoratorFn, Meta, Story } from '@storybook/react' +import { addDays, startOfDay, subDays } from 'date-fns' + +import { BrandedStory } from '@sourcegraph/branded/src/components/BrandedStory' +import webStyles from '@sourcegraph/web/src/SourcegraphWebApp.scss' + +import { Badge, Text } from '..' + +import { Calendar } from './Calendar' + +const decorator: DecoratorFn = story => ( + + {() => ( +
+ + This is an{' '} + + Experimental + {' '} + component and built on top of `react-calendar` package with Sourcegraph CSS styling on top. It + intentionally, omits other `react-calendar` props/features to not over-complicate and use as simple + calendar, in case if we migrate to another calendar library or build our own. + +
{story()}
+
+ )} +
+) + +const config: Meta = { + title: 'wildcard/Calendar', + component: Calendar, + decorators: [decorator], +} + +export default config + +// NOTE: hardcoded in order to screenshot test the calendar +const today = startOfDay(new Date('2022-08-22')) + +export const Single: Story = () => { + const [value, onChange] = useState(today) + return +} + +Single.parameters = { + chromatic: { + enableDarkMode: true, + disableSnapshot: false, + }, +} + +export const Range: Story = () => { + const [value, onChange] = useState<[Date, Date]>([subDays(today, 7), today]) + return +} + +Range.parameters = { + chromatic: { + enableDarkMode: true, + disableSnapshot: false, + }, +} + +export const MinMaxDates: Story = () => { + const [value, onChange] = useState<[Date, Date]>([subDays(today, 7), today]) + return ( + + ) +} + +MinMaxDates.parameters = { + chromatic: { + enableDarkMode: true, + disableSnapshot: false, + }, +} + +export const HighlightToday: Story = () => { + const [value, onChange] = useState(new Date()) + return +} + +HighlightToday.parameters = { + chromatic: { + enableDarkMode: true, + disableSnapshot: true, + }, +} + +export const HighlightTodayRange: Story = () => { + const [value, onChange] = useState<[Date, Date]>([subDays(new Date(), 4), addDays(new Date(), 3)]) + return +} + +HighlightTodayRange.parameters = { + chromatic: { + enableDarkMode: true, + disableSnapshot: true, + }, +} diff --git a/client/wildcard/src/components/Calendar/Calendar.tsx b/client/wildcard/src/components/Calendar/Calendar.tsx new file mode 100644 index 00000000000..0761dafe640 --- /dev/null +++ b/client/wildcard/src/components/Calendar/Calendar.tsx @@ -0,0 +1,62 @@ +import classNames from 'classnames' +import ReactCalendar from 'react-calendar' + +import { Container } from '@sourcegraph/wildcard' + +import styles from './Calendar.module.scss' + +interface CalendarDateProps { + isRange?: false + value?: Date | null + onChange: (value: Date) => void +} + +interface CalendarDateRangeProps { + isRange: true + value?: [Date | null, Date | null] | null + onChange: (value: [Date, Date]) => void +} + +type CalendarProps = { + className?: string + maxDate?: Date + minDate?: Date + highlightToday?: boolean +} & (CalendarDateRangeProps | CalendarDateProps) + +/** + * Renders a calendar component which supports single date or range selection. + * + * **NOTE:** This is an `EXPERIMENTAL` component and built on top of `react-calendar` package with Sourcegraph CSS styling on top. + * It intentionally, omits other `react-calendar` props/features to not over-complicate and use as simple calendar, in case if we migrate to another calendar library or build our own. + * + * Depending on `isRange` value `true | false`, the component will render a single or range selection calendar as well as infer correct TS props for the range selection. + * + */ +export const Calendar: React.FunctionComponent = ({ + className, + value, + onChange, + isRange, + minDate, + maxDate, + highlightToday, +}) => ( + + + +) diff --git a/client/wildcard/src/components/Calendar/index.ts b/client/wildcard/src/components/Calendar/index.ts new file mode 100644 index 00000000000..8c50cf89e87 --- /dev/null +++ b/client/wildcard/src/components/Calendar/index.ts @@ -0,0 +1 @@ +export * from './Calendar' diff --git a/client/wildcard/src/components/index.ts b/client/wildcard/src/components/index.ts index f1218bf3991..bd745d4aa25 100644 --- a/client/wildcard/src/components/index.ts +++ b/client/wildcard/src/components/index.ts @@ -2,6 +2,7 @@ export { Button, ButtonGroup, BUTTON_SIZES } from './Button' export type { ButtonGroupProps } from './Button' export { Alert, AlertLink } from './Alert' +export { Calendar } from './Calendar' export { Container } from './Container' export { LineChart, BarChart, PieChart, LegendList, LegendItem, ScrollBox, ParentSize } from './Charts' export { diff --git a/package.json b/package.json index b863c94ae3b..efc2cce6ca8 100644 --- a/package.json +++ b/package.json @@ -202,6 +202,7 @@ "@types/pollyjs__persister-fs": "2.0.1", "@types/puppeteer": "^5.4.5", "@types/react": "18.0.8", + "@types/react-calendar": "^3.5.2", "@types/react-circular-progressbar": "1.0.2", "@types/react-dom": "18.0.2", "@types/react-grid-layout": "1.3.0", @@ -449,6 +450,7 @@ "pretty-bytes": "^5.3.0", "prop-types": "^15.7.2", "react": "18.1.0", + "react-calendar": "^3.7.0", "react-circular-progressbar": "^2.0.3", "react-dom": "18.1.0", "react-dom-confetti": "^0.1.4", diff --git a/yarn.lock b/yarn.lock index 6e9e74ccbd5..d9421b18528 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6079,6 +6079,13 @@ resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.2.tgz#fa8e1ad1d474688a757140c91de6dace6f4abc8d" integrity sha512-HtKGu+qG1NPvYe1z7ezLsyIaXYyi8SoAVqWDZgDQ8dLrsZvSzUNCwZyfX33uhWxL/SU0ZDQZ3nwZ0nimt507Kw== +"@types/react-calendar@^3.5.2": + version "3.5.2" + resolved "https://registry.npmjs.org/@types/react-calendar/-/react-calendar-3.5.2.tgz#e401034e4bb82f4510ba87aa490e98b5746e16e0" + integrity sha512-8gkU9KaE33VVbu3YWvxXjEk4BsalgSYR3c/5XF9XNJiQ/2MKxiGkTg/PfOHUX/BvcADykRBMAEJiCi6jFPEE3A== + dependencies: + "@types/react" "*" + "@types/react-circular-progressbar@1.0.2": version "1.0.2" resolved "https://registry.npmjs.org/@types/react-circular-progressbar/-/react-circular-progressbar-1.0.2.tgz#a23e2d0f4e14a89d75c7c2286f8e96702f9862f2" @@ -6978,6 +6985,11 @@ resolved "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.5.1.tgz#b5fde2f0f79c1e120307c415a4c1d5eb15a6f278" integrity sha512-4vSVUiOPJLmr45S8rMGy7WDvpWxfFxfP/Qx/cxZFCfvoypTYpPPL1X8VIZMe0WTA+Jr7blUxwUSEZNkjoMTgSw== +"@wojtekmaj/date-utils@^1.0.2": + version "1.0.3" + resolved "https://registry.npmjs.org/@wojtekmaj/date-utils/-/date-utils-1.0.3.tgz#2dcfd92881425c5923e429c2aec86fb3609032a1" + integrity sha512-1VPkkTBk07gMR1fjpBtse4G+oJqpmE+0gUFB0dg3VIL7qJmUVaBoD/vlzMm/jNeOPfvlmerl1lpnsZyBUFIRuw== + "@wry/context@^0.6.0": version "0.6.0" resolved "https://registry.npmjs.org/@wry/context/-/context-0.6.0.tgz#f903eceb89d238ef7e8168ed30f4511f92d83e06" @@ -13680,6 +13692,13 @@ get-uri@3: fs-extra "^8.1.0" ftp "^0.3.10" +get-user-locale@^1.2.0: + version "1.5.1" + resolved "https://registry.npmjs.org/get-user-locale/-/get-user-locale-1.5.1.tgz#18a9ba2cfeed0e713ea00968efa75d620523a5ea" + integrity sha512-WiNpoFRcHn1qxP9VabQljzGwkAQDrcpqUtaP0rNBEkFxJdh4f3tik6MfZsMYZc+UgQJdGCxWEjL9wnCUlRQXag== + dependencies: + lodash.memoize "^4.1.1" + get-value@^2.0.3, get-value@^2.0.6: version "2.0.6" resolved "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" @@ -17521,7 +17540,7 @@ lodash.isstring@^4.0.1: resolved "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= -lodash.memoize@^4.1.2: +lodash.memoize@^4.1.1, lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= @@ -18009,6 +18028,11 @@ meow@^9.0.0: type-fest "^0.18.0" yargs-parser "^20.2.3" +merge-class-names@^1.1.1: + version "1.4.2" + resolved "https://registry.npmjs.org/merge-class-names/-/merge-class-names-1.4.2.tgz#78d6d95ab259e7e647252a7988fd25a27d5a8835" + integrity sha512-bOl98VzwCGi25Gcn3xKxnR5p/WrhWFQB59MS/aGENcmUc6iSm96yrFDF0XSNurX9qN4LbJm0R9kfvsQ17i8zCw== + merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" @@ -20863,6 +20887,16 @@ rc@1.2.8, rc@^1.2.7, rc@^1.2.8: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-calendar@^3.7.0: + version "3.7.0" + resolved "https://registry.npmjs.org/react-calendar/-/react-calendar-3.7.0.tgz#951d56e91afb33b1c1e019cb790349fbffcc6894" + integrity sha512-zkK95zWLWLC6w3O7p3SHx/FJXEyyD2UMd4jr3CrKD+G73N+G5vEwrXxYQCNivIPoFNBjqoyYYGlkHA+TBDPLCw== + dependencies: + "@wojtekmaj/date-utils" "^1.0.2" + get-user-locale "^1.2.0" + merge-class-names "^1.1.1" + prop-types "^15.6.0" + react-circular-progressbar@^2.0.3: version "2.0.3" resolved "https://registry.npmjs.org/react-circular-progressbar/-/react-circular-progressbar-2.0.3.tgz#fa8eb59f8db168d2904bae4590641792c80f5991"