diff --git a/docusaurus/docs/date-picker/input-date-picker.md b/docusaurus/docs/date-picker/input-date-picker.md index 74a1eadd..58d8cf67 100644 --- a/docusaurus/docs/date-picker/input-date-picker.md +++ b/docusaurus/docs/date-picker/input-date-picker.md @@ -119,6 +119,10 @@ The start year when the component is rendered. Defaults to `1800`. `Type: number | undefined` The end year when the component is rendered. Defaults to `2200`. +**startWeekOnMonday** +`Type: boolean | undefined` +Flag indicating if calendar grid sould show monday as the first column. Defaults to `false`. + **inputEnabled** `Type: boolean | undefined` Flag indicating if the component should be enabled or not. Behavior similarly to `disabled`. Defaults to `true`. diff --git a/docusaurus/docs/date-picker/multiple-dates-picker.md b/docusaurus/docs/date-picker/multiple-dates-picker.md index de00f3b4..571ebe4b 100644 --- a/docusaurus/docs/date-picker/multiple-dates-picker.md +++ b/docusaurus/docs/date-picker/multiple-dates-picker.md @@ -131,6 +131,10 @@ The start year when the component is rendered. Defaults to `1800`. `Type: number | undefined` The end year when the component is rendered. Defaults to `2200`. +**startWeekOnMonday** +`Type: boolean | undefined` +Flag indicating if calendar grid sould show monday as the first column. Defaults to `false`. + **closeIcon** `Type: string | undefined` The icon used to close the component. Defaults to `close`. You can pass the name of an icon from [MaterialCommunityIcons](https://materialdesignicons.com/). diff --git a/docusaurus/docs/date-picker/range-date-picker.md b/docusaurus/docs/date-picker/range-date-picker.md index bb9ddd63..76e09a78 100644 --- a/docusaurus/docs/date-picker/range-date-picker.md +++ b/docusaurus/docs/date-picker/range-date-picker.md @@ -134,6 +134,10 @@ The start year when the component is rendered. Defaults to `1800`. `Type: number | undefined` The end year when the component is rendered. Defaults to `2200`. +**startWeekOnMonday** +`Type: boolean | undefined` +Flag indicating if calendar grid sould show monday as the first column. Defaults to `false`. + **closeIcon** `Type: string | undefined` The icon used to close the component. Defaults to `close`. You can pass the name of an icon from [MaterialCommunityIcons](https://materialdesignicons.com/). diff --git a/docusaurus/docs/date-picker/single-date-picker.md b/docusaurus/docs/date-picker/single-date-picker.md index 8d3d573c..b68bc3d9 100644 --- a/docusaurus/docs/date-picker/single-date-picker.md +++ b/docusaurus/docs/date-picker/single-date-picker.md @@ -121,6 +121,10 @@ The start year when the component is rendered. Defaults to `1800`. `Type: number | undefined` The end year when the component is rendered. Defaults to `2200`. +**startWeekOnMonday** +`Type: boolean | undefined` +Flag indicating if calendar grid sould show monday as the first column. Defaults to `false`. + **closeIcon** `Type: string | undefined` The icon used to close the component. Defaults to `close`. You can pass the name of an icon from [MaterialCommunityIcons](https://materialdesignicons.com/). diff --git a/example/src/ReadMeExampleMultiple.tsx b/example/src/ReadMeExampleMultiple.tsx index b42ae45f..5fd6a627 100644 --- a/example/src/ReadMeExampleMultiple.tsx +++ b/example/src/ReadMeExampleMultiple.tsx @@ -44,6 +44,7 @@ export default function ReadMeExampleMultiple() { // animationType="slide" // optional, default is slide on ios/android and none on web // startYear={2000} // optional, default is 1800 // endYear={2100} // optional, default is 2200 + // startWeekOnMonday={true} // optional, default is false /> ) diff --git a/example/src/ReadMeExampleRange.tsx b/example/src/ReadMeExampleRange.tsx index 12d24d2e..e00802df 100644 --- a/example/src/ReadMeExampleRange.tsx +++ b/example/src/ReadMeExampleRange.tsx @@ -52,6 +52,7 @@ export default function ReadMeExampleRange() { // animationType="slide" // optional, default is slide on ios/android and none on web // startYear={2000} // optional, default is 1800 // endYear={2100} // optional, default is 2200 + // startWeekOnMonday={true} // optional, default is false /> ) diff --git a/example/src/ReadMeExampleSingle.tsx b/example/src/ReadMeExampleSingle.tsx index 6e4c2f5f..cd18b4cb 100644 --- a/example/src/ReadMeExampleSingle.tsx +++ b/example/src/ReadMeExampleSingle.tsx @@ -43,6 +43,7 @@ export default function ReadMeExampleSingle() { // animationType="slide" // optional, default is 'slide' on ios/android and 'none' on web // startYear={2000} // optional, default is 1800 // endYear={2100} // optional, default is 2200 + // startWeekOnMonday={true} // optional, default is false // /> diff --git a/src/Date/Calendar.tsx b/src/Date/Calendar.tsx index 76dbc538..780e7036 100644 --- a/src/Date/Calendar.tsx +++ b/src/Date/Calendar.tsx @@ -33,6 +33,7 @@ export type BaseCalendarProps = { validRange?: ValidRangeType startYear?: number endYear?: number + startWeekOnMonday?: boolean // here they are optional but in final implementation they are required date?: CalendarDate @@ -95,6 +96,7 @@ function Calendar( dates, validRange, dateMode, + startWeekOnMonday, } = props const theme = useTheme() @@ -177,6 +179,7 @@ function Calendar( initialIndex={getInitialIndex(firstDate)} selectedYear={selectedYear} scrollMode={scrollMode} + startWeekOnMonday={startWeekOnMonday || false} renderItem={({ index }) => ( )} renderHeader={({ onPrev, onNext }) => ( @@ -205,6 +209,7 @@ function Calendar( onNext={onNext} scrollMode={scrollMode} disableWeekDays={disableWeekDays} + startWeekOnMonday={startWeekOnMonday || false} /> )} /> diff --git a/src/Date/CalendarHeader.tsx b/src/Date/CalendarHeader.tsx index d95206b2..1b845200 100644 --- a/src/Date/CalendarHeader.tsx +++ b/src/Date/CalendarHeader.tsx @@ -27,12 +27,14 @@ function CalendarHeader({ onNext, disableWeekDays, locale, + startWeekOnMonday, }: { locale: undefined | string scrollMode: 'horizontal' | 'vertical' onPrev: () => any onNext: () => any disableWeekDays?: DisableWeekDaysType + startWeekOnMonday: boolean }) { const theme = useTheme() const isHorizontal = scrollMode === 'horizontal' @@ -67,7 +69,11 @@ function CalendarHeader({ ) : null} - + ) } diff --git a/src/Date/DatePickerInput.shared.tsx b/src/Date/DatePickerInput.shared.tsx index 44b71df0..bb0f896f 100644 --- a/src/Date/DatePickerInput.shared.tsx +++ b/src/Date/DatePickerInput.shared.tsx @@ -27,6 +27,7 @@ export type DatePickerInputProps = { disableStatusBarPadding?: boolean animationType?: 'slide' | 'fade' | 'none' presentationStyle?: 'pageSheet' | 'overFullScreen' + startWeekOnMonday?: boolean } & Omit< React.ComponentProps, 'value' | 'onChange' | 'onChangeText' | 'inputMode' diff --git a/src/Date/DatePickerInput.tsx b/src/Date/DatePickerInput.tsx index fc5608c7..2decd08f 100644 --- a/src/Date/DatePickerInput.tsx +++ b/src/Date/DatePickerInput.tsx @@ -60,6 +60,7 @@ function DatePickerInput( endYear, inputEnabled, disableStatusBarPadding, + startWeekOnMonday, }) => withModal ? ( ) : null } diff --git a/src/Date/DatePickerInputWithoutModal.tsx b/src/Date/DatePickerInputWithoutModal.tsx index d86d6ef8..47d0be2c 100644 --- a/src/Date/DatePickerInputWithoutModal.tsx +++ b/src/Date/DatePickerInputWithoutModal.tsx @@ -29,6 +29,7 @@ function DatePickerInputWithoutModal( onChangeText, inputEnabled, disableStatusBarPadding, + startWeekOnMonday, ...rest }: DatePickerInputProps & { modal?: (params: { @@ -43,6 +44,7 @@ function DatePickerInputWithoutModal( endYear: DatePickerInputProps['endYear'] inputEnabled: DatePickerInputProps['inputEnabled'] disableStatusBarPadding: DatePickerInputProps['disableStatusBarPadding'] + startWeekOnMonday?: DatePickerInputProps['startWeekOnMonday'] }) => any inputButton?: React.ReactNode }, @@ -118,6 +120,7 @@ function DatePickerInputWithoutModal( endYear, inputEnabled, disableStatusBarPadding, + startWeekOnMonday, })} ) diff --git a/src/Date/DatePickerModalContent.tsx b/src/Date/DatePickerModalContent.tsx index 08b2877f..98f91fef 100644 --- a/src/Date/DatePickerModalContent.tsx +++ b/src/Date/DatePickerModalContent.tsx @@ -93,6 +93,7 @@ export function DatePickerModalContent( startYear, endYear, statusBarOnTopOfBackdrop, + startWeekOnMonday, } = props const anyProps = props as any @@ -199,6 +200,7 @@ export function DatePickerModalContent( dateMode={dateMode} startYear={startYear} endYear={endYear} + startWeekOnMonday={startWeekOnMonday} /> } calendarEdit={ diff --git a/src/Date/DayNames.tsx b/src/Date/DayNames.tsx index 898969c0..6584cd9c 100644 --- a/src/Date/DayNames.tsx +++ b/src/Date/DayNames.tsx @@ -6,31 +6,35 @@ import { DisableWeekDaysType, showWeekDay } from './dateUtils' export const dayNamesHeight = 44 -// TODO: wait for a better Intl api ;-) -const weekdays = [ - new Date(2020, 7, 2), - new Date(2020, 7, 3), - new Date(2020, 7, 4), - new Date(2020, 7, 5), - new Date(2020, 7, 6), - new Date(2020, 7, 7), - new Date(2020, 7, 8), -] - function DayNames({ disableWeekDays, locale, + startWeekOnMonday, }: { locale: undefined | string disableWeekDays?: DisableWeekDaysType + startWeekOnMonday: boolean }) { const theme = useTheme() const shortDayNames = React.useMemo(() => { + // TODO: wait for a better Intl api ;-) + const weekdays = [ + new Date(2020, 7, 2), + new Date(2020, 7, 3), + new Date(2020, 7, 4), + new Date(2020, 7, 5), + new Date(2020, 7, 6), + new Date(2020, 7, 7), + new Date(2020, 7, 8), + ] + if (startWeekOnMonday) { + weekdays.push(weekdays.shift() as Date) + } const formatter = new Intl.DateTimeFormat(locale, { weekday: 'narrow', }) return weekdays.map((date) => formatter.format(date)) - }, [locale]) + }, [locale, startWeekOnMonday]) return ( { + return monthGrid(index, startWeekOnMonday).map(({ days, weekGrid }) => { return { weekIndex: weekGrid, generatedDays: days.map((_, dayIndex) => { @@ -248,6 +250,7 @@ function Month(props: MonthSingleProps | MonthRangeProps | MonthMultiProps) { endDate, dates, date, + startWeekOnMonday, ]) let textFont = theme?.isV3 @@ -263,7 +266,12 @@ function Month(props: MonthSingleProps | MonthRangeProps | MonthMultiProps) { const iconSource = theme.isV3 ? iconSourceV3 : iconSourceV2 return ( - + { - return Array(getGridCount(index)) +const monthGrid = (index: number, startWeekOnMonday: boolean) => { + return Array(getGridCount(index, startWeekOnMonday)) .fill(null) .map((_, weekGrid) => { const days = Array(7).fill(null) @@ -405,7 +413,7 @@ function getIndexCount(index: number): number { return -(startAtIndex - index) } -function weeksOffset(index: number): number { +function weeksOffset(index: number, startWeekOnMonday: boolean): number { if (index === startAtIndex) { return 0 } @@ -413,12 +421,12 @@ function weeksOffset(index: number): number { if (index > startAtIndex) { for (let i = 0; i < index - startAtIndex; i++) { const cIndex = startAtIndex + i - off += gridCounts[cIndex] || getGridCount(cIndex) + off += gridCounts[cIndex] || getGridCount(cIndex, startWeekOnMonday) } } else { for (let i = 0; i < startAtIndex - index; i++) { const cIndex = startAtIndex - i - 1 - off -= gridCounts[cIndex] || getGridCount(cIndex) + off -= gridCounts[cIndex] || getGridCount(cIndex, startWeekOnMonday) } } return off @@ -431,10 +439,13 @@ export function getIndexFromHorizontalOffset( return startAtIndex + Math.floor(offset / width) } -export function getIndexFromVerticalOffset(offset: number): number { +export function getIndexFromVerticalOffset( + offset: number, + startWeekOnMonday: boolean +): number { let estimatedIndex = startAtIndex + Math.ceil(offset / estimatedMonthHeight) - const realOffset = getVerticalMonthsOffset(estimatedIndex) + const realOffset = getVerticalMonthsOffset(estimatedIndex, startWeekOnMonday) const difference = (realOffset - beginOffset - offset) / estimatedMonthHeight if (difference >= 1 || difference <= -1) { estimatedIndex -= Math.floor(difference) @@ -449,9 +460,12 @@ export function getHorizontalMonthOffset(index: number, width: number) { return width * index } -export function getVerticalMonthsOffset(index: number) { +export function getVerticalMonthsOffset( + index: number, + startWeekOnMonday: boolean +) { const count = getIndexCount(index) - const ob = weeksOffset(index) + const ob = weeksOffset(index, startWeekOnMonday) const monthsHeight = weekSize * ob const c = monthsHeight + count * (dayNamesHeight + montHeaderHeight) @@ -460,10 +474,11 @@ export function getVerticalMonthsOffset(index: number) { export function getMonthHeight( scrollMode: 'horizontal' | 'vertical', - index: number + index: number, + startWeekOnMonday: boolean ): number { const calendarHeight = getCalendarHeaderHeight(scrollMode) - const gc = getGridCount(index) + const gc = getGridCount(index, startWeekOnMonday) const currentMonthHeight = weekSize * gc const extraHeight = diff --git a/src/Date/Swiper.native.tsx b/src/Date/Swiper.native.tsx index f80d2ccf..28278536 100644 --- a/src/Date/Swiper.native.tsx +++ b/src/Date/Swiper.native.tsx @@ -56,6 +56,7 @@ function SwiperInner({ initialIndex, width, height, + startWeekOnMonday, }: SwiperProps & { width: number; height: number }) { const idx = React.useRef(initialIndex) const isHorizontal = scrollMode === 'horizontal' @@ -75,7 +76,7 @@ function SwiperInner({ } const offset = isHorizontal ? getHorizontalMonthOffset(index, width) - : getVerticalMonthsOffset(index) - montHeaderHeight + : getVerticalMonthsOffset(index, startWeekOnMonday) - montHeaderHeight if (isHorizontal) { parentRef.current.scrollTo({ @@ -91,7 +92,7 @@ function SwiperInner({ }) } }, - [parentRef, isHorizontal, width, height] + [parentRef, isHorizontal, width, height, startWeekOnMonday] ) const onPrev = React.useCallback(() => { @@ -112,7 +113,10 @@ function SwiperInner({ const viewSize = e.nativeEvent.layoutMeasurement const newIndex = isHorizontal ? Math.floor(contentOffset.x / viewSize.width) - : getIndexFromVerticalOffset(contentOffset.y - beginOffset) + : getIndexFromVerticalOffset( + contentOffset.y - beginOffset, + startWeekOnMonday + ) if (newIndex === 0) { return @@ -123,7 +127,7 @@ function SwiperInner({ setVisibleIndexes(getVisibleArray(newIndex, { isHorizontal, height })) } }, - [idx, height, isHorizontal] + [idx, height, isHorizontal, startWeekOnMonday] ) const renderProps = { @@ -179,7 +183,10 @@ function SwiperInner({ style={{ top: isHorizontal ? 0 - : getVerticalMonthsOffset(visibleIndexes[vi]), + : getVerticalMonthsOffset( + visibleIndexes[vi], + startWeekOnMonday + ), left: isHorizontal ? getHorizontalMonthOffset(visibleIndexes[vi], width) : 0, @@ -189,7 +196,11 @@ function SwiperInner({ width: isHorizontal ? width : undefined, height: isHorizontal ? undefined - : getMonthHeight(scrollMode, visibleIndexes[vi]), + : getMonthHeight( + scrollMode, + visibleIndexes[vi], + startWeekOnMonday + ), }} > {renderItem({ diff --git a/src/Date/Swiper.tsx b/src/Date/Swiper.tsx index 33f3ccd2..2cfe90de 100644 --- a/src/Date/Swiper.tsx +++ b/src/Date/Swiper.tsx @@ -20,6 +20,7 @@ function Swiper({ renderFooter, selectedYear, initialIndex, + startWeekOnMonday, }: SwiperProps) { const isHorizontal = scrollMode === 'horizontal' const [index, setIndex] = React.useState(initialIndex) @@ -66,6 +67,7 @@ function Swiper({ initialIndex={initialIndex} estimatedHeight={estimatedMonthHeight} renderItem={renderItem} + startWeekOnMonday={startWeekOnMonday} /> )} @@ -83,12 +85,14 @@ function VerticalScroller({ initialIndex, estimatedHeight, renderItem, + startWeekOnMonday, }: { renderItem: (renderProps: RenderProps) => any width: number height: number initialIndex: number estimatedHeight: number + startWeekOnMonday: boolean }) { const idx = React.useRef(initialIndex) const [visibleIndexes, setVisibleIndexes] = React.useState( @@ -101,7 +105,8 @@ function VerticalScroller({ if (!element) { return } - const top = getVerticalMonthsOffset(idx.current) - montHeaderHeight + const top = + getVerticalMonthsOffset(idx.current, startWeekOnMonday) - montHeaderHeight element.scrollTo({ top, @@ -118,14 +123,14 @@ function VerticalScroller({ } const offset = top - beginOffset - const index = getIndexFromVerticalOffset(offset) + const index = getIndexFromVerticalOffset(offset, startWeekOnMonday) if (idx.current !== index) { idx.current = index setVisibleIndexesThrottled(visibleArray(index)) } }, - [setVisibleIndexesThrottled] + [setVisibleIndexesThrottled, startWeekOnMonday] ) return ( @@ -153,12 +158,17 @@ function VerticalScroller({ style={{ willChange: 'transform', transform: `translateY(${getVerticalMonthsOffset( - visibleIndexes[vi] + visibleIndexes[vi], + startWeekOnMonday )}px)`, left: 0, right: 0, position: 'absolute', - height: getMonthHeight('vertical', visibleIndexes[vi]), + height: getMonthHeight( + 'vertical', + visibleIndexes[vi], + startWeekOnMonday + ), }} > {renderItem({ diff --git a/src/Date/SwiperUtils.ts b/src/Date/SwiperUtils.ts index 8a98be84..c6f6fd2a 100644 --- a/src/Date/SwiperUtils.ts +++ b/src/Date/SwiperUtils.ts @@ -21,6 +21,7 @@ export type SwiperProps = { renderHeader?: (renderProps: RenderProps) => any renderFooter?: (renderProps: RenderProps) => any selectedYear: number | undefined + startWeekOnMonday: boolean } export function useYearChange( diff --git a/src/Date/dateUtils.tsx b/src/Date/dateUtils.tsx index 26131a3a..c4a59ce1 100644 --- a/src/Date/dateUtils.tsx +++ b/src/Date/dateUtils.tsx @@ -58,11 +58,19 @@ export function getDaysInMonth({ export function getFirstDayOfMonth({ year, month, + startWeekOnMonday, }: { year: number month: number + startWeekOnMonday: boolean }): number { - return new Date(year, month, 1).getDay() + let dayOfWeek = new Date(year, month, 1).getDay() + if (startWeekOnMonday) { + // Map Sunday (0) to 6, Monday (1) to 0, etc. + dayOfWeek = (dayOfWeek + 6) % 7 + } + + return dayOfWeek } export function useRangeChecker(validRange: ValidRangeType | undefined) { @@ -167,22 +175,22 @@ export const totalMonths = startAtIndex * 2 export const beginOffset = estimatedMonthHeight * startAtIndex export const gridCounts = new Array(totalMonths) -export function getGridCount(index: number) { +export function getGridCount(index: number, startWeekOnMonday: boolean) { const cHeight = gridCounts[index] if (cHeight) { return cHeight } const monthDate = addMonths(new Date(), getRealIndex(index)) - const h = getGridCountForDate(monthDate) + const h = getGridCountForDate(monthDate, startWeekOnMonday) gridCounts[index] = h return h } -export function getGridCountForDate(date: Date) { +export function getGridCountForDate(date: Date, startWeekOnMonday: boolean) { const year = date.getFullYear() const month = date.getMonth() const daysInMonth = getDaysInMonth({ year, month }) - const dayOfWeek = getFirstDayOfMonth({ year, month }) + const dayOfWeek = getFirstDayOfMonth({ year, month, startWeekOnMonday }) return Math.ceil((daysInMonth + dayOfWeek) / 7) } diff --git a/src/__tests__/Date/CalendarHeader.test.tsx b/src/__tests__/Date/CalendarHeader.test.tsx index 8fa1b4cb..e9601585 100644 --- a/src/__tests__/Date/CalendarHeader.test.tsx +++ b/src/__tests__/Date/CalendarHeader.test.tsx @@ -11,6 +11,7 @@ it('renders CalendarHeader', () => { onPrev={() => null} onNext={() => null} scrollMode={'vertical'} + startWeekOnMonday={false} /> ) .toJSON() diff --git a/src/__tests__/Date/DayNames.test.tsx b/src/__tests__/Date/DayNames.test.tsx index 1a16ed1f..c59e620c 100644 --- a/src/__tests__/Date/DayNames.test.tsx +++ b/src/__tests__/Date/DayNames.test.tsx @@ -4,7 +4,9 @@ import renderer from 'react-test-renderer' import DayNames from '../../Date/DayNames' it('renders DayNames', () => { - const tree = renderer.create().toJSON() + const tree = renderer + .create() + .toJSON() expect(tree).toMatchSnapshot() }) diff --git a/src/__tests__/Date/dateUtils.test.tsx b/src/__tests__/Date/dateUtils.test.tsx index 3de4975e..165f27ea 100644 --- a/src/__tests__/Date/dateUtils.test.tsx +++ b/src/__tests__/Date/dateUtils.test.tsx @@ -73,10 +73,16 @@ describe('timeUtils', () => { it('should return correct gridCount for October 2021', () => { expect( - getGridCountForDate(addMonths(new Date(2018, 10 - 1, 1), 12 * 3)) + getGridCountForDate(addMonths(new Date(2018, 10 - 1, 1), 12 * 3), false) ).toBe(6) }) + it('should return correct gridCount for October 2021 with weeks starting on monday', () => { + expect( + getGridCountForDate(addMonths(new Date(2018, 10 - 1, 1), 12 * 3), true) + ).toBe(5) + }) + it('should get correct difference in month between dates', () => { expect(differenceInMonths(new Date(2022, 1, 1), new Date(2022, 2, 1))).toBe( 1