From 94945e66006682ee328322797f8b8721d018390c Mon Sep 17 00:00:00 2001 From: flavien Date: Fri, 3 Jan 2025 15:03:39 +0100 Subject: [PATCH 001/136] [POC][pickers] Explore the Base UI Calendar component --- .eslintrc.js | 2 + packages/x-date-pickers/package.json | 1 + .../internals/base/Calendar/index.parts.ts | 1 + .../src/internals/base/Calendar/index.ts | 1 + .../base/Calendar/root/CalendarRoot.tsx | 17 ++++++ .../base/Calendar/root/CalendarRootContext.ts | 19 +++++++ .../base/Calendar/root/useCalendarRoot.ts | 57 +++++++++++++++++++ pnpm-lock.yaml | 45 +++++++++++++++ 8 files changed, 143 insertions(+) create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/index.parts.ts create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/index.ts create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRoot.tsx create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts diff --git a/.eslintrc.js b/.eslintrc.js index fac05b6d0e35d..50df141c1532d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -135,6 +135,8 @@ module.exports = { rules: { ...baseline.rules, ...(ENABLE_REACT_COMPILER_PLUGIN ? { 'react-compiler/react-compiler': 'error' } : {}), + '@typescript-eslint/no-redeclare': 'off', + 'import/export': 'off', // Mostly handled by Typescript itself. ESLint produces false positives with declaration merging. // TODO move to @mui/monorepo, codebase is moving away from default exports https://github.com/mui/material-ui/issues/21862 'import/prefer-default-export': 'off', 'import/no-relative-packages': 'error', diff --git a/packages/x-date-pickers/package.json b/packages/x-date-pickers/package.json index 264cacf9d5eac..18b06e4edba77 100644 --- a/packages/x-date-pickers/package.json +++ b/packages/x-date-pickers/package.json @@ -46,6 +46,7 @@ }, "dependencies": { "@babel/runtime": "^7.26.0", + "@base-ui-components/react": "1.0.0-alpha.4", "@mui/utils": "^5.16.6 || ^6.0.0", "@mui/x-internals": "workspace:*", "@types/react-transition-group": "^4.4.12", diff --git a/packages/x-date-pickers/src/internals/base/Calendar/index.parts.ts b/packages/x-date-pickers/src/internals/base/Calendar/index.parts.ts new file mode 100644 index 0000000000000..c9def239f1c6a --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/Calendar/index.parts.ts @@ -0,0 +1 @@ +export { CalendarRoot as Root } from './root/CalendarRoot'; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/index.ts b/packages/x-date-pickers/src/internals/base/Calendar/index.ts new file mode 100644 index 0000000000000..d1ee976609fac --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/Calendar/index.ts @@ -0,0 +1 @@ +export * as Calendar from './index.parts'; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRoot.tsx b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRoot.tsx new file mode 100644 index 0000000000000..ad0ef7f33a72b --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRoot.tsx @@ -0,0 +1,17 @@ +'use client'; +import * as React from 'react'; + +const CalendarRoot = React.forwardRef(function CalendarRoot( + props: CalendarRoot.Props, + forwardedRef: React.ForwardedRef, +) { + return
TEST
; +}); + +export namespace CalendarRoot { + export interface State {} + + export interface Props {} +} + +export { CalendarRoot }; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts new file mode 100644 index 0000000000000..28cfdbf1f64f0 --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts @@ -0,0 +1,19 @@ +import * as React from 'react'; + +export interface CalendarRootContext {} + +export const CalendarRootContext = React.createContext(undefined); + +if (process.env.NODE_ENV !== 'production') { + CalendarRootContext.displayName = 'CalendarRootContext'; +} + +export function useCalendarRootContext() { + const context = React.useContext(CalendarRootContext); + if (context === undefined) { + throw new Error( + 'Base UI X: CalendarRootContext is missing. Calendar parts must be placed withing .', + ); + } + return context; +} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts new file mode 100644 index 0000000000000..1b4f6b98930fc --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts @@ -0,0 +1,57 @@ +import { PickerValidDate, TimezoneProps } from '../../../../models'; +import { useControlledValueWithTimezone } from '../../../hooks/useValueWithTimezone'; +import { singleItemValueManager } from '../../../utils/valueManagers'; + +export function useCalendarRoot(parameters) { + const { + defaultValue, + onValueChange, + value: valueProp, + timezone: timezoneProp, + referenceDate: referenceDateProp, + } = parameters; + + const { value, handleValueChange, timezone } = useControlledValueWithTimezone({ + name: 'CalendarRoot', + timezone: timezoneProp, + value: valueProp, + defaultValue, + referenceDate: referenceDateProp, + onChange: onValueChange, + valueManager: singleItemValueManager, + }); + + const getRootProps = React.useCallback(() => { + return {}; + }, []); + + return React.useMemo(() => ({})); +} + +export namespace useCalendarRoot { + export interface Parameters extends TimezoneProps { + /** + * The controlled value that should be selected. + * + * To render an uncontrolled Date Calendar, use the `defaultValue` prop instead. + */ + value?: PickerValidDate | null; + /** + * The uncontrolled value that should be initially selected. + * + * To render a controlled accordion, use the `value` prop instead. + */ + defaultValue?: PickerValidDate | null; + /** + * Event handler called when the selected value changes. + * Provides the new value as an argument. + * @param {PickerValidDate | null} value The new selected value. + */ + onValueChange: (value: PickerValidDate | null) => void; + /** + * The date used to generate the new value when both `value` and `defaultValue` are empty. + * @default The closest valid date using the validation props, except callbacks such as `shouldDisableDate`. + */ + referenceDate?: PickerValidDate; + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1c33a24c2018d..1d209da1fd8fc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1222,6 +1222,9 @@ importers: '@babel/runtime': specifier: ^7.26.0 version: 7.26.0 + '@base-ui-components/react': + specifier: 1.0.0-alpha.4 + version: 1.0.0-alpha.4(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@emotion/react': specifier: ^11.9.0 version: 11.14.0(@types/react@19.0.2)(react@19.0.0) @@ -2419,6 +2422,17 @@ packages: resolution: {integrity: sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==} engines: {node: '>=6.9.0'} + '@base-ui-components/react@1.0.0-alpha.4': + resolution: {integrity: sha512-hURFNvqx+gBZ6E1T2KpsZrv7RGfNECfHPovNYNr2+LLim62MeC04wgObRd8bjxvwU9L2uYvYQFkErql4LHsU6Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0 || ^19.0.0-rc + react: ^17.0.0 || ^18.0.0 || ^19.0 || ^19.0.0-rc + react-dom: ^17.0.0 || ^18.0.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} @@ -2878,6 +2892,12 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' + '@floating-ui/react@0.27.2': + resolution: {integrity: sha512-k/yP6a9K9QwhLfIu87iUZxCH6XN5z5j/VUHHq0dEnbZYY2Y9jz68E/LXFtK8dkiaYltS2WYohnyKC0VcwVneVg==} + peerDependencies: + react: '>=17.0.0' + react-dom: '>=17.0.0' + '@floating-ui/utils@0.2.8': resolution: {integrity: sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==} @@ -9606,6 +9626,9 @@ packages: resolution: {integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==} engines: {node: ^14.18.0 || >=16.0.0} + tabbable@6.2.0: + resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} + tapable@0.1.10: resolution: {integrity: sha512-jX8Et4hHg57mug1/079yitEKWGB3LCwoxByLsNim89LABq8NqgiX+6iYVOsq0vX8uJHkU+DZ5fnq95f800bEsQ==} engines: {node: '>=0.6'} @@ -11402,6 +11425,18 @@ snapshots: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 + '@base-ui-components/react@1.0.0-alpha.4(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@babel/runtime': 7.26.0 + '@floating-ui/react': 0.27.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@floating-ui/utils': 0.2.8 + prop-types: 15.8.1 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + use-sync-external-store: 1.4.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.2 + '@bcoe/v8-coverage@0.2.3': {} '@bundled-es-modules/cookie@2.0.1': @@ -11773,6 +11808,14 @@ snapshots: react: 19.0.0 react-dom: 19.0.0(react@19.0.0) + '@floating-ui/react@0.27.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@floating-ui/react-dom': 2.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@floating-ui/utils': 0.2.8 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + tabbable: 6.2.0 + '@floating-ui/utils@0.2.8': {} '@gitbeaker/core@38.12.1': @@ -19887,6 +19930,8 @@ snapshots: '@pkgr/core': 0.1.1 tslib: 2.8.1 + tabbable@6.2.0: {} + tapable@0.1.10: {} tapable@2.2.1: {} From 7b43c65f5883e83897971631955ac2cfab93ec46 Mon Sep 17 00:00:00 2001 From: flavien Date: Fri, 3 Jan 2025 17:25:35 +0100 Subject: [PATCH 002/136] Work --- .../internals/base/Calendar/index.parts.ts | 2 + .../Calendar/month-cell/CalendarMonthCell.tsx | 69 ++++++++++++ .../month-cell/useCalendarMonthCell.ts | 42 ++++++++ .../Calendar/month-list/CalendarMonthList.tsx | 35 ++++++ .../month-list/useCalendarMonthList.ts | 44 ++++++++ .../base/Calendar/root/CalendarRoot.tsx | 18 ++-- .../base/Calendar/root/CalendarRootContext.ts | 7 +- .../base/Calendar/root/useCalendarRoot.ts | 53 ++++++++-- .../base/utils/defaultRenderFunctions.tsx | 40 +++++++ .../base/utils/evaluateRenderProp.ts | 13 +++ .../internals/base/utils/getStyleHookProps.ts | 29 +++++ .../internals/base/utils/mergeReactProps.ts | 87 +++++++++++++++ .../src/internals/base/utils/reactVersion.ts | 9 ++ .../internals/base/utils/resolveClassName.ts | 13 +++ .../src/internals/base/utils/types.ts | 58 ++++++++++ .../base/utils/useComponentRender.ts | 100 ++++++++++++++++++ .../src/internals/base/utils/useForkRef.ts | 52 +++++++++ .../base/utils/useRenderPropForkRef.ts | 22 ++++ 18 files changed, 678 insertions(+), 15 deletions(-) create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/month-cell/CalendarMonthCell.tsx create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/month-cell/useCalendarMonthCell.ts create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/month-list/CalendarMonthList.tsx create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/month-list/useCalendarMonthList.ts create mode 100644 packages/x-date-pickers/src/internals/base/utils/defaultRenderFunctions.tsx create mode 100644 packages/x-date-pickers/src/internals/base/utils/evaluateRenderProp.ts create mode 100644 packages/x-date-pickers/src/internals/base/utils/getStyleHookProps.ts create mode 100644 packages/x-date-pickers/src/internals/base/utils/mergeReactProps.ts create mode 100644 packages/x-date-pickers/src/internals/base/utils/reactVersion.ts create mode 100644 packages/x-date-pickers/src/internals/base/utils/resolveClassName.ts create mode 100644 packages/x-date-pickers/src/internals/base/utils/types.ts create mode 100644 packages/x-date-pickers/src/internals/base/utils/useComponentRender.ts create mode 100644 packages/x-date-pickers/src/internals/base/utils/useForkRef.ts create mode 100644 packages/x-date-pickers/src/internals/base/utils/useRenderPropForkRef.ts diff --git a/packages/x-date-pickers/src/internals/base/Calendar/index.parts.ts b/packages/x-date-pickers/src/internals/base/Calendar/index.parts.ts index c9def239f1c6a..f29712ddd4156 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/index.parts.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/index.parts.ts @@ -1 +1,3 @@ export { CalendarRoot as Root } from './root/CalendarRoot'; +export { CalendarMonthList as MonthList } from './month-list/CalendarMonthList'; +export { CalendarMonthCell as MonthCell } from './month-cell/CalendarMonthCell'; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/month-cell/CalendarMonthCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/month-cell/CalendarMonthCell.tsx new file mode 100644 index 0000000000000..7cbc002c0bfac --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/Calendar/month-cell/CalendarMonthCell.tsx @@ -0,0 +1,69 @@ +'use client'; +import * as React from 'react'; +import { useUtils } from '../../../hooks/useUtils'; +import { useComponentRenderer } from '../../utils/useComponentRender'; +import { useCalendarRootContext } from '../root/CalendarRootContext'; +import { useCalendarMonthCell } from './useCalendarMonthCell'; +import { BaseUIComponentProps } from '../../utils/types'; + +const InnerCalendarMonthCell = React.forwardRef(function CalendarMonthCell( + props: InnerCalendarMonthCellProps, + forwardedRef: React.ForwardedRef, +) { + const { className, render, value, ctx, ...otherProps } = props; + const { getMonthCellProps } = useCalendarMonthCell({ value, ctx }); + const state = React.useMemo(() => ({ selected: ctx.isSelected }), [ctx.isSelected]); + + const { renderElement } = useComponentRenderer({ + propGetter: getMonthCellProps, + render: render ?? 'button', + ref: forwardedRef, + className, + state, + extraProps: otherProps, + }); + + return renderElement(); +}); + +const MemoizedInnerCalendarMonthCell = React.memo(InnerCalendarMonthCell); + +const CalendarMonthCell = React.forwardRef(function CalendarMonthCell( + props: CalendarMonthCell.Props, + ref: React.ForwardedRef, +) { + const calendarRootContext = useCalendarRootContext(); + const utils = useUtils(); + + const isSelected = React.useMemo( + () => + calendarRootContext.value == null + ? false + : utils.isSameDay(calendarRootContext.value, props.value), + [calendarRootContext.value, props.value, utils], + ); + + const ctx = React.useMemo( + () => ({ + isSelected, + selectMonth: calendarRootContext.selectMonth, + }), + [isSelected, calendarRootContext.selectMonth], + ); + + return ; +}); + +export namespace CalendarMonthCell { + export interface State {} + + export interface Props + extends Omit, + BaseUIComponentProps<'div', State> {} +} + +interface InnerCalendarMonthCellProps + extends useCalendarMonthCell.Parameters, + BaseUIComponentProps<'div', CalendarMonthCell.State> {} + +export { CalendarMonthCell }; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/month-cell/useCalendarMonthCell.ts b/packages/x-date-pickers/src/internals/base/Calendar/month-cell/useCalendarMonthCell.ts new file mode 100644 index 0000000000000..609a0792d3c10 --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/Calendar/month-cell/useCalendarMonthCell.ts @@ -0,0 +1,42 @@ +import * as React from 'react'; +import useEventCallback from '@mui/utils/useEventCallback'; +import { PickerValidDate } from '../../../../models'; +import { GenericHTMLProps } from '../../utils/types'; +import { mergeReactProps } from '../../utils/mergeReactProps'; +import { useUtils } from '../../../hooks/useUtils'; + +export function useCalendarMonthCell(parameters: useCalendarMonthCell.Parameters) { + const { value, ctx } = parameters; + const utils = useUtils(); + + const onClick = useEventCallback(() => { + ctx.selectMonth(value); + }); + + const getMonthCellProps = React.useCallback( + (externalProps: GenericHTMLProps) => { + return mergeReactProps(externalProps, { + type: 'button' as const, + role: 'radio', + 'aria-checked': ctx.isSelected, + children: utils.format(value, 'monthShort'), + onClick, + }); + }, + [utils, value, ctx.isSelected, onClick], + ); + + return React.useMemo(() => ({ getMonthCellProps }), [getMonthCellProps]); +} + +export namespace useCalendarMonthCell { + export interface Parameters { + value: PickerValidDate; + ctx: Context; + } + + export interface Context { + isSelected: boolean; + selectMonth: (value: PickerValidDate) => void; + } +} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/month-list/CalendarMonthList.tsx b/packages/x-date-pickers/src/internals/base/Calendar/month-list/CalendarMonthList.tsx new file mode 100644 index 0000000000000..15ee485d0c397 --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/Calendar/month-list/CalendarMonthList.tsx @@ -0,0 +1,35 @@ +'use client'; +import * as React from 'react'; +import { useCalendarMonthList } from './useCalendarMonthList'; +import { BaseUIComponentProps } from '../../utils/types'; +import { useComponentRenderer } from '../../utils/useComponentRender'; + +const CalendarMonthList = React.forwardRef(function CalendarMonthList( + props: CalendarMonthList.Props, + forwardedRef: React.ForwardedRef, +) { + const { className, render, ...otherProps } = props; + const { getMonthListProps } = useCalendarMonthList(otherProps); + const state = React.useMemo(() => ({}), []); + + const { renderElement } = useComponentRenderer({ + propGetter: getMonthListProps, + render: render ?? 'div', + ref: forwardedRef, + className, + state, + extraProps: otherProps, + }); + + return renderElement(); +}); + +export namespace CalendarMonthList { + export interface State {} + + export interface Props + extends Omit, 'children'>, + useCalendarMonthList.Parameters {} +} + +export { CalendarMonthList }; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/month-list/useCalendarMonthList.ts b/packages/x-date-pickers/src/internals/base/Calendar/month-list/useCalendarMonthList.ts new file mode 100644 index 0000000000000..b6383fb0d30b3 --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/Calendar/month-list/useCalendarMonthList.ts @@ -0,0 +1,44 @@ +import * as React from 'react'; +import { PickerValidDate } from '../../../../models'; +import { getMonthsInYear } from '../../../utils/date-utils'; +import { useUtils } from '../../../hooks/useUtils'; +import { useCalendarRootContext } from '../root/CalendarRootContext'; +import { GenericHTMLProps } from '../../utils/types'; +import { mergeReactProps } from '../../utils/mergeReactProps'; + +export function useCalendarMonthList(parameters: useCalendarMonthList.Parameters) { + const utils = useUtils(); + const calendarRoot = useCalendarRootContext(); + + const months = React.useMemo( + () => getMonthsInYear(utils, calendarRoot.value ?? calendarRoot.referenceDate), + [utils, calendarRoot.value, calendarRoot.referenceDate], + ); + + const getMonthListProps = React.useCallback( + ( + externalProps: Omit & { + children?: (parameters: useCalendarMonthList.ChildrenParameters) => React.ReactNode; + }, + ) => { + const { children, ...otherExternalProps } = externalProps; + return mergeReactProps(otherExternalProps, { + role: 'radiogroup', + children: children == null ? null : children({ months }), + }); + }, + [months], + ); + + return React.useMemo(() => ({ getMonthListProps }), [getMonthListProps]); +} + +export namespace useCalendarMonthList { + export interface Parameters { + children?: (parameters: ChildrenParameters) => React.ReactNode; + } + + export interface ChildrenParameters { + months: PickerValidDate[]; + } +} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRoot.tsx b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRoot.tsx index ad0ef7f33a72b..0787945151e4e 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRoot.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRoot.tsx @@ -1,17 +1,21 @@ 'use client'; import * as React from 'react'; +import { CalendarRootContext } from './CalendarRootContext'; +import { useCalendarRoot } from './useCalendarRoot'; -const CalendarRoot = React.forwardRef(function CalendarRoot( - props: CalendarRoot.Props, - forwardedRef: React.ForwardedRef, -) { - return
TEST
; -}); +const CalendarRoot: React.FC = function CalendarRoot(props) { + const { children } = props; + const { context } = useCalendarRoot(props); + + return {children}; +}; export namespace CalendarRoot { export interface State {} - export interface Props {} + export interface Props extends useCalendarRoot.Parameters { + children: React.ReactNode; + } } export { CalendarRoot }; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts index 28cfdbf1f64f0..8c01b95a89889 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts @@ -1,6 +1,11 @@ import * as React from 'react'; +import { PickerValidDate } from '../../../../models'; -export interface CalendarRootContext {} +export interface CalendarRootContext { + value: PickerValidDate | null; + selectMonth: (value: PickerValidDate) => void; + referenceDate: PickerValidDate; +} export const CalendarRootContext = React.createContext(undefined); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts index 1b4f6b98930fc..2bac14c872092 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts @@ -1,9 +1,16 @@ +import * as React from 'react'; +import useEventCallback from '@mui/utils/useEventCallback'; import { PickerValidDate, TimezoneProps } from '../../../../models'; import { useControlledValueWithTimezone } from '../../../hooks/useValueWithTimezone'; +import { useUtils } from '../../../hooks/useUtils'; +import { SECTION_TYPE_GRANULARITY } from '../../../utils/getDefaultReferenceDate'; import { singleItemValueManager } from '../../../utils/valueManagers'; +import { FormProps } from '../../../models'; +import { CalendarRootContext } from './CalendarRootContext'; -export function useCalendarRoot(parameters) { +export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { const { + readOnly, defaultValue, onValueChange, value: valueProp, @@ -11,6 +18,8 @@ export function useCalendarRoot(parameters) { referenceDate: referenceDateProp, } = parameters; + const utils = useUtils(); + const { value, handleValueChange, timezone } = useControlledValueWithTimezone({ name: 'CalendarRoot', timezone: timezoneProp, @@ -21,15 +30,41 @@ export function useCalendarRoot(parameters) { valueManager: singleItemValueManager, }); - const getRootProps = React.useCallback(() => { - return {}; - }, []); + const referenceDate = React.useMemo( + () => { + return singleItemValueManager.getInitialReferenceValue({ + value, + utils, + timezone, + // TODO: Add validation props. + props: {}, + referenceDate: referenceDateProp, + granularity: SECTION_TYPE_GRANULARITY.day, + }); + }, + // We want the `referenceDate` to update on prop and `timezone` change (https://github.com/mui/mui-x/issues/10804) + // eslint-disable-next-line react-hooks/exhaustive-deps + [referenceDateProp, timezone], + ); + + const selectMonth = useEventCallback((newValue: PickerValidDate) => { + if (readOnly) { + return; + } + + handleValueChange(newValue); + }); - return React.useMemo(() => ({})); + const context: CalendarRootContext = React.useMemo( + () => ({ value, selectMonth, referenceDate }), + [value, selectMonth, referenceDate], + ); + + return React.useMemo(() => ({ context }), [context]); } export namespace useCalendarRoot { - export interface Parameters extends TimezoneProps { + export interface Parameters extends TimezoneProps, FormProps { /** * The controlled value that should be selected. * @@ -47,11 +82,15 @@ export namespace useCalendarRoot { * Provides the new value as an argument. * @param {PickerValidDate | null} value The new selected value. */ - onValueChange: (value: PickerValidDate | null) => void; + onValueChange?: (value: PickerValidDate | null) => void; /** * The date used to generate the new value when both `value` and `defaultValue` are empty. * @default The closest valid date using the validation props, except callbacks such as `shouldDisableDate`. */ referenceDate?: PickerValidDate; } + + export interface ReturnValue { + context: CalendarRootContext; + } } diff --git a/packages/x-date-pickers/src/internals/base/utils/defaultRenderFunctions.tsx b/packages/x-date-pickers/src/internals/base/utils/defaultRenderFunctions.tsx new file mode 100644 index 0000000000000..416e96a123412 --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/utils/defaultRenderFunctions.tsx @@ -0,0 +1,40 @@ +import * as React from 'react'; + +export const defaultRenderFunctions = { + button: (props: React.ComponentPropsWithRef<'button'>) => { + return + + + + )} + {(activeSection === 'month' || activeSection === 'day') && ( +
+ + + +
+ )} ); } @@ -23,7 +67,7 @@ function Header(props: { activeSection: 'day' | 'month' | 'year' }) { export default function YearMonthDayCalendar() { const [value, setValue] = React.useState(null); const [activeSection, setActiveSection] = React.useState<'day' | 'month' | 'year'>( - 'year', + 'day', ); const handleValueChange = React.useCallback( @@ -45,7 +89,10 @@ export default function YearMonthDayCalendar() {
-
+
{activeSection === 'year' && ( {({ years }) => diff --git a/docs/data/date-pickers/base-calendar/calendar.module.css b/docs/data/date-pickers/base-calendar/calendar.module.css index 902e91a214ce8..4545ebf6491fa 100644 --- a/docs/data/date-pickers/base-calendar/calendar.module.css +++ b/docs/data/date-pickers/base-calendar/calendar.module.css @@ -1,13 +1,26 @@ .Root { border: 1px solid #78716c; width: 344px; - height: 320px; + height: 368px; display: flex; flex-direction: column; } .Header { + box-sizing: border-box; padding: 8px 12px; + height: 40px; + display: flex; + justify-content: space-between; +} + +.HeaderBlock { + display: flex; + gap: 4px; +} + +.HeaderMonthButton { + min-width: 96px; } .MonthsList, diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-list/useCalendarMonthsList.ts b/packages/x-date-pickers/src/internals/base/Calendar/months-list/useCalendarMonthsList.ts index 2b0c8f83319eb..b3c0b9970d131 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-list/useCalendarMonthsList.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-list/useCalendarMonthsList.ts @@ -16,8 +16,8 @@ export function useCalendarMonthsList(parameters: useCalendarMonthsList.Paramete const calendarMonthsCellRefs = React.useRef<(HTMLElement | null)[]>([]); const months = React.useMemo( - () => getMonthsInYear(utils, calendarRootContext.value ?? calendarRootContext.referenceDate), - [utils, calendarRootContext.value, calendarRootContext.referenceDate], + () => getMonthsInYear(utils, calendarRootContext.visibleDate), + [utils, calendarRootContext.visibleDate], ); const onKeyDown = useEventCallback((event: React.KeyboardEvent) => { diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts index 595bdd526aa38..5a367ade931eb 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts @@ -13,6 +13,7 @@ export interface CalendarRootContext { isDateDisabled: (day: PickerValidDate | null) => boolean; validationProps: ValidateDateProps; visibleDate: PickerValidDate; + setVisibleDate: (visibleDate: PickerValidDate) => void; } export const CalendarRootContext = React.createContext(undefined); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts index 6336bb56a1b8c..3cadea5dac5ea 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts @@ -113,6 +113,7 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { isDateDisabled, validationProps, visibleDate, + setVisibleDate, }), [ value, @@ -124,6 +125,7 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { isDateDisabled, validationProps, visibleDate, + setVisibleDate, ], ); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/useCalendarContext/useCalendarContext.ts b/packages/x-date-pickers/src/internals/base/Calendar/useCalendarContext/useCalendarContext.ts index 162f46cb27c7e..23f0a0ba81e27 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/useCalendarContext/useCalendarContext.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/useCalendarContext/useCalendarContext.ts @@ -6,5 +6,6 @@ export function useCalendarContext() { return { visibleDate: calendarRootContext.visibleDate, + setVisibleDate: calendarRootContext.setVisibleDate, }; } From 6be6fb8015b86657533a1bfbca59f7e49106606e Mon Sep 17 00:00:00 2001 From: flavien Date: Sun, 5 Jan 2025 14:07:38 +0100 Subject: [PATCH 018/136] Work --- .../date-pickers/base-calendar/DayCalendar.js | 98 +++++++++++++++++++ .../base-calendar/DayCalendar.tsx | 98 +++++++++++++++++++ .../base-calendar/YearMonthDayCalendar.js | 65 +++++++++++- .../base-calendar/YearMonthDayCalendar.tsx | 2 +- .../base-calendar/base-calendar.md | 6 +- .../base-calendar/calendar.module.css | 6 +- 6 files changed, 267 insertions(+), 8 deletions(-) create mode 100644 docs/data/date-pickers/base-calendar/DayCalendar.js create mode 100644 docs/data/date-pickers/base-calendar/DayCalendar.tsx diff --git a/docs/data/date-pickers/base-calendar/DayCalendar.js b/docs/data/date-pickers/base-calendar/DayCalendar.js new file mode 100644 index 0000000000000..b563d54a1dd8e --- /dev/null +++ b/docs/data/date-pickers/base-calendar/DayCalendar.js @@ -0,0 +1,98 @@ +import * as React from 'react'; + +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +// eslint-disable-next-line no-restricted-imports +import { + Calendar, + useCalendarContext, +} from '@mui/x-date-pickers/internals/base/Calendar'; +import styles from './calendar.module.css'; + +function Header() { + const { visibleDate, setVisibleDate } = useCalendarContext(); + + return ( +
+
+ + {visibleDate.format('MMMM')} + +
+
+ + {visibleDate.format('YYYY')} + +
+
+ ); +} + +export default function DayCalendar() { + const [value, setValue] = React.useState(null); + + return ( + + +
+
+ + + {({ days }) => + days.map((day) => ( + + )) + } + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => + days.map((day) => ( +
+ +
+ )) + } +
+ )) + } +
+
+
+
+
+ ); +} diff --git a/docs/data/date-pickers/base-calendar/DayCalendar.tsx b/docs/data/date-pickers/base-calendar/DayCalendar.tsx new file mode 100644 index 0000000000000..f8c2c3e8b9459 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/DayCalendar.tsx @@ -0,0 +1,98 @@ +import * as React from 'react'; +import { Dayjs } from 'dayjs'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +// eslint-disable-next-line no-restricted-imports +import { + Calendar, + useCalendarContext, +} from '@mui/x-date-pickers/internals/base/Calendar'; +import styles from './calendar.module.css'; + +function Header() { + const { visibleDate, setVisibleDate } = useCalendarContext(); + + return ( +
+
+ + {visibleDate.format('MMMM')} + +
+
+ + {visibleDate.format('YYYY')} + +
+
+ ); +} + +export default function DayCalendar() { + const [value, setValue] = React.useState(null); + + return ( + + +
+
+ + + {({ days }) => + days.map((day) => ( + + )) + } + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => + days.map((day) => ( +
+ +
+ )) + } +
+ )) + } +
+
+
+
+
+ ); +} diff --git a/docs/data/date-pickers/base-calendar/YearMonthDayCalendar.js b/docs/data/date-pickers/base-calendar/YearMonthDayCalendar.js index 8c3a46ca7b14a..76cbd683a97f9 100644 --- a/docs/data/date-pickers/base-calendar/YearMonthDayCalendar.js +++ b/docs/data/date-pickers/base-calendar/YearMonthDayCalendar.js @@ -3,9 +3,64 @@ import * as React from 'react'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports -import { Calendar } from '@mui/x-date-pickers/internals/base/Calendar'; +import { + Calendar, + useCalendarContext, +} from '@mui/x-date-pickers/internals/base/Calendar'; import styles from './calendar.module.css'; +function Header(props) { + const { activeSection, onActiveSectionChange } = props; + const { visibleDate, setVisibleDate } = useCalendarContext(); + + return ( +
+ {activeSection === 'day' && ( +
+ + + +
+ )} + {(activeSection === 'month' || activeSection === 'day') && ( +
+ + + +
+ )} +
+ ); +} + export default function YearMonthDayCalendar() { const [value, setValue] = React.useState(null); const [activeSection, setActiveSection] = React.useState('day'); @@ -24,9 +79,12 @@ export default function YearMonthDayCalendar() { return ( - +
-
Base UI Calendar
+
{activeSection === 'year' && ( {({ years }) => @@ -83,7 +141,6 @@ export default function YearMonthDayCalendar() {
)) diff --git a/docs/data/date-pickers/base-calendar/YearMonthDayCalendar.tsx b/docs/data/date-pickers/base-calendar/YearMonthDayCalendar.tsx index 3ae44d021d7e9..adc9c6ab8de78 100644 --- a/docs/data/date-pickers/base-calendar/YearMonthDayCalendar.tsx +++ b/docs/data/date-pickers/base-calendar/YearMonthDayCalendar.tsx @@ -29,7 +29,7 @@ function Header(props: { diff --git a/docs/data/date-pickers/base-calendar/base-calendar.md b/docs/data/date-pickers/base-calendar/base-calendar.md index c19e8cb29f0d7..af8ca67b4d578 100644 --- a/docs/data/date-pickers/base-calendar/base-calendar.md +++ b/docs/data/date-pickers/base-calendar/base-calendar.md @@ -8,6 +8,10 @@ packageName: '@mui/x-date-pickers'

POC of a Calendar component using the Base UI DX.

-## Basic usage +## Day view + +{{"demo": "DayCalendar.js"}} + +## Day, month and year view {{"demo": "YearMonthDayCalendar.js"}} diff --git a/docs/data/date-pickers/base-calendar/calendar.module.css b/docs/data/date-pickers/base-calendar/calendar.module.css index 4545ebf6491fa..1e0a394ea6db7 100644 --- a/docs/data/date-pickers/base-calendar/calendar.module.css +++ b/docs/data/date-pickers/base-calendar/calendar.module.css @@ -16,10 +16,12 @@ .HeaderBlock { display: flex; - gap: 4px; + gap: 8px; + flex-grow: 1; + justify-content: center; } -.HeaderMonthButton { +.HeaderMonthLabel { min-width: 96px; } From cc6d56dd30076041bcdc4b86c61ac79b6054da17 Mon Sep 17 00:00:00 2001 From: flavien Date: Sun, 5 Jan 2025 14:42:48 +0100 Subject: [PATCH 019/136] Add keyboard navigation across months --- .../base-calendar/DayCalendar.tsx | 2 +- .../base-calendar/base-calendar.md | 2 +- .../days-grid-body/useCalendarDaysGridBody.ts | 39 +++++- .../days-week-row/useCalendarDaysWeekRow.ts | 3 +- .../base/Calendar/utils/keyboardNavigation.ts | 124 ++++++++++++++++-- 5 files changed, 155 insertions(+), 15 deletions(-) diff --git a/docs/data/date-pickers/base-calendar/DayCalendar.tsx b/docs/data/date-pickers/base-calendar/DayCalendar.tsx index f8c2c3e8b9459..0c1cd69205c67 100644 --- a/docs/data/date-pickers/base-calendar/DayCalendar.tsx +++ b/docs/data/date-pickers/base-calendar/DayCalendar.tsx @@ -53,7 +53,7 @@ export default function DayCalendar() { return ( - +
diff --git a/docs/data/date-pickers/base-calendar/base-calendar.md b/docs/data/date-pickers/base-calendar/base-calendar.md index af8ca67b4d578..fb8d7efae15ef 100644 --- a/docs/data/date-pickers/base-calendar/base-calendar.md +++ b/docs/data/date-pickers/base-calendar/base-calendar.md @@ -14,4 +14,4 @@ packageName: '@mui/x-date-pickers' ## Day, month and year view -{{"demo": "YearMonthDayCalendar.js"}} + diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-grid-body/useCalendarDaysGridBody.ts b/packages/x-date-pickers/src/internals/base/Calendar/days-grid-body/useCalendarDaysGridBody.ts index 0fdb6af37d8f0..3f0d931f9f453 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-grid-body/useCalendarDaysGridBody.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-grid-body/useCalendarDaysGridBody.ts @@ -1,14 +1,24 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; +import useTimeout from '@mui/utils/useTimeout'; import { PickerValidDate } from '../../../../models'; +import { useUtils } from '../../../hooks/useUtils'; import { useCalendarDaysGridContext } from '../days-grid/CalendarDaysGridContext'; import { mergeReactProps } from '../../utils/mergeReactProps'; import { GenericHTMLProps } from '../../utils/types'; -import { navigateInGrid } from '../utils/keyboardNavigation'; +import { + applyInitialFocusInGrid, + navigateInGrid, + NavigateInGridChangePage, + PageNavigationTarget, +} from '../utils/keyboardNavigation'; import { CalendarDaysGridBodyContext } from './CalendarDaysGridBodyContext'; +import { useCalendarRootContext } from '../root/CalendarRootContext'; export function useCalendarDaysGridBody(parameters: useCalendarDaysGridBody.Parameters) { const { children } = parameters; + const utils = useUtils(); + const calendarRootContext = useCalendarRootContext(); const calendarDaysGridContext = useCalendarDaysGridContext(); const calendarWeekRowRefs = React.useRef<(HTMLElement | null)[]>([]); const calendarWeekRowsCellsRef = React.useRef< @@ -17,13 +27,40 @@ export function useCalendarDaysGridBody(parameters: useCalendarDaysGridBody.Para cellsRef: React.RefObject<(HTMLElement | null)[]>; }[] >([]); + const pageNavigationTargetRef = React.useRef(null); + + const timeout = useTimeout(); + React.useEffect(() => { + if (pageNavigationTargetRef.current) { + const target = pageNavigationTargetRef.current; + timeout.start(0, () => { + applyInitialFocusInGrid({ + rows: calendarWeekRowRefs.current, + rowsCells: calendarWeekRowsCellsRef.current, + target, + }); + }); + } + }, [calendarRootContext.visibleDate, timeout]); const onKeyDown = useEventCallback((event: React.KeyboardEvent) => { + const changePage: NavigateInGridChangePage = (params) => { + if (params.direction === 'next') { + calendarRootContext.setVisibleDate(utils.addMonths(calendarRootContext.visibleDate, 1)); + } + if (params.direction === 'previous') { + calendarRootContext.setVisibleDate(utils.addMonths(calendarRootContext.visibleDate, -1)); + } + + pageNavigationTargetRef.current = params.target; + }; + navigateInGrid({ rows: calendarWeekRowRefs.current, rowsCells: calendarWeekRowsCellsRef.current, target: event.target as HTMLElement, event, + changePage, }); }); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/useCalendarDaysWeekRow.ts b/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/useCalendarDaysWeekRow.ts index f7538db5c8b82..e4bd25d23afbb 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/useCalendarDaysWeekRow.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/useCalendarDaysWeekRow.ts @@ -1,4 +1,5 @@ import * as React from 'react'; +import useEnhancedEffect from '@mui/utils/useEnhancedEffect'; import { PickerValidDate } from '../../../../models'; import { GenericHTMLProps } from '../../utils/types'; import { mergeReactProps } from '../../utils/mergeReactProps'; @@ -22,7 +23,7 @@ export function useCalendarDaysWeekRow(parameters: useCalendarDaysWeekRow.Parame ); const registerWeekRowCells = ctx.registerWeekRowCells; - React.useEffect(() => { + useEnhancedEffect(() => { return registerWeekRowCells(ref, calendarDayCellRefs); }, [registerWeekRowCells]); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts index 8b85429fb015f..07cc0ae4a6c22 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts @@ -1,3 +1,6 @@ +import { ta } from 'date-fns/locale'; +import { get } from 'http'; + const LIST_NAVIGATION_SUPPORTED_KEYS = ['ArrowDown', 'ArrowUp', 'Home', 'End']; export function navigateInList({ @@ -68,26 +71,16 @@ const GRID_NAVIGATION_SUPPORTED_KEYS = [ 'End', ]; -export function navigateInGrid({ +function getCellsInGrid({ rows, rowsCells, - target, - event, }: { rows: (HTMLElement | null)[]; rowsCells: { rowRef: React.RefObject; cellsRef: React.RefObject<(HTMLElement | null)[]>; }[]; - target: HTMLElement; - event: React.KeyboardEvent; }) { - if (!GRID_NAVIGATION_SUPPORTED_KEYS.includes(event.key)) { - return; - } - - event.preventDefault(); - const cells: HTMLElement[][] = []; for (let i = 0; i < rows.length; i += 1) { const row = rows[i]; @@ -99,6 +92,32 @@ export function navigateInGrid({ } } + return cells; +} + +export function navigateInGrid({ + rows, + rowsCells, + target, + event, + changePage, +}: { + rows: (HTMLElement | null)[]; + rowsCells: { + rowRef: React.RefObject; + cellsRef: React.RefObject<(HTMLElement | null)[]>; + }[]; + target: HTMLElement; + event: React.KeyboardEvent; + changePage: NavigateInGridChangePage | undefined; +}) { + if (!GRID_NAVIGATION_SUPPORTED_KEYS.includes(event.key)) { + return; + } + + event.preventDefault(); + + const cells = getCellsInGrid({ rows, rowsCells }); if (cells.length === 0) { return; } @@ -108,7 +127,16 @@ export function navigateInGrid({ const currentColIndex = cells[currentRowIndex].indexOf(target); let nextRowIndex = -1; let i = currentRowIndex + 1; + while (nextRowIndex === -1 && i < currentRowIndex + cells.length) { + if (changePage && i > cells.length - 1) { + changePage({ + direction: 'next', + target: { type: 'first-cell-in-col', colIndex: currentColIndex }, + }); + break; + } + const rowIndex = i % cells.length; const cell = cells[rowIndex][currentColIndex]; if (isNavigable(cell)) { @@ -128,6 +156,14 @@ export function navigateInGrid({ let nextRowIndex = -1; let i = currentRowIndex - 1; while (nextRowIndex === -1 && i > currentRowIndex - cells.length) { + if (changePage && i < 0) { + changePage({ + direction: 'previous', + target: { type: 'last-cell-in-col', colIndex: currentColIndex }, + }); + break; + } + const rowIndex = (cells.length + i) % cells.length; const cell = cells[rowIndex][currentColIndex]; if (isNavigable(cell)) { @@ -148,6 +184,14 @@ export function navigateInGrid({ let i = currentCellIndex + 1; while (nextCellIndex === -1 && i < currentCellIndex + flatCells.length) { + if (changePage && i > flatCells.length - 1) { + changePage({ + direction: 'previous', + target: { type: 'first-cell' }, + }); + break; + } + const cellIndex = i % flatCells.length; const cell = flatCells[cellIndex]; if (isNavigable(cell)) { @@ -168,6 +212,14 @@ export function navigateInGrid({ let i = currentCellIndex - 1; while (nextCellIndex === -1 && i > currentCellIndex - flatCells.length) { + if (changePage && i < 0) { + changePage({ + direction: 'previous', + target: { type: 'last-cell' }, + }); + break; + } + const cellIndex = (flatCells.length + i) % flatCells.length; const cell = flatCells[cellIndex]; if (isNavigable(cell)) { @@ -221,6 +273,56 @@ export function navigateInGrid({ } } +export type PageNavigationTarget = + | { type: 'first-cell' } + | { type: 'last-cell' } + | { type: 'first-cell-in-col'; colIndex: number } + | { type: 'last-cell-in-col'; colIndex: number } + | { type: 'first-cell-in-row'; rowIndex: number } + | { type: 'last-cell-in-row'; rowIndex: number }; + +export type NavigateInGridChangePage = (params: { + direction: 'next' | 'previous'; + target: PageNavigationTarget; +}) => void; + +export function applyInitialFocusInGrid({ + rows, + rowsCells, + target, +}: { + rows: (HTMLElement | null)[]; + rowsCells: { + rowRef: React.RefObject; + cellsRef: React.RefObject<(HTMLElement | null)[]>; + }[]; + target: PageNavigationTarget; +}) { + console.log(rows); + const cells = getCellsInGrid({ rows, rowsCells }); + let cell: HTMLElement | undefined; + + if (target.type === 'first-cell') { + cell = cells.flat().find(isNavigable); + } + + if (target.type === 'last-cell') { + cell = cells.flat().findLast(isNavigable); + } + + if (target.type === 'first-cell-in-col') { + cell = cells.map((row) => row[target.colIndex]).find(isNavigable); + } + + if (target.type === 'last-cell-in-col') { + cell = cells.map((row) => row[target.colIndex]).findLast(isNavigable); + } + + if (cell) { + cell.focus(); + } +} + function isNavigable(element: HTMLElement | null): element is HTMLElement { if (element === null) { return false; From d137663f8203757a4a0cf4c5895e4d52ea5f0db1 Mon Sep 17 00:00:00 2001 From: flavien Date: Sun, 5 Jan 2025 14:45:15 +0100 Subject: [PATCH 020/136] Fix --- .../base/Calendar/days-grid-body/useCalendarDaysGridBody.ts | 1 + .../base/Calendar/days-week-row/useCalendarDaysWeekRow.ts | 3 +-- .../src/internals/base/Calendar/utils/keyboardNavigation.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-grid-body/useCalendarDaysGridBody.ts b/packages/x-date-pickers/src/internals/base/Calendar/days-grid-body/useCalendarDaysGridBody.ts index 3f0d931f9f453..040947bca6529 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-grid-body/useCalendarDaysGridBody.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-grid-body/useCalendarDaysGridBody.ts @@ -45,6 +45,7 @@ export function useCalendarDaysGridBody(parameters: useCalendarDaysGridBody.Para const onKeyDown = useEventCallback((event: React.KeyboardEvent) => { const changePage: NavigateInGridChangePage = (params) => { + // TODO: Jump over months with no valid date. if (params.direction === 'next') { calendarRootContext.setVisibleDate(utils.addMonths(calendarRootContext.visibleDate, 1)); } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/useCalendarDaysWeekRow.ts b/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/useCalendarDaysWeekRow.ts index e4bd25d23afbb..f7538db5c8b82 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/useCalendarDaysWeekRow.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/useCalendarDaysWeekRow.ts @@ -1,5 +1,4 @@ import * as React from 'react'; -import useEnhancedEffect from '@mui/utils/useEnhancedEffect'; import { PickerValidDate } from '../../../../models'; import { GenericHTMLProps } from '../../utils/types'; import { mergeReactProps } from '../../utils/mergeReactProps'; @@ -23,7 +22,7 @@ export function useCalendarDaysWeekRow(parameters: useCalendarDaysWeekRow.Parame ); const registerWeekRowCells = ctx.registerWeekRowCells; - useEnhancedEffect(() => { + React.useEffect(() => { return registerWeekRowCells(ref, calendarDayCellRefs); }, [registerWeekRowCells]); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts index 07cc0ae4a6c22..39ed93d03ee69 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts @@ -186,7 +186,7 @@ export function navigateInGrid({ while (nextCellIndex === -1 && i < currentCellIndex + flatCells.length) { if (changePage && i > flatCells.length - 1) { changePage({ - direction: 'previous', + direction: 'next', target: { type: 'first-cell' }, }); break; From 3bc4ff6bdf253c8a2ff4a5bc89130399f5756357 Mon Sep 17 00:00:00 2001 From: flavien Date: Sun, 5 Jan 2025 14:47:12 +0100 Subject: [PATCH 021/136] Work --- docs/data/date-pickers/base-calendar/base-calendar.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/data/date-pickers/base-calendar/base-calendar.md b/docs/data/date-pickers/base-calendar/base-calendar.md index fb8d7efae15ef..af8ca67b4d578 100644 --- a/docs/data/date-pickers/base-calendar/base-calendar.md +++ b/docs/data/date-pickers/base-calendar/base-calendar.md @@ -14,4 +14,4 @@ packageName: '@mui/x-date-pickers' ## Day, month and year view - +{{"demo": "YearMonthDayCalendar.js"}} From 35aec85eba902bb72ef288c94e9afc4189e78e51 Mon Sep 17 00:00:00 2001 From: flavien Date: Sun, 5 Jan 2025 14:49:54 +0100 Subject: [PATCH 022/136] Work --- .../src/internals/base/composite/composite.ts | 372 ------------------ .../base/composite/item/CompositeItem.tsx | 84 ---- .../base/composite/item/useCompositeItem.ts | 35 -- .../base/composite/root/CompositeRoot.tsx | 167 -------- .../composite/root/CompositeRootContext.ts | 26 -- .../base/composite/root/useCompositeRoot.ts | 276 ------------- 6 files changed, 960 deletions(-) delete mode 100644 packages/x-date-pickers/src/internals/base/composite/composite.ts delete mode 100644 packages/x-date-pickers/src/internals/base/composite/item/CompositeItem.tsx delete mode 100644 packages/x-date-pickers/src/internals/base/composite/item/useCompositeItem.ts delete mode 100644 packages/x-date-pickers/src/internals/base/composite/root/CompositeRoot.tsx delete mode 100644 packages/x-date-pickers/src/internals/base/composite/root/CompositeRootContext.ts delete mode 100644 packages/x-date-pickers/src/internals/base/composite/root/useCompositeRoot.ts diff --git a/packages/x-date-pickers/src/internals/base/composite/composite.ts b/packages/x-date-pickers/src/internals/base/composite/composite.ts deleted file mode 100644 index b6760d8647dbb..0000000000000 --- a/packages/x-date-pickers/src/internals/base/composite/composite.ts +++ /dev/null @@ -1,372 +0,0 @@ -import * as React from 'react'; -import { hasComputedStyleMapSupport } from '../utils/hasComputedStyleMapSupport'; -import { ownerWindow } from '../utils/owner'; -import type { TextDirection } from '../direction-provider/DirectionContext'; - -export interface Dimensions { - width: number; - height: number; -} - -export const ARROW_UP = 'ArrowUp'; -export const ARROW_DOWN = 'ArrowDown'; -export const ARROW_LEFT = 'ArrowLeft'; -export const ARROW_RIGHT = 'ArrowRight'; -export const HOME = 'Home'; -export const END = 'End'; - -export const HORIZONTAL_KEYS = [ARROW_LEFT, ARROW_RIGHT]; -export const HORIZONTAL_KEYS_WITH_EXTRA_KEYS = [ARROW_LEFT, ARROW_RIGHT, HOME, END]; -export const VERTICAL_KEYS = [ARROW_UP, ARROW_DOWN]; -export const VERTICAL_KEYS_WITH_EXTRA_KEYS = [ARROW_UP, ARROW_DOWN, HOME, END]; -export const ARROW_KEYS = [...HORIZONTAL_KEYS, ...VERTICAL_KEYS]; -export const ALL_KEYS = [...ARROW_KEYS, HOME, END]; - -function stopEvent(event: Event | React.SyntheticEvent) { - event.preventDefault(); - event.stopPropagation(); -} - -export function isDifferentRow(index: number, cols: number, prevRow: number) { - return Math.floor(index / cols) !== prevRow; -} - -export function isIndexOutOfBounds( - listRef: React.MutableRefObject>, - index: number, -) { - return index < 0 || index >= listRef.current.length; -} - -export function getMinIndex( - listRef: React.MutableRefObject>, - disabledIndices: Array | undefined, -) { - return findNonDisabledIndex(listRef, { disabledIndices }); -} - -export function getMaxIndex( - listRef: React.MutableRefObject>, - disabledIndices: Array | undefined, -) { - return findNonDisabledIndex(listRef, { - decrement: true, - startingIndex: listRef.current.length, - disabledIndices, - }); -} - -export function findNonDisabledIndex( - listRef: React.MutableRefObject>, - { - startingIndex = -1, - decrement = false, - disabledIndices, - amount = 1, - }: { - startingIndex?: number; - decrement?: boolean; - disabledIndices?: Array; - amount?: number; - } = {}, -): number { - const list = listRef.current; - - let index = startingIndex; - do { - index += decrement ? -amount : amount; - } while (index >= 0 && index <= list.length - 1 && isDisabled(list, index, disabledIndices)); - - return index; -} - -export function getGridNavigatedIndex( - elementsRef: React.MutableRefObject>, - { - event, - orientation, - loop, - cols, - disabledIndices, - minIndex, - maxIndex, - prevIndex, - rtl, - stopEvent: stop = false, - }: { - event: React.KeyboardEvent; - orientation: 'horizontal' | 'vertical' | 'both'; - loop: boolean; - cols: number; - disabledIndices: Array | undefined; - minIndex: number; - maxIndex: number; - prevIndex: number; - rtl: boolean; - stopEvent?: boolean; - }, -) { - let nextIndex = prevIndex; - - if (event.key === ARROW_UP) { - if (stop) { - stopEvent(event); - } - - if (prevIndex === -1) { - nextIndex = maxIndex; - } else { - nextIndex = findNonDisabledIndex(elementsRef, { - startingIndex: nextIndex, - amount: cols, - decrement: true, - disabledIndices, - }); - - if (loop && (prevIndex - cols < minIndex || nextIndex < 0)) { - const col = prevIndex % cols; - const maxCol = maxIndex % cols; - const offset = maxIndex - (maxCol - col); - - if (maxCol === col) { - nextIndex = maxIndex; - } else { - nextIndex = maxCol > col ? offset : offset - cols; - } - } - } - - if (isIndexOutOfBounds(elementsRef, nextIndex)) { - nextIndex = prevIndex; - } - } - - if (event.key === ARROW_DOWN) { - if (stop) { - stopEvent(event); - } - - if (prevIndex === -1) { - nextIndex = minIndex; - } else { - nextIndex = findNonDisabledIndex(elementsRef, { - startingIndex: prevIndex, - amount: cols, - disabledIndices, - }); - - if (loop && prevIndex + cols > maxIndex) { - nextIndex = findNonDisabledIndex(elementsRef, { - startingIndex: (prevIndex % cols) - cols, - amount: cols, - disabledIndices, - }); - } - } - - if (isIndexOutOfBounds(elementsRef, nextIndex)) { - nextIndex = prevIndex; - } - } - - // Remains on the same row/column. - if (orientation === 'both') { - const nextKey = rtl ? ARROW_LEFT : ARROW_RIGHT; - const prevKey = rtl ? ARROW_RIGHT : ARROW_LEFT; - - const prevRow = Math.floor(prevIndex / cols); - - if (event.key === nextKey) { - if (stop) { - stopEvent(event); - } - - if (prevIndex % cols !== cols - 1) { - nextIndex = findNonDisabledIndex(elementsRef, { - startingIndex: prevIndex, - disabledIndices, - }); - - if (loop && isDifferentRow(nextIndex, cols, prevRow)) { - nextIndex = findNonDisabledIndex(elementsRef, { - startingIndex: prevIndex - (prevIndex % cols) - 1, - disabledIndices, - }); - } - } else if (loop) { - nextIndex = findNonDisabledIndex(elementsRef, { - startingIndex: prevIndex - (prevIndex % cols) - 1, - disabledIndices, - }); - } - - if (isDifferentRow(nextIndex, cols, prevRow)) { - nextIndex = prevIndex; - } - } - - if (event.key === prevKey) { - if (stop) { - stopEvent(event); - } - - if (prevIndex % cols !== 0) { - nextIndex = findNonDisabledIndex(elementsRef, { - startingIndex: prevIndex, - decrement: true, - disabledIndices, - }); - - if (loop && isDifferentRow(nextIndex, cols, prevRow)) { - nextIndex = findNonDisabledIndex(elementsRef, { - startingIndex: prevIndex + (cols - (prevIndex % cols)), - decrement: true, - disabledIndices, - }); - } - } else if (loop) { - nextIndex = findNonDisabledIndex(elementsRef, { - startingIndex: prevIndex + (cols - (prevIndex % cols)), - decrement: true, - disabledIndices, - }); - } - - if (isDifferentRow(nextIndex, cols, prevRow)) { - nextIndex = prevIndex; - } - } - - const lastRow = Math.floor(maxIndex / cols) === prevRow; - - if (isIndexOutOfBounds(elementsRef, nextIndex)) { - if (loop && lastRow) { - nextIndex = - event.key === prevKey - ? maxIndex - : findNonDisabledIndex(elementsRef, { - startingIndex: prevIndex - (prevIndex % cols) - 1, - disabledIndices, - }); - } else { - nextIndex = prevIndex; - } - } - } - - return nextIndex; -} - -/** For each cell index, gets the item index that occupies that cell */ -export function buildCellMap( - sizes: Array<{ width: number; height: number }>, - cols: number, - dense: boolean, -) { - const cellMap: (number | undefined)[] = []; - let startIndex = 0; - - sizes.forEach(({ width, height }, index) => { - if (width > cols) { - if (process.env.NODE_ENV !== 'production') { - throw new Error( - `[Base UI]: Invalid grid - item width at index ${index} is greater than grid columns`, - ); - } - } - - let itemPlaced = false; - if (dense) { - startIndex = 0; - } - while (!itemPlaced) { - const targetCells: number[] = []; - for (let i = 0; i < width; i += 1) { - for (let j = 0; j < height; j += 1) { - targetCells.push(startIndex + i + j * cols); - } - } - if ( - (startIndex % cols) + width <= cols && - targetCells.every((cell) => cellMap[cell] == null) - ) { - targetCells.forEach((cell) => { - cellMap[cell] = index; - }); - itemPlaced = true; - } else { - startIndex += 1; - } - } - }); - - // convert into a non-sparse array - return [...cellMap]; -} - -/** Gets cell index of an item's corner or -1 when index is -1. */ -export function getCellIndexOfCorner( - index: number, - sizes: Dimensions[], - cellMap: (number | undefined)[], - cols: number, - corner: 'tl' | 'tr' | 'bl' | 'br', -) { - if (index === -1) { - return -1; - } - - const firstCellIndex = cellMap.indexOf(index); - const sizeItem = sizes[index]; - - switch (corner) { - case 'tl': - return firstCellIndex; - case 'tr': - if (!sizeItem) { - return firstCellIndex; - } - return firstCellIndex + sizeItem.width - 1; - case 'bl': - if (!sizeItem) { - return firstCellIndex; - } - return firstCellIndex + (sizeItem.height - 1) * cols; - case 'br': - return cellMap.lastIndexOf(index); - default: - return -1; - } -} - -/** Gets all cell indices that correspond to the specified indices */ -export function getCellIndices(indices: (number | undefined)[], cellMap: (number | undefined)[]) { - return cellMap.flatMap((index, cellIndex) => (indices.includes(index) ? [cellIndex] : [])); -} - -export function isDisabled( - list: Array, - index: number, - disabledIndices?: Array, -) { - if (disabledIndices) { - return disabledIndices.includes(index); - } - - const element = list[index]; - return ( - element == null || - element.hasAttribute('disabled') || - element.getAttribute('aria-disabled') === 'true' - ); -} - -export function getTextDirection(element: HTMLElement): TextDirection { - if (hasComputedStyleMapSupport()) { - const direction = element.computedStyleMap().get('direction'); - - return (direction as CSSKeywordValue)?.value as TextDirection; - } - - return ownerWindow(element).getComputedStyle(element).direction as TextDirection; -} diff --git a/packages/x-date-pickers/src/internals/base/composite/item/CompositeItem.tsx b/packages/x-date-pickers/src/internals/base/composite/item/CompositeItem.tsx deleted file mode 100644 index 6fbd1f9cd09e8..0000000000000 --- a/packages/x-date-pickers/src/internals/base/composite/item/CompositeItem.tsx +++ /dev/null @@ -1,84 +0,0 @@ -'use client'; -import * as React from 'react'; -import PropTypes from 'prop-types'; -import { useComponentRenderer } from '../../utils/useComponentRenderer'; -import { useForkRef } from '../../utils/useForkRef'; -import { useCompositeRootContext } from '../root/CompositeRootContext'; -import { useCompositeItem } from './useCompositeItem'; -import { refType } from '../../utils/proptypes'; -import type { BaseUIComponentProps } from '../../utils/types'; - -/** - * @ignore - internal component. - */ -function CompositeItem(props: CompositeItem.Props) { - const { render, className, itemRef, metadata, ...otherProps } = props; - - const { highlightedIndex } = useCompositeRootContext(); - const { getItemProps, ref, index } = useCompositeItem({ metadata }); - - const state: CompositeItem.State = React.useMemo( - () => ({ - highlighted: index === highlightedIndex, - }), - [index, highlightedIndex], - ); - - const mergedRef = useForkRef(itemRef, ref); - - const { renderElement } = useComponentRenderer({ - propGetter: getItemProps, - ref: mergedRef, - render: render ?? 'div', - state, - className, - extraProps: otherProps, - }); - - return renderElement(); -} - -namespace CompositeItem { - export interface State { - highlighted: boolean; - } - - export interface Props extends Omit, 'itemRef'> { - // the itemRef name collides with https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/itemref - itemRef?: React.RefObject; - metadata?: Metadata; - } -} - -export { CompositeItem }; - -CompositeItem.propTypes /* remove-proptypes */ = { - // ┌────────────────────────────── Warning ──────────────────────────────┐ - // │ These PropTypes are generated from the TypeScript type definitions. │ - // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ - // └─────────────────────────────────────────────────────────────────────┘ - /** - * @ignore - */ - children: PropTypes.node, - /** - * CSS class applied to the element, or a function that - * returns a class based on the component’s state. - */ - className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), - /** - * @ignore - */ - itemRef: refType, - /** - * @ignore - */ - metadata: PropTypes.any, - /** - * Allows you to replace the component’s HTML element - * with a different tag, or compose it with another component. - * - * Accepts a `ReactElement` or a function that returns the element to render. - */ - render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), -} as any; diff --git a/packages/x-date-pickers/src/internals/base/composite/item/useCompositeItem.ts b/packages/x-date-pickers/src/internals/base/composite/item/useCompositeItem.ts deleted file mode 100644 index bf19457ccb23f..0000000000000 --- a/packages/x-date-pickers/src/internals/base/composite/item/useCompositeItem.ts +++ /dev/null @@ -1,35 +0,0 @@ -'use client'; -import * as React from 'react'; -import { useCompositeRootContext } from '../root/CompositeRootContext'; -import { useCompositeListItem } from '../list/useCompositeListItem'; -import { mergeReactProps } from '../../utils/mergeReactProps'; - -export interface UseCompositeItemParameters { - metadata?: Metadata; -} - -export function useCompositeItem(params: UseCompositeItemParameters = {}) { - const { highlightedIndex, onHighlightedIndexChange } = useCompositeRootContext(); - const { ref, index } = useCompositeListItem(params); - const isHighlighted = highlightedIndex === index; - - const getItemProps = React.useCallback( - (externalProps = {}) => - mergeReactProps<'div'>(externalProps, { - tabIndex: isHighlighted ? 0 : -1, - onFocus() { - onHighlightedIndexChange(index); - }, - }), - [isHighlighted, index, onHighlightedIndexChange], - ); - - return React.useMemo( - () => ({ - getItemProps, - ref, - index, - }), - [getItemProps, ref, index], - ); -} diff --git a/packages/x-date-pickers/src/internals/base/composite/root/CompositeRoot.tsx b/packages/x-date-pickers/src/internals/base/composite/root/CompositeRoot.tsx deleted file mode 100644 index dbea9b691a24e..0000000000000 --- a/packages/x-date-pickers/src/internals/base/composite/root/CompositeRoot.tsx +++ /dev/null @@ -1,167 +0,0 @@ -'use client'; -import * as React from 'react'; -import PropTypes from 'prop-types'; -import type { TextDirection } from '@base-ui-components/react/direction-provider'; -import { CompositeList, type CompositeMetadata } from '../list/CompositeList'; -import { useCompositeRoot } from './useCompositeRoot'; -import { CompositeRootContext } from './CompositeRootContext'; -import { refType } from '../../utils/proptypes'; -import { useComponentRenderer } from '../../utils/useComponentRenderer'; -import type { BaseUIComponentProps } from '../../utils/types'; -import type { Dimensions } from '../composite'; - -/** - * @ignore - internal component. - */ -function CompositeRoot(props: CompositeRoot.Props) { - const { - render, - className, - highlightedIndex: highlightedIndexProp, - onHighlightedIndexChange: onHighlightedIndexChangeProp, - orientation, - dense, - itemSizes, - loop, - cols, - direction, - enableHomeAndEndKeys, - onMapChange, - stopEventPropagation, - rootRef, - ...otherProps - } = props; - - const { getRootProps, highlightedIndex, onHighlightedIndexChange, elementsRef } = - useCompositeRoot({ - itemSizes, - cols, - loop, - dense, - orientation, - highlightedIndex: highlightedIndexProp, - onHighlightedIndexChange: onHighlightedIndexChangeProp, - rootRef, - stopEventPropagation, - enableHomeAndEndKeys, - direction, - }); - - const { renderElement } = useComponentRenderer({ - propGetter: getRootProps, - render: render ?? 'div', - state: {}, - className, - extraProps: otherProps, - }); - - const contextValue: CompositeRootContext = React.useMemo( - () => ({ highlightedIndex, onHighlightedIndexChange }), - [highlightedIndex, onHighlightedIndexChange], - ); - - return ( - - elementsRef={elementsRef} onMapChange={onMapChange}> - {renderElement()} - - - ); -} - -namespace CompositeRoot { - export interface State {} - - export interface Props extends BaseUIComponentProps<'div', State> { - orientation?: 'horizontal' | 'vertical' | 'both'; - cols?: number; - loop?: boolean; - highlightedIndex?: number; - onHighlightedIndexChange?: (index: number) => void; - itemSizes?: Dimensions[]; - dense?: boolean; - direction?: TextDirection; - enableHomeAndEndKeys?: boolean; - onMapChange?: (newMap: Map | null>) => void; - stopEventPropagation?: boolean; - rootRef?: React.RefObject; - } -} - -export { CompositeRoot }; - -CompositeRoot.propTypes /* remove-proptypes */ = { - // ┌────────────────────────────── Warning ──────────────────────────────┐ - // │ These PropTypes are generated from the TypeScript type definitions. │ - // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ - // └─────────────────────────────────────────────────────────────────────┘ - /** - * @ignore - */ - children: PropTypes.node, - /** - * CSS class applied to the element, or a function that - * returns a class based on the component’s state. - */ - className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), - /** - * @ignore - */ - cols: PropTypes.number, - /** - * @ignore - */ - dense: PropTypes.bool, - /** - * @ignore - */ - direction: PropTypes.oneOf(['ltr', 'rtl']), - /** - * @ignore - */ - enableHomeAndEndKeys: PropTypes.bool, - /** - * @ignore - */ - highlightedIndex: PropTypes.number, - /** - * @ignore - */ - itemSizes: PropTypes.arrayOf( - PropTypes.shape({ - height: PropTypes.number.isRequired, - width: PropTypes.number.isRequired, - }), - ), - /** - * @ignore - */ - loop: PropTypes.bool, - /** - * @ignore - */ - onHighlightedIndexChange: PropTypes.func, - /** - * @ignore - */ - onMapChange: PropTypes.func, - /** - * @ignore - */ - orientation: PropTypes.oneOf(['both', 'horizontal', 'vertical']), - /** - * Allows you to replace the component’s HTML element - * with a different tag, or compose it with another component. - * - * Accepts a `ReactElement` or a function that returns the element to render. - */ - render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), - /** - * @ignore - */ - rootRef: refType, - /** - * @ignore - */ - stopEventPropagation: PropTypes.bool, -} as any; diff --git a/packages/x-date-pickers/src/internals/base/composite/root/CompositeRootContext.ts b/packages/x-date-pickers/src/internals/base/composite/root/CompositeRootContext.ts deleted file mode 100644 index 347ec9d1a7f3f..0000000000000 --- a/packages/x-date-pickers/src/internals/base/composite/root/CompositeRootContext.ts +++ /dev/null @@ -1,26 +0,0 @@ -'use client'; -import * as React from 'react'; - -export interface CompositeRootContext { - highlightedIndex: number; - onHighlightedIndexChange: (index: number) => void; -} - -export const CompositeRootContext = React.createContext( - undefined, -); - -if (process.env.NODE_ENV !== 'production') { - CompositeRootContext.displayName = 'CompositeRootContext'; -} - -export function useCompositeRootContext() { - const context = React.useContext(CompositeRootContext); - if (context === undefined) { - throw new Error( - 'Base UI: CompositeRootContext is missing. Composite parts must be placed within .', - ); - } - - return context; -} diff --git a/packages/x-date-pickers/src/internals/base/composite/root/useCompositeRoot.ts b/packages/x-date-pickers/src/internals/base/composite/root/useCompositeRoot.ts deleted file mode 100644 index 0945e669cdc9d..0000000000000 --- a/packages/x-date-pickers/src/internals/base/composite/root/useCompositeRoot.ts +++ /dev/null @@ -1,276 +0,0 @@ -'use client'; -import * as React from 'react'; -import type { TextDirection } from '@base-ui-components/react/direction-provider'; -import { mergeReactProps } from '../../utils/mergeReactProps'; -import { useEventCallback } from '../../utils/useEventCallback'; -import { useForkRef } from '../../utils/useForkRef'; -import { - ALL_KEYS, - ARROW_KEYS, - ARROW_DOWN, - ARROW_LEFT, - ARROW_RIGHT, - ARROW_UP, - HOME, - END, - buildCellMap, - findNonDisabledIndex, - getCellIndexOfCorner, - getCellIndices, - getGridNavigatedIndex, - getMaxIndex, - getMinIndex, - getTextDirection, - HORIZONTAL_KEYS, - HORIZONTAL_KEYS_WITH_EXTRA_KEYS, - isDisabled, - isIndexOutOfBounds, - VERTICAL_KEYS, - VERTICAL_KEYS_WITH_EXTRA_KEYS, - type Dimensions, -} from '../composite'; - -export interface UseCompositeRootParameters { - orientation?: 'horizontal' | 'vertical' | 'both'; - cols?: number; - loop?: boolean; - highlightedIndex?: number; - onHighlightedIndexChange?: (index: number) => void; - dense?: boolean; - direction?: TextDirection; - itemSizes?: Array; - rootRef?: React.Ref; - /** - * When `true`, pressing the Home key moves focus to the first item, - * and pressing the End key moves focus to the last item. - * @default false - */ - enableHomeAndEndKeys?: boolean; - /** - * When `true`, keypress events on Composite's navigation keys - * be stopped with event.stopPropagation() - * @default false - */ - stopEventPropagation?: boolean; -} - -// Advanced options of Composite, to be implemented later if needed. -const disabledIndices = undefined; - -/** - * @ignore - internal hook. - */ -export function useCompositeRoot(params: UseCompositeRootParameters) { - const { - itemSizes, - cols = 1, - loop = true, - dense = false, - orientation = 'both', - direction, - highlightedIndex: externalHighlightedIndex, - onHighlightedIndexChange: externalSetHighlightedIndex, - rootRef: externalRef, - enableHomeAndEndKeys = false, - stopEventPropagation = false, - } = params; - - const [internalHighlightedIndex, internalSetHighlightedIndex] = React.useState(0); - - const isGrid = cols > 1; - - const highlightedIndex = externalHighlightedIndex ?? internalHighlightedIndex; - const onHighlightedIndexChange = useEventCallback( - externalSetHighlightedIndex ?? internalSetHighlightedIndex, - ); - - const textDirectionRef = React.useRef(direction ?? null); - - const rootRef = React.useRef(null); - const mergedRef = useForkRef(rootRef, externalRef); - - const elementsRef = React.useRef>([]); - - const getRootProps = React.useCallback( - (externalProps = {}) => - mergeReactProps<'div'>(externalProps, { - 'aria-orientation': orientation === 'both' ? undefined : orientation, - ref: mergedRef, - onKeyDown(event) { - const RELEVANT_KEYS = enableHomeAndEndKeys ? ALL_KEYS : ARROW_KEYS; - if (!RELEVANT_KEYS.includes(event.key)) { - return; - } - - const element = rootRef.current; - if (!element) { - return; - } - - if (textDirectionRef?.current == null) { - textDirectionRef.current = getTextDirection(element); - } - const isRtl = textDirectionRef.current === 'rtl'; - - let nextIndex = highlightedIndex; - const minIndex = getMinIndex(elementsRef, disabledIndices); - const maxIndex = getMaxIndex(elementsRef, disabledIndices); - - if (isGrid) { - const sizes = - itemSizes || - Array.from({ length: elementsRef.current.length }, () => ({ - width: 1, - height: 1, - })); - // To calculate movements on the grid, we use hypothetical cell indices - // as if every item was 1x1, then convert back to real indices. - const cellMap = buildCellMap(sizes, cols, dense); - const minGridIndex = cellMap.findIndex( - (index) => index != null && !isDisabled(elementsRef.current, index, disabledIndices), - ); - // last enabled index - const maxGridIndex = cellMap.reduce( - (foundIndex: number, index, cellIndex) => - index != null && !isDisabled(elementsRef.current, index, disabledIndices) - ? cellIndex - : foundIndex, - -1, - ); - - nextIndex = cellMap[ - getGridNavigatedIndex( - { - current: cellMap.map((itemIndex) => - itemIndex ? elementsRef.current[itemIndex] : null, - ), - }, - { - event, - orientation, - loop, - cols, - // treat undefined (empty grid spaces) as disabled indices so we - // don't end up in them - disabledIndices: getCellIndices( - [ - ...(disabledIndices || - elementsRef.current.map((_, index) => - isDisabled(elementsRef.current, index) ? index : undefined, - )), - undefined, - ], - cellMap, - ), - minIndex: minGridIndex, - maxIndex: maxGridIndex, - prevIndex: getCellIndexOfCorner( - highlightedIndex > maxIndex ? minIndex : highlightedIndex, - sizes, - cellMap, - cols, - // use a corner matching the edge closest to the direction we're - // moving in so we don't end up in the same item. Prefer - // top/left over bottom/right. - // eslint-disable-next-line no-nested-ternary - event.key === ARROW_DOWN ? 'bl' : event.key === ARROW_RIGHT ? 'tr' : 'tl', - ), - rtl: isRtl, - }, - ) - ] as number; // navigated cell will never be nullish - } - - const horizontalEndKey = isRtl ? ARROW_LEFT : ARROW_RIGHT; - const toEndKeys = { - horizontal: [horizontalEndKey], - vertical: [ARROW_DOWN], - both: [horizontalEndKey, ARROW_DOWN], - }[orientation]; - - const horizontalStartKey = isRtl ? ARROW_RIGHT : ARROW_LEFT; - const toStartKeys = { - horizontal: [horizontalStartKey], - vertical: [ARROW_UP], - both: [horizontalStartKey, ARROW_UP], - }[orientation]; - - const preventedKeys = isGrid - ? RELEVANT_KEYS - : { - horizontal: enableHomeAndEndKeys - ? HORIZONTAL_KEYS_WITH_EXTRA_KEYS - : HORIZONTAL_KEYS, - vertical: enableHomeAndEndKeys ? VERTICAL_KEYS_WITH_EXTRA_KEYS : VERTICAL_KEYS, - both: RELEVANT_KEYS, - }[orientation]; - - if (enableHomeAndEndKeys) { - if (event.key === HOME) { - nextIndex = minIndex; - } else if (event.key === END) { - nextIndex = maxIndex; - } - } - - if ( - nextIndex === highlightedIndex && - [...toEndKeys, ...toStartKeys].includes(event.key) - ) { - if (loop && nextIndex === maxIndex && toEndKeys.includes(event.key)) { - nextIndex = minIndex; - } else if (loop && nextIndex === minIndex && toStartKeys.includes(event.key)) { - nextIndex = maxIndex; - } else { - nextIndex = findNonDisabledIndex(elementsRef, { - startingIndex: nextIndex, - decrement: toStartKeys.includes(event.key), - disabledIndices, - }); - } - } - - if (nextIndex !== highlightedIndex && !isIndexOutOfBounds(elementsRef, nextIndex)) { - if (stopEventPropagation) { - event.stopPropagation(); - } - - if (preventedKeys.includes(event.key)) { - event.preventDefault(); - } - - onHighlightedIndexChange(nextIndex); - - // Wait for FocusManager `returnFocus` to execute. - queueMicrotask(() => { - elementsRef.current[nextIndex]?.focus(); - }); - } - }, - }), - [ - highlightedIndex, - stopEventPropagation, - cols, - dense, - elementsRef, - isGrid, - itemSizes, - loop, - mergedRef, - onHighlightedIndexChange, - orientation, - enableHomeAndEndKeys, - ], - ); - - return React.useMemo( - () => ({ - getRootProps, - highlightedIndex, - onHighlightedIndexChange, - elementsRef, - }), - [getRootProps, highlightedIndex, onHighlightedIndexChange, elementsRef], - ); -} From f9bb1efff40d796e3193b68b51aa9d23b1438d00 Mon Sep 17 00:00:00 2001 From: flavien Date: Sun, 5 Jan 2025 14:58:52 +0100 Subject: [PATCH 023/136] Work --- .../base-calendar/DayCalendar.tsx | 10 +++- .../base/Calendar/root/CalendarRootContext.ts | 5 +- .../base/Calendar/root/useCalendarRoot.ts | 46 ++++++++++++++++--- .../base/Calendar/utils/keyboardNavigation.ts | 1 - 4 files changed, 53 insertions(+), 9 deletions(-) diff --git a/docs/data/date-pickers/base-calendar/DayCalendar.tsx b/docs/data/date-pickers/base-calendar/DayCalendar.tsx index 0c1cd69205c67..3653996eee979 100644 --- a/docs/data/date-pickers/base-calendar/DayCalendar.tsx +++ b/docs/data/date-pickers/base-calendar/DayCalendar.tsx @@ -51,9 +51,17 @@ function Header() { export default function DayCalendar() { const [value, setValue] = React.useState(null); + const handleValueChange = React.useCallback( + (newValue: Dayjs | null, context: Calendar.Root.ValueChangeHandlerContext) => { + console.log(context); + setValue(newValue); + }, + [], + ); + return ( - +
diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts index 5a367ade931eb..35ac2bbe46c13 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts @@ -5,7 +5,10 @@ import type { useCalendarRoot } from './useCalendarRoot'; export interface CalendarRootContext { value: PickerValidDate | null; - setValue: (value: PickerValidDate, context: useCalendarRoot.ValueChangeHandlerContext) => void; + setValue: ( + value: PickerValidDate | null, + context: Pick, + ) => void; referenceDate: PickerValidDate; timezone: PickersTimezone; disabled: boolean; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts index 3cadea5dac5ea..c443267211bde 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts @@ -1,14 +1,25 @@ import * as React from 'react'; -import { PickerValidDate, TimezoneProps } from '../../../../models'; +import useEventCallback from '@mui/utils/useEventCallback'; +import { + DateValidationError, + OnErrorProps, + PickerValidDate, + TimezoneProps, +} from '../../../../models'; import { useIsDateDisabled } from '../../../../DateCalendar/useIsDateDisabled'; -import { ExportedValidateDateProps, ValidateDateProps } from '../../../../validation/validateDate'; +import { + ExportedValidateDateProps, + validateDate, + ValidateDateProps, +} from '../../../../validation/validateDate'; import { useControlledValueWithTimezone } from '../../../hooks/useValueWithTimezone'; import { useDefaultDates, useUtils } from '../../../hooks/useUtils'; import { SECTION_TYPE_GRANULARITY } from '../../../utils/getDefaultReferenceDate'; import { singleItemValueManager } from '../../../utils/valueManagers'; import { applyDefaultDate } from '../../../utils/date-utils'; -import { FormProps } from '../../../models'; +import { FormProps, PickerValue } from '../../../models'; import { CalendarRootContext } from './CalendarRootContext'; +import { useValidation } from '../../../../validation'; function useAddDefaultsToValidateDateProps( validationDate: ExportedValidateDateProps, @@ -54,6 +65,7 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { const { readOnly = false, disabled = false, + onError, defaultValue, onValueChange, value: valueProp, @@ -90,6 +102,13 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { [referenceDateProp, timezone], ); + const { getValidationErrorForNewValue } = useValidation({ + props: { ...validationProps, onError }, + value, + timezone, + validator: validateDate, + }); + const [visibleDate, setVisibleDate] = React.useState(referenceDate); const [prevValue, setPrevValue] = React.useState(value); if (value !== prevValue && utils.isValid(value)) { @@ -102,10 +121,17 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { timezone, }); + const setValue = useEventCallback((newValue, context) => { + handleValueChange(newValue, { + ...context, + validationError: getValidationErrorForNewValue(newValue), + }); + }); + const context: CalendarRootContext = React.useMemo( () => ({ value, - setValue: handleValueChange, + setValue, referenceDate, timezone, disabled, @@ -117,7 +143,7 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { }), [ value, - handleValueChange, + setValue, referenceDate, timezone, disabled, @@ -133,7 +159,11 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { } export namespace useCalendarRoot { - export interface Parameters extends TimezoneProps, FormProps, ExportedValidateDateProps { + export interface Parameters + extends TimezoneProps, + FormProps, + OnErrorProps, + ExportedValidateDateProps { /** * The controlled value that should be selected. * @@ -171,5 +201,9 @@ export namespace useCalendarRoot { * The section handled by the UI that triggered the change. */ section: 'day' | 'month' | 'year'; + /** + * The validation error associated to the new value. + */ + validationError: DateValidationError; } } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts index 39ed93d03ee69..29f7dea6b167d 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts @@ -298,7 +298,6 @@ export function applyInitialFocusInGrid({ }[]; target: PageNavigationTarget; }) { - console.log(rows); const cells = getCellsInGrid({ rows, rowsCells }); let cell: HTMLElement | undefined; From e1f46632f68eeac79c4a23f760b961bff6329008 Mon Sep 17 00:00:00 2001 From: flavien Date: Sun, 5 Jan 2025 18:32:00 +0100 Subject: [PATCH 024/136] Add navigation buttons --- .../{DayCalendar.js => DayCalendarDemo.js} | 21 +++- .../{DayCalendar.tsx => DayCalendarDemo.tsx} | 58 ++++----- .../base-calendar/DayCalendarDemo.tsx.preview | 1 + .../base-calendar/YearMonthDayCalendar.tsx | 32 +---- .../base-calendar/base-calendar.md | 2 +- .../internals/base/Calendar/index.parts.ts | 10 ++ .../base/Calendar/root/CalendarRootContext.ts | 1 + .../base/Calendar/root/useCalendarRoot.ts | 10 ++ .../CalendarSetVisibleMonth.tsx | 116 ++++++++++++++++++ .../useCalendarSetVisibleMonth.ts | 35 ++++++ .../CalendarSetVisibleYear.tsx | 112 +++++++++++++++++ .../useCalendarSetVisibleYear.ts | 35 ++++++ .../useCalendarContext/useCalendarContext.ts | 1 - 13 files changed, 365 insertions(+), 69 deletions(-) rename docs/data/date-pickers/base-calendar/{DayCalendar.js => DayCalendarDemo.js} (87%) rename docs/data/date-pickers/base-calendar/{DayCalendar.tsx => DayCalendarDemo.tsx} (70%) create mode 100644 docs/data/date-pickers/base-calendar/DayCalendarDemo.tsx.preview create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/useCalendarSetVisibleMonth.ts create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/useCalendarSetVisibleYear.ts diff --git a/docs/data/date-pickers/base-calendar/DayCalendar.js b/docs/data/date-pickers/base-calendar/DayCalendarDemo.js similarity index 87% rename from docs/data/date-pickers/base-calendar/DayCalendar.js rename to docs/data/date-pickers/base-calendar/DayCalendarDemo.js index b563d54a1dd8e..d6eb091198bdf 100644 --- a/docs/data/date-pickers/base-calendar/DayCalendar.js +++ b/docs/data/date-pickers/base-calendar/DayCalendarDemo.js @@ -48,12 +48,10 @@ function Header() { ); } -export default function DayCalendar() { - const [value, setValue] = React.useState(null); - +function DayCalendar(props) { return ( - +
@@ -96,3 +94,18 @@ export default function DayCalendar() { ); } + +export default function DayCalendarDemo() { + const [value, setValue] = React.useState(null); + + const handleValueChange = React.useCallback((newValue, context) => { + console.log(context); + setValue(newValue); + }, []); + + return ( + + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/DayCalendar.tsx b/docs/data/date-pickers/base-calendar/DayCalendarDemo.tsx similarity index 70% rename from docs/data/date-pickers/base-calendar/DayCalendar.tsx rename to docs/data/date-pickers/base-calendar/DayCalendarDemo.tsx index 3653996eee979..db3554947a79d 100644 --- a/docs/data/date-pickers/base-calendar/DayCalendar.tsx +++ b/docs/data/date-pickers/base-calendar/DayCalendarDemo.tsx @@ -10,58 +10,28 @@ import { import styles from './calendar.module.css'; function Header() { - const { visibleDate, setVisibleDate } = useCalendarContext(); + const { visibleDate } = useCalendarContext(); return (
- + {visibleDate.format('MMMM')} - +
- + {visibleDate.format('YYYY')} - +
); } -export default function DayCalendar() { - const [value, setValue] = React.useState(null); - - const handleValueChange = React.useCallback( - (newValue: Dayjs | null, context: Calendar.Root.ValueChangeHandlerContext) => { - console.log(context); - setValue(newValue); - }, - [], - ); - +function DayCalendar(props: Omit) { return ( - +
@@ -104,3 +74,17 @@ export default function DayCalendar() { ); } + +export default function DayCalendarDemo() { + const [value, setValue] = React.useState(null); + + const handleValueChange = React.useCallback((newValue: Dayjs | null) => { + setValue(newValue); + }, []); + + return ( + + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/DayCalendarDemo.tsx.preview b/docs/data/date-pickers/base-calendar/DayCalendarDemo.tsx.preview new file mode 100644 index 0000000000000..72b23c81a5a56 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/DayCalendarDemo.tsx.preview @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/data/date-pickers/base-calendar/YearMonthDayCalendar.tsx b/docs/data/date-pickers/base-calendar/YearMonthDayCalendar.tsx index adc9c6ab8de78..46a4eed73020c 100644 --- a/docs/data/date-pickers/base-calendar/YearMonthDayCalendar.tsx +++ b/docs/data/date-pickers/base-calendar/YearMonthDayCalendar.tsx @@ -14,18 +14,13 @@ function Header(props: { onActiveSectionChange: (newActiveSection: 'day' | 'month' | 'year') => void; }) { const { activeSection, onActiveSectionChange } = props; - const { visibleDate, setVisibleDate } = useCalendarContext(); + const { visibleDate } = useCalendarContext(); return (
{activeSection === 'day' && (
- + - +
)} {(activeSection === 'month' || activeSection === 'day') && (
- + - +
)}
@@ -87,7 +67,7 @@ export default function YearMonthDayCalendar() { return ( - +
void; + monthPageSize: number; } export const CalendarRootContext = React.createContext(undefined); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts index c443267211bde..8cbbb745f5469 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts @@ -71,6 +71,7 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { value: valueProp, timezone: timezoneProp, referenceDate: referenceDateProp, + monthPageSize = 1, } = parameters; const utils = useUtils(); @@ -140,6 +141,7 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { validationProps, visibleDate, setVisibleDate, + monthPageSize, }), [ value, @@ -152,6 +154,7 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { validationProps, visibleDate, setVisibleDate, + monthPageSize, ], ); @@ -191,11 +194,18 @@ export namespace useCalendarRoot { * @default The closest valid date using the validation props, except callbacks such as `shouldDisableDate`. */ referenceDate?: PickerValidDate; + /** + * The amount of months to navigate by when pressing or when using keyboard navigation in the day grid. + * This is mostly useful when displaying multiple day grids. + * @default 1 + */ + monthPageSize?: number; } export interface ReturnValue { context: CalendarRootContext; } + export interface ValueChangeHandlerContext { /** * The section handled by the UI that triggered the change. diff --git a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx new file mode 100644 index 0000000000000..55fa91401c6c0 --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx @@ -0,0 +1,116 @@ +'use client'; +import * as React from 'react'; +import useEventCallback from '@mui/utils/useEventCallback'; +import { useNow, useUtils } from '../../../hooks/useUtils'; +import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import { useCalendarRootContext } from '../root/CalendarRootContext'; +import { useCalendarSetVisibleMonth } from './useCalendarSetVisibleMonth'; +import { BaseUIComponentProps } from '../../utils/types'; + +const InnerCalendarSetVisibleMonth = React.forwardRef(function InnerCalendarSetVisibleMonth( + props: InnerCalendarSetVisibleMonthProps, + forwardedRef: React.ForwardedRef, +) { + const { className, render, ctx, target, ...otherProps } = props; + const { getSetVisibleMonthProps } = useCalendarSetVisibleMonth({ ctx, target }); + + const state: CalendarSetVisibleMonth.State = React.useMemo(() => ({}), []); + + const { renderElement } = useComponentRenderer({ + propGetter: getSetVisibleMonthProps, + render: render ?? 'button', + ref: forwardedRef, + className, + state, + extraProps: otherProps, + }); + + return renderElement(); +}); + +const MemoizedInnerCalendarSetVisibleMonth = React.memo(InnerCalendarSetVisibleMonth); + +const CalendarSetVisibleMonth = React.forwardRef(function CalendarSetVisibleMonth( + props: CalendarSetVisibleMonth.Props, + forwardedRef: React.ForwardedRef, +) { + const calendarRootContext = useCalendarRootContext(); + const utils = useUtils(); + const now = useNow(calendarRootContext.timezone); + + const targetDate = React.useMemo(() => { + if (props.target === 'previous') { + return utils.startOfMonth( + utils.addMonths(calendarRootContext.visibleDate, -calendarRootContext.monthPageSize), + ); + } + + return utils.startOfMonth( + utils.addMonths(calendarRootContext.visibleDate, calendarRootContext.monthPageSize), + ); + }, [calendarRootContext.visibleDate, calendarRootContext.monthPageSize, utils, props.target]); + + const isDisabled = React.useMemo(() => { + if (calendarRootContext.disabled) { + return true; + } + + if (props.target === 'previous') { + const firstEnabledMonth = utils.startOfMonth( + calendarRootContext.validationProps.disablePast && + utils.isAfter(now, calendarRootContext.validationProps.minDate) + ? now + : calendarRootContext.validationProps.minDate, + ); + + return utils.isAfter(firstEnabledMonth, targetDate); + } + + const lastEnabledMonth = utils.startOfMonth( + calendarRootContext.validationProps.disableFuture && + utils.isBefore(now, calendarRootContext.validationProps.maxDate) + ? now + : calendarRootContext.validationProps.maxDate, + ); + + return utils.isBefore(lastEnabledMonth, targetDate); + }, [ + calendarRootContext.disabled, + calendarRootContext.validationProps, + props.target, + targetDate, + utils, + now, + ]); + + const setTarget = useEventCallback(() => { + if (isDisabled) { + return; + } + calendarRootContext.setVisibleDate(targetDate); + }); + + const ctx = React.useMemo( + () => ({ + setTarget, + isDisabled, + }), + [setTarget, isDisabled], + ); + + return ; +}); + +export namespace CalendarSetVisibleMonth { + export interface State {} + + export interface Props + extends Omit, + BaseUIComponentProps<'div', State> {} +} + +interface InnerCalendarSetVisibleMonthProps + extends useCalendarSetVisibleMonth.Parameters, + BaseUIComponentProps<'div', CalendarSetVisibleMonth.State> {} + +export { CalendarSetVisibleMonth }; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/useCalendarSetVisibleMonth.ts b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/useCalendarSetVisibleMonth.ts new file mode 100644 index 0000000000000..7ff31f2be0907 --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/useCalendarSetVisibleMonth.ts @@ -0,0 +1,35 @@ +import * as React from 'react'; +import { GenericHTMLProps } from '../../utils/types'; +import { mergeReactProps } from '../../utils/mergeReactProps'; + +export function useCalendarSetVisibleMonth(parameters: useCalendarSetVisibleMonth.Parameters) { + const { ctx } = parameters; + + const getSetVisibleMonthProps = React.useCallback( + (externalProps: GenericHTMLProps) => { + return mergeReactProps(externalProps, { + type: 'button' as const, + disabled: ctx.isDisabled, + onClick: ctx.setTarget, + }); + }, + [ctx.isDisabled, ctx.setTarget], + ); + + return React.useMemo(() => ({ getSetVisibleMonthProps }), [getSetVisibleMonthProps]); +} + +export namespace useCalendarSetVisibleMonth { + export interface Parameters { + /** + * The month to navigate to. + */ + target: 'previous' | 'next'; + ctx: Context; + } + + export interface Context { + setTarget: () => void; + isDisabled: boolean; + } +} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx new file mode 100644 index 0000000000000..4032a87e15374 --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx @@ -0,0 +1,112 @@ +'use client'; +import * as React from 'react'; +import useEventCallback from '@mui/utils/useEventCallback'; +import { useNow, useUtils } from '../../../hooks/useUtils'; +import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import { useCalendarRootContext } from '../root/CalendarRootContext'; +import { useCalendarSetVisibleYear } from './useCalendarSetVisibleYear'; +import { BaseUIComponentProps } from '../../utils/types'; + +const InnerCalendarSetVisibleYear = React.forwardRef(function InnerCalendarSetVisibleYear( + props: InnerCalendarSetVisibleYearProps, + forwardedRef: React.ForwardedRef, +) { + const { className, render, ctx, target, ...otherProps } = props; + const { getSetVisibleYearProps } = useCalendarSetVisibleYear({ ctx, target }); + + const state: CalendarSetVisibleYear.State = React.useMemo(() => ({}), []); + + const { renderElement } = useComponentRenderer({ + propGetter: getSetVisibleYearProps, + render: render ?? 'button', + ref: forwardedRef, + className, + state, + extraProps: otherProps, + }); + + return renderElement(); +}); + +const MemoizedInnerCalendarSetVisibleYear = React.memo(InnerCalendarSetVisibleYear); + +const CalendarSetVisibleYear = React.forwardRef(function CalendarSetVisibleYear( + props: CalendarSetVisibleYear.Props, + forwardedRef: React.ForwardedRef, +) { + const calendarRootContext = useCalendarRootContext(); + const utils = useUtils(); + const now = useNow(calendarRootContext.timezone); + + const targetDate = React.useMemo(() => { + if (props.target === 'previous') { + return utils.startOfYear(utils.addYears(calendarRootContext.visibleDate, -1)); + } + + return utils.startOfYear(utils.addYears(calendarRootContext.visibleDate, 1)); + }, [calendarRootContext.visibleDate, utils, props.target]); + + const isDisabled = React.useMemo(() => { + if (calendarRootContext.disabled) { + return true; + } + + if (props.target === 'previous') { + const firstEnabledYear = utils.startOfYear( + calendarRootContext.validationProps.disablePast && + utils.isAfter(now, calendarRootContext.validationProps.minDate) + ? now + : calendarRootContext.validationProps.minDate, + ); + + return utils.isAfter(firstEnabledYear, targetDate); + } + + const lastEnabledYear = utils.startOfYear( + calendarRootContext.validationProps.disableFuture && + utils.isBefore(now, calendarRootContext.validationProps.maxDate) + ? now + : calendarRootContext.validationProps.maxDate, + ); + + return utils.isBefore(lastEnabledYear, targetDate); + }, [ + calendarRootContext.disabled, + calendarRootContext.validationProps, + props.target, + targetDate, + utils, + now, + ]); + + const setTarget = useEventCallback(() => { + if (isDisabled) { + return; + } + calendarRootContext.setVisibleDate(targetDate); + }); + + const ctx = React.useMemo( + () => ({ + setTarget, + isDisabled, + }), + [setTarget, isDisabled], + ); + + return ; +}); + +export namespace CalendarSetVisibleYear { + export interface State {} + + export interface Props + extends Omit, + BaseUIComponentProps<'div', State> {} +} + +interface InnerCalendarSetVisibleYearProps + extends useCalendarSetVisibleYear.Parameters, + BaseUIComponentProps<'div', CalendarSetVisibleYear.State> {} + +export { CalendarSetVisibleYear }; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/useCalendarSetVisibleYear.ts b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/useCalendarSetVisibleYear.ts new file mode 100644 index 0000000000000..0b8cf6e23178e --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/useCalendarSetVisibleYear.ts @@ -0,0 +1,35 @@ +import * as React from 'react'; +import { GenericHTMLProps } from '../../utils/types'; +import { mergeReactProps } from '../../utils/mergeReactProps'; + +export function useCalendarSetVisibleYear(parameters: useCalendarSetVisibleYear.Parameters) { + const { ctx } = parameters; + + const getSetVisibleYearProps = React.useCallback( + (externalProps: GenericHTMLProps) => { + return mergeReactProps(externalProps, { + type: 'button' as const, + disabled: ctx.isDisabled, + onClick: ctx.setTarget, + }); + }, + [ctx.isDisabled, ctx.setTarget], + ); + + return React.useMemo(() => ({ getSetVisibleYearProps }), [getSetVisibleYearProps]); +} + +export namespace useCalendarSetVisibleYear { + export interface Parameters { + /** + * The month to navigate to. + */ + target: 'previous' | 'next'; + ctx: Context; + } + + export interface Context { + setTarget: () => void; + isDisabled: boolean; + } +} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/useCalendarContext/useCalendarContext.ts b/packages/x-date-pickers/src/internals/base/Calendar/useCalendarContext/useCalendarContext.ts index 23f0a0ba81e27..162f46cb27c7e 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/useCalendarContext/useCalendarContext.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/useCalendarContext/useCalendarContext.ts @@ -6,6 +6,5 @@ export function useCalendarContext() { return { visibleDate: calendarRootContext.visibleDate, - setVisibleDate: calendarRootContext.setVisibleDate, }; } From 38e7853aafd1bf1bc1a5f6fd48c8d0b77ff75adc Mon Sep 17 00:00:00 2001 From: flavien Date: Sun, 5 Jan 2025 19:01:54 +0100 Subject: [PATCH 025/136] Work on demos --- .../base-calendar/DayCalendarDemo.js | 42 ++------ .../base-calendar/DayCalendarDemo.tsx | 17 ++-- .../base-calendar/YearMonthDayCalendar.js | 97 ++++++++++--------- .../base-calendar/YearMonthDayCalendar.tsx | 73 +++++++++----- .../base-calendar/calendar.module.css | 71 +++++++++++--- .../CalendarSetVisibleMonth.tsx | 4 +- .../CalendarSetVisibleYear.tsx | 8 +- 7 files changed, 182 insertions(+), 130 deletions(-) diff --git a/docs/data/date-pickers/base-calendar/DayCalendarDemo.js b/docs/data/date-pickers/base-calendar/DayCalendarDemo.js index d6eb091198bdf..8f31c07463ef1 100644 --- a/docs/data/date-pickers/base-calendar/DayCalendarDemo.js +++ b/docs/data/date-pickers/base-calendar/DayCalendarDemo.js @@ -10,40 +10,17 @@ import { import styles from './calendar.module.css'; function Header() { - const { visibleDate, setVisibleDate } = useCalendarContext(); + const { visibleDate } = useCalendarContext(); return (
-
- - {visibleDate.format('MMMM')} - -
-
- - {visibleDate.format('YYYY')} - -
+ + ◀ + + {visibleDate.format('MMMM YYYY')} + + ▶ +
); } @@ -98,8 +75,7 @@ function DayCalendar(props) { export default function DayCalendarDemo() { const [value, setValue] = React.useState(null); - const handleValueChange = React.useCallback((newValue, context) => { - console.log(context); + const handleValueChange = React.useCallback((newValue) => { setValue(newValue); }, []); diff --git a/docs/data/date-pickers/base-calendar/DayCalendarDemo.tsx b/docs/data/date-pickers/base-calendar/DayCalendarDemo.tsx index db3554947a79d..b577c8b2b38d1 100644 --- a/docs/data/date-pickers/base-calendar/DayCalendarDemo.tsx +++ b/docs/data/date-pickers/base-calendar/DayCalendarDemo.tsx @@ -14,16 +14,13 @@ function Header() { return (
-
- - {visibleDate.format('MMMM')} - -
-
- - {visibleDate.format('YYYY')} - -
+ + ◀ + + {visibleDate.format('MMMM YYYY')} + + ▶ +
); } diff --git a/docs/data/date-pickers/base-calendar/YearMonthDayCalendar.js b/docs/data/date-pickers/base-calendar/YearMonthDayCalendar.js index 76cbd683a97f9..790ff8defd5aa 100644 --- a/docs/data/date-pickers/base-calendar/YearMonthDayCalendar.js +++ b/docs/data/date-pickers/base-calendar/YearMonthDayCalendar.js @@ -11,52 +11,61 @@ import styles from './calendar.module.css'; function Header(props) { const { activeSection, onActiveSectionChange } = props; - const { visibleDate, setVisibleDate } = useCalendarContext(); + const { visibleDate } = useCalendarContext(); return (
- {activeSection === 'day' && ( -
- - - -
- )} - {(activeSection === 'month' || activeSection === 'day') && ( -
- - - -
- )} +
+ + ◀ + + + + ▶ + +
+
+ + ◀ + + + + ▶ + +
); } @@ -79,7 +88,7 @@ export default function YearMonthDayCalendar() { return ( - +
- {activeSection === 'day' && ( -
- - - -
- )} - {(activeSection === 'month' || activeSection === 'day') && ( -
- - - -
- )} +
+ + ◀ + + + + ▶ + +
+
+ + ◀ + + + + ▶ + +
); } diff --git a/docs/data/date-pickers/base-calendar/calendar.module.css b/docs/data/date-pickers/base-calendar/calendar.module.css index 1e0a394ea6db7..bd6921b5d9a9e 100644 --- a/docs/data/date-pickers/base-calendar/calendar.module.css +++ b/docs/data/date-pickers/base-calendar/calendar.module.css @@ -1,7 +1,7 @@ .Root { border: 1px solid #78716c; - width: 344px; - height: 368px; + width: 276px; + height: 300px; display: flex; flex-direction: column; } @@ -12,17 +12,54 @@ height: 40px; display: flex; justify-content: space-between; + align-items: center; } .HeaderBlock { display: flex; - gap: 8px; + gap: 4px; flex-grow: 1; justify-content: center; } -.HeaderMonthLabel { - min-width: 96px; +.SetVisibleMonth, +.SetVisibleYear, +.SetActiveSectionMonth, +.SetActiveSectionYear { + border: none; + background-color: transparent; + border-radius: 4px; + font-size: 12px; + height: 24px; + width: 24px; + text-align: center; + cursor: pointer; + + &:hover { + background-color: #e0f2fe; + } + + &:focus-visible { + outline: 2px solid #0ea5e9; + } + + &:disabled { + pointer-events: none; + } +} + +.SetVisibleMonth, +.SetVisibleYear { + height: 24px; + width: 24px; +} + +.SetActiveSectionMonth { + min-width: 72px; +} + +.SetActiveSectionYear { + min-width: 42px; } .MonthsList, @@ -45,34 +82,38 @@ padding: 12px; display: flex; flex-direction: column; - gap: 8px; + gap: 4px; } .DaysGridBody { display: flex; flex-direction: column; - gap: 8px; + gap: 4px; } .DaysWeekRow, .DaysGridHeader { display: flex; justify-content: center; - gap: 8px; + gap: 4px; } .DaysCellWrapper { - height: 36px; - width: 40px; - min-width: 40px; + height: 32px; + width: 32px; } .DaysCell { - height: 40px; - width: 40px; - border-radius: 8px; + height: 32px; + width: 32px; + border-radius: 4px; border: none; background-color: transparent; + cursor: pointer; + + &:not([data-selected]):hover { + background-color: #e0f2fe; + } &[data-current] { outline: 2px solid #9ca3af; @@ -96,7 +137,7 @@ } .DaysGridHeaderCell { - width: 48px; + width: 32px; text-align: center; } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx index 55fa91401c6c0..517e3ac6a8913 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx @@ -106,11 +106,11 @@ export namespace CalendarSetVisibleMonth { export interface Props extends Omit, - BaseUIComponentProps<'div', State> {} + BaseUIComponentProps<'button', State> {} } interface InnerCalendarSetVisibleMonthProps extends useCalendarSetVisibleMonth.Parameters, - BaseUIComponentProps<'div', CalendarSetVisibleMonth.State> {} + BaseUIComponentProps<'button', CalendarSetVisibleMonth.State> {} export { CalendarSetVisibleMonth }; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx index 4032a87e15374..3eb3b8cc6b286 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx @@ -40,10 +40,10 @@ const CalendarSetVisibleYear = React.forwardRef(function CalendarSetVisibleYear( const targetDate = React.useMemo(() => { if (props.target === 'previous') { - return utils.startOfYear(utils.addYears(calendarRootContext.visibleDate, -1)); + return utils.startOfMonth(utils.addYears(calendarRootContext.visibleDate, -1)); } - return utils.startOfYear(utils.addYears(calendarRootContext.visibleDate, 1)); + return utils.startOfMonth(utils.addYears(calendarRootContext.visibleDate, 1)); }, [calendarRootContext.visibleDate, utils, props.target]); const isDisabled = React.useMemo(() => { @@ -102,11 +102,11 @@ export namespace CalendarSetVisibleYear { export interface Props extends Omit, - BaseUIComponentProps<'div', State> {} + BaseUIComponentProps<'button', State> {} } interface InnerCalendarSetVisibleYearProps extends useCalendarSetVisibleYear.Parameters, - BaseUIComponentProps<'div', CalendarSetVisibleYear.State> {} + BaseUIComponentProps<'button', CalendarSetVisibleYear.State> {} export { CalendarSetVisibleYear }; From cb59fcd237afa39e6939b172f5fe83dc630490f0 Mon Sep 17 00:00:00 2001 From: flavien Date: Sun, 5 Jan 2025 19:05:16 +0100 Subject: [PATCH 026/136] Work on demos --- docs/data/date-pickers/base-calendar/calendar.module.css | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/data/date-pickers/base-calendar/calendar.module.css b/docs/data/date-pickers/base-calendar/calendar.module.css index bd6921b5d9a9e..f458719261f14 100644 --- a/docs/data/date-pickers/base-calendar/calendar.module.css +++ b/docs/data/date-pickers/base-calendar/calendar.module.css @@ -1,5 +1,6 @@ .Root { border: 1px solid #78716c; + border-radius: 8px; width: 276px; height: 300px; display: flex; From fc01fb89cc7188ece19e8ed23c3befcdc9b6abb1 Mon Sep 17 00:00:00 2001 From: flavien Date: Sun, 5 Jan 2025 19:45:23 +0100 Subject: [PATCH 027/136] Fix --- .../src/internals/base/Calendar/utils/keyboardNavigation.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts index 29f7dea6b167d..361b0f0342780 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts @@ -1,6 +1,3 @@ -import { ta } from 'date-fns/locale'; -import { get } from 'http'; - const LIST_NAVIGATION_SUPPORTED_KEYS = ['ArrowDown', 'ArrowUp', 'Home', 'End']; export function navigateInList({ From d9de26fd7df7b353891490712351848575ab9ed8 Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 6 Jan 2025 09:55:59 +0100 Subject: [PATCH 028/136] Add support for tab index --- .../base-calendar/DayCalendarDemo.js | 75 +++++----- .../base-calendar/DayCalendarDemo.tsx | 75 +++++----- .../base-calendar/YearMonthDayCalendar.js | 140 +++++++++--------- .../base-calendar/YearMonthDayCalendar.tsx | 140 +++++++++--------- .../base-calendar/calendar.module.css | 8 +- .../Calendar/days-cell/CalendarDaysCell.tsx | 18 ++- .../Calendar/days-cell/useCalendarDaysCell.ts | 12 +- .../days-grid/CalendarDaysGridContext.ts | 1 + .../Calendar/days-grid/useCalendarDaysGrid.ts | 14 +- .../months-cell/CalendarMonthsCell.tsx | 15 +- .../months-cell/useCalendarMonthsCell.ts | 4 +- .../base/Calendar/root/CalendarRoot.tsx | 71 ++++++++- .../base/Calendar/root/CalendarRootContext.ts | 1 + .../base/Calendar/root/useCalendarRoot.ts | 21 ++- .../base/Calendar/utils/keyboardNavigation.ts | 3 +- .../Calendar/years-cell/CalendarYearsCell.tsx | 15 +- .../years-cell/useCalendarYearsCell.ts | 4 +- 17 files changed, 362 insertions(+), 255 deletions(-) diff --git a/docs/data/date-pickers/base-calendar/DayCalendarDemo.js b/docs/data/date-pickers/base-calendar/DayCalendarDemo.js index 8f31c07463ef1..5775f0e2605f6 100644 --- a/docs/data/date-pickers/base-calendar/DayCalendarDemo.js +++ b/docs/data/date-pickers/base-calendar/DayCalendarDemo.js @@ -28,45 +28,42 @@ function Header() { function DayCalendar(props) { return ( - -
-
- - - {({ days }) => - days.map((day) => ( - - )) - } - - - {({ weeks }) => - weeks.map((week) => ( - - {({ days }) => - days.map((day) => ( -
- -
- )) - } -
- )) - } -
-
-
+ +
+ + + {({ days }) => + days.map((day) => ( + + )) + } + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => + days.map((day) => ( + + )) + } + + )) + } + + ); diff --git a/docs/data/date-pickers/base-calendar/DayCalendarDemo.tsx b/docs/data/date-pickers/base-calendar/DayCalendarDemo.tsx index b577c8b2b38d1..8f83700f9e1bf 100644 --- a/docs/data/date-pickers/base-calendar/DayCalendarDemo.tsx +++ b/docs/data/date-pickers/base-calendar/DayCalendarDemo.tsx @@ -28,45 +28,42 @@ function Header() { function DayCalendar(props: Omit) { return ( - -
-
- - - {({ days }) => - days.map((day) => ( - - )) - } - - - {({ weeks }) => - weeks.map((week) => ( - - {({ days }) => - days.map((day) => ( -
- -
- )) - } -
- )) - } -
-
-
+ +
+ + + {({ days }) => + days.map((day) => ( + + )) + } + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => + days.map((day) => ( + + )) + } + + )) + } + + ); diff --git a/docs/data/date-pickers/base-calendar/YearMonthDayCalendar.js b/docs/data/date-pickers/base-calendar/YearMonthDayCalendar.js index 790ff8defd5aa..918056876d8d1 100644 --- a/docs/data/date-pickers/base-calendar/YearMonthDayCalendar.js +++ b/docs/data/date-pickers/base-calendar/YearMonthDayCalendar.js @@ -75,92 +75,86 @@ export default function YearMonthDayCalendar() { const [activeSection, setActiveSection] = React.useState('day'); const handleValueChange = React.useCallback((newValue, context) => { - if (context.section === 'month') { + if (context.section === 'month' || context.section === 'year') { setActiveSection('day'); } - if (context.section === 'year') { - setActiveSection('month'); - } - setValue(newValue); }, []); return ( - -
-
- {activeSection === 'year' && ( - - {({ years }) => - years.map((year) => ( - +
+ {activeSection === 'year' && ( + + {({ years }) => + years.map((year) => ( + + )) + } + + )} + {activeSection === 'month' && ( + + {({ months }) => + months.map((month) => ( + + )) + } + + )} + {activeSection === 'day' && ( + + + {({ days }) => + days.map((day) => ( + )) } - - )} - {activeSection === 'month' && ( - - {({ months }) => - months.map((month) => ( - + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => + days.map((day) => ( + + )) + } + )) } - - )} - {activeSection === 'day' && ( - - - {({ days }) => - days.map((day) => ( - - )) - } - - - {({ weeks }) => - weeks.map((week) => ( - - {({ days }) => - days.map((day) => ( -
- -
- )) - } -
- )) - } -
-
- )} -
+ + + )}
); diff --git a/docs/data/date-pickers/base-calendar/YearMonthDayCalendar.tsx b/docs/data/date-pickers/base-calendar/YearMonthDayCalendar.tsx index 2f9b6f456b7e1..b6f87a3cde5e8 100644 --- a/docs/data/date-pickers/base-calendar/YearMonthDayCalendar.tsx +++ b/docs/data/date-pickers/base-calendar/YearMonthDayCalendar.tsx @@ -81,14 +81,10 @@ export default function YearMonthDayCalendar() { const handleValueChange = React.useCallback( (newValue: Dayjs | null, context: Calendar.Root.ValueChangeHandlerContext) => { - if (context.section === 'month') { + if (context.section === 'month' || context.section === 'year') { setActiveSection('day'); } - if (context.section === 'year') { - setActiveSection('month'); - } - setValue(newValue); }, [], @@ -96,79 +92,77 @@ export default function YearMonthDayCalendar() { return ( - -
-
- {activeSection === 'year' && ( - - {({ years }) => - years.map((year) => ( - +
+ {activeSection === 'year' && ( + + {({ years }) => + years.map((year) => ( + + )) + } + + )} + {activeSection === 'month' && ( + + {({ months }) => + months.map((month) => ( + + )) + } + + )} + {activeSection === 'day' && ( + + + {({ days }) => + days.map((day) => ( + )) } - - )} - {activeSection === 'month' && ( - - {({ months }) => - months.map((month) => ( - + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => + days.map((day) => ( + + )) + } + )) } - - )} - {activeSection === 'day' && ( - - - {({ days }) => - days.map((day) => ( - - )) - } - - - {({ weeks }) => - weeks.map((week) => ( - - {({ days }) => - days.map((day) => ( -
- -
- )) - } -
- )) - } -
-
- )} -
+ + + )}
); diff --git a/docs/data/date-pickers/base-calendar/calendar.module.css b/docs/data/date-pickers/base-calendar/calendar.module.css index f458719261f14..1277d7047eb1a 100644 --- a/docs/data/date-pickers/base-calendar/calendar.module.css +++ b/docs/data/date-pickers/base-calendar/calendar.module.css @@ -99,11 +99,6 @@ gap: 4px; } -.DaysCellWrapper { - height: 32px; - width: 32px; -} - .DaysCell { height: 32px; width: 32px; @@ -133,7 +128,8 @@ } &[data-outsidemonth] { - display: none; + opacity: 0; + pointer-events: 'none'; } } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx index 9df530c18722b..f037667f7f16f 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx @@ -74,15 +74,31 @@ const CalendarDaysCell = React.forwardRef(function CalendarDaysCell( return isDateDisabled(props.value); }, [calendarRootContext.disabled, isDateDisabled, props.value]); + const isTabbable = React.useMemo( + () => + calendarMonthsListContext.tabbableDay == null + ? false + : utils.isSameDay(calendarMonthsListContext.tabbableDay, props.value), + [utils, calendarMonthsListContext.tabbableDay, props.value], + ); + const ctx = React.useMemo( () => ({ colIndex, isSelected, isDisabled, + isTabbable, isOutsideCurrentMonth, selectDay: calendarMonthsListContext.selectDay, }), - [isSelected, isDisabled, isOutsideCurrentMonth, calendarMonthsListContext.selectDay, colIndex], + [ + isSelected, + isDisabled, + isTabbable, + isOutsideCurrentMonth, + calendarMonthsListContext.selectDay, + colIndex, + ], ); return ; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-cell/useCalendarDaysCell.ts b/packages/x-date-pickers/src/internals/base/Calendar/days-cell/useCalendarDaysCell.ts index 4ecc007a8a235..d3484f1f7dd98 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-cell/useCalendarDaysCell.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-cell/useCalendarDaysCell.ts @@ -29,10 +29,19 @@ export function useCalendarDaysCell(parameters: useCalendarDaysCell.Parameters) 'aria-colindex': ctx.colIndex + 1, children: formattedValue, disabled: ctx.isDisabled, + tabIndex: ctx.isTabbable ? 0 : -1, onClick, }); }, - [formattedValue, ctx.isSelected, ctx.isDisabled, ctx.colIndex, isCurrent, onClick], + [ + formattedValue, + ctx.isSelected, + ctx.isDisabled, + ctx.isTabbable, + ctx.colIndex, + isCurrent, + onClick, + ], ); return React.useMemo(() => ({ getDaysCellProps, isCurrent }), [getDaysCellProps, isCurrent]); @@ -53,6 +62,7 @@ export namespace useCalendarDaysCell { colIndex: number; isSelected: boolean; isDisabled: boolean; + isTabbable: boolean; isOutsideCurrentMonth: boolean; selectDay: (value: PickerValidDate) => void; } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-grid/CalendarDaysGridContext.ts b/packages/x-date-pickers/src/internals/base/Calendar/days-grid/CalendarDaysGridContext.ts index ae8a49436e9e1..6ce23bdffd0be 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-grid/CalendarDaysGridContext.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-grid/CalendarDaysGridContext.ts @@ -4,6 +4,7 @@ import { PickerValidDate } from '../../../../models'; export interface CalendarDaysGridContext { selectDay: (value: PickerValidDate) => void; currentMonth: PickerValidDate; + tabbableDay: PickerValidDate | null; daysGrid: PickerValidDate[][]; } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-grid/useCalendarDaysGrid.ts b/packages/x-date-pickers/src/internals/base/Calendar/days-grid/useCalendarDaysGrid.ts index 01c8c66ac5384..1e8b2a860a018 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-grid/useCalendarDaysGrid.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-grid/useCalendarDaysGrid.ts @@ -60,9 +60,19 @@ export function useCalendarDaysGrid(parameters: useCalendarDaysGrid.Parameters) calendarRootContext.setValue(newCleanValue, { section: 'day' }); }); + const tabbableDay = React.useMemo(() => { + const flatDays = daysGrid.flat(); + const tempTabbableDay = calendarRootContext.value ?? calendarRootContext.referenceDate; + if (flatDays.some((day) => utils.isSameDay(day, tempTabbableDay))) { + return tempTabbableDay; + } + + return flatDays.find((day) => utils.isSameMonth(day, currentMonth)) ?? null; + }, [calendarRootContext.value, calendarRootContext.referenceDate, daysGrid, utils, currentMonth]); + const context: CalendarDaysGridContext = React.useMemo( - () => ({ selectDay, daysGrid, currentMonth }), - [selectDay, daysGrid, currentMonth], + () => ({ selectDay, daysGrid, currentMonth, tabbableDay }), + [selectDay, daysGrid, currentMonth, tabbableDay], ); return React.useMemo(() => ({ getDaysGridProps, context }), [getDaysGridProps, context]); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx index aecda2b337568..be58ae1af7ea5 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx @@ -90,13 +90,22 @@ const CalendarMonthsCell = React.forwardRef(function CalendarMonthsCell( return calendarRootContext.validationProps.shouldDisableMonth(monthToValidate); }, [calendarRootContext.disabled, calendarRootContext.validationProps, props.value, now, utils]); + const isTabbable = React.useMemo( + () => + utils.isValid(calendarRootContext.value) + ? isSelected + : utils.isSameMonth(calendarRootContext.referenceDate, props.value), + [utils, calendarRootContext.value, calendarRootContext.referenceDate, isSelected, props.value], + ); + const ctx = React.useMemo( () => ({ isSelected, isDisabled, + isTabbable, selectMonth: calendarMonthsListContext.selectMonth, }), - [isSelected, isDisabled, calendarMonthsListContext.selectMonth], + [isSelected, isDisabled, isTabbable, calendarMonthsListContext.selectMonth], ); return ; @@ -109,11 +118,11 @@ export namespace CalendarMonthsCell { export interface Props extends Omit, - BaseUIComponentProps<'div', State> {} + Omit, 'value'> {} } interface InnerCalendarMonthsCellProps extends useCalendarMonthsCell.Parameters, - BaseUIComponentProps<'div', CalendarMonthsCell.State> {} + Omit, 'value'> {} export { CalendarMonthsCell }; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/useCalendarMonthsCell.ts b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/useCalendarMonthsCell.ts index 8bea73b069a0c..d7a54f723fda2 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/useCalendarMonthsCell.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/useCalendarMonthsCell.ts @@ -28,11 +28,12 @@ export function useCalendarMonthsCell(parameters: useCalendarMonthsCell.Paramete 'aria-checked': ctx.isSelected, 'aria-current': isCurrent ? 'date' : undefined, disabled: ctx.isDisabled, + tabIndex: ctx.isTabbable ? 0 : -1, children: formattedValue, onClick, }); }, - [formattedValue, ctx.isSelected, ctx.isDisabled, onClick, isCurrent], + [formattedValue, ctx.isSelected, ctx.isDisabled, ctx.isTabbable, onClick, isCurrent], ); return React.useMemo(() => ({ getMonthCellProps, isCurrent }), [getMonthCellProps, isCurrent]); @@ -52,6 +53,7 @@ export namespace useCalendarMonthsCell { export interface Context { isSelected: boolean; isDisabled: boolean; + isTabbable: boolean; selectMonth: (value: PickerValidDate) => void; } } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRoot.tsx b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRoot.tsx index f4a8f02aa5506..454b20431eacd 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRoot.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRoot.tsx @@ -2,18 +2,77 @@ import * as React from 'react'; import { CalendarRootContext } from './CalendarRootContext'; import { useCalendarRoot } from './useCalendarRoot'; +import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import { BaseUIComponentProps } from '../../utils/types'; -const CalendarRoot: React.FC = function CalendarRoot(props) { - const { children } = props; - const { context } = useCalendarRoot(props); +const CalendarRoot = React.forwardRef(function CalendarRoot( + props: CalendarRoot.Props, + forwardedRef: React.ForwardedRef, +) { + const { + className, + render, + readOnly, + disabled, + autoFocus, + onError, + defaultValue, + onValueChange, + value, + timezone, + referenceDate, + monthPageSize, + shouldDisableDate, + shouldDisableMonth, + shouldDisableYear, + disablePast, + disableFuture, + minDate, + maxDate, + ...otherProps + } = props; + const { context, getRootProps } = useCalendarRoot({ + readOnly, + disabled, + autoFocus, + onError, + defaultValue, + onValueChange, + value, + timezone, + referenceDate, + monthPageSize, + shouldDisableDate, + shouldDisableMonth, + shouldDisableYear, + disablePast, + disableFuture, + minDate, + maxDate, + }); - return {children}; -}; + const state: CalendarRoot.State = React.useMemo(() => ({}), []); + + const { renderElement } = useComponentRenderer({ + propGetter: getRootProps, + render: render ?? 'div', + ref: forwardedRef, + className, + state, + extraProps: otherProps, + }); + + return ( + {renderElement()} + ); +}); export namespace CalendarRoot { export interface State {} - export interface Props extends useCalendarRoot.Parameters { + export interface Props + extends useCalendarRoot.Parameters, + Omit, 'value' | 'defaultValue' | 'onError'> { children: React.ReactNode; } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts index 72f874be593e7..d00e387ad7aaa 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts @@ -13,6 +13,7 @@ export interface CalendarRootContext { timezone: PickersTimezone; disabled: boolean; readOnly: boolean; + autoFocus: boolean; isDateDisabled: (day: PickerValidDate | null) => boolean; validationProps: ValidateDateProps; visibleDate: PickerValidDate; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts index 8cbbb745f5469..f95c15d70b9c2 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts @@ -20,6 +20,8 @@ import { applyDefaultDate } from '../../../utils/date-utils'; import { FormProps, PickerValue } from '../../../models'; import { CalendarRootContext } from './CalendarRootContext'; import { useValidation } from '../../../../validation'; +import { mergeReactProps } from '../../utils/mergeReactProps'; +import { GenericHTMLProps } from '../../utils/types'; function useAddDefaultsToValidateDateProps( validationDate: ExportedValidateDateProps, @@ -65,6 +67,7 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { const { readOnly = false, disabled = false, + autoFocus = false, onError, defaultValue, onValueChange, @@ -137,6 +140,7 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { timezone, disabled, readOnly, + autoFocus, isDateDisabled, validationProps, visibleDate, @@ -150,6 +154,7 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { timezone, disabled, readOnly, + autoFocus, isDateDisabled, validationProps, visibleDate, @@ -158,7 +163,11 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { ], ); - return React.useMemo(() => ({ context }), [context]); + const getRootProps = React.useCallback((externalProps: GenericHTMLProps) => { + return mergeReactProps(externalProps, {}); + }, []); + + return React.useMemo(() => ({ getRootProps, context }), [getRootProps, context]); } export namespace useCalendarRoot { @@ -194,6 +203,12 @@ export namespace useCalendarRoot { * @default The closest valid date using the validation props, except callbacks such as `shouldDisableDate`. */ referenceDate?: PickerValidDate; + /** + * If `true`, one of the cells will be automatically focused when the component is mounted. + * If a value or a default value is provided, the focused cell will be the one corresponding to the selected date. + * @default false + */ + autoFocus?: boolean; /** * The amount of months to navigate by when pressing or when using keyboard navigation in the day grid. * This is mostly useful when displaying multiple day grids. @@ -202,10 +217,6 @@ export namespace useCalendarRoot { monthPageSize?: number; } - export interface ReturnValue { - context: CalendarRootContext; - } - export interface ValueChangeHandlerContext { /** * The section handled by the UI that triggered the change. diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts index 361b0f0342780..9edc8c5c45cfe 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts @@ -328,8 +328,7 @@ function isNavigable(element: HTMLElement | null): element is HTMLElement { return false; } - const dimensions = element?.getBoundingClientRect(); - if (dimensions?.width === 0 || dimensions?.height === 0) { + if (element.dataset.outsidemonth != null) { return false; } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx index d1c8224104990..8e2c6fb8603ed 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx @@ -86,13 +86,22 @@ const CalendarYearsCell = React.forwardRef(function CalendarsYearCell( return calendarRootContext.validationProps.shouldDisableYear(yearToValidate); }, [calendarRootContext.disabled, calendarRootContext.validationProps, props.value, now, utils]); + const isTabbable = React.useMemo( + () => + utils.isValid(calendarRootContext.value) + ? isSelected + : utils.isSameYear(calendarRootContext.referenceDate, props.value), + [utils, calendarRootContext.value, calendarRootContext.referenceDate, isSelected, props.value], + ); + const ctx = React.useMemo( () => ({ isSelected, isDisabled, + isTabbable, selectYear: calendarYearsListContext.selectYear, }), - [isSelected, isDisabled, calendarYearsListContext.selectYear], + [isSelected, isDisabled, isTabbable, calendarYearsListContext.selectYear], ); return ; @@ -105,11 +114,11 @@ export namespace CalendarYearsCell { export interface Props extends Omit, - BaseUIComponentProps<'div', State> {} + Omit, 'value'> {} } interface InnerCalendarYearsCellProps extends useCalendarYearsCell.Parameters, - BaseUIComponentProps<'div', CalendarYearsCell.State> {} + Omit, 'value'> {} export { CalendarYearsCell }; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-cell/useCalendarYearsCell.ts b/packages/x-date-pickers/src/internals/base/Calendar/years-cell/useCalendarYearsCell.ts index 69d07190049a4..0dd6845e8d6c8 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-cell/useCalendarYearsCell.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-cell/useCalendarYearsCell.ts @@ -28,11 +28,12 @@ export function useCalendarYearsCell(parameters: useCalendarYearsCell.Parameters 'aria-checked': ctx.isSelected, 'aria-current': isCurrent ? 'date' : undefined, disabled: ctx.isDisabled, + tabIndex: ctx.isTabbable ? 0 : -1, children: formattedValue, onClick, }); }, - [formattedValue, ctx.isSelected, ctx.isDisabled, onClick, isCurrent], + [formattedValue, ctx.isSelected, ctx.isDisabled, ctx.isTabbable, onClick, isCurrent], ); return React.useMemo(() => ({ getYearCellProps, isCurrent }), [getYearCellProps, isCurrent]); @@ -52,6 +53,7 @@ export namespace useCalendarYearsCell { export interface Context { isSelected: boolean; isDisabled: boolean; + isTabbable: boolean; selectYear: (value: PickerValidDate) => void; } } From be70f637a4f9343763a89898ef26a631e56b11a9 Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 6 Jan 2025 10:28:17 +0100 Subject: [PATCH 029/136] Work --- .../base-calendar/DayCalendarTwoMonthsDemo.js | 104 ++++++++++++++++++ .../DayCalendarTwoMonthsDemo.tsx | 104 ++++++++++++++++++ .../DayCalendarTwoMonthsDemo.tsx.preview | 1 + .../base-calendar/base-calendar.md | 4 + .../base-calendar/calendar.module.css | 26 +++++ .../Calendar/days-grid/CalendarDaysGrid.tsx | 3 +- .../Calendar/days-grid/useCalendarDaysGrid.ts | 16 ++- 7 files changed, 252 insertions(+), 6 deletions(-) create mode 100644 docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.js create mode 100644 docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.tsx create mode 100644 docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.tsx.preview diff --git a/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.js b/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.js new file mode 100644 index 0000000000000..c140a5bddc1d7 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.js @@ -0,0 +1,104 @@ +import * as React from 'react'; +import clsx from 'clsx'; + +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +// eslint-disable-next-line no-restricted-imports +import { + Calendar, + useCalendarContext, +} from '@mui/x-date-pickers/internals/base/Calendar'; +import styles from './calendar.module.css'; + +function Header(props) { + const { offset } = props; + const { visibleDate } = useCalendarContext(); + + const date = visibleDate.add(offset, 'month'); + + return ( +
+ + ◀ + + {date.format('MMMM YYYY')} + + ▶ + +
+ ); +} + +function DayGrid(props) { + const { offset } = props; + return ( +
+
+ + + {({ days }) => + days.map((day) => ( + + )) + } + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => + days.map((day) => ( + + )) + } + + )) + } + + +
+ ); +} + +function DayCalendar(props) { + return ( + + + + + + + ); +} + +export default function DayCalendarTwoMonthsDemo() { + const [value, setValue] = React.useState(null); + + const handleValueChange = React.useCallback((newValue) => { + setValue(newValue); + }, []); + + return ( + + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.tsx b/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.tsx new file mode 100644 index 0000000000000..b004785f12226 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.tsx @@ -0,0 +1,104 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import { Dayjs } from 'dayjs'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +// eslint-disable-next-line no-restricted-imports +import { + Calendar, + useCalendarContext, +} from '@mui/x-date-pickers/internals/base/Calendar'; +import styles from './calendar.module.css'; + +function Header(props: { offset: 0 | 1 }) { + const { offset } = props; + const { visibleDate } = useCalendarContext(); + + const date = visibleDate.add(offset, 'month'); + + return ( +
+ + ◀ + + {date.format('MMMM YYYY')} + + ▶ + +
+ ); +} + +function DayGrid(props: { offset: 0 | 1 }) { + const { offset } = props; + return ( +
+
+ + + {({ days }) => + days.map((day) => ( + + )) + } + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => + days.map((day) => ( + + )) + } + + )) + } + + +
+ ); +} + +function DayCalendar(props: Omit) { + return ( + + + + + + + ); +} + +export default function DayCalendarTwoMonthsDemo() { + const [value, setValue] = React.useState(null); + + const handleValueChange = React.useCallback((newValue: Dayjs | null) => { + setValue(newValue); + }, []); + + return ( + + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.tsx.preview b/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.tsx.preview new file mode 100644 index 0000000000000..72b23c81a5a56 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.tsx.preview @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/data/date-pickers/base-calendar/base-calendar.md b/docs/data/date-pickers/base-calendar/base-calendar.md index 04c1271e0210e..82f032259b845 100644 --- a/docs/data/date-pickers/base-calendar/base-calendar.md +++ b/docs/data/date-pickers/base-calendar/base-calendar.md @@ -15,3 +15,7 @@ packageName: '@mui/x-date-pickers' ## Day, month and year view {{"demo": "YearMonthDayCalendar.js"}} + +## Multiple months + +{{"demo": "DayCalendarTwoMonthsDemo.js"}} diff --git a/docs/data/date-pickers/base-calendar/calendar.module.css b/docs/data/date-pickers/base-calendar/calendar.module.css index 1277d7047eb1a..1d033a86e137c 100644 --- a/docs/data/date-pickers/base-calendar/calendar.module.css +++ b/docs/data/date-pickers/base-calendar/calendar.module.css @@ -5,6 +5,17 @@ height: 300px; display: flex; flex-direction: column; + font-family: 'Roboto', 'Helvetica', 'Arial', sans-serif; +} + +.RootTwoPanels { + width: 540px; + flex-direction: row; + justify-content: space-between; +} + +.Panel { + width: 276px; } .Header { @@ -131,11 +142,17 @@ opacity: 0; pointer-events: 'none'; } + + &:disabled { + pointer-events: none; + } } .DaysGridHeaderCell { width: 32px; text-align: center; + font-size: 0.75rem; + color: #64748b; } .MonthsCell, @@ -151,4 +168,13 @@ background-color: #7dd3fc; } } + + &:disabled { + pointer-events: none; + } +} + +.Hidden { + opacity: 0; + pointer-events: none; } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-grid/CalendarDaysGrid.tsx b/packages/x-date-pickers/src/internals/base/Calendar/days-grid/CalendarDaysGrid.tsx index 744d262c7bafb..d6d43a7b1b80c 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-grid/CalendarDaysGrid.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-grid/CalendarDaysGrid.tsx @@ -9,9 +9,10 @@ const CalendarDaysGrid = React.forwardRef(function CalendarDaysGrid( props: CalendarDaysGrid.Props, forwardedRef: React.ForwardedRef, ) { - const { className, render, fixedWeekNumber, ...otherProps } = props; + const { className, render, fixedWeekNumber, offset, ...otherProps } = props; const { getDaysGridProps, context } = useCalendarDaysGrid({ fixedWeekNumber, + offset, }); const state = React.useMemo(() => ({}), []); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-grid/useCalendarDaysGrid.ts b/packages/x-date-pickers/src/internals/base/Calendar/days-grid/useCalendarDaysGrid.ts index 1e8b2a860a018..50d775dcde7ad 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-grid/useCalendarDaysGrid.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-grid/useCalendarDaysGrid.ts @@ -9,14 +9,14 @@ import { mergeReactProps } from '../../utils/mergeReactProps'; import { CalendarDaysGridContext } from './CalendarDaysGridContext'; export function useCalendarDaysGrid(parameters: useCalendarDaysGrid.Parameters) { - const { fixedWeekNumber } = parameters; + const { fixedWeekNumber, offset = 0 } = parameters; const utils = useUtils(); const calendarRootContext = useCalendarRootContext(); - const currentMonth = React.useMemo( - () => utils.startOfMonth(calendarRootContext.visibleDate), - [utils, calendarRootContext.visibleDate], - ); + const currentMonth = React.useMemo(() => { + const cleanVisibleDate = utils.startOfMonth(calendarRootContext.visibleDate); + return offset === 0 ? cleanVisibleDate : utils.addMonths(cleanVisibleDate, offset); + }, [utils, calendarRootContext.visibleDate, offset]); const daysGrid = React.useMemo(() => { const toDisplay = utils.getWeekArray(currentMonth); @@ -85,5 +85,11 @@ export namespace useCalendarDaysGrid { * Put it to 6 to have a fixed number of weeks in Gregorian calendars */ fixedWeekNumber?: number; + /** + * The offset to apply to the rendered month compared to the current month. + * This is mostly useful when displaying multiple day grids. + * @default 0 + */ + offset?: number; } } From 75154f533fb1cf365b228320227c93292e3315b9 Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 6 Jan 2025 11:43:03 +0100 Subject: [PATCH 030/136] 1st version of multi grid keyboard navigation works --- .../DayCalendarTwoMonthsDemo.tsx | 6 +- .../days-grid-body/useCalendarDaysGridBody.ts | 89 +++------ .../base/Calendar/root/CalendarRootContext.ts | 6 + .../root/useCalendarDaysGridsNavigation.ts | 91 +++++++++ .../base/Calendar/root/useCalendarRoot.ts | 13 ++ .../base/Calendar/utils/keyboardNavigation.ts | 183 +++++++++++------- 6 files changed, 253 insertions(+), 135 deletions(-) create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarDaysGridsNavigation.ts diff --git a/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.tsx b/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.tsx index b004785f12226..cf96fd3038c71 100644 --- a/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.tsx +++ b/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.tsx @@ -81,7 +81,11 @@ function DayGrid(props: { offset: 0 | 1 }) { function DayCalendar(props: Omit) { return ( - + diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-grid-body/useCalendarDaysGridBody.ts b/packages/x-date-pickers/src/internals/base/Calendar/days-grid-body/useCalendarDaysGridBody.ts index 040947bca6529..be7af2b304df2 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-grid-body/useCalendarDaysGridBody.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-grid-body/useCalendarDaysGridBody.ts @@ -1,69 +1,18 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; -import useTimeout from '@mui/utils/useTimeout'; import { PickerValidDate } from '../../../../models'; -import { useUtils } from '../../../hooks/useUtils'; import { useCalendarDaysGridContext } from '../days-grid/CalendarDaysGridContext'; import { mergeReactProps } from '../../utils/mergeReactProps'; import { GenericHTMLProps } from '../../utils/types'; -import { - applyInitialFocusInGrid, - navigateInGrid, - NavigateInGridChangePage, - PageNavigationTarget, -} from '../utils/keyboardNavigation'; import { CalendarDaysGridBodyContext } from './CalendarDaysGridBodyContext'; import { useCalendarRootContext } from '../root/CalendarRootContext'; export function useCalendarDaysGridBody(parameters: useCalendarDaysGridBody.Parameters) { const { children } = parameters; - const utils = useUtils(); const calendarRootContext = useCalendarRootContext(); const calendarDaysGridContext = useCalendarDaysGridContext(); - const calendarWeekRowRefs = React.useRef<(HTMLElement | null)[]>([]); - const calendarWeekRowsCellsRef = React.useRef< - { - rowRef: React.RefObject; - cellsRef: React.RefObject<(HTMLElement | null)[]>; - }[] - >([]); - const pageNavigationTargetRef = React.useRef(null); - - const timeout = useTimeout(); - React.useEffect(() => { - if (pageNavigationTargetRef.current) { - const target = pageNavigationTargetRef.current; - timeout.start(0, () => { - applyInitialFocusInGrid({ - rows: calendarWeekRowRefs.current, - rowsCells: calendarWeekRowsCellsRef.current, - target, - }); - }); - } - }, [calendarRootContext.visibleDate, timeout]); - - const onKeyDown = useEventCallback((event: React.KeyboardEvent) => { - const changePage: NavigateInGridChangePage = (params) => { - // TODO: Jump over months with no valid date. - if (params.direction === 'next') { - calendarRootContext.setVisibleDate(utils.addMonths(calendarRootContext.visibleDate, 1)); - } - if (params.direction === 'previous') { - calendarRootContext.setVisibleDate(utils.addMonths(calendarRootContext.visibleDate, -1)); - } - - pageNavigationTargetRef.current = params.target; - }; - - navigateInGrid({ - rows: calendarWeekRowRefs.current, - rowsCells: calendarWeekRowsCellsRef.current, - target: event.target as HTMLElement, - event, - changePage, - }); - }); + const rowsRef: useCalendarDaysGridBody.RowsRef = React.useRef([]); + const cellsRef: useCalendarDaysGridBody.CellsRef = React.useRef([]); const getDaysGridBodyProps = React.useCallback( (externalProps: GenericHTMLProps) => { @@ -73,35 +22,42 @@ export function useCalendarDaysGridBody(parameters: useCalendarDaysGridBody.Para children == null ? null : children({ weeks: calendarDaysGridContext.daysGrid.map((week) => week[0]) }), - onKeyDown, + onKeyDown: calendarRootContext.applyDayGridKeyboardNavigation, }); }, - [calendarDaysGridContext.daysGrid, children, onKeyDown], + [ + calendarDaysGridContext.daysGrid, + calendarRootContext.applyDayGridKeyboardNavigation, + children, + ], ); const registerWeekRowCells = useEventCallback( ( weekRowRef: React.RefObject, - cellsRef: React.RefObject<(HTMLElement | null)[]>, + weekCellsRef: React.RefObject<(HTMLElement | null)[]>, ) => { - calendarWeekRowsCellsRef.current.push({ rowRef: weekRowRef, cellsRef }); + cellsRef.current.push({ rowRef: weekRowRef, cellsRef: weekCellsRef }); return () => { - calendarWeekRowsCellsRef.current = calendarWeekRowsCellsRef.current.filter( - (entry) => entry.rowRef !== weekRowRef, - ); + cellsRef.current = cellsRef.current.filter((entry) => entry.rowRef !== weekRowRef); }; }, ); + const registerDaysGridCells = calendarRootContext.registerDaysGridCells; + React.useEffect(() => { + return registerDaysGridCells(cellsRef, rowsRef); + }, [registerDaysGridCells]); + const context: CalendarDaysGridBodyContext = React.useMemo( () => ({ registerWeekRowCells }), [registerWeekRowCells], ); return React.useMemo( - () => ({ getDaysGridBodyProps, context, calendarWeekRowRefs }), - [getDaysGridBodyProps, context, calendarWeekRowRefs], + () => ({ getDaysGridBodyProps, context, calendarWeekRowRefs: rowsRef }), + [getDaysGridBodyProps, context, rowsRef], ); } @@ -113,4 +69,13 @@ export namespace useCalendarDaysGridBody { export interface ChildrenParameters { weeks: PickerValidDate[]; } + + export type CellsRef = React.RefObject< + { + rowRef: React.RefObject; + cellsRef: React.RefObject<(HTMLElement | null)[]>; + }[] + >; + + export type RowsRef = React.RefObject<(HTMLElement | null)[]>; } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts index d00e387ad7aaa..435dd42b1fe98 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts @@ -2,6 +2,7 @@ import * as React from 'react'; import { PickersTimezone, PickerValidDate } from '../../../../models'; import { ValidateDateProps } from '../../../../validation'; import type { useCalendarRoot } from './useCalendarRoot'; +import type { useCalendarDaysGridBody } from '../days-grid-body/useCalendarDaysGridBody'; export interface CalendarRootContext { value: PickerValidDate | null; @@ -19,6 +20,11 @@ export interface CalendarRootContext { visibleDate: PickerValidDate; setVisibleDate: (visibleDate: PickerValidDate) => void; monthPageSize: number; + applyDayGridKeyboardNavigation: (event: React.KeyboardEvent) => void; + registerDaysGridCells: ( + cellsRef: useCalendarDaysGridBody.CellsRef, + rowsRef: useCalendarDaysGridBody.RowsRef, + ) => () => void; } export const CalendarRootContext = React.createContext(undefined); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarDaysGridsNavigation.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarDaysGridsNavigation.ts new file mode 100644 index 0000000000000..a941f99c3aa80 --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarDaysGridsNavigation.ts @@ -0,0 +1,91 @@ +import * as React from 'react'; +import useEventCallback from '@mui/utils/useEventCallback'; +import useTimeout from '@mui/utils/useTimeout'; +import { PickerValidDate } from '../../../../models'; +import { useUtils } from '../../../hooks/useUtils'; +import type { useCalendarDaysGridBody } from '../days-grid-body/useCalendarDaysGridBody'; +import { + applyInitialFocusInGrid, + navigateInGrid, + NavigateInGridChangePage, + PageNavigationTarget, +} from '../utils/keyboardNavigation'; +import type { CalendarRootContext } from './CalendarRootContext'; + +/** + * This logic needs to be in Calendar.Root to support multiple Calendar.DaysGrid. + * We could introduce a Calendar.MultipleDaysGrid component that would handle this logic if we want to avoid having it in Calendar.Root. + */ +export function useCalendarDaysGridNavigation( + parameters: useCalendarDaysGridNavigation.Parameters, +) { + const { visibleDate, setVisibleDate, monthPageSize } = parameters; + const utils = useUtils(); + const gridsRef = React.useRef< + { cells: useCalendarDaysGridBody.CellsRef; rows: useCalendarDaysGridBody.RowsRef }[] + >([]); + const pageNavigationTargetRef = React.useRef(null); + + const timeout = useTimeout(); + React.useEffect(() => { + if (pageNavigationTargetRef.current) { + const target = pageNavigationTargetRef.current; + timeout.start(0, () => { + applyInitialFocusInGrid({ + grids: gridsRef.current, + target, + }); + }); + } + }, [visibleDate, timeout]); + + const applyDayGridKeyboardNavigation = useEventCallback((event: React.KeyboardEvent) => { + const changePage: NavigateInGridChangePage = (params) => { + // TODO: Jump over months with no valid date. + if (params.direction === 'next') { + setVisibleDate(utils.addMonths(visibleDate, monthPageSize)); + } + if (params.direction === 'previous') { + setVisibleDate(utils.addMonths(visibleDate, -monthPageSize)); + } + + pageNavigationTargetRef.current = params.target; + }; + + navigateInGrid({ + grids: gridsRef.current, + target: event.target as HTMLElement, + event, + changePage, + }); + }); + + const registerDaysGridCells = useEventCallback( + ( + gridCellsRef: useCalendarDaysGridBody.CellsRef, + gridRowsRef: useCalendarDaysGridBody.RowsRef, + ) => { + gridsRef.current.push({ cells: gridCellsRef, rows: gridRowsRef }); + + return () => { + gridsRef.current = gridsRef.current.filter((entry) => entry.cells !== gridCellsRef); + }; + }, + ); + + return { + registerDaysGridCells, + applyDayGridKeyboardNavigation, + }; +} + +export namespace useCalendarDaysGridNavigation { + export interface Parameters { + visibleDate: PickerValidDate; + setVisibleDate: (visibleDate: PickerValidDate) => void; + monthPageSize: number; + } + + export interface ReturnValue + extends Pick {} +} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts index f95c15d70b9c2..ea55deb60b98c 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts @@ -22,6 +22,7 @@ import { CalendarRootContext } from './CalendarRootContext'; import { useValidation } from '../../../../validation'; import { mergeReactProps } from '../../utils/mergeReactProps'; import { GenericHTMLProps } from '../../utils/types'; +import { useCalendarDaysGridNavigation } from './useCalendarDaysGridsNavigation'; function useAddDefaultsToValidateDateProps( validationDate: ExportedValidateDateProps, @@ -115,6 +116,8 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { const [visibleDate, setVisibleDate] = React.useState(referenceDate); const [prevValue, setPrevValue] = React.useState(value); + + // TODO: Should not change visible date when clicking on a day cell in a grid with an offset. if (value !== prevValue && utils.isValid(value)) { setVisibleDate(value); setPrevValue(value); @@ -132,6 +135,12 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { }); }); + const { applyDayGridKeyboardNavigation, registerDaysGridCells } = useCalendarDaysGridNavigation({ + visibleDate, + setVisibleDate, + monthPageSize, + }); + const context: CalendarRootContext = React.useMemo( () => ({ value, @@ -146,6 +155,8 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { visibleDate, setVisibleDate, monthPageSize, + applyDayGridKeyboardNavigation, + registerDaysGridCells, }), [ value, @@ -160,6 +171,8 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { visibleDate, setVisibleDate, monthPageSize, + applyDayGridKeyboardNavigation, + registerDaysGridCells, ], ); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts index 9edc8c5c45cfe..7b201bd727fea 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts @@ -1,3 +1,5 @@ +import type { useCalendarDaysGridBody } from '../days-grid-body/useCalendarDaysGridBody'; + const LIST_NAVIGATION_SUPPORTED_KEYS = ['ArrowDown', 'ArrowUp', 'Home', 'End']; export function navigateInList({ @@ -68,42 +70,87 @@ const GRID_NAVIGATION_SUPPORTED_KEYS = [ 'End', ]; -function getCellsInGrid({ - rows, - rowsCells, -}: { - rows: (HTMLElement | null)[]; - rowsCells: { - rowRef: React.RefObject; - cellsRef: React.RefObject<(HTMLElement | null)[]>; - }[]; -}) { - const cells: HTMLElement[][] = []; - for (let i = 0; i < rows.length; i += 1) { - const row = rows[i]; - const rowCells = rowsCells - .find((entry) => entry.rowRef.current === row) - ?.cellsRef.current?.filter((cell) => cell !== null); - if (rowCells && rowCells.length > 0) { - cells.push(rowCells); +type Grid = { cells: useCalendarDaysGridBody.CellsRef; rows: useCalendarDaysGridBody.RowsRef }; + +/* eslint-disable no-bitwise */ +function sortGridByDocumentPosition(a: HTMLElement[][], b: HTMLElement[][]) { + const position = a[0][0].compareDocumentPosition(b[0][0]); + + if ( + position & Node.DOCUMENT_POSITION_FOLLOWING || + position & Node.DOCUMENT_POSITION_CONTAINED_BY + ) { + return -1; + } + + if (position & Node.DOCUMENT_POSITION_PRECEDING || position & Node.DOCUMENT_POSITION_CONTAINS) { + return 1; + } + + return 0; +} +/* eslint-enable no-bitwise */ + +function getCellsInCalendar(grids: Grid[]) { + const cells: HTMLElement[][][] = []; + + let rowCount = 0; + for (let i = 0; i < grids.length; i += 1) { + const grid = grids[i]; + const gridCells: HTMLElement[][] = []; + for (let j = 0; j < grid.rows.current.length; j += 1) { + const row = grid.rows.current[j]; + const rowCells = grid.cells.current + .find((entry) => entry.rowRef.current === row) + ?.cellsRef.current?.filter((cell) => cell !== null); + if (rowCells && rowCells.length > 0) { + rowCount += 1; + gridCells.push(rowCells); + } + } + + if (gridCells.length > 0) { + cells.push(gridCells); } } - return cells; + const sortedCells = cells.sort(sortGridByDocumentPosition); + + return { + list: sortedCells.flat(2), + rows: sortedCells.flat(1), + nested: sortedCells, + rowCount, + }; } +const localeTargetInCalendar = ( + target: HTMLElement, + cells: ReturnType, +) => { + let rowInAllCalendars = 0; + for (let i = 0; i < cells.nested.length; i += 1) { + for (let j = 0; j < cells.nested[i].length; j += 1) { + for (let k = 0; k < cells.nested[i][j].length; k += 1) { + if (cells.nested[i][j][k] === target) { + return { calendar: i, row: rowInAllCalendars, col: k, rowInCalendar: j }; + } + } + + rowInAllCalendars += 1; + } + } + + throw new Error('Could not find target in calendar'); +}; + export function navigateInGrid({ - rows, - rowsCells, + grids, target, event, changePage, }: { - rows: (HTMLElement | null)[]; - rowsCells: { - rowRef: React.RefObject; - cellsRef: React.RefObject<(HTMLElement | null)[]>; - }[]; + grids: Grid[]; target: HTMLElement; event: React.KeyboardEvent; changePage: NavigateInGridChangePage | undefined; @@ -114,28 +161,28 @@ export function navigateInGrid({ event.preventDefault(); - const cells = getCellsInGrid({ rows, rowsCells }); - if (cells.length === 0) { + const cells = getCellsInCalendar(grids); + if (cells.nested.length === 0) { return; } + const coordinates = localeTargetInCalendar(target, cells); + const moveToRowBelow = () => { - const currentRowIndex = cells.findIndex((row) => row.includes(target)); - const currentColIndex = cells[currentRowIndex].indexOf(target); let nextRowIndex = -1; - let i = currentRowIndex + 1; + let i = coordinates.row + 1; - while (nextRowIndex === -1 && i < currentRowIndex + cells.length) { - if (changePage && i > cells.length - 1) { + while (nextRowIndex === -1 && i < coordinates.row + cells.rowCount) { + if (changePage && i > cells.rowCount - 1) { changePage({ direction: 'next', - target: { type: 'first-cell-in-col', colIndex: currentColIndex }, + target: { type: 'first-cell-in-col', colIndex: coordinates.col }, }); break; } - const rowIndex = i % cells.length; - const cell = cells[rowIndex][currentColIndex]; + const rowIndex = i % cells.rowCount; + const cell = cells.rows[rowIndex][coordinates.col]; if (isNavigable(cell)) { nextRowIndex = rowIndex; } @@ -143,26 +190,24 @@ export function navigateInGrid({ } if (nextRowIndex > -1) { - cells[nextRowIndex][currentColIndex].focus(); + cells.rows[nextRowIndex][coordinates.col].focus(); } }; const moveToRowAbove = () => { - const currentRowIndex = cells.findIndex((row) => row.includes(target)); - const currentColIndex = cells[currentRowIndex].indexOf(target); let nextRowIndex = -1; - let i = currentRowIndex - 1; - while (nextRowIndex === -1 && i > currentRowIndex - cells.length) { + let i = coordinates.row - 1; + while (nextRowIndex === -1 && i > coordinates.row - cells.rowCount) { if (changePage && i < 0) { changePage({ direction: 'previous', - target: { type: 'last-cell-in-col', colIndex: currentColIndex }, + target: { type: 'last-cell-in-col', colIndex: coordinates.col }, }); break; } - const rowIndex = (cells.length + i) % cells.length; - const cell = cells[rowIndex][currentColIndex]; + const rowIndex = (cells.rowCount + i) % cells.rowCount; + const cell = cells.rows[rowIndex][coordinates.col]; if (isNavigable(cell)) { nextRowIndex = rowIndex; } @@ -170,18 +215,17 @@ export function navigateInGrid({ } if (nextRowIndex > -1) { - cells[nextRowIndex][currentColIndex].focus(); + cells.rows[nextRowIndex][coordinates.col].focus(); } }; const moveToRowOnTheRight = () => { - const flatCells = cells.flat(); - const currentCellIndex = flatCells.indexOf(target); + const currentCellIndex = cells.list.indexOf(target); let nextCellIndex = -1; let i = currentCellIndex + 1; - while (nextCellIndex === -1 && i < currentCellIndex + flatCells.length) { - if (changePage && i > flatCells.length - 1) { + while (nextCellIndex === -1 && i < currentCellIndex + cells.list.length) { + if (changePage && i > cells.list.length - 1) { changePage({ direction: 'next', target: { type: 'first-cell' }, @@ -189,8 +233,8 @@ export function navigateInGrid({ break; } - const cellIndex = i % flatCells.length; - const cell = flatCells[cellIndex]; + const cellIndex = i % cells.list.length; + const cell = cells.list[cellIndex]; if (isNavigable(cell)) { nextCellIndex = cellIndex; } @@ -198,17 +242,16 @@ export function navigateInGrid({ } if (nextCellIndex > -1) { - flatCells[nextCellIndex].focus(); + cells.list[nextCellIndex].focus(); } }; const moveToRowOnTheLeft = () => { - const flatCells = cells.flat(); - const currentCellIndex = flatCells.indexOf(target); + const currentCellIndex = cells.list.indexOf(target); let nextCellIndex = -1; let i = currentCellIndex - 1; - while (nextCellIndex === -1 && i > currentCellIndex - flatCells.length) { + while (nextCellIndex === -1 && i > currentCellIndex - cells.list.length) { if (changePage && i < 0) { changePage({ direction: 'previous', @@ -217,8 +260,8 @@ export function navigateInGrid({ break; } - const cellIndex = (flatCells.length + i) % flatCells.length; - const cell = flatCells[cellIndex]; + const cellIndex = (cells.list.length + i) % cells.list.length; + const cell = cells.list[cellIndex]; if (isNavigable(cell)) { nextCellIndex = cellIndex; } @@ -226,19 +269,19 @@ export function navigateInGrid({ } if (nextCellIndex > -1) { - flatCells[nextCellIndex].focus(); + cells.list[nextCellIndex].focus(); } }; const moveToFirstCell = () => { - const cell = cells.flat().find(isNavigable); + const cell = cells.list.find(isNavigable); if (cell) { cell.focus(); } }; const moveToLastCell = () => { - const cell = cells.flat().findLast(isNavigable); + const cell = cells.list.findLast(isNavigable); if (cell) { cell.focus(); } @@ -284,34 +327,30 @@ export type NavigateInGridChangePage = (params: { }) => void; export function applyInitialFocusInGrid({ - rows, - rowsCells, + grids, target, }: { - rows: (HTMLElement | null)[]; - rowsCells: { - rowRef: React.RefObject; - cellsRef: React.RefObject<(HTMLElement | null)[]>; - }[]; + grids: Grid[]; target: PageNavigationTarget; }) { - const cells = getCellsInGrid({ rows, rowsCells }); + const cells = getCellsInCalendar(grids); let cell: HTMLElement | undefined; if (target.type === 'first-cell') { - cell = cells.flat().find(isNavigable); + cell = cells.list.find(isNavigable); } if (target.type === 'last-cell') { - cell = cells.flat().findLast(isNavigable); + cell = cells.list.findLast(isNavigable); } if (target.type === 'first-cell-in-col') { - cell = cells.map((row) => row[target.colIndex]).find(isNavigable); + cell = cells.rows.map((row) => row[target.colIndex]).find(isNavigable); } + // TODO: Support when the 1st month is fully disabled. if (target.type === 'last-cell-in-col') { - cell = cells.map((row) => row[target.colIndex]).findLast(isNavigable); + cell = cells.rows.map((row) => row[target.colIndex]).findLast(isNavigable); } if (cell) { From 9020f78f7774f1a5239f2f0a8d56b90c91101289 Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 6 Jan 2025 12:00:04 +0100 Subject: [PATCH 031/136] Fix CSS --- docs/data/date-pickers/base-calendar/calendar.module.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/data/date-pickers/base-calendar/calendar.module.css b/docs/data/date-pickers/base-calendar/calendar.module.css index 1d033a86e137c..901312d580ab7 100644 --- a/docs/data/date-pickers/base-calendar/calendar.module.css +++ b/docs/data/date-pickers/base-calendar/calendar.module.css @@ -123,7 +123,7 @@ } &[data-current] { - outline: 2px solid #9ca3af; + outline: 1px solid #9ca3af; } &[data-selected] { From 9c7e6da14f5d51e5321ba8fd67e4f833a58497c6 Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 6 Jan 2025 12:01:56 +0100 Subject: [PATCH 032/136] Remove unused code --- .../src/internals/base/utils/proptypes.ts | 57 ------------------- 1 file changed, 57 deletions(-) delete mode 100644 packages/x-date-pickers/src/internals/base/utils/proptypes.ts diff --git a/packages/x-date-pickers/src/internals/base/utils/proptypes.ts b/packages/x-date-pickers/src/internals/base/utils/proptypes.ts deleted file mode 100644 index 9e29cad16f581..0000000000000 --- a/packages/x-date-pickers/src/internals/base/utils/proptypes.ts +++ /dev/null @@ -1,57 +0,0 @@ -import PropTypes, { ValidationMap } from 'prop-types'; - -export const refType = PropTypes.oneOfType([PropTypes.func, PropTypes.object]); - -export function HTMLElementType( - props: { [key: string]: unknown }, - propName: string, - componentName: string, - location: string, - propFullName: string, -): Error | null { - if (process.env.NODE_ENV === 'production') { - return null; - } - - const propValue = props[propName]; - const safePropName = propFullName || propName; - - if (propValue == null) { - return null; - } - - if (propValue && (propValue as any).nodeType !== 1) { - return new Error( - `Invalid ${location} \`${safePropName}\` supplied to \`${componentName}\`. ` + - `Expected an HTMLElement.`, - ); - } - - return null; -} - -const specialProperty = 'exact-prop: \u200b'; - -// This module is based on https://github.com/airbnb/prop-types-exact repository. -// However, in order to reduce the number of dependencies and to remove some extra safe checks -// the module was forked. -export function exactProp(propTypes: ValidationMap): ValidationMap { - if (process.env.NODE_ENV === 'production') { - return propTypes; - } - - return { - ...propTypes, - [specialProperty]: (props: { [key: string]: unknown }) => { - const unsupportedProps = Object.keys(props).filter((prop) => !propTypes.hasOwnProperty(prop)); - if (unsupportedProps.length > 0) { - return new Error( - `The following props are not supported: ${unsupportedProps - .map((prop) => `\`${prop}\``) - .join(', ')}. Please remove them.`, - ); - } - return null; - }, - }; -} From 3fdfb5318e3ce9873e0646497ad833e025664703 Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 6 Jan 2025 12:06:56 +0100 Subject: [PATCH 033/136] Fix --- .../base/Calendar/days-week-row/CalendarDaysWeekRow.tsx | 2 +- .../base/Calendar/root/useCalendarDaysGridsNavigation.ts | 1 - .../src/internals/base/Calendar/utils/keyboardNavigation.ts | 3 +-- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/CalendarDaysWeekRow.tsx b/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/CalendarDaysWeekRow.tsx index 8c399926fef80..8f4d4c37202f6 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/CalendarDaysWeekRow.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/CalendarDaysWeekRow.tsx @@ -46,7 +46,7 @@ const CalendarDaysWeekRow = React.forwardRef(function CalendarDaysWeekRow( // TODO: Improve how we pass the week to this component. const days = React.useMemo( - () => calendarDaysGridContext.daysGrid.find((week) => week[0] === props.value), + () => calendarDaysGridContext.daysGrid.find((week) => week[0] === props.value) ?? [], [calendarDaysGridContext.daysGrid, props.value], ); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarDaysGridsNavigation.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarDaysGridsNavigation.ts index a941f99c3aa80..5bf141e9f21f8 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarDaysGridsNavigation.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarDaysGridsNavigation.ts @@ -54,7 +54,6 @@ export function useCalendarDaysGridNavigation( navigateInGrid({ grids: gridsRef.current, - target: event.target as HTMLElement, event, changePage, }); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts index 7b201bd727fea..e3886e5638bd8 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts @@ -146,12 +146,10 @@ const localeTargetInCalendar = ( export function navigateInGrid({ grids, - target, event, changePage, }: { grids: Grid[]; - target: HTMLElement; event: React.KeyboardEvent; changePage: NavigateInGridChangePage | undefined; }) { @@ -166,6 +164,7 @@ export function navigateInGrid({ return; } + const target = event.target as HTMLElement; const coordinates = localeTargetInCalendar(target, cells); const moveToRowBelow = () => { From 6a3efe525f9ed4845563d37e22d0bc701acabc1f Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 6 Jan 2025 12:24:20 +0100 Subject: [PATCH 034/136] Work --- .../base/Calendar/utils/keyboardNavigation.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts index e3886e5638bd8..6b87ce0dca602 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts @@ -128,16 +128,16 @@ const localeTargetInCalendar = ( target: HTMLElement, cells: ReturnType, ) => { - let rowInAllCalendars = 0; + let rowInAllGrids = 0; for (let i = 0; i < cells.nested.length; i += 1) { for (let j = 0; j < cells.nested[i].length; j += 1) { for (let k = 0; k < cells.nested[i][j].length; k += 1) { if (cells.nested[i][j][k] === target) { - return { calendar: i, row: rowInAllCalendars, col: k, rowInCalendar: j }; + return { calendar: i, row: rowInAllGrids, col: k }; } } - rowInAllCalendars += 1; + rowInAllGrids += 1; } } @@ -272,15 +272,15 @@ export function navigateInGrid({ } }; - const moveToFirstCell = () => { - const cell = cells.list.find(isNavigable); + const moveToFirstCellInGrid = () => { + const cell = cells.nested[coordinates.calendar].flat().find(isNavigable); if (cell) { cell.focus(); } }; - const moveToLastCell = () => { - const cell = cells.list.findLast(isNavigable); + const moveToLastCellInGrid = () => { + const cell = cells.nested[coordinates.calendar].flat().findLast(isNavigable); if (cell) { cell.focus(); } @@ -302,10 +302,10 @@ export function navigateInGrid({ moveToRowAbove(); break; case 'Home': - moveToFirstCell(); + moveToFirstCellInGrid(); break; case 'End': - moveToLastCell(); + moveToLastCellInGrid(); break; default: break; From b786c3a90c7be236bbe117dac328a03b64a659f9 Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 6 Jan 2025 12:56:33 +0100 Subject: [PATCH 035/136] Fix value section with multiple days --- .../base-calendar/DayCalendarDemo.tsx | 2 +- .../Calendar/days-grid/useCalendarDaysGrid.ts | 5 ++ .../months-list/useCalendarMonthsList.ts | 11 +++- .../base/Calendar/root/CalendarRootContext.ts | 19 +++++-- .../base/Calendar/root/useCalendarRoot.ts | 54 +++++++++++++++---- .../years-list/useCalendarYearsList.ts | 5 ++ 6 files changed, 81 insertions(+), 15 deletions(-) diff --git a/docs/data/date-pickers/base-calendar/DayCalendarDemo.tsx b/docs/data/date-pickers/base-calendar/DayCalendarDemo.tsx index 8f83700f9e1bf..f3540af494627 100644 --- a/docs/data/date-pickers/base-calendar/DayCalendarDemo.tsx +++ b/docs/data/date-pickers/base-calendar/DayCalendarDemo.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Dayjs } from 'dayjs'; +import dayjs, { Dayjs } from 'dayjs'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-grid/useCalendarDaysGrid.ts b/packages/x-date-pickers/src/internals/base/Calendar/days-grid/useCalendarDaysGrid.ts index 50d775dcde7ad..e1369be665a25 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-grid/useCalendarDaysGrid.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-grid/useCalendarDaysGrid.ts @@ -70,6 +70,11 @@ export function useCalendarDaysGrid(parameters: useCalendarDaysGrid.Parameters) return flatDays.find((day) => utils.isSameMonth(day, currentMonth)) ?? null; }, [calendarRootContext.value, calendarRootContext.referenceDate, daysGrid, utils, currentMonth]); + const registerSection = calendarRootContext.registerSection; + React.useEffect(() => { + return registerSection({ type: 'day', value: currentMonth }); + }, [registerSection, currentMonth]); + const context: CalendarDaysGridContext = React.useMemo( () => ({ selectDay, daysGrid, currentMonth, tabbableDay }), [selectDay, daysGrid, currentMonth, tabbableDay], diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-list/useCalendarMonthsList.ts b/packages/x-date-pickers/src/internals/base/Calendar/months-list/useCalendarMonthsList.ts index b3c0b9970d131..1530658ce0c3b 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-list/useCalendarMonthsList.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-list/useCalendarMonthsList.ts @@ -15,11 +15,13 @@ export function useCalendarMonthsList(parameters: useCalendarMonthsList.Paramete const calendarRootContext = useCalendarRootContext(); const calendarMonthsCellRefs = React.useRef<(HTMLElement | null)[]>([]); - const months = React.useMemo( - () => getMonthsInYear(utils, calendarRootContext.visibleDate), + const currentYear = React.useMemo( + () => utils.startOfYear(calendarRootContext.visibleDate), [utils, calendarRootContext.visibleDate], ); + const months = React.useMemo(() => getMonthsInYear(utils, currentYear), [utils, currentYear]); + const onKeyDown = useEventCallback((event: React.KeyboardEvent) => { navigateInList({ cells: calendarMonthsCellRefs.current, @@ -75,6 +77,11 @@ export function useCalendarMonthsList(parameters: useCalendarMonthsList.Paramete } }); + const registerSection = calendarRootContext.registerSection; + React.useEffect(() => { + return registerSection({ type: 'month', value: currentYear }); + }, [registerSection, currentYear]); + const context: CalendarMonthsListContext = React.useMemo(() => ({ selectMonth }), [selectMonth]); return React.useMemo( diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts index 435dd42b1fe98..22655caf7db40 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts @@ -1,15 +1,27 @@ import * as React from 'react'; import { PickersTimezone, PickerValidDate } from '../../../../models'; import { ValidateDateProps } from '../../../../validation'; +import { PickerValue } from '../../../models'; import type { useCalendarRoot } from './useCalendarRoot'; import type { useCalendarDaysGridBody } from '../days-grid-body/useCalendarDaysGridBody'; export interface CalendarRootContext { - value: PickerValidDate | null; + /** + * The current value of the calendar. + */ + value: PickerValue; + /** + * Set the current value of the calendar. + * @param {PickerValue} value The new value of the calendar. + * @param {Pick} options The options to customize the behavior of this update. + */ setValue: ( - value: PickerValidDate | null, - context: Pick, + value: PickerValue, + options: Pick, ) => void; + /** + * The reference date of the calendar. + */ referenceDate: PickerValidDate; timezone: PickersTimezone; disabled: boolean; @@ -25,6 +37,7 @@ export interface CalendarRootContext { cellsRef: useCalendarDaysGridBody.CellsRef, rowsRef: useCalendarDaysGridBody.RowsRef, ) => () => void; + registerSection: (parameters: useCalendarRoot.RegisterSectionParameters) => () => void; } export const CalendarRootContext = React.createContext(undefined); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts index ea55deb60b98c..90c5133ffa5c9 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts @@ -114,15 +114,6 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { validator: validateDate, }); - const [visibleDate, setVisibleDate] = React.useState(referenceDate); - const [prevValue, setPrevValue] = React.useState(value); - - // TODO: Should not change visible date when clicking on a day cell in a grid with an offset. - if (value !== prevValue && utils.isValid(value)) { - setVisibleDate(value); - setPrevValue(value); - } - const isDateDisabled = useIsDateDisabled({ ...validationProps, timezone, @@ -135,6 +126,44 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { }); }); + const sectionsRef = React.useRef< + Record<'day' | 'month' | 'year', Record> + >({ + day: {}, + month: {}, + year: {}, + }); + const registerSection = useEventCallback((section: useCalendarRoot.RegisterSectionParameters) => { + const id = Math.random(); + sectionsRef.current[section.type][id] = section.value; + return () => { + delete sectionsRef.current[section.type][id]; + }; + }); + + const [visibleDate, setVisibleDate] = React.useState(referenceDate); + const [prevValue, setPrevValue] = React.useState(value); + + if (value !== prevValue && utils.isValid(value)) { + let shouldNavigate; + if (Object.values(sectionsRef.current.day).length > 0) { + shouldNavigate = Object.values(sectionsRef.current.day).every( + (month) => !utils.isSameMonth(value, month), + ); + } else if (Object.values(sectionsRef.current.month).length > 0) { + shouldNavigate = Object.values(sectionsRef.current.month).every( + (year) => !utils.isSameYear(value, year), + ); + } else { + shouldNavigate = true; + } + + setPrevValue(value); + if (shouldNavigate) { + setVisibleDate(value); + } + } + const { applyDayGridKeyboardNavigation, registerDaysGridCells } = useCalendarDaysGridNavigation({ visibleDate, setVisibleDate, @@ -157,6 +186,7 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { monthPageSize, applyDayGridKeyboardNavigation, registerDaysGridCells, + registerSection, }), [ value, @@ -173,6 +203,7 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { monthPageSize, applyDayGridKeyboardNavigation, registerDaysGridCells, + registerSection, ], ); @@ -240,4 +271,9 @@ export namespace useCalendarRoot { */ validationError: DateValidationError; } + + export interface RegisterSectionParameters { + type: 'day' | 'month' | 'year'; + value: PickerValidDate; + } } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-list/useCalendarYearsList.ts b/packages/x-date-pickers/src/internals/base/Calendar/years-list/useCalendarYearsList.ts index 848354a800419..3c7bc7b624afb 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-list/useCalendarYearsList.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-list/useCalendarYearsList.ts @@ -83,6 +83,11 @@ export function useCalendarYearsList(parameters: useCalendarYearsList.Parameters } }); + const registerSection = calendarRootContext.registerSection; + React.useEffect(() => { + return registerSection({ type: 'month', value: calendarRootContext.visibleDate }); + }, [registerSection, calendarRootContext.visibleDate]); + const context: CalendarYearsListContext = React.useMemo(() => ({ selectYear }), [selectYear]); return React.useMemo( From f200c6354909ee64c7a7594afc025af016b9594a Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 6 Jan 2025 12:57:43 +0100 Subject: [PATCH 036/136] Work --- .../src/internals/base/Calendar/root/useCalendarRoot.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts index 90c5133ffa5c9..e7d0e2e1e3a64 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts @@ -145,7 +145,7 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { const [prevValue, setPrevValue] = React.useState(value); if (value !== prevValue && utils.isValid(value)) { - let shouldNavigate; + let shouldNavigate: boolean; if (Object.values(sectionsRef.current.day).length > 0) { shouldNavigate = Object.values(sectionsRef.current.day).every( (month) => !utils.isSameMonth(value, month), From 3fd505f80e9d335aa6d9fef6af2068d0efa79597 Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 6 Jan 2025 13:05:12 +0100 Subject: [PATCH 037/136] Work --- .../root/useCalendarDaysGridsNavigation.ts | 58 +++++-- .../base/Calendar/utils/keyboardNavigation.ts | 145 ++++++------------ 2 files changed, 96 insertions(+), 107 deletions(-) diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarDaysGridsNavigation.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarDaysGridsNavigation.ts index 5bf141e9f21f8..7d804156d2561 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarDaysGridsNavigation.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarDaysGridsNavigation.ts @@ -31,10 +31,8 @@ export function useCalendarDaysGridNavigation( if (pageNavigationTargetRef.current) { const target = pageNavigationTargetRef.current; timeout.start(0, () => { - applyInitialFocusInGrid({ - grids: gridsRef.current, - target, - }); + const cells = getCellsInCalendar(gridsRef.current); + applyInitialFocusInGrid({ cells, target }); }); } }, [visibleDate, timeout]); @@ -52,11 +50,8 @@ export function useCalendarDaysGridNavigation( pageNavigationTargetRef.current = params.target; }; - navigateInGrid({ - grids: gridsRef.current, - event, - changePage, - }); + const cells = getCellsInCalendar(gridsRef.current); + navigateInGrid({ cells, event, changePage }); }); const registerDaysGridCells = useEventCallback( @@ -88,3 +83,48 @@ export namespace useCalendarDaysGridNavigation { export interface ReturnValue extends Pick {} } + +/* eslint-disable no-bitwise */ +function sortGridByDocumentPosition(a: HTMLElement[][], b: HTMLElement[][]) { + const position = a[0][0].compareDocumentPosition(b[0][0]); + + if ( + position & Node.DOCUMENT_POSITION_FOLLOWING || + position & Node.DOCUMENT_POSITION_CONTAINED_BY + ) { + return -1; + } + + if (position & Node.DOCUMENT_POSITION_PRECEDING || position & Node.DOCUMENT_POSITION_CONTAINS) { + return 1; + } + + return 0; +} +/* eslint-enable no-bitwise */ + +function getCellsInCalendar( + grids: { cells: useCalendarDaysGridBody.CellsRef; rows: useCalendarDaysGridBody.RowsRef }[], +) { + const cells: HTMLElement[][][] = []; + + for (let i = 0; i < grids.length; i += 1) { + const grid = grids[i]; + const gridCells: HTMLElement[][] = []; + for (let j = 0; j < grid.rows.current.length; j += 1) { + const row = grid.rows.current[j]; + const rowCells = grid.cells.current + .find((entry) => entry.rowRef.current === row) + ?.cellsRef.current?.filter((cell) => cell !== null); + if (rowCells && rowCells.length > 0) { + gridCells.push(rowCells); + } + } + + if (gridCells.length > 0) { + cells.push(gridCells); + } + } + + return cells.sort(sortGridByDocumentPosition); +} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts index 6b87ce0dca602..62fd8b6f49fb9 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts @@ -1,5 +1,3 @@ -import type { useCalendarDaysGridBody } from '../days-grid-body/useCalendarDaysGridBody'; - const LIST_NAVIGATION_SUPPORTED_KEYS = ['ArrowDown', 'ArrowUp', 'Home', 'End']; export function navigateInList({ @@ -70,69 +68,12 @@ const GRID_NAVIGATION_SUPPORTED_KEYS = [ 'End', ]; -type Grid = { cells: useCalendarDaysGridBody.CellsRef; rows: useCalendarDaysGridBody.RowsRef }; - -/* eslint-disable no-bitwise */ -function sortGridByDocumentPosition(a: HTMLElement[][], b: HTMLElement[][]) { - const position = a[0][0].compareDocumentPosition(b[0][0]); - - if ( - position & Node.DOCUMENT_POSITION_FOLLOWING || - position & Node.DOCUMENT_POSITION_CONTAINED_BY - ) { - return -1; - } - - if (position & Node.DOCUMENT_POSITION_PRECEDING || position & Node.DOCUMENT_POSITION_CONTAINS) { - return 1; - } - - return 0; -} -/* eslint-enable no-bitwise */ - -function getCellsInCalendar(grids: Grid[]) { - const cells: HTMLElement[][][] = []; - - let rowCount = 0; - for (let i = 0; i < grids.length; i += 1) { - const grid = grids[i]; - const gridCells: HTMLElement[][] = []; - for (let j = 0; j < grid.rows.current.length; j += 1) { - const row = grid.rows.current[j]; - const rowCells = grid.cells.current - .find((entry) => entry.rowRef.current === row) - ?.cellsRef.current?.filter((cell) => cell !== null); - if (rowCells && rowCells.length > 0) { - rowCount += 1; - gridCells.push(rowCells); - } - } - - if (gridCells.length > 0) { - cells.push(gridCells); - } - } - - const sortedCells = cells.sort(sortGridByDocumentPosition); - - return { - list: sortedCells.flat(2), - rows: sortedCells.flat(1), - nested: sortedCells, - rowCount, - }; -} - -const localeTargetInCalendar = ( - target: HTMLElement, - cells: ReturnType, -) => { +const getTargetCoordinates = (target: HTMLElement, cells: HTMLElement[][][]) => { let rowInAllGrids = 0; - for (let i = 0; i < cells.nested.length; i += 1) { - for (let j = 0; j < cells.nested[i].length; j += 1) { - for (let k = 0; k < cells.nested[i][j].length; k += 1) { - if (cells.nested[i][j][k] === target) { + for (let i = 0; i < cells.length; i += 1) { + for (let j = 0; j < cells[i].length; j += 1) { + for (let k = 0; k < cells[i][j].length; k += 1) { + if (cells[i][j][k] === target) { return { calendar: i, row: rowInAllGrids, col: k }; } } @@ -145,11 +86,11 @@ const localeTargetInCalendar = ( }; export function navigateInGrid({ - grids, + cells, event, changePage, }: { - grids: Grid[]; + cells: HTMLElement[][][]; event: React.KeyboardEvent; changePage: NavigateInGridChangePage | undefined; }) { @@ -159,20 +100,20 @@ export function navigateInGrid({ event.preventDefault(); - const cells = getCellsInCalendar(grids); - if (cells.nested.length === 0) { + if (cells.length === 0) { return; } const target = event.target as HTMLElement; - const coordinates = localeTargetInCalendar(target, cells); + const coordinates = getTargetCoordinates(target, cells); const moveToRowBelow = () => { + const rowList = cells.flat(1); let nextRowIndex = -1; let i = coordinates.row + 1; - while (nextRowIndex === -1 && i < coordinates.row + cells.rowCount) { - if (changePage && i > cells.rowCount - 1) { + while (nextRowIndex === -1 && i < coordinates.row + rowList.length) { + if (changePage && i > rowList.length - 1) { changePage({ direction: 'next', target: { type: 'first-cell-in-col', colIndex: coordinates.col }, @@ -180,8 +121,8 @@ export function navigateInGrid({ break; } - const rowIndex = i % cells.rowCount; - const cell = cells.rows[rowIndex][coordinates.col]; + const rowIndex = i % rowList.length; + const cell = rowList[rowIndex][coordinates.col]; if (isNavigable(cell)) { nextRowIndex = rowIndex; } @@ -189,14 +130,15 @@ export function navigateInGrid({ } if (nextRowIndex > -1) { - cells.rows[nextRowIndex][coordinates.col].focus(); + rowList[nextRowIndex][coordinates.col].focus(); } }; const moveToRowAbove = () => { + const rowList = cells.flat(1); let nextRowIndex = -1; let i = coordinates.row - 1; - while (nextRowIndex === -1 && i > coordinates.row - cells.rowCount) { + while (nextRowIndex === -1 && i > coordinates.row - rowList.length) { if (changePage && i < 0) { changePage({ direction: 'previous', @@ -205,8 +147,8 @@ export function navigateInGrid({ break; } - const rowIndex = (cells.rowCount + i) % cells.rowCount; - const cell = cells.rows[rowIndex][coordinates.col]; + const rowIndex = (rowList.length + i) % rowList.length; + const cell = rowList[rowIndex][coordinates.col]; if (isNavigable(cell)) { nextRowIndex = rowIndex; } @@ -214,17 +156,18 @@ export function navigateInGrid({ } if (nextRowIndex > -1) { - cells.rows[nextRowIndex][coordinates.col].focus(); + rowList[nextRowIndex][coordinates.col].focus(); } }; const moveToRowOnTheRight = () => { - const currentCellIndex = cells.list.indexOf(target); + const cellList = cells.flat(2); + const currentCellIndex = cellList.indexOf(target); let nextCellIndex = -1; let i = currentCellIndex + 1; - while (nextCellIndex === -1 && i < currentCellIndex + cells.list.length) { - if (changePage && i > cells.list.length - 1) { + while (nextCellIndex === -1 && i < currentCellIndex + cellList.length) { + if (changePage && i > cellList.length - 1) { changePage({ direction: 'next', target: { type: 'first-cell' }, @@ -232,8 +175,8 @@ export function navigateInGrid({ break; } - const cellIndex = i % cells.list.length; - const cell = cells.list[cellIndex]; + const cellIndex = i % cellList.length; + const cell = cellList[cellIndex]; if (isNavigable(cell)) { nextCellIndex = cellIndex; } @@ -241,16 +184,17 @@ export function navigateInGrid({ } if (nextCellIndex > -1) { - cells.list[nextCellIndex].focus(); + cellList[nextCellIndex].focus(); } }; const moveToRowOnTheLeft = () => { - const currentCellIndex = cells.list.indexOf(target); + const cellList = cells.flat(2); + const currentCellIndex = cellList.indexOf(target); let nextCellIndex = -1; let i = currentCellIndex - 1; - while (nextCellIndex === -1 && i > currentCellIndex - cells.list.length) { + while (nextCellIndex === -1 && i > currentCellIndex - cellList.length) { if (changePage && i < 0) { changePage({ direction: 'previous', @@ -259,8 +203,8 @@ export function navigateInGrid({ break; } - const cellIndex = (cells.list.length + i) % cells.list.length; - const cell = cells.list[cellIndex]; + const cellIndex = (cellList.length + i) % cellList.length; + const cell = cellList[cellIndex]; if (isNavigable(cell)) { nextCellIndex = cellIndex; } @@ -268,19 +212,19 @@ export function navigateInGrid({ } if (nextCellIndex > -1) { - cells.list[nextCellIndex].focus(); + cellList[nextCellIndex].focus(); } }; const moveToFirstCellInGrid = () => { - const cell = cells.nested[coordinates.calendar].flat().find(isNavigable); + const cell = cells[coordinates.calendar].flat().find(isNavigable); if (cell) { cell.focus(); } }; const moveToLastCellInGrid = () => { - const cell = cells.nested[coordinates.calendar].flat().findLast(isNavigable); + const cell = cells[coordinates.calendar].flat().findLast(isNavigable); if (cell) { cell.focus(); } @@ -326,30 +270,35 @@ export type NavigateInGridChangePage = (params: { }) => void; export function applyInitialFocusInGrid({ - grids, + cells, target, }: { - grids: Grid[]; + cells: HTMLElement[][][]; target: PageNavigationTarget; }) { - const cells = getCellsInCalendar(grids); let cell: HTMLElement | undefined; if (target.type === 'first-cell') { - cell = cells.list.find(isNavigable); + cell = cells.flat(2).find(isNavigable); } if (target.type === 'last-cell') { - cell = cells.list.findLast(isNavigable); + cell = cells.flat(2).findLast(isNavigable); } if (target.type === 'first-cell-in-col') { - cell = cells.rows.map((row) => row[target.colIndex]).find(isNavigable); + cell = cells + .flat(1) + .map((row) => row[target.colIndex]) + .find(isNavigable); } // TODO: Support when the 1st month is fully disabled. if (target.type === 'last-cell-in-col') { - cell = cells.rows.map((row) => row[target.colIndex]).findLast(isNavigable); + cell = cells + .flat(1) + .map((row) => row[target.colIndex]) + .findLast(isNavigable); } if (cell) { From c9e6e9e66a9165cf9eb399508f0e8266070aaf3b Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 6 Jan 2025 13:09:32 +0100 Subject: [PATCH 038/136] Fix demos --- .../date-pickers/base-calendar/DayCalendarTwoMonthsDemo.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.js b/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.js index c140a5bddc1d7..2c70ecd52b7f9 100644 --- a/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.js +++ b/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.js @@ -81,7 +81,11 @@ function DayGrid(props) { function DayCalendar(props) { return ( - + From 2c9b3ecf2e144ee11a8a01864f10d2fd448c7edf Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 6 Jan 2025 13:43:07 +0100 Subject: [PATCH 039/136] Add support for month grid' --- .../base-calendar/MonthCalendarDemo.js | 49 ++++++++++++++ .../base-calendar/MonthCalendarDemo.tsx | 49 ++++++++++++++ .../MonthCalendarDemo.tsx.preview | 14 ++++ .../base-calendar/base-calendar.md | 4 ++ .../base-calendar/calendar.module.css | 32 +++++++++ .../CalendarDaysGridBodyContext.ts | 2 +- .../days-grid/CalendarDaysGridContext.ts | 2 +- .../internals/base/Calendar/index.parts.ts | 1 + .../months-cell/CalendarMonthsCell.tsx | 8 +-- .../months-grid/CalendarMonthsGrid.tsx | 44 +++++++++++++ .../months-grid/useCalendarMonthsGrid.ts | 65 ++++++++++++++++++ .../months-list/CalendarMonthsList.tsx | 6 +- .../months-list/CalendarMonthsListContext.ts | 24 ------- .../months-list/useCalendarMonthsList.ts | 58 +--------------- .../base/Calendar/root/CalendarRootContext.ts | 2 +- .../base/Calendar/utils/keyboardNavigation.ts | 4 +- .../CalendarMonthCellCollectionContext.ts | 24 +++++++ .../useCalendarMonthCellCollection.ts | 66 +++++++++++++++++++ .../years-list/CalendarYearsListContext.ts | 2 +- .../years-list/useCalendarYearsList.ts | 1 - 20 files changed, 362 insertions(+), 95 deletions(-) create mode 100644 docs/data/date-pickers/base-calendar/MonthCalendarDemo.js create mode 100644 docs/data/date-pickers/base-calendar/MonthCalendarDemo.tsx create mode 100644 docs/data/date-pickers/base-calendar/MonthCalendarDemo.tsx.preview create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/months-grid/CalendarMonthsGrid.tsx create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/months-grid/useCalendarMonthsGrid.ts delete mode 100644 packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsListContext.ts create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/utils/month-cell-collection/CalendarMonthCellCollectionContext.ts create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/utils/month-cell-collection/useCalendarMonthCellCollection.ts diff --git a/docs/data/date-pickers/base-calendar/MonthCalendarDemo.js b/docs/data/date-pickers/base-calendar/MonthCalendarDemo.js new file mode 100644 index 0000000000000..4dcb327f74126 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/MonthCalendarDemo.js @@ -0,0 +1,49 @@ +import * as React from 'react'; + +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +// eslint-disable-next-line no-restricted-imports +import { + Calendar, + useCalendarContext, +} from '@mui/x-date-pickers/internals/base/Calendar'; +import styles from './calendar.module.css'; + +function Header() { + const { visibleDate } = useCalendarContext(); + + return ( +
+ + ◀ + + {visibleDate.format('YYYY')} + + ▶ + +
+ ); +} + +export default function MonthCalendarDemo() { + const [value, setValue] = React.useState(null); + + return ( + + +
+ + {({ months }) => + months.map((month) => ( + + )) + } + + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/MonthCalendarDemo.tsx b/docs/data/date-pickers/base-calendar/MonthCalendarDemo.tsx new file mode 100644 index 0000000000000..61146fe490457 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/MonthCalendarDemo.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import { Dayjs } from 'dayjs'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +// eslint-disable-next-line no-restricted-imports +import { + Calendar, + useCalendarContext, +} from '@mui/x-date-pickers/internals/base/Calendar'; +import styles from './calendar.module.css'; + +function Header() { + const { visibleDate } = useCalendarContext(); + + return ( +
+ + ◀ + + {visibleDate.format('YYYY')} + + ▶ + +
+ ); +} + +export default function MonthCalendarDemo() { + const [value, setValue] = React.useState(null); + + return ( + + +
+ + {({ months }) => + months.map((month) => ( + + )) + } + + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/MonthCalendarDemo.tsx.preview b/docs/data/date-pickers/base-calendar/MonthCalendarDemo.tsx.preview new file mode 100644 index 0000000000000..2dcd2a095dd1b --- /dev/null +++ b/docs/data/date-pickers/base-calendar/MonthCalendarDemo.tsx.preview @@ -0,0 +1,14 @@ + +
+ + {({ months }) => + months.map((month) => ( + + )) + } + + \ No newline at end of file diff --git a/docs/data/date-pickers/base-calendar/base-calendar.md b/docs/data/date-pickers/base-calendar/base-calendar.md index 82f032259b845..caf5f2f70045b 100644 --- a/docs/data/date-pickers/base-calendar/base-calendar.md +++ b/docs/data/date-pickers/base-calendar/base-calendar.md @@ -12,6 +12,10 @@ packageName: '@mui/x-date-pickers' {{"demo": "DayCalendarDemo.js"}} +## Month view + +{{"demo": "MonthCalendarDemo.js"}} + ## Day, month and year view {{"demo": "YearMonthDayCalendar.js"}} diff --git a/docs/data/date-pickers/base-calendar/calendar.module.css b/docs/data/date-pickers/base-calendar/calendar.module.css index 901312d580ab7..7ce1a860bf221 100644 --- a/docs/data/date-pickers/base-calendar/calendar.module.css +++ b/docs/data/date-pickers/base-calendar/calendar.module.css @@ -84,10 +84,42 @@ overflow-y: scroll; } +.MonthsGrid { + padding: 12px; + display: grid; + grid-template-columns: repeat(2, 1fr); + column-gap: 4px; + row-gap: 12px; +} + .MonthsCell, .YearsCell { height: 24px; min-height: 24px; + background-color: transparent; + border: none; + cursor: pointer; + border-radius: 4px; + + &:not([data-selected]):hover { + background-color: #e0f2fe; + } + + &[data-selected] { + background-color: #7dd3fc; + + &:focus-visible { + outline: 2px solid #0ea5e9; + } + } + + &:focus-visible { + outline: 2px solid #0ea5e9; + } + + &:disabled { + pointer-events: none; + } } .DaysGrid { diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-grid-body/CalendarDaysGridBodyContext.ts b/packages/x-date-pickers/src/internals/base/Calendar/days-grid-body/CalendarDaysGridBodyContext.ts index 1fb92097d9adc..1197380a6f592 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-grid-body/CalendarDaysGridBodyContext.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-grid-body/CalendarDaysGridBodyContext.ts @@ -19,7 +19,7 @@ export function useCalendarDaysGridBodyContext() { const context = React.useContext(CalendarDaysGridBodyContext); if (context === undefined) { throw new Error( - 'Base UI X: CalendarDaysGridBodyContext is missing. Calendar Days Grid parts must be placed withing .', + 'Base UI X: CalendarDaysGridBodyContext is missing. Calendar Days Grid parts must be placed within .', ); } return context; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-grid/CalendarDaysGridContext.ts b/packages/x-date-pickers/src/internals/base/Calendar/days-grid/CalendarDaysGridContext.ts index 6ce23bdffd0be..e1c6cfd65153f 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-grid/CalendarDaysGridContext.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-grid/CalendarDaysGridContext.ts @@ -20,7 +20,7 @@ export function useCalendarDaysGridContext() { const context = React.useContext(CalendarDaysGridContext); if (context === undefined) { throw new Error( - 'Base UI X: CalendarDaysGridContext is missing. Calendar Days Grid parts must be placed withing .', + 'Base UI X: CalendarDaysGridContext is missing. Calendar Days Grid parts must be placed within .', ); } return context; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/index.parts.ts b/packages/x-date-pickers/src/internals/base/Calendar/index.parts.ts index d0984aa784a51..e72d5899cb0cf 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/index.parts.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/index.parts.ts @@ -10,6 +10,7 @@ export { CalendarDaysCell as DaysCell } from './days-cell/CalendarDaysCell'; // Months export { CalendarMonthsList as MonthsList } from './months-list/CalendarMonthsList'; +export { CalendarMonthsGrid as MonthsGrid } from './months-grid/CalendarMonthsGrid'; export { CalendarMonthsCell as MonthsCell } from './months-cell/CalendarMonthsCell'; // Years diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx index be58ae1af7ea5..63e6f7f45327b 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx @@ -7,7 +7,7 @@ import { useCalendarRootContext } from '../root/CalendarRootContext'; import { useCalendarMonthsCell } from './useCalendarMonthsCell'; import { BaseUIComponentProps } from '../../utils/types'; import { useCompositeListItem } from '../../composite/list/useCompositeListItem'; -import { useCalendarMonthsListContext } from '../months-list/CalendarMonthsListContext'; +import { useCalendarMonthCellCollectionContext } from '../utils/month-cell-collection/CalendarMonthCellCollectionContext'; const InnerCalendarMonthsCell = React.forwardRef(function InnerCalendarMonthsCell( props: InnerCalendarMonthsCellProps, @@ -40,7 +40,7 @@ const CalendarMonthsCell = React.forwardRef(function CalendarMonthsCell( forwardedRef: React.ForwardedRef, ) { const calendarRootContext = useCalendarRootContext(); - const calendarMonthsListContext = useCalendarMonthsListContext(); + const calendarMonthCellCollectionContext = useCalendarMonthCellCollectionContext(); const { ref: listItemRef } = useCompositeListItem(); const utils = useUtils(); const now = useNow(calendarRootContext.timezone); @@ -103,9 +103,9 @@ const CalendarMonthsCell = React.forwardRef(function CalendarMonthsCell( isSelected, isDisabled, isTabbable, - selectMonth: calendarMonthsListContext.selectMonth, + selectMonth: calendarMonthCellCollectionContext.selectMonth, }), - [isSelected, isDisabled, isTabbable, calendarMonthsListContext.selectMonth], + [isSelected, isDisabled, isTabbable, calendarMonthCellCollectionContext.selectMonth], ); return ; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-grid/CalendarMonthsGrid.tsx b/packages/x-date-pickers/src/internals/base/Calendar/months-grid/CalendarMonthsGrid.tsx new file mode 100644 index 0000000000000..518d7ce62c1d3 --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-grid/CalendarMonthsGrid.tsx @@ -0,0 +1,44 @@ +'use client'; +import * as React from 'react'; +import { useCalendarMonthsGrid } from './useCalendarMonthsGrid'; +import { BaseUIComponentProps } from '../../utils/types'; +import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import { CompositeList } from '../../composite/list/CompositeList'; +import { CalendarMonthCellCollectionContext } from '../utils/month-cell-collection/CalendarMonthCellCollectionContext'; + +const CalendarMonthsGrid = React.forwardRef(function CalendarMonthsList( + props: CalendarMonthsGrid.Props, + forwardedRef: React.ForwardedRef, +) { + const { className, render, children, cellsPerRow, ...otherProps } = props; + const { getMonthGridProps, context, calendarMonthsCellRefs } = useCalendarMonthsGrid({ + children, + cellsPerRow, + }); + const state = React.useMemo(() => ({}), []); + + const { renderElement } = useComponentRenderer({ + propGetter: getMonthGridProps, + render: render ?? 'div', + ref: forwardedRef, + className, + state, + extraProps: otherProps, + }); + + return ( + + {renderElement()} + + ); +}); + +export namespace CalendarMonthsGrid { + export interface State {} + + export interface Props + extends Omit, 'children'>, + useCalendarMonthsGrid.Parameters {} +} + +export { CalendarMonthsGrid }; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-grid/useCalendarMonthsGrid.ts b/packages/x-date-pickers/src/internals/base/Calendar/months-grid/useCalendarMonthsGrid.ts new file mode 100644 index 0000000000000..9e6b0d71034bc --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-grid/useCalendarMonthsGrid.ts @@ -0,0 +1,65 @@ +import * as React from 'react'; +import useEventCallback from '@mui/utils/useEventCallback'; +import { PickerValidDate } from '../../../../models'; +import { GenericHTMLProps } from '../../utils/types'; +import { mergeReactProps } from '../../utils/mergeReactProps'; +import { navigateInGrid } from '../utils/keyboardNavigation'; +import { useCalendarMonthCellCollection } from '../utils/month-cell-collection/useCalendarMonthCellCollection'; + +export function useCalendarMonthsGrid(parameters: useCalendarMonthsGrid.Parameters) { + const { children, cellsPerRow } = parameters; + const calendarMonthsCellRefs = React.useRef<(HTMLElement | null)[]>([]); + const { months, context } = useCalendarMonthCellCollection(); + + const onKeyDown = useEventCallback((event: React.KeyboardEvent) => { + const grid: HTMLElement[][] = Array.from( + { + length: Math.ceil(calendarMonthsCellRefs.current.length / cellsPerRow), + }, + () => [], + ); + calendarMonthsCellRefs.current.forEach((cell, index) => { + const rowIndex = Math.floor(index / cellsPerRow); + if (cell != null) { + grid[rowIndex].push(cell); + } + }); + + navigateInGrid({ + cells: [grid], + event, + changePage: undefined, + }); + }); + + const getMonthGridProps = React.useCallback( + (externalProps: GenericHTMLProps) => { + return mergeReactProps(externalProps, { + role: 'radiogroup', + children: children == null ? null : children({ months }), + onKeyDown, + }); + }, + [months, children, onKeyDown], + ); + + return React.useMemo( + () => ({ getMonthGridProps, context, calendarMonthsCellRefs }), + [getMonthGridProps, context, calendarMonthsCellRefs], + ); +} + +export namespace useCalendarMonthsGrid { + export interface Parameters { + /** + * Cells rendered per row. + * This is used to make sure the keyboard navigation works correctly. + */ + cellsPerRow: number; + children?: (parameters: ChildrenParameters) => React.ReactNode; + } + + export interface ChildrenParameters { + months: PickerValidDate[]; + } +} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsList.tsx b/packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsList.tsx index 86165c073d73d..a050af13dc5e1 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsList.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsList.tsx @@ -4,7 +4,7 @@ import { useCalendarMonthsList } from './useCalendarMonthsList'; import { BaseUIComponentProps } from '../../utils/types'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; import { CompositeList } from '../../composite/list/CompositeList'; -import { CalendarMonthsListContext } from './CalendarMonthsListContext'; +import { CalendarMonthCellCollectionContext } from '../utils/month-cell-collection/CalendarMonthCellCollectionContext'; const CalendarMonthsList = React.forwardRef(function CalendarMonthsList( props: CalendarMonthsList.Props, @@ -27,9 +27,9 @@ const CalendarMonthsList = React.forwardRef(function CalendarMonthsList( }); return ( - + {renderElement()} - + ); }); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsListContext.ts b/packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsListContext.ts deleted file mode 100644 index 4df865c5915c3..0000000000000 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsListContext.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as React from 'react'; -import { PickerValidDate } from '../../../../models'; - -export interface CalendarMonthsListContext { - selectMonth: (value: PickerValidDate) => void; -} - -export const CalendarMonthsListContext = React.createContext( - undefined, -); - -if (process.env.NODE_ENV !== 'production') { - CalendarMonthsListContext.displayName = 'CalendarMonthsListContext'; -} - -export function useCalendarMonthsListContext() { - const context = React.useContext(CalendarMonthsListContext); - if (context === undefined) { - throw new Error( - 'Base UI X: CalendarMonthsListContext is missing. Calendar Month List parts must be placed withing .', - ); - } - return context; -} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-list/useCalendarMonthsList.ts b/packages/x-date-pickers/src/internals/base/Calendar/months-list/useCalendarMonthsList.ts index 1530658ce0c3b..d27f746798372 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-list/useCalendarMonthsList.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-list/useCalendarMonthsList.ts @@ -1,31 +1,19 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; import { PickerValidDate } from '../../../../models'; -import { findClosestEnabledDate, getMonthsInYear } from '../../../utils/date-utils'; -import { useUtils } from '../../../hooks/useUtils'; -import { useCalendarRootContext } from '../root/CalendarRootContext'; import { GenericHTMLProps } from '../../utils/types'; import { mergeReactProps } from '../../utils/mergeReactProps'; -import { CalendarMonthsListContext } from './CalendarMonthsListContext'; import { navigateInList } from '../utils/keyboardNavigation'; +import { useCalendarMonthCellCollection } from '../utils/month-cell-collection/useCalendarMonthCellCollection'; export function useCalendarMonthsList(parameters: useCalendarMonthsList.Parameters) { const { children, loop = true } = parameters; - const utils = useUtils(); - const calendarRootContext = useCalendarRootContext(); const calendarMonthsCellRefs = React.useRef<(HTMLElement | null)[]>([]); - - const currentYear = React.useMemo( - () => utils.startOfYear(calendarRootContext.visibleDate), - [utils, calendarRootContext.visibleDate], - ); - - const months = React.useMemo(() => getMonthsInYear(utils, currentYear), [utils, currentYear]); + const { months, context } = useCalendarMonthCellCollection(); const onKeyDown = useEventCallback((event: React.KeyboardEvent) => { navigateInList({ cells: calendarMonthsCellRefs.current, - target: event.target as HTMLElement, event, loop, }); @@ -42,48 +30,6 @@ export function useCalendarMonthsList(parameters: useCalendarMonthsList.Paramete [months, children, onKeyDown], ); - const selectMonth = useEventCallback((newValue: PickerValidDate) => { - if (calendarRootContext.readOnly) { - return; - } - - const newCleanValue = utils.setMonth( - calendarRootContext.value ?? calendarRootContext.referenceDate, - utils.getMonth(newValue), - ); - - const startOfMonth = utils.startOfMonth(newCleanValue); - const endOfMonth = utils.endOfMonth(newCleanValue); - - const closestEnabledDate = calendarRootContext.isDateDisabled(newCleanValue) - ? findClosestEnabledDate({ - utils, - date: newCleanValue, - minDate: utils.isBefore(calendarRootContext.validationProps.minDate, startOfMonth) - ? startOfMonth - : calendarRootContext.validationProps.minDate, - maxDate: utils.isAfter(calendarRootContext.validationProps.maxDate, endOfMonth) - ? endOfMonth - : calendarRootContext.validationProps.maxDate, - disablePast: calendarRootContext.validationProps.disablePast, - disableFuture: calendarRootContext.validationProps.disableFuture, - isDateDisabled: calendarRootContext.isDateDisabled, - timezone: calendarRootContext.timezone, - }) - : newCleanValue; - - if (closestEnabledDate) { - calendarRootContext.setValue(closestEnabledDate, { section: 'month' }); - } - }); - - const registerSection = calendarRootContext.registerSection; - React.useEffect(() => { - return registerSection({ type: 'month', value: currentYear }); - }, [registerSection, currentYear]); - - const context: CalendarMonthsListContext = React.useMemo(() => ({ selectMonth }), [selectMonth]); - return React.useMemo( () => ({ getMonthListProps, context, calendarMonthsCellRefs }), [getMonthListProps, context, calendarMonthsCellRefs], diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts index 22655caf7db40..2d9511127bd61 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts @@ -50,7 +50,7 @@ export function useCalendarRootContext() { const context = React.useContext(CalendarRootContext); if (context === undefined) { throw new Error( - 'Base UI X: CalendarRootContext is missing. Calendar parts must be placed withing .', + 'Base UI X: CalendarRootContext is missing. Calendar parts must be placed within .', ); } return context; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts index 62fd8b6f49fb9..87ee8b25c0b07 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts @@ -2,12 +2,10 @@ const LIST_NAVIGATION_SUPPORTED_KEYS = ['ArrowDown', 'ArrowUp', 'Home', 'End']; export function navigateInList({ cells, - target, event, loop, }: { cells: (HTMLElement | null)[]; - target: HTMLElement; event: React.KeyboardEvent; loop: boolean; }) { @@ -26,7 +24,7 @@ export function navigateInList({ } const lastIndex = navigableCells.length - 1; - const currentIndex = navigableCells.indexOf(target); + const currentIndex = navigableCells.indexOf(event.target as HTMLElement); let nextIndex = -1; switch (event.key) { diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/month-cell-collection/CalendarMonthCellCollectionContext.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/month-cell-collection/CalendarMonthCellCollectionContext.ts new file mode 100644 index 0000000000000..1e828ca11ba18 --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/Calendar/utils/month-cell-collection/CalendarMonthCellCollectionContext.ts @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { PickerValidDate } from '../../../../../models'; + +export interface CalendarMonthCellCollectionContext { + selectMonth: (value: PickerValidDate) => void; +} + +export const CalendarMonthCellCollectionContext = React.createContext< + CalendarMonthCellCollectionContext | undefined +>(undefined); + +if (process.env.NODE_ENV !== 'production') { + CalendarMonthCellCollectionContext.displayName = 'CalendarMonthCellCollectionContext'; +} + +export function useCalendarMonthCellCollectionContext() { + const context = React.useContext(CalendarMonthCellCollectionContext); + if (context === undefined) { + throw new Error( + 'Base UI X: CalendarMonthCellCollectionContext is missing. Calendar Month parts must be placed within or ``.', + ); + } + return context; +} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/month-cell-collection/useCalendarMonthCellCollection.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/month-cell-collection/useCalendarMonthCellCollection.ts new file mode 100644 index 0000000000000..e2ea269ca725c --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/Calendar/utils/month-cell-collection/useCalendarMonthCellCollection.ts @@ -0,0 +1,66 @@ +import * as React from 'react'; +import useEventCallback from '@mui/utils/useEventCallback'; +import { PickerValidDate } from '../../../../../models'; +import { findClosestEnabledDate, getMonthsInYear } from '../../../../utils/date-utils'; +import { useUtils } from '../../../../hooks/useUtils'; +import { useCalendarRootContext } from '../../root/CalendarRootContext'; +import { CalendarMonthCellCollectionContext } from './CalendarMonthCellCollectionContext'; + +export function useCalendarMonthCellCollection() { + const calendarRootContext = useCalendarRootContext(); + const utils = useUtils(); + + const currentYear = React.useMemo( + () => utils.startOfYear(calendarRootContext.visibleDate), + [utils, calendarRootContext.visibleDate], + ); + + const months = React.useMemo(() => getMonthsInYear(utils, currentYear), [utils, currentYear]); + + const selectMonth = useEventCallback((newValue: PickerValidDate) => { + if (calendarRootContext.readOnly) { + return; + } + + const newCleanValue = utils.setMonth( + calendarRootContext.value ?? calendarRootContext.referenceDate, + utils.getMonth(newValue), + ); + + const startOfMonth = utils.startOfMonth(newCleanValue); + const endOfMonth = utils.endOfMonth(newCleanValue); + + const closestEnabledDate = calendarRootContext.isDateDisabled(newCleanValue) + ? findClosestEnabledDate({ + utils, + date: newCleanValue, + minDate: utils.isBefore(calendarRootContext.validationProps.minDate, startOfMonth) + ? startOfMonth + : calendarRootContext.validationProps.minDate, + maxDate: utils.isAfter(calendarRootContext.validationProps.maxDate, endOfMonth) + ? endOfMonth + : calendarRootContext.validationProps.maxDate, + disablePast: calendarRootContext.validationProps.disablePast, + disableFuture: calendarRootContext.validationProps.disableFuture, + isDateDisabled: calendarRootContext.isDateDisabled, + timezone: calendarRootContext.timezone, + }) + : newCleanValue; + + if (closestEnabledDate) { + calendarRootContext.setValue(closestEnabledDate, { section: 'month' }); + } + }); + + const registerSection = calendarRootContext.registerSection; + React.useEffect(() => { + return registerSection({ type: 'month', value: currentYear }); + }, [registerSection, currentYear]); + + const context: CalendarMonthCellCollectionContext = React.useMemo( + () => ({ selectMonth }), + [selectMonth], + ); + + return { months, context }; +} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-list/CalendarYearsListContext.ts b/packages/x-date-pickers/src/internals/base/Calendar/years-list/CalendarYearsListContext.ts index 3d8ebf5d04cde..852f7e83a0d75 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-list/CalendarYearsListContext.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-list/CalendarYearsListContext.ts @@ -17,7 +17,7 @@ export function useCalendarYearsListContext() { const context = React.useContext(CalendarYearsListContext); if (context === undefined) { throw new Error( - 'Base UI X: CalendarYearsListContext is missing. Calendar Year List parts must be placed withing .', + 'Base UI X: CalendarYearsListContext is missing. Calendar Year List parts must be placed within .', ); } return context; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-list/useCalendarYearsList.ts b/packages/x-date-pickers/src/internals/base/Calendar/years-list/useCalendarYearsList.ts index 3c7bc7b624afb..cf39c01b527c0 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-list/useCalendarYearsList.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-list/useCalendarYearsList.ts @@ -31,7 +31,6 @@ export function useCalendarYearsList(parameters: useCalendarYearsList.Parameters const onKeyDown = useEventCallback((event: React.KeyboardEvent) => { navigateInList({ cells: calendarYearsCellRefs.current, - target: event.target as HTMLElement, event, loop, }); From d1458c260338d6b3ef557b30138b1798a5d8846b Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 6 Jan 2025 13:49:57 +0100 Subject: [PATCH 040/136] Work --- .../base/Calendar/root/useCalendarRoot.ts | 35 ++++++++++++------- .../CalendarSetVisibleMonth.tsx | 2 +- .../CalendarSetVisibleYear.tsx | 2 +- .../useCalendarMonthCellCollection.ts | 1 + 4 files changed, 26 insertions(+), 14 deletions(-) diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts index e7d0e2e1e3a64..792904a14823c 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts @@ -144,22 +144,23 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { const [visibleDate, setVisibleDate] = React.useState(referenceDate); const [prevValue, setPrevValue] = React.useState(value); - if (value !== prevValue && utils.isValid(value)) { - let shouldNavigate: boolean; + const isDateCellVisible = (date: PickerValidDate) => { if (Object.values(sectionsRef.current.day).length > 0) { - shouldNavigate = Object.values(sectionsRef.current.day).every( - (month) => !utils.isSameMonth(value, month), + return Object.values(sectionsRef.current.day).every( + (month) => !utils.isSameMonth(date, month), ); - } else if (Object.values(sectionsRef.current.month).length > 0) { - shouldNavigate = Object.values(sectionsRef.current.month).every( - (year) => !utils.isSameYear(value, year), + } + if (Object.values(sectionsRef.current.month).length > 0) { + return Object.values(sectionsRef.current.month).every( + (year) => !utils.isSameYear(date, year), ); - } else { - shouldNavigate = true; } + return true; + }; + if (value !== prevValue && utils.isValid(value)) { setPrevValue(value); - if (shouldNavigate) { + if (isDateCellVisible(value)) { setVisibleDate(value); } } @@ -170,6 +171,16 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { monthPageSize, }); + const handleVisibleDateChange = useEventCallback( + (newVisibleDate: PickerValidDate, skipIfAlreadyVisible: boolean) => { + if (skipIfAlreadyVisible && isDateCellVisible(newVisibleDate)) { + return; + } + + setVisibleDate(newVisibleDate); + }, + ); + const context: CalendarRootContext = React.useMemo( () => ({ value, @@ -182,7 +193,7 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { isDateDisabled, validationProps, visibleDate, - setVisibleDate, + setVisibleDate: handleVisibleDateChange, monthPageSize, applyDayGridKeyboardNavigation, registerDaysGridCells, @@ -199,7 +210,7 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { isDateDisabled, validationProps, visibleDate, - setVisibleDate, + handleVisibleDateChange, monthPageSize, applyDayGridKeyboardNavigation, registerDaysGridCells, diff --git a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx index 517e3ac6a8913..dd8ca95fd655b 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx @@ -87,7 +87,7 @@ const CalendarSetVisibleMonth = React.forwardRef(function CalendarSetVisibleMont if (isDisabled) { return; } - calendarRootContext.setVisibleDate(targetDate); + calendarRootContext.setVisibleDate(targetDate, false); }); const ctx = React.useMemo( diff --git a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx index 3eb3b8cc6b286..8d7de77494916 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx @@ -83,7 +83,7 @@ const CalendarSetVisibleYear = React.forwardRef(function CalendarSetVisibleYear( if (isDisabled) { return; } - calendarRootContext.setVisibleDate(targetDate); + calendarRootContext.setVisibleDate(targetDate, false); }); const ctx = React.useMemo( diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/month-cell-collection/useCalendarMonthCellCollection.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/month-cell-collection/useCalendarMonthCellCollection.ts index e2ea269ca725c..8d199022cb2bf 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/utils/month-cell-collection/useCalendarMonthCellCollection.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/utils/month-cell-collection/useCalendarMonthCellCollection.ts @@ -48,6 +48,7 @@ export function useCalendarMonthCellCollection() { : newCleanValue; if (closestEnabledDate) { + calendarRootContext.setVisibleDate(closestEnabledDate, true); calendarRootContext.setValue(closestEnabledDate, { section: 'month' }); } }); From e26e97b7a057ab7268d95ce879687a02e801947c Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 6 Jan 2025 13:53:35 +0100 Subject: [PATCH 041/136] Work --- .../date-pickers/base-calendar/calendar.module.css | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/docs/data/date-pickers/base-calendar/calendar.module.css b/docs/data/date-pickers/base-calendar/calendar.module.css index 7ce1a860bf221..abb4eaeed1918 100644 --- a/docs/data/date-pickers/base-calendar/calendar.module.css +++ b/docs/data/date-pickers/base-calendar/calendar.module.css @@ -150,6 +150,10 @@ background-color: transparent; cursor: pointer; + &[data-selected] { + background-color: #7dd3fc; + } + &:not([data-selected]):hover { background-color: #e0f2fe; } @@ -159,8 +163,6 @@ } &[data-selected] { - background-color: #7dd3fc; - &:focus-visible { outline: 2px solid #0ea5e9; } @@ -190,15 +192,11 @@ .MonthsCell, .YearsCell { &[data-selected] { - background-color: #0ea5e9; + background-color: #7dd3fc; } &:focus-visible { border-width: 2px; - - &[data-selected] { - background-color: #7dd3fc; - } } &:disabled { From ea655c83cc57d7bfff9e853277ba848f659b3a16 Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 6 Jan 2025 13:54:53 +0100 Subject: [PATCH 042/136] Work --- .../base/Calendar/months-grid/useCalendarMonthsGrid.ts | 1 + .../src/internals/base/Calendar/root/CalendarRootContext.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-grid/useCalendarMonthsGrid.ts b/packages/x-date-pickers/src/internals/base/Calendar/months-grid/useCalendarMonthsGrid.ts index 9e6b0d71034bc..93820134b1421 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-grid/useCalendarMonthsGrid.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-grid/useCalendarMonthsGrid.ts @@ -11,6 +11,7 @@ export function useCalendarMonthsGrid(parameters: useCalendarMonthsGrid.Paramete const calendarMonthsCellRefs = React.useRef<(HTMLElement | null)[]>([]); const { months, context } = useCalendarMonthCellCollection(); + // TODO: Add support for multiple months grids. const onKeyDown = useEventCallback((event: React.KeyboardEvent) => { const grid: HTMLElement[][] = Array.from( { diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts index 2d9511127bd61..fc4c8537b772f 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts @@ -30,7 +30,7 @@ export interface CalendarRootContext { isDateDisabled: (day: PickerValidDate | null) => boolean; validationProps: ValidateDateProps; visibleDate: PickerValidDate; - setVisibleDate: (visibleDate: PickerValidDate) => void; + setVisibleDate: (visibleDate: PickerValidDate, skipIfAlreadyVisible: boolean) => void; monthPageSize: number; applyDayGridKeyboardNavigation: (event: React.KeyboardEvent) => void; registerDaysGridCells: ( From 63f5361a73f6463ab95d72ac065aa988f1f2f524 Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 6 Jan 2025 13:59:51 +0100 Subject: [PATCH 043/136] Work --- .../DayCalendarWithValidationDemo.js | 107 ++++++++++++++++++ .../DayCalendarWithValidationDemo.tsx | 107 ++++++++++++++++++ .../DayCalendarWithValidationDemo.tsx.preview | 8 ++ .../base-calendar/base-calendar.md | 4 + 4 files changed, 226 insertions(+) create mode 100644 docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.js create mode 100644 docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.tsx create mode 100644 docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.tsx.preview diff --git a/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.js b/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.js new file mode 100644 index 0000000000000..115550411b3d1 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.js @@ -0,0 +1,107 @@ +import * as React from 'react'; +import dayjs from 'dayjs'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +// eslint-disable-next-line no-restricted-imports +import { + Calendar, + useCalendarContext, +} from '@mui/x-date-pickers/internals/base/Calendar'; +import styles from './calendar.module.css'; + +function Header() { + const { visibleDate } = useCalendarContext(); + + return ( +
+ + ◀ + + {visibleDate.format('MMMM YYYY')} + + ▶ + +
+ ); +} + +function DayCalendar(props) { + return ( + + +
+ + + {({ days }) => + days.map((day) => ( + + )) + } + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => + days.map((day) => ( + + )) + } + + )) + } + + + + + ); +} + +const ALREADY_BOOKED_NIGHTS = [ + dayjs().add(3, 'day'), + dayjs().add(8, 'day'), + dayjs().add(9, 'day'), + dayjs().add(10, 'day'), + dayjs().add(13, 'day'), + dayjs().add(14, 'day'), + dayjs().add(15, 'day'), + dayjs().add(16, 'day'), + dayjs().add(17, 'day'), +]; + +const ALREADY_BOOKED_NIGHTS_SET = new Set( + ALREADY_BOOKED_NIGHTS.map((date) => date.format('YYYY-MM-DD')), +); + +export default function DayCalendarWithValidationDemo() { + const [value, setValue] = React.useState(null); + + const handleValueChange = React.useCallback((newValue) => { + setValue(newValue); + }, []); + + return ( + + + ALREADY_BOOKED_NIGHTS_SET.has(date.format('YYYY-MM-DD')) + } + /> + + ); +} diff --git a/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.tsx b/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.tsx new file mode 100644 index 0000000000000..55e67f0cc7866 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.tsx @@ -0,0 +1,107 @@ +import * as React from 'react'; +import dayjs, { Dayjs } from 'dayjs'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +// eslint-disable-next-line no-restricted-imports +import { + Calendar, + useCalendarContext, +} from '@mui/x-date-pickers/internals/base/Calendar'; +import styles from './calendar.module.css'; + +function Header() { + const { visibleDate } = useCalendarContext(); + + return ( +
+ + ◀ + + {visibleDate.format('MMMM YYYY')} + + ▶ + +
+ ); +} + +function DayCalendar(props: Omit) { + return ( + + +
+ + + {({ days }) => + days.map((day) => ( + + )) + } + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => + days.map((day) => ( + + )) + } + + )) + } + + + + + ); +} + +const ALREADY_BOOKED_NIGHTS = [ + dayjs().add(3, 'day'), + dayjs().add(8, 'day'), + dayjs().add(9, 'day'), + dayjs().add(10, 'day'), + dayjs().add(13, 'day'), + dayjs().add(14, 'day'), + dayjs().add(15, 'day'), + dayjs().add(16, 'day'), + dayjs().add(17, 'day'), +]; + +const ALREADY_BOOKED_NIGHTS_SET = new Set( + ALREADY_BOOKED_NIGHTS.map((date) => date.format('YYYY-MM-DD')), +); + +export default function DayCalendarWithValidationDemo() { + const [value, setValue] = React.useState(null); + + const handleValueChange = React.useCallback((newValue: Dayjs | null) => { + setValue(newValue); + }, []); + + return ( + + + ALREADY_BOOKED_NIGHTS_SET.has(date.format('YYYY-MM-DD')) + } + /> + + ); +} diff --git a/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.tsx.preview b/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.tsx.preview new file mode 100644 index 0000000000000..7ed1d697455e7 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.tsx.preview @@ -0,0 +1,8 @@ + + ALREADY_BOOKED_NIGHTS_SET.has(date.format('YYYY-MM-DD')) + } +/> \ No newline at end of file diff --git a/docs/data/date-pickers/base-calendar/base-calendar.md b/docs/data/date-pickers/base-calendar/base-calendar.md index caf5f2f70045b..d929965b6752c 100644 --- a/docs/data/date-pickers/base-calendar/base-calendar.md +++ b/docs/data/date-pickers/base-calendar/base-calendar.md @@ -23,3 +23,7 @@ packageName: '@mui/x-date-pickers' ## Multiple months {{"demo": "DayCalendarTwoMonthsDemo.js"}} + +## Validation + +{{"demo": "DayCalendarWithValidationDemo.js"}} From 90b80304d09389a930d88939a4e08d52b025a49f Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 6 Jan 2025 14:00:45 +0100 Subject: [PATCH 044/136] Work --- .../base-calendar/DayCalendarWithValidationDemo.js | 8 ++++++++ .../base-calendar/DayCalendarWithValidationDemo.tsx | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.js b/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.js index 115550411b3d1..d3b3d728802d0 100644 --- a/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.js +++ b/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.js @@ -79,6 +79,14 @@ const ALREADY_BOOKED_NIGHTS = [ dayjs().add(15, 'day'), dayjs().add(16, 'day'), dayjs().add(17, 'day'), + dayjs().add(27, 'day'), + dayjs().add(28, 'day'), + dayjs().add(29, 'day'), + dayjs().add(30, 'day'), + dayjs().add(45, 'day'), + dayjs().add(46, 'day'), + dayjs().add(48, 'day'), + dayjs().add(49, 'day'), ]; const ALREADY_BOOKED_NIGHTS_SET = new Set( diff --git a/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.tsx b/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.tsx index 55e67f0cc7866..9f1fc9b1228b4 100644 --- a/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.tsx +++ b/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.tsx @@ -79,6 +79,14 @@ const ALREADY_BOOKED_NIGHTS = [ dayjs().add(15, 'day'), dayjs().add(16, 'day'), dayjs().add(17, 'day'), + dayjs().add(27, 'day'), + dayjs().add(28, 'day'), + dayjs().add(29, 'day'), + dayjs().add(30, 'day'), + dayjs().add(45, 'day'), + dayjs().add(46, 'day'), + dayjs().add(48, 'day'), + dayjs().add(49, 'day'), ]; const ALREADY_BOOKED_NIGHTS_SET = new Set( From 4b3c24f5469832c29ad529388e2b115c28488f71 Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 6 Jan 2025 14:05:23 +0100 Subject: [PATCH 045/136] Work --- .../base-calendar/base-calendar.md | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/docs/data/date-pickers/base-calendar/base-calendar.md b/docs/data/date-pickers/base-calendar/base-calendar.md index d929965b6752c..fc6dfe8ed08de 100644 --- a/docs/data/date-pickers/base-calendar/base-calendar.md +++ b/docs/data/date-pickers/base-calendar/base-calendar.md @@ -8,22 +8,30 @@ packageName: '@mui/x-date-pickers'

POC of a Calendar component using the Base UI DX.

-## Day view +## Day Calendar + +### One month {{"demo": "DayCalendarDemo.js"}} -## Month view +### Two months -{{"demo": "MonthCalendarDemo.js"}} +{{"demo": "DayCalendarTwoMonthsDemo.js"}} -## Day, month and year view +### With validation -{{"demo": "YearMonthDayCalendar.js"}} +{{"demo": "DayCalendarWithValidationDemo.js"}} -## Multiple months +## Month Calendar -{{"demo": "DayCalendarTwoMonthsDemo.js"}} +### One year -## Validation +{{"demo": "MonthCalendarDemo.js"}} -{{"demo": "DayCalendarWithValidationDemo.js"}} +### Two years + +TODO + +## Full Date Calendar + +{{"demo": "YearMonthDayCalendar.js"}} From d87c1173acac4fe6a2385a011288b5d965d4721a Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 6 Jan 2025 14:22:05 +0100 Subject: [PATCH 046/136] Work --- .../root/useCalendarDaysGridsNavigation.ts | 25 ++++++++++++++--- .../base/Calendar/root/useCalendarRoot.ts | 1 + .../CalendarSetVisibleMonth.tsx | 27 +++++++------------ .../src/internals/base/Calendar/utils/date.ts | 26 ++++++++++++++++++ 4 files changed, 58 insertions(+), 21 deletions(-) create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/utils/date.ts diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarDaysGridsNavigation.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarDaysGridsNavigation.ts index 7d804156d2561..c2f5e59f33862 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarDaysGridsNavigation.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarDaysGridsNavigation.ts @@ -2,6 +2,7 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; import useTimeout from '@mui/utils/useTimeout'; import { PickerValidDate } from '../../../../models'; +import { ValidateDateProps } from '../../../../validation'; import { useUtils } from '../../../hooks/useUtils'; import type { useCalendarDaysGridBody } from '../days-grid-body/useCalendarDaysGridBody'; import { @@ -11,6 +12,7 @@ import { PageNavigationTarget, } from '../utils/keyboardNavigation'; import type { CalendarRootContext } from './CalendarRootContext'; +import { getFirstEnabledMonth, getLastEnabledMonth } from '../utils/date'; /** * This logic needs to be in Calendar.Root to support multiple Calendar.DaysGrid. @@ -19,7 +21,7 @@ import type { CalendarRootContext } from './CalendarRootContext'; export function useCalendarDaysGridNavigation( parameters: useCalendarDaysGridNavigation.Parameters, ) { - const { visibleDate, setVisibleDate, monthPageSize } = parameters; + const { visibleDate, setVisibleDate, monthPageSize, validationProps } = parameters; const utils = useUtils(); const gridsRef = React.useRef< { cells: useCalendarDaysGridBody.CellsRef; rows: useCalendarDaysGridBody.RowsRef }[] @@ -40,12 +42,26 @@ export function useCalendarDaysGridNavigation( const applyDayGridKeyboardNavigation = useEventCallback((event: React.KeyboardEvent) => { const changePage: NavigateInGridChangePage = (params) => { // TODO: Jump over months with no valid date. - if (params.direction === 'next') { - setVisibleDate(utils.addMonths(visibleDate, monthPageSize)); - } if (params.direction === 'previous') { + const targetDate = utils.addMonths(utils.startOfMonth(visibleDate), -monthPageSize); + const lastMonthInNewPage = utils.addMonths(targetDate, monthPageSize - 1); + + // All the months before the visible ones are fully disabled, we skip the navigation. + if (utils.isAfter(getFirstEnabledMonth(utils, validationProps), lastMonthInNewPage)) { + return; + } + setVisibleDate(utils.addMonths(visibleDate, -monthPageSize)); } + if (params.direction === 'next') { + const targetDate = utils.addMonths(utils.startOfMonth(visibleDate), monthPageSize); + + // All the months after the visible ones are fully disabled, we skip the navigation. + if (utils.isBefore(getLastEnabledMonth(utils, validationProps), targetDate)) { + return; + } + setVisibleDate(utils.addMonths(visibleDate, monthPageSize)); + } pageNavigationTargetRef.current = params.target; }; @@ -78,6 +94,7 @@ export namespace useCalendarDaysGridNavigation { visibleDate: PickerValidDate; setVisibleDate: (visibleDate: PickerValidDate) => void; monthPageSize: number; + validationProps: ValidateDateProps; } export interface ReturnValue diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts index 792904a14823c..7aa7bd0ebf393 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts @@ -169,6 +169,7 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { visibleDate, setVisibleDate, monthPageSize, + validationProps, }); const handleVisibleDateChange = useEventCallback( diff --git a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx index dd8ca95fd655b..9d4d18eb51ca0 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx @@ -1,11 +1,12 @@ 'use client'; import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; -import { useNow, useUtils } from '../../../hooks/useUtils'; +import { useUtils } from '../../../hooks/useUtils'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; import { useCalendarRootContext } from '../root/CalendarRootContext'; import { useCalendarSetVisibleMonth } from './useCalendarSetVisibleMonth'; import { BaseUIComponentProps } from '../../utils/types'; +import { getFirstEnabledMonth, getLastEnabledMonth } from '../utils/date'; const InnerCalendarSetVisibleMonth = React.forwardRef(function InnerCalendarSetVisibleMonth( props: InnerCalendarSetVisibleMonthProps, @@ -36,7 +37,6 @@ const CalendarSetVisibleMonth = React.forwardRef(function CalendarSetVisibleMont ) { const calendarRootContext = useCalendarRootContext(); const utils = useUtils(); - const now = useNow(calendarRootContext.timezone); const targetDate = React.useMemo(() => { if (props.target === 'previous') { @@ -55,32 +55,25 @@ const CalendarSetVisibleMonth = React.forwardRef(function CalendarSetVisibleMont return true; } + // All the months before the visible ones are fully disabled, we skip the navigation. if (props.target === 'previous') { - const firstEnabledMonth = utils.startOfMonth( - calendarRootContext.validationProps.disablePast && - utils.isAfter(now, calendarRootContext.validationProps.minDate) - ? now - : calendarRootContext.validationProps.minDate, + return utils.isAfter( + getFirstEnabledMonth(utils, calendarRootContext.validationProps), + targetDate, ); - - return utils.isAfter(firstEnabledMonth, targetDate); } - const lastEnabledMonth = utils.startOfMonth( - calendarRootContext.validationProps.disableFuture && - utils.isBefore(now, calendarRootContext.validationProps.maxDate) - ? now - : calendarRootContext.validationProps.maxDate, + // All the months after the visible ones are fully disabled, we skip the navigation. + return utils.isBefore( + getLastEnabledMonth(utils, calendarRootContext.validationProps), + targetDate, ); - - return utils.isBefore(lastEnabledMonth, targetDate); }, [ calendarRootContext.disabled, calendarRootContext.validationProps, props.target, targetDate, utils, - now, ]); const setTarget = useEventCallback(() => { diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/date.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/date.ts new file mode 100644 index 0000000000000..83db6141da54b --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/Calendar/utils/date.ts @@ -0,0 +1,26 @@ +import { ValidateDateProps } from '../../../../validation'; +import { MuiPickersAdapter } from '../../../../models'; + +export const getFirstEnabledMonth = ( + utils: MuiPickersAdapter, + validationProps: ValidateDateProps, +) => { + const now = utils.date(); + return utils.startOfMonth( + validationProps.disablePast && utils.isAfter(now, validationProps.minDate) + ? now + : validationProps.minDate, + ); +}; + +export const getLastEnabledMonth = ( + utils: MuiPickersAdapter, + validationProps: ValidateDateProps, +) => { + const now = utils.date(); + return utils.startOfMonth( + validationProps.disableFuture && utils.isBefore(now, validationProps.maxDate) + ? now + : validationProps.maxDate, + ); +}; From b3191fd3f841e09e3cc7fcba69e8e604c1c1322f Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 6 Jan 2025 14:23:11 +0100 Subject: [PATCH 047/136] Work --- .../base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx index 9d4d18eb51ca0..31c90eded6ecc 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx @@ -55,6 +55,8 @@ const CalendarSetVisibleMonth = React.forwardRef(function CalendarSetVisibleMont return true; } + // TODO: Check if the logic below works correctly when multiple months are rendered at once. + // All the months before the visible ones are fully disabled, we skip the navigation. if (props.target === 'previous') { return utils.isAfter( From 99b125c5a93fa1e25576a262bdc1ea0e00b3cea8 Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 6 Jan 2025 14:26:13 +0100 Subject: [PATCH 048/136] Work --- docs/data/date-pickers/base-calendar/calendar.module.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/data/date-pickers/base-calendar/calendar.module.css b/docs/data/date-pickers/base-calendar/calendar.module.css index abb4eaeed1918..da2e26bbf2ee0 100644 --- a/docs/data/date-pickers/base-calendar/calendar.module.css +++ b/docs/data/date-pickers/base-calendar/calendar.module.css @@ -173,7 +173,7 @@ } &[data-outsidemonth] { - opacity: 0; + opacity: 0.3; pointer-events: 'none'; } From e8c25cdb1701910e6d4efe059ec72d9a672a7a58 Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 6 Jan 2025 14:26:42 +0100 Subject: [PATCH 049/136] Work --- docs/data/date-pickers/base-calendar/calendar.module.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/data/date-pickers/base-calendar/calendar.module.css b/docs/data/date-pickers/base-calendar/calendar.module.css index da2e26bbf2ee0..f340374e11309 100644 --- a/docs/data/date-pickers/base-calendar/calendar.module.css +++ b/docs/data/date-pickers/base-calendar/calendar.module.css @@ -174,7 +174,7 @@ &[data-outsidemonth] { opacity: 0.3; - pointer-events: 'none'; + pointer-events: none; } &:disabled { From c8cca4e2ad2715898fa8d47358d9efa339cd1f75 Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 6 Jan 2025 14:28:51 +0100 Subject: [PATCH 050/136] Work --- docs/data/date-pickers/base-calendar/calendar.module.css | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/data/date-pickers/base-calendar/calendar.module.css b/docs/data/date-pickers/base-calendar/calendar.module.css index f340374e11309..53baec52f458c 100644 --- a/docs/data/date-pickers/base-calendar/calendar.module.css +++ b/docs/data/date-pickers/base-calendar/calendar.module.css @@ -150,10 +150,6 @@ background-color: transparent; cursor: pointer; - &[data-selected] { - background-color: #7dd3fc; - } - &:not([data-selected]):hover { background-color: #e0f2fe; } @@ -162,7 +158,9 @@ outline: 1px solid #9ca3af; } - &[data-selected] { + &:not([data-outsidemonth])[data-selected] { + background-color: #7dd3fc; + &:focus-visible { outline: 2px solid #0ea5e9; } From 88a7adc9614c3add19a07b2fd666b81b1a4efd73 Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 6 Jan 2025 14:50:45 +0100 Subject: [PATCH 051/136] Work --- .../base-calendar/calendar.module.css | 65 ++++++++++--------- 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/docs/data/date-pickers/base-calendar/calendar.module.css b/docs/data/date-pickers/base-calendar/calendar.module.css index 53baec52f458c..a41ba94436ca5 100644 --- a/docs/data/date-pickers/base-calendar/calendar.module.css +++ b/docs/data/date-pickers/base-calendar/calendar.module.css @@ -101,22 +101,6 @@ cursor: pointer; border-radius: 4px; - &:not([data-selected]):hover { - background-color: #e0f2fe; - } - - &[data-selected] { - background-color: #7dd3fc; - - &:focus-visible { - outline: 2px solid #0ea5e9; - } - } - - &:focus-visible { - outline: 2px solid #0ea5e9; - } - &:disabled { pointer-events: none; } @@ -150,26 +134,10 @@ background-color: transparent; cursor: pointer; - &:not([data-selected]):hover { - background-color: #e0f2fe; - } - &[data-current] { outline: 1px solid #9ca3af; } - &:not([data-outsidemonth])[data-selected] { - background-color: #7dd3fc; - - &:focus-visible { - outline: 2px solid #0ea5e9; - } - } - - &:focus-visible { - outline: 2px solid #0ea5e9; - } - &[data-outsidemonth] { opacity: 0.3; pointer-events: none; @@ -202,7 +170,40 @@ } } +.DaysCell, +.MonthsCell, +.YearsCell { + &:not([data-selected]):hover { + background-color: #e0f2fe; + } + + &:not([data-outsidemonth])[data-selected] { + background-color: #7dd3fc; + } + + &:focus-visible { + outline: 2px solid #0ea5e9; + } +} + .Hidden { opacity: 0; pointer-events: none; } + +:global(.mode-dark) :where(.DaysCell, .MonthsCell, .YearsCell) { + &:not([data-selected]):hover { + background-color: #075985; + } + + &:not([data-outsidemonth])[data-selected] { + background-color: #0369a1; + } +} + +:global(.mode-dark) + :where(.SetVisibleMonth, .SetVisibleYear, .SetActiveSectionMonth, .SetActiveSectionYear) { + &:hover { + background-color: #075985; + } +} From b85ee12e9ab848bca6fea4260be1f5357441af4c Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 6 Jan 2025 15:00:48 +0100 Subject: [PATCH 052/136] Work --- .../src/internals/base/Calendar/utils/date.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/date.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/date.ts index 83db6141da54b..cde8fcfa965b3 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/utils/date.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/utils/date.ts @@ -1,10 +1,10 @@ import { ValidateDateProps } from '../../../../validation'; -import { MuiPickersAdapter } from '../../../../models'; +import { MuiPickersAdapter, PickerValidDate } from '../../../../models'; export const getFirstEnabledMonth = ( utils: MuiPickersAdapter, validationProps: ValidateDateProps, -) => { +): PickerValidDate => { const now = utils.date(); return utils.startOfMonth( validationProps.disablePast && utils.isAfter(now, validationProps.minDate) @@ -16,7 +16,7 @@ export const getFirstEnabledMonth = ( export const getLastEnabledMonth = ( utils: MuiPickersAdapter, validationProps: ValidateDateProps, -) => { +): PickerValidDate => { const now = utils.date(); return utils.startOfMonth( validationProps.disableFuture && utils.isBefore(now, validationProps.maxDate) From d43110db241de0191ff6d8e14ace820eb37926e3 Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 6 Jan 2025 15:02:09 +0100 Subject: [PATCH 053/136] Work --- .../useCalendarMonthCellCollection.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/month-cell-collection/useCalendarMonthCellCollection.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/month-cell-collection/useCalendarMonthCellCollection.ts index 8d199022cb2bf..25db60cd98d23 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/utils/month-cell-collection/useCalendarMonthCellCollection.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/utils/month-cell-collection/useCalendarMonthCellCollection.ts @@ -6,7 +6,7 @@ import { useUtils } from '../../../../hooks/useUtils'; import { useCalendarRootContext } from '../../root/CalendarRootContext'; import { CalendarMonthCellCollectionContext } from './CalendarMonthCellCollectionContext'; -export function useCalendarMonthCellCollection() { +export function useCalendarMonthCellCollection(): useCalendarMonthCellCollection.ReturnValue { const calendarRootContext = useCalendarRootContext(); const utils = useUtils(); @@ -65,3 +65,10 @@ export function useCalendarMonthCellCollection() { return { months, context }; } + +export namespace useCalendarMonthCellCollection { + export interface ReturnValue { + months: PickerValidDate[]; + context: CalendarMonthCellCollectionContext; + } +} From be9e91a160a0bd07c2467b0e38c74b9d259791f3 Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 6 Jan 2025 15:25:01 +0100 Subject: [PATCH 054/136] Work --- .../base-calendar/DayCalendarTwoMonthsDemo.js | 2 +- .../DayCalendarTwoMonthsDemo.tsx | 2 +- .../DayCalendarWithWeekNumberDemo.js | 106 ++++++++++++++++++ .../DayCalendarWithWeekNumberDemo.tsx | 106 ++++++++++++++++++ .../DayCalendarWithWeekNumberDemo.tsx.preview | 1 + .../base-calendar/base-calendar.md | 4 + .../base-calendar/calendar.module.css | 16 ++- .../base/Calendar/root/useCalendarRoot.ts | 2 - 8 files changed, 234 insertions(+), 5 deletions(-) create mode 100644 docs/data/date-pickers/base-calendar/DayCalendarWithWeekNumberDemo.js create mode 100644 docs/data/date-pickers/base-calendar/DayCalendarWithWeekNumberDemo.tsx create mode 100644 docs/data/date-pickers/base-calendar/DayCalendarWithWeekNumberDemo.tsx.preview diff --git a/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.js b/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.js index 2c70ecd52b7f9..402f9d876a3dd 100644 --- a/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.js +++ b/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.js @@ -84,7 +84,7 @@ function DayCalendar(props) { diff --git a/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.tsx b/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.tsx index cf96fd3038c71..feb44956b6066 100644 --- a/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.tsx +++ b/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.tsx @@ -84,7 +84,7 @@ function DayCalendar(props: Omit) { diff --git a/docs/data/date-pickers/base-calendar/DayCalendarWithWeekNumberDemo.js b/docs/data/date-pickers/base-calendar/DayCalendarWithWeekNumberDemo.js new file mode 100644 index 0000000000000..f0f8db396db0d --- /dev/null +++ b/docs/data/date-pickers/base-calendar/DayCalendarWithWeekNumberDemo.js @@ -0,0 +1,106 @@ +import * as React from 'react'; +import clsx from 'clsx'; + +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +// eslint-disable-next-line no-restricted-imports +import { + Calendar, + useCalendarContext, +} from '@mui/x-date-pickers/internals/base/Calendar'; +import styles from './calendar.module.css'; + +function Header() { + const { visibleDate } = useCalendarContext(); + + return ( +
+ + ◀ + + {visibleDate.format('MMMM YYYY')} + + ▶ + +
+ ); +} + +function DayCalendar(props) { + return ( + + +
+ + + {({ days }) => ( + + + # + + {days.map((day) => ( + + ))} + + )} + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => ( + + + {days[0].week()} + + {days.map((day) => ( + + ))} + + )} + + )) + } + + + + + ); +} + +export default function DayCalendarWithWeekNumberDemo() { + const [value, setValue] = React.useState(null); + + const handleValueChange = React.useCallback((newValue) => { + setValue(newValue); + }, []); + + return ( + + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/DayCalendarWithWeekNumberDemo.tsx b/docs/data/date-pickers/base-calendar/DayCalendarWithWeekNumberDemo.tsx new file mode 100644 index 0000000000000..b58264f112eca --- /dev/null +++ b/docs/data/date-pickers/base-calendar/DayCalendarWithWeekNumberDemo.tsx @@ -0,0 +1,106 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import { Dayjs } from 'dayjs'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +// eslint-disable-next-line no-restricted-imports +import { + Calendar, + useCalendarContext, +} from '@mui/x-date-pickers/internals/base/Calendar'; +import styles from './calendar.module.css'; + +function Header() { + const { visibleDate } = useCalendarContext(); + + return ( +
+ + ◀ + + {visibleDate.format('MMMM YYYY')} + + ▶ + +
+ ); +} + +function DayCalendar(props: Omit) { + return ( + + +
+ + + {({ days }) => ( + + + # + + {days.map((day) => ( + + ))} + + )} + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => ( + + + {days[0].week()} + + {days.map((day) => ( + + ))} + + )} + + )) + } + + + + + ); +} + +export default function DayCalendarWithWeekNumberDemo() { + const [value, setValue] = React.useState(null); + + const handleValueChange = React.useCallback((newValue: Dayjs | null) => { + setValue(newValue); + }, []); + + return ( + + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/DayCalendarWithWeekNumberDemo.tsx.preview b/docs/data/date-pickers/base-calendar/DayCalendarWithWeekNumberDemo.tsx.preview new file mode 100644 index 0000000000000..72b23c81a5a56 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/DayCalendarWithWeekNumberDemo.tsx.preview @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/data/date-pickers/base-calendar/base-calendar.md b/docs/data/date-pickers/base-calendar/base-calendar.md index fc6dfe8ed08de..284474e8a7cc6 100644 --- a/docs/data/date-pickers/base-calendar/base-calendar.md +++ b/docs/data/date-pickers/base-calendar/base-calendar.md @@ -22,6 +22,10 @@ packageName: '@mui/x-date-pickers' {{"demo": "DayCalendarWithValidationDemo.js"}} +### With week number + +{{"demo": "DayCalendarWithWeekNumberDemo.js"}} + ## Month Calendar ### One year diff --git a/docs/data/date-pickers/base-calendar/calendar.module.css b/docs/data/date-pickers/base-calendar/calendar.module.css index a41ba94436ca5..b6d79c1187dad 100644 --- a/docs/data/date-pickers/base-calendar/calendar.module.css +++ b/docs/data/date-pickers/base-calendar/calendar.module.css @@ -8,7 +8,11 @@ font-family: 'Roboto', 'Helvetica', 'Arial', sans-serif; } -.RootTwoPanels { +.RootWithWeekNumber { + width: 312px; +} + +.RootWithTwoPanels { width: 540px; flex-direction: row; justify-content: space-between; @@ -126,6 +130,16 @@ gap: 4px; } +.DaysWeekNumber { + height: 32px; + width: 32px; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 0.75rem; + color: #64748b; +} + .DaysCell { height: 32px; width: 32px; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts index 7aa7bd0ebf393..ca52d1f491366 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts @@ -234,13 +234,11 @@ export namespace useCalendarRoot { ExportedValidateDateProps { /** * The controlled value that should be selected. - * * To render an uncontrolled Date Calendar, use the `defaultValue` prop instead. */ value?: PickerValidDate | null; /** * The uncontrolled value that should be initially selected. - * * To render a controlled accordion, use the `value` prop instead. */ defaultValue?: PickerValidDate | null; From 879292f396ae40d46373d67e0e0edd9769641230 Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 6 Jan 2025 15:37:06 +0100 Subject: [PATCH 055/136] Add cross year keyboard navigation in the month grid --- .../months-grid/useCalendarMonthsGrid.ts | 82 +++++++++++++++++-- .../base/Calendar/root/CalendarRoot.tsx | 2 + .../base/Calendar/root/CalendarRootContext.ts | 1 + .../base/Calendar/root/useCalendarRoot.ts | 9 ++ .../CalendarSetVisibleYear.tsx | 28 +++---- .../src/internals/base/Calendar/utils/date.ts | 36 ++++++-- 6 files changed, 130 insertions(+), 28 deletions(-) diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-grid/useCalendarMonthsGrid.ts b/packages/x-date-pickers/src/internals/base/Calendar/months-grid/useCalendarMonthsGrid.ts index 93820134b1421..0829f28b74651 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-grid/useCalendarMonthsGrid.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-grid/useCalendarMonthsGrid.ts @@ -1,18 +1,29 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; +import useTimeout from '@mui/utils/useTimeout'; import { PickerValidDate } from '../../../../models'; +import { useUtils } from '../../../hooks/useUtils'; import { GenericHTMLProps } from '../../utils/types'; import { mergeReactProps } from '../../utils/mergeReactProps'; -import { navigateInGrid } from '../utils/keyboardNavigation'; +import { + applyInitialFocusInGrid, + navigateInGrid, + NavigateInGridChangePage, + PageNavigationTarget, +} from '../utils/keyboardNavigation'; import { useCalendarMonthCellCollection } from '../utils/month-cell-collection/useCalendarMonthCellCollection'; +import { useCalendarRootContext } from '../root/CalendarRootContext'; +import { getFirstEnabledYear, getLastEnabledYear } from '../utils/date'; export function useCalendarMonthsGrid(parameters: useCalendarMonthsGrid.Parameters) { const { children, cellsPerRow } = parameters; + const utils = useUtils(); + const calendarRootContext = useCalendarRootContext(); const calendarMonthsCellRefs = React.useRef<(HTMLElement | null)[]>([]); const { months, context } = useCalendarMonthCellCollection(); + const pageNavigationTargetRef = React.useRef(null); - // TODO: Add support for multiple months grids. - const onKeyDown = useEventCallback((event: React.KeyboardEvent) => { + const getCellsInCalendar = useEventCallback(() => { const grid: HTMLElement[][] = Array.from( { length: Math.ceil(calendarMonthsCellRefs.current.length / cellsPerRow), @@ -26,10 +37,71 @@ export function useCalendarMonthsGrid(parameters: useCalendarMonthsGrid.Paramete } }); + return [grid]; + }); + + const timeout = useTimeout(); + React.useEffect(() => { + if (pageNavigationTargetRef.current) { + const target = pageNavigationTargetRef.current; + timeout.start(0, () => { + const cells = getCellsInCalendar(); + applyInitialFocusInGrid({ cells, target }); + }); + } + }, [calendarRootContext.visibleDate, timeout, getCellsInCalendar]); + + // TODO: Add support for multiple months grids. + const onKeyDown = useEventCallback((event: React.KeyboardEvent) => { + const changePage: NavigateInGridChangePage = (params) => { + // TODO: Jump over months with no valid date. + if (params.direction === 'previous') { + const targetDate = utils.addYears( + utils.startOfYear(calendarRootContext.visibleDate), + -calendarRootContext.yearPageSize, + ); + const lastYearInNewPage = utils.addYears(targetDate, calendarRootContext.yearPageSize - 1); + + // All the years before the visible ones are fully disabled, we skip the navigation. + if ( + utils.isAfter( + getFirstEnabledYear(utils, calendarRootContext.validationProps), + lastYearInNewPage, + ) + ) { + return; + } + + calendarRootContext.setVisibleDate( + utils.addYears(calendarRootContext.visibleDate, -calendarRootContext.yearPageSize), + false, + ); + } + if (params.direction === 'next') { + const targetDate = utils.addYears( + utils.startOfYear(calendarRootContext.visibleDate), + calendarRootContext.yearPageSize, + ); + + // All the years after the visible ones are fully disabled, we skip the navigation. + if ( + utils.isBefore(getLastEnabledYear(utils, calendarRootContext.validationProps), targetDate) + ) { + return; + } + calendarRootContext.setVisibleDate( + utils.addYears(calendarRootContext.visibleDate, calendarRootContext.yearPageSize), + false, + ); + } + + pageNavigationTargetRef.current = params.target; + }; + navigateInGrid({ - cells: [grid], + cells: getCellsInCalendar(), event, - changePage: undefined, + changePage, }); }); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRoot.tsx b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRoot.tsx index 454b20431eacd..10580929d35c4 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRoot.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRoot.tsx @@ -22,6 +22,7 @@ const CalendarRoot = React.forwardRef(function CalendarRoot( timezone, referenceDate, monthPageSize, + yearPageSize, shouldDisableDate, shouldDisableMonth, shouldDisableYear, @@ -42,6 +43,7 @@ const CalendarRoot = React.forwardRef(function CalendarRoot( timezone, referenceDate, monthPageSize, + yearPageSize, shouldDisableDate, shouldDisableMonth, shouldDisableYear, diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts index fc4c8537b772f..f8c223d68cd46 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts @@ -32,6 +32,7 @@ export interface CalendarRootContext { visibleDate: PickerValidDate; setVisibleDate: (visibleDate: PickerValidDate, skipIfAlreadyVisible: boolean) => void; monthPageSize: number; + yearPageSize: number; applyDayGridKeyboardNavigation: (event: React.KeyboardEvent) => void; registerDaysGridCells: ( cellsRef: useCalendarDaysGridBody.CellsRef, diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts index ca52d1f491366..ff50a7cb3c040 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts @@ -76,6 +76,7 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { timezone: timezoneProp, referenceDate: referenceDateProp, monthPageSize = 1, + yearPageSize = 1, } = parameters; const utils = useUtils(); @@ -196,6 +197,7 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { visibleDate, setVisibleDate: handleVisibleDateChange, monthPageSize, + yearPageSize, applyDayGridKeyboardNavigation, registerDaysGridCells, registerSection, @@ -213,6 +215,7 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { visibleDate, handleVisibleDateChange, monthPageSize, + yearPageSize, applyDayGridKeyboardNavigation, registerDaysGridCells, registerSection, @@ -269,6 +272,12 @@ export namespace useCalendarRoot { * @default 1 */ monthPageSize?: number; + /** + * The amount of months to navigate by when pressing or when using keyboard navigation in the month grid or the month list. + * This is mostly useful when displaying multiple month grids or month lists. + * @default 1 + */ + yearPageSize?: number; } export interface ValueChangeHandlerContext { diff --git a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx index 8d7de77494916..b6ec59c71f24a 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx @@ -1,11 +1,12 @@ 'use client'; import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; -import { useNow, useUtils } from '../../../hooks/useUtils'; +import { useUtils } from '../../../hooks/useUtils'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; import { useCalendarRootContext } from '../root/CalendarRootContext'; import { useCalendarSetVisibleYear } from './useCalendarSetVisibleYear'; import { BaseUIComponentProps } from '../../utils/types'; +import { getFirstEnabledYear, getLastEnabledYear } from '../utils/date'; const InnerCalendarSetVisibleYear = React.forwardRef(function InnerCalendarSetVisibleYear( props: InnerCalendarSetVisibleYearProps, @@ -36,7 +37,6 @@ const CalendarSetVisibleYear = React.forwardRef(function CalendarSetVisibleYear( ) { const calendarRootContext = useCalendarRootContext(); const utils = useUtils(); - const now = useNow(calendarRootContext.timezone); const targetDate = React.useMemo(() => { if (props.target === 'previous') { @@ -51,32 +51,26 @@ const CalendarSetVisibleYear = React.forwardRef(function CalendarSetVisibleYear( return true; } + // TODO: Check if the logic below works correctly when multiple months are rendered at once. + // All the months before the visible ones are fully disabled, we skip the navigation. if (props.target === 'previous') { - const firstEnabledYear = utils.startOfYear( - calendarRootContext.validationProps.disablePast && - utils.isAfter(now, calendarRootContext.validationProps.minDate) - ? now - : calendarRootContext.validationProps.minDate, + return utils.isAfter( + getFirstEnabledYear(utils, calendarRootContext.validationProps), + targetDate, ); - - return utils.isAfter(firstEnabledYear, targetDate); } - const lastEnabledYear = utils.startOfYear( - calendarRootContext.validationProps.disableFuture && - utils.isBefore(now, calendarRootContext.validationProps.maxDate) - ? now - : calendarRootContext.validationProps.maxDate, + // All the months after the visible ones are fully disabled, we skip the navigation. + return utils.isBefore( + getLastEnabledYear(utils, calendarRootContext.validationProps), + targetDate, ); - - return utils.isBefore(lastEnabledYear, targetDate); }, [ calendarRootContext.disabled, calendarRootContext.validationProps, props.target, targetDate, utils, - now, ]); const setTarget = useEventCallback(() => { diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/date.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/date.ts index cde8fcfa965b3..2363c9fefbba6 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/utils/date.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/utils/date.ts @@ -1,26 +1,50 @@ import { ValidateDateProps } from '../../../../validation'; import { MuiPickersAdapter, PickerValidDate } from '../../../../models'; -export const getFirstEnabledMonth = ( +export function getFirstEnabledMonth( utils: MuiPickersAdapter, validationProps: ValidateDateProps, -): PickerValidDate => { +): PickerValidDate { const now = utils.date(); return utils.startOfMonth( validationProps.disablePast && utils.isAfter(now, validationProps.minDate) ? now : validationProps.minDate, ); -}; +} -export const getLastEnabledMonth = ( +export function getLastEnabledMonth( utils: MuiPickersAdapter, validationProps: ValidateDateProps, -): PickerValidDate => { +): PickerValidDate { const now = utils.date(); return utils.startOfMonth( validationProps.disableFuture && utils.isBefore(now, validationProps.maxDate) ? now : validationProps.maxDate, ); -}; +} + +export function getFirstEnabledYear( + utils: MuiPickersAdapter, + validationProps: ValidateDateProps, +): PickerValidDate { + const now = utils.date(); + return utils.startOfYear( + validationProps.disablePast && utils.isAfter(now, validationProps.minDate) + ? now + : validationProps.minDate, + ); +} + +export function getLastEnabledYear( + utils: MuiPickersAdapter, + validationProps: ValidateDateProps, +): PickerValidDate { + const now = utils.date(); + return utils.startOfYear( + validationProps.disableFuture && utils.isBefore(now, validationProps.maxDate) + ? now + : validationProps.maxDate, + ); +} From 1111663aab9487df33fbb333a3d6f2d149a10130 Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 6 Jan 2025 15:48:12 +0100 Subject: [PATCH 056/136] Work --- docs/data/date-pickers/base-calendar/calendar.module.css | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/data/date-pickers/base-calendar/calendar.module.css b/docs/data/date-pickers/base-calendar/calendar.module.css index b6d79c1187dad..f8529cdc48425 100644 --- a/docs/data/date-pickers/base-calendar/calendar.module.css +++ b/docs/data/date-pickers/base-calendar/calendar.module.css @@ -45,7 +45,6 @@ border: none; background-color: transparent; border-radius: 4px; - font-size: 12px; height: 24px; width: 24px; text-align: center; @@ -68,14 +67,17 @@ .SetVisibleYear { height: 24px; width: 24px; + font-size: 0.75rem; } .SetActiveSectionMonth { - min-width: 72px; + min-width: 80px; + font-size: 0.85rem; } .SetActiveSectionYear { min-width: 42px; + font-size: 0.85rem; } .MonthsList, From d8923b8b8302c2c8d355dcfc18e9c22b869bc6ee Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 6 Jan 2025 15:57:26 +0100 Subject: [PATCH 057/136] Work --- ...onthDayCalendar.js => DateCalendarDemo.js} | 2 +- ...thDayCalendar.tsx => DateCalendarDemo.tsx} | 2 +- .../base-calendar/DateCalendarMD2Demo.js | 123 +++++++++++++++++ .../base-calendar/DateCalendarMD2Demo.tsx | 129 ++++++++++++++++++ .../base-calendar/base-calendar.md | 8 +- .../base-calendar/calendar.module.css | 17 ++- .../CalendarSetVisibleMonth.tsx | 1 - 7 files changed, 275 insertions(+), 7 deletions(-) rename docs/data/date-pickers/base-calendar/{YearMonthDayCalendar.js => DateCalendarDemo.js} (99%) rename docs/data/date-pickers/base-calendar/{YearMonthDayCalendar.tsx => DateCalendarDemo.tsx} (99%) create mode 100644 docs/data/date-pickers/base-calendar/DateCalendarMD2Demo.js create mode 100644 docs/data/date-pickers/base-calendar/DateCalendarMD2Demo.tsx diff --git a/docs/data/date-pickers/base-calendar/YearMonthDayCalendar.js b/docs/data/date-pickers/base-calendar/DateCalendarDemo.js similarity index 99% rename from docs/data/date-pickers/base-calendar/YearMonthDayCalendar.js rename to docs/data/date-pickers/base-calendar/DateCalendarDemo.js index 918056876d8d1..f8c9f3d4f763c 100644 --- a/docs/data/date-pickers/base-calendar/YearMonthDayCalendar.js +++ b/docs/data/date-pickers/base-calendar/DateCalendarDemo.js @@ -70,7 +70,7 @@ function Header(props) { ); } -export default function YearMonthDayCalendar() { +export default function DateCalendarDemo() { const [value, setValue] = React.useState(null); const [activeSection, setActiveSection] = React.useState('day'); diff --git a/docs/data/date-pickers/base-calendar/YearMonthDayCalendar.tsx b/docs/data/date-pickers/base-calendar/DateCalendarDemo.tsx similarity index 99% rename from docs/data/date-pickers/base-calendar/YearMonthDayCalendar.tsx rename to docs/data/date-pickers/base-calendar/DateCalendarDemo.tsx index b6f87a3cde5e8..e3513f66953be 100644 --- a/docs/data/date-pickers/base-calendar/YearMonthDayCalendar.tsx +++ b/docs/data/date-pickers/base-calendar/DateCalendarDemo.tsx @@ -73,7 +73,7 @@ function Header(props: { ); } -export default function YearMonthDayCalendar() { +export default function DateCalendarDemo() { const [value, setValue] = React.useState(null); const [activeSection, setActiveSection] = React.useState<'day' | 'month' | 'year'>( 'day', diff --git a/docs/data/date-pickers/base-calendar/DateCalendarMD2Demo.js b/docs/data/date-pickers/base-calendar/DateCalendarMD2Demo.js new file mode 100644 index 0000000000000..14a92edd10f41 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/DateCalendarMD2Demo.js @@ -0,0 +1,123 @@ +import * as React from 'react'; + +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +// eslint-disable-next-line no-restricted-imports +import { + Calendar, + useCalendarContext, +} from '@mui/x-date-pickers/internals/base/Calendar'; +import styles from './calendar.module.css'; + +function Header(props) { + const { activeSection, onActiveSectionChange } = props; + const { visibleDate } = useCalendarContext(); + + return ( +
+ +
+ + ◀ + + + ▶ + +
+
+ ); +} + +export default function DateCalendarMD2Demo() { + const [value, setValue] = React.useState(null); + const [activeSection, setActiveSection] = React.useState('day'); + + const handleValueChange = React.useCallback((newValue, context) => { + if (context.section === 'year') { + setActiveSection('day'); + } + + setValue(newValue); + }, []); + + return ( + + +
+ {activeSection === 'year' && ( + + {({ years }) => + years.map((year) => ( + + )) + } + + )} + {activeSection === 'day' && ( + + + {({ days }) => + days.map((day) => ( + + )) + } + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => + days.map((day) => ( + + )) + } + + )) + } + + + )} + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/DateCalendarMD2Demo.tsx b/docs/data/date-pickers/base-calendar/DateCalendarMD2Demo.tsx new file mode 100644 index 0000000000000..f682f537dbfe9 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/DateCalendarMD2Demo.tsx @@ -0,0 +1,129 @@ +import * as React from 'react'; +import { Dayjs } from 'dayjs'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +// eslint-disable-next-line no-restricted-imports +import { + Calendar, + useCalendarContext, +} from '@mui/x-date-pickers/internals/base/Calendar'; +import styles from './calendar.module.css'; + +function Header(props: { + activeSection: 'day' | 'month' | 'year'; + onActiveSectionChange: (newActiveSection: 'day' | 'month' | 'year') => void; +}) { + const { activeSection, onActiveSectionChange } = props; + const { visibleDate } = useCalendarContext(); + + return ( +
+ +
+ + ◀ + + + ▶ + +
+
+ ); +} + +export default function DateCalendarMD2Demo() { + const [value, setValue] = React.useState(null); + const [activeSection, setActiveSection] = React.useState<'day' | 'year'>('day'); + + const handleValueChange = React.useCallback( + (newValue: Dayjs | null, context: Calendar.Root.ValueChangeHandlerContext) => { + if (context.section === 'year') { + setActiveSection('day'); + } + + setValue(newValue); + }, + [], + ); + + return ( + + +
+ {activeSection === 'year' && ( + + {({ years }) => + years.map((year) => ( + + )) + } + + )} + {activeSection === 'day' && ( + + + {({ days }) => + days.map((day) => ( + + )) + } + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => + days.map((day) => ( + + )) + } + + )) + } + + + )} + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/base-calendar.md b/docs/data/date-pickers/base-calendar/base-calendar.md index 284474e8a7cc6..c323ee24b60de 100644 --- a/docs/data/date-pickers/base-calendar/base-calendar.md +++ b/docs/data/date-pickers/base-calendar/base-calendar.md @@ -38,4 +38,10 @@ TODO ## Full Date Calendar -{{"demo": "YearMonthDayCalendar.js"}} +### MD2-ish layout + +{{"demo": "DateCalendarMD2Demo.js"}} + +### MD3-ish layout + +{{"demo": "DateCalendarDemo.js"}} diff --git a/docs/data/date-pickers/base-calendar/calendar.module.css b/docs/data/date-pickers/base-calendar/calendar.module.css index f8529cdc48425..e7fb0c44bdf25 100644 --- a/docs/data/date-pickers/base-calendar/calendar.module.css +++ b/docs/data/date-pickers/base-calendar/calendar.module.css @@ -34,14 +34,14 @@ .HeaderBlock { display: flex; gap: 4px; - flex-grow: 1; justify-content: center; } .SetVisibleMonth, .SetVisibleYear, .SetActiveSectionMonth, -.SetActiveSectionYear { +.SetActiveSectionYear, +.SetActiveSectionYearMD2 { border: none; background-color: transparent; border-radius: 4px; @@ -80,6 +80,11 @@ font-size: 0.85rem; } +.SetActiveSectionYearMD2 { + min-width: 132px; + font-size: 0.85rem; +} + .MonthsList, .YearsList { padding: 12px; @@ -218,7 +223,13 @@ } :global(.mode-dark) - :where(.SetVisibleMonth, .SetVisibleYear, .SetActiveSectionMonth, .SetActiveSectionYear) { + :where( + .SetVisibleMonth, + .SetVisibleYear, + .SetActiveSectionMonth, + .SetActiveSectionYear, + .SetActiveSectionYearMD2 + ) { &:hover { background-color: #075985; } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx index 31c90eded6ecc..a9e84125fb760 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx @@ -56,7 +56,6 @@ const CalendarSetVisibleMonth = React.forwardRef(function CalendarSetVisibleMont } // TODO: Check if the logic below works correctly when multiple months are rendered at once. - // All the months before the visible ones are fully disabled, we skip the navigation. if (props.target === 'previous') { return utils.isAfter( From 96c9ed541e1962f62f170743f775bd64b0370791 Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 6 Jan 2025 16:05:34 +0100 Subject: [PATCH 058/136] Work --- docs/data/date-pickers/base-calendar/DateCalendarMD2Demo.js | 1 + docs/data/date-pickers/base-calendar/DateCalendarMD2Demo.tsx | 1 + docs/data/date-pickers/base-calendar/calendar.module.css | 3 ++- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/data/date-pickers/base-calendar/DateCalendarMD2Demo.js b/docs/data/date-pickers/base-calendar/DateCalendarMD2Demo.js index 14a92edd10f41..630737371a112 100644 --- a/docs/data/date-pickers/base-calendar/DateCalendarMD2Demo.js +++ b/docs/data/date-pickers/base-calendar/DateCalendarMD2Demo.js @@ -23,6 +23,7 @@ function Header(props) { className={styles.SetActiveSectionYearMD2} > {visibleDate.format('MMMM YYYY')} +     {activeSection === 'day' ? '▼' : '▲'}
diff --git a/docs/data/date-pickers/base-calendar/DateCalendarMD2Demo.tsx b/docs/data/date-pickers/base-calendar/DateCalendarMD2Demo.tsx index f682f537dbfe9..5202e58ea1dd3 100644 --- a/docs/data/date-pickers/base-calendar/DateCalendarMD2Demo.tsx +++ b/docs/data/date-pickers/base-calendar/DateCalendarMD2Demo.tsx @@ -26,6 +26,7 @@ function Header(props: { className={styles.SetActiveSectionYearMD2} > {visibleDate.format('MMMM YYYY')} +     {activeSection === 'day' ? '▼' : '▲'}
diff --git a/docs/data/date-pickers/base-calendar/calendar.module.css b/docs/data/date-pickers/base-calendar/calendar.module.css index e7fb0c44bdf25..5b7857967ce04 100644 --- a/docs/data/date-pickers/base-calendar/calendar.module.css +++ b/docs/data/date-pickers/base-calendar/calendar.module.css @@ -81,8 +81,9 @@ } .SetActiveSectionYearMD2 { - min-width: 132px; font-size: 0.85rem; + min-width: fit-content; + text-wrap: nowrap; } .MonthsList, From e1561e224c08747b084e7c60b91f14634c06297f Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 6 Jan 2025 18:36:26 +0100 Subject: [PATCH 059/136] Add decade demo --- .../base-calendar/DateCalendarMD2Demo.tsx | 4 +- .../base-calendar/YearCalendarDemo.js | 29 +++++++ .../base-calendar/YearCalendarDemo.tsx | 29 +++++++ .../YearCalendarDemo.tsx.preview | 13 ++++ .../YearCalendarWithDecadeNavigationDemo.js | 71 ++++++++++++++++++ .../YearCalendarWithDecadeNavigationDemo.tsx | 71 ++++++++++++++++++ ...lendarWithDecadeNavigationDemo.tsx.preview | 4 + .../base-calendar/base-calendar.md | 10 +++ .../base-calendar/calendar.module.css | 6 +- .../Calendar/days-cell/CalendarDaysCell.tsx | 33 ++++---- .../days-grid-body/useCalendarDaysGridBody.ts | 16 ++-- .../useCalendarDaysGridHeader.ts | 6 +- .../Calendar/days-grid/useCalendarDaysGrid.ts | 18 ++--- .../days-week-row/CalendarDaysWeekRow.tsx | 16 ++-- .../days-week-row/useCalendarDaysWeekRow.ts | 8 +- .../internals/base/Calendar/index.parts.ts | 1 + .../months-cell/CalendarMonthsCell.tsx | 49 ++++++------ .../months-cell/useCalendarMonthsCell.ts | 4 +- .../months-grid/CalendarMonthsGrid.tsx | 12 +-- .../months-grid/useCalendarMonthsGrid.ts | 47 ++++++------ .../months-list/CalendarMonthsList.tsx | 10 +-- .../months-list/useCalendarMonthsList.ts | 12 +-- .../CalendarSetVisibleMonth.tsx | 40 +++++----- .../useCalendarSetVisibleMonth.ts | 3 +- .../CalendarSetVisibleYear.tsx | 41 +++++----- .../useCalendarSetVisibleYear.ts | 5 +- .../useCalendarContext/useCalendarContext.ts | 4 +- .../CalendarMonthCellCollectionContext.ts | 24 ------ .../useCalendarMonthCellCollection.ts | 74 ------------------ .../CalendarMonthsCellCollectionContext.ts | 24 ++++++ .../useCalendarMonthsCellCollection.ts | 74 ++++++++++++++++++ .../CalendarYearsCellCollectionContext.ts | 24 ++++++ .../useCalendarYearsCellCollection.ts | 75 +++++++++++++++++++ .../Calendar/years-cell/CalendarYearsCell.tsx | 45 ++++++----- .../Calendar/years-grid/CalendarYearsGrid.tsx | 44 +++++++++++ .../years-grid/useCalendarYearsGrid.ts | 70 +++++++++++++++++ .../Calendar/years-list/CalendarYearsList.tsx | 12 +-- .../years-list/CalendarYearsListContext.ts | 24 ------ .../years-list/useCalendarYearsList.ts | 73 ++---------------- 39 files changed, 734 insertions(+), 391 deletions(-) create mode 100644 docs/data/date-pickers/base-calendar/YearCalendarDemo.js create mode 100644 docs/data/date-pickers/base-calendar/YearCalendarDemo.tsx create mode 100644 docs/data/date-pickers/base-calendar/YearCalendarDemo.tsx.preview create mode 100644 docs/data/date-pickers/base-calendar/YearCalendarWithDecadeNavigationDemo.js create mode 100644 docs/data/date-pickers/base-calendar/YearCalendarWithDecadeNavigationDemo.tsx create mode 100644 docs/data/date-pickers/base-calendar/YearCalendarWithDecadeNavigationDemo.tsx.preview delete mode 100644 packages/x-date-pickers/src/internals/base/Calendar/utils/month-cell-collection/CalendarMonthCellCollectionContext.ts delete mode 100644 packages/x-date-pickers/src/internals/base/Calendar/utils/month-cell-collection/useCalendarMonthCellCollection.ts create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/utils/months-cell-collection/CalendarMonthsCellCollectionContext.ts create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/utils/months-cell-collection/useCalendarMonthsCellCollection.ts create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/utils/years-cell-collection/CalendarYearsCellCollectionContext.ts create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/utils/years-cell-collection/useCalendarYearsCellCollection.ts create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/years-grid/CalendarYearsGrid.tsx create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/years-grid/useCalendarYearsGrid.ts delete mode 100644 packages/x-date-pickers/src/internals/base/Calendar/years-list/CalendarYearsListContext.ts diff --git a/docs/data/date-pickers/base-calendar/DateCalendarMD2Demo.tsx b/docs/data/date-pickers/base-calendar/DateCalendarMD2Demo.tsx index 5202e58ea1dd3..ff6bc46b5f6aa 100644 --- a/docs/data/date-pickers/base-calendar/DateCalendarMD2Demo.tsx +++ b/docs/data/date-pickers/base-calendar/DateCalendarMD2Demo.tsx @@ -10,8 +10,8 @@ import { import styles from './calendar.module.css'; function Header(props: { - activeSection: 'day' | 'month' | 'year'; - onActiveSectionChange: (newActiveSection: 'day' | 'month' | 'year') => void; + activeSection: 'day' | 'year'; + onActiveSectionChange: (newActiveSection: 'day' | 'year') => void; }) { const { activeSection, onActiveSectionChange } = props; const { visibleDate } = useCalendarContext(); diff --git a/docs/data/date-pickers/base-calendar/YearCalendarDemo.js b/docs/data/date-pickers/base-calendar/YearCalendarDemo.js new file mode 100644 index 0000000000000..88856c14bca51 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/YearCalendarDemo.js @@ -0,0 +1,29 @@ +import * as React from 'react'; + +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +// eslint-disable-next-line no-restricted-imports +import { Calendar } from '@mui/x-date-pickers/internals/base/Calendar'; +import styles from './calendar.module.css'; + +export default function YearCalendarDemo() { + const [value, setValue] = React.useState(null); + + return ( + + + + {({ years }) => + years.map((year) => ( + + )) + } + + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/YearCalendarDemo.tsx b/docs/data/date-pickers/base-calendar/YearCalendarDemo.tsx new file mode 100644 index 0000000000000..1b7dfc8b8ffef --- /dev/null +++ b/docs/data/date-pickers/base-calendar/YearCalendarDemo.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; +import { Dayjs } from 'dayjs'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +// eslint-disable-next-line no-restricted-imports +import { Calendar } from '@mui/x-date-pickers/internals/base/Calendar'; +import styles from './calendar.module.css'; + +export default function YearCalendarDemo() { + const [value, setValue] = React.useState(null); + + return ( + + + + {({ years }) => + years.map((year) => ( + + )) + } + + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/YearCalendarDemo.tsx.preview b/docs/data/date-pickers/base-calendar/YearCalendarDemo.tsx.preview new file mode 100644 index 0000000000000..eb5474b39e01e --- /dev/null +++ b/docs/data/date-pickers/base-calendar/YearCalendarDemo.tsx.preview @@ -0,0 +1,13 @@ + + + {({ years }) => + years.map((year) => ( + + )) + } + + \ No newline at end of file diff --git a/docs/data/date-pickers/base-calendar/YearCalendarWithDecadeNavigationDemo.js b/docs/data/date-pickers/base-calendar/YearCalendarWithDecadeNavigationDemo.js new file mode 100644 index 0000000000000..73420672dc424 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/YearCalendarWithDecadeNavigationDemo.js @@ -0,0 +1,71 @@ +import * as React from 'react'; + +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +// eslint-disable-next-line no-restricted-imports +import { + Calendar, + useCalendarContext, +} from '@mui/x-date-pickers/internals/base/Calendar'; +import styles from './calendar.module.css'; + +function Header() { + const { visibleDate } = useCalendarContext(); + + const startOfDecade = React.useMemo( + () => visibleDate.set('year', Math.floor(visibleDate.year() / 10) * 10), + [visibleDate], + ); + + return ( +
+ + ◀ + + {startOfDecade.format('YYYY')}s + + ▶ + +
+ ); +} + +function YearsGrid() { + const { visibleDate } = useCalendarContext(); + const decade = Math.floor(visibleDate.year() / 10) * 10; + + return ( + + {({ years }) => + years + .filter((year) => Math.floor(year.year() / 10) * 10 === decade) + .map((year) => ( + + )) + } + + ); +} + +export default function YearCalendarWithDecadeNavigationDemo() { + const [value, setValue] = React.useState(null); + + return ( + + +
+ + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/YearCalendarWithDecadeNavigationDemo.tsx b/docs/data/date-pickers/base-calendar/YearCalendarWithDecadeNavigationDemo.tsx new file mode 100644 index 0000000000000..a236b2cdae110 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/YearCalendarWithDecadeNavigationDemo.tsx @@ -0,0 +1,71 @@ +import * as React from 'react'; +import { Dayjs } from 'dayjs'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +// eslint-disable-next-line no-restricted-imports +import { + Calendar, + useCalendarContext, +} from '@mui/x-date-pickers/internals/base/Calendar'; +import styles from './calendar.module.css'; + +function Header() { + const { visibleDate } = useCalendarContext(); + + const startOfDecade = React.useMemo( + () => visibleDate.set('year', Math.floor(visibleDate.year() / 10) * 10), + [visibleDate], + ); + + return ( +
+ + ◀ + + {startOfDecade.format('YYYY')}s + + ▶ + +
+ ); +} + +function YearsGrid() { + const { visibleDate } = useCalendarContext(); + const decade = Math.floor(visibleDate.year() / 10) * 10; + + return ( + + {({ years }) => + years + .filter((year: Dayjs) => Math.floor(year.year() / 10) * 10 === decade) + .map((year) => ( + + )) + } + + ); +} + +export default function YearCalendarWithDecadeNavigationDemo() { + const [value, setValue] = React.useState(null); + + return ( + + +
+ + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/YearCalendarWithDecadeNavigationDemo.tsx.preview b/docs/data/date-pickers/base-calendar/YearCalendarWithDecadeNavigationDemo.tsx.preview new file mode 100644 index 0000000000000..a205ab02e7254 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/YearCalendarWithDecadeNavigationDemo.tsx.preview @@ -0,0 +1,4 @@ + +
+ + \ No newline at end of file diff --git a/docs/data/date-pickers/base-calendar/base-calendar.md b/docs/data/date-pickers/base-calendar/base-calendar.md index c323ee24b60de..0ed2e8378e980 100644 --- a/docs/data/date-pickers/base-calendar/base-calendar.md +++ b/docs/data/date-pickers/base-calendar/base-calendar.md @@ -36,6 +36,16 @@ packageName: '@mui/x-date-pickers' TODO +## Year Calendar + +### Full list + +{{"demo": "YearCalendarDemo.js"}} + +### Grouped by decade + +{{"demo": "YearCalendarWithDecadeNavigationDemo.js"}} + ## Full Date Calendar ### MD2-ish layout diff --git a/docs/data/date-pickers/base-calendar/calendar.module.css b/docs/data/date-pickers/base-calendar/calendar.module.css index 5b7857967ce04..6cb51fff59b5e 100644 --- a/docs/data/date-pickers/base-calendar/calendar.module.css +++ b/docs/data/date-pickers/base-calendar/calendar.module.css @@ -93,15 +93,17 @@ flex-direction: column; align-items: stretch; gap: 8px; - overflow-y: scroll; + overflow-y: auto; } -.MonthsGrid { +.MonthsGrid, +.YearsGrid { padding: 12px; display: grid; grid-template-columns: repeat(2, 1fr); column-gap: 4px; row-gap: 12px; + overflow-y: auto; } .MonthsCell, diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx index f037667f7f16f..8454109936188 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx @@ -43,43 +43,40 @@ const CalendarDaysCell = React.forwardRef(function CalendarDaysCell( props: CalendarDaysCell.Props, forwardedRef: React.ForwardedRef, ) { - const calendarRootContext = useCalendarRootContext(); - const calendarMonthsListContext = useCalendarDaysGridContext(); + const rootContext = useCalendarRootContext(); + const monthsListContext = useCalendarDaysGridContext(); const { ref: listItemRef, index: colIndex } = useCompositeListItem(); const utils = useUtils(); const mergedRef = useForkRef(forwardedRef, listItemRef); const isSelected = React.useMemo( - () => - calendarRootContext.value == null - ? false - : utils.isSameDay(calendarRootContext.value, props.value), - [calendarRootContext.value, props.value, utils], + () => (rootContext.value == null ? false : utils.isSameDay(rootContext.value, props.value)), + [rootContext.value, props.value, utils], ); const isOutsideCurrentMonth = React.useMemo( () => - calendarMonthsListContext.currentMonth == null + monthsListContext.currentMonth == null ? false - : !utils.isSameMonth(calendarMonthsListContext.currentMonth, props.value), - [calendarMonthsListContext.currentMonth, props.value, utils], + : !utils.isSameMonth(monthsListContext.currentMonth, props.value), + [monthsListContext.currentMonth, props.value, utils], ); - const isDateDisabled = calendarRootContext.isDateDisabled; + const isDateDisabled = rootContext.isDateDisabled; const isDisabled = React.useMemo(() => { - if (calendarRootContext.disabled) { + if (rootContext.disabled) { return true; } return isDateDisabled(props.value); - }, [calendarRootContext.disabled, isDateDisabled, props.value]); + }, [rootContext.disabled, isDateDisabled, props.value]); const isTabbable = React.useMemo( () => - calendarMonthsListContext.tabbableDay == null + monthsListContext.tabbableDay == null ? false - : utils.isSameDay(calendarMonthsListContext.tabbableDay, props.value), - [utils, calendarMonthsListContext.tabbableDay, props.value], + : utils.isSameDay(monthsListContext.tabbableDay, props.value), + [utils, monthsListContext.tabbableDay, props.value], ); const ctx = React.useMemo( @@ -89,14 +86,14 @@ const CalendarDaysCell = React.forwardRef(function CalendarDaysCell( isDisabled, isTabbable, isOutsideCurrentMonth, - selectDay: calendarMonthsListContext.selectDay, + selectDay: monthsListContext.selectDay, }), [ isSelected, isDisabled, isTabbable, isOutsideCurrentMonth, - calendarMonthsListContext.selectDay, + monthsListContext.selectDay, colIndex, ], ); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-grid-body/useCalendarDaysGridBody.ts b/packages/x-date-pickers/src/internals/base/Calendar/days-grid-body/useCalendarDaysGridBody.ts index be7af2b304df2..a16fa0376e207 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-grid-body/useCalendarDaysGridBody.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-grid-body/useCalendarDaysGridBody.ts @@ -9,8 +9,8 @@ import { useCalendarRootContext } from '../root/CalendarRootContext'; export function useCalendarDaysGridBody(parameters: useCalendarDaysGridBody.Parameters) { const { children } = parameters; - const calendarRootContext = useCalendarRootContext(); - const calendarDaysGridContext = useCalendarDaysGridContext(); + const rootContext = useCalendarRootContext(); + const daysGridContext = useCalendarDaysGridContext(); const rowsRef: useCalendarDaysGridBody.RowsRef = React.useRef([]); const cellsRef: useCalendarDaysGridBody.CellsRef = React.useRef([]); @@ -21,15 +21,11 @@ export function useCalendarDaysGridBody(parameters: useCalendarDaysGridBody.Para children: children == null ? null - : children({ weeks: calendarDaysGridContext.daysGrid.map((week) => week[0]) }), - onKeyDown: calendarRootContext.applyDayGridKeyboardNavigation, + : children({ weeks: daysGridContext.daysGrid.map((week) => week[0]) }), + onKeyDown: rootContext.applyDayGridKeyboardNavigation, }); }, - [ - calendarDaysGridContext.daysGrid, - calendarRootContext.applyDayGridKeyboardNavigation, - children, - ], + [daysGridContext.daysGrid, rootContext.applyDayGridKeyboardNavigation, children], ); const registerWeekRowCells = useEventCallback( @@ -45,7 +41,7 @@ export function useCalendarDaysGridBody(parameters: useCalendarDaysGridBody.Para }, ); - const registerDaysGridCells = calendarRootContext.registerDaysGridCells; + const registerDaysGridCells = rootContext.registerDaysGridCells; React.useEffect(() => { return registerDaysGridCells(cellsRef, rowsRef); }, [registerDaysGridCells]); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-grid-header/useCalendarDaysGridHeader.ts b/packages/x-date-pickers/src/internals/base/Calendar/days-grid-header/useCalendarDaysGridHeader.ts index 2adc9c63fdf03..50a92f6b6eec4 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-grid-header/useCalendarDaysGridHeader.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-grid-header/useCalendarDaysGridHeader.ts @@ -9,11 +9,11 @@ import { mergeReactProps } from '../../utils/mergeReactProps'; export function useCalendarDaysGridHeader(parameters: useCalendarDaysGridHeader.Parameters) { const { children } = parameters; const utils = useUtils(); - const calendarRootContext = useCalendarRootContext(); + const rootContext = useCalendarRootContext(); const days = React.useMemo( - () => getWeekdays(utils, calendarRootContext.value ?? calendarRootContext.referenceDate), - [utils, calendarRootContext.value, calendarRootContext.referenceDate], + () => getWeekdays(utils, rootContext.value ?? rootContext.referenceDate), + [utils, rootContext.value, rootContext.referenceDate], ); const getDaysGridHeaderProps = React.useCallback( diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-grid/useCalendarDaysGrid.ts b/packages/x-date-pickers/src/internals/base/Calendar/days-grid/useCalendarDaysGrid.ts index e1369be665a25..a11f056b08687 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-grid/useCalendarDaysGrid.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-grid/useCalendarDaysGrid.ts @@ -11,12 +11,12 @@ import { CalendarDaysGridContext } from './CalendarDaysGridContext'; export function useCalendarDaysGrid(parameters: useCalendarDaysGrid.Parameters) { const { fixedWeekNumber, offset = 0 } = parameters; const utils = useUtils(); - const calendarRootContext = useCalendarRootContext(); + const rootContext = useCalendarRootContext(); const currentMonth = React.useMemo(() => { - const cleanVisibleDate = utils.startOfMonth(calendarRootContext.visibleDate); + const cleanVisibleDate = utils.startOfMonth(rootContext.visibleDate); return offset === 0 ? cleanVisibleDate : utils.addMonths(cleanVisibleDate, offset); - }, [utils, calendarRootContext.visibleDate, offset]); + }, [utils, rootContext.visibleDate, offset]); const daysGrid = React.useMemo(() => { const toDisplay = utils.getWeekArray(currentMonth); @@ -47,30 +47,30 @@ export function useCalendarDaysGrid(parameters: useCalendarDaysGrid.Parameters) }, []); const selectDay = useEventCallback((newValue: PickerValidDate) => { - if (calendarRootContext.readOnly) { + if (rootContext.readOnly) { return; } const newCleanValue = mergeDateAndTime( utils, newValue, - calendarRootContext.value ?? calendarRootContext.referenceDate, + rootContext.value ?? rootContext.referenceDate, ); - calendarRootContext.setValue(newCleanValue, { section: 'day' }); + rootContext.setValue(newCleanValue, { section: 'day' }); }); const tabbableDay = React.useMemo(() => { const flatDays = daysGrid.flat(); - const tempTabbableDay = calendarRootContext.value ?? calendarRootContext.referenceDate; + const tempTabbableDay = rootContext.value ?? rootContext.referenceDate; if (flatDays.some((day) => utils.isSameDay(day, tempTabbableDay))) { return tempTabbableDay; } return flatDays.find((day) => utils.isSameMonth(day, currentMonth)) ?? null; - }, [calendarRootContext.value, calendarRootContext.referenceDate, daysGrid, utils, currentMonth]); + }, [rootContext.value, rootContext.referenceDate, daysGrid, utils, currentMonth]); - const registerSection = calendarRootContext.registerSection; + const registerSection = rootContext.registerSection; React.useEffect(() => { return registerSection({ type: 'day', value: currentMonth }); }, [registerSection, currentMonth]); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/CalendarDaysWeekRow.tsx b/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/CalendarDaysWeekRow.tsx index 8f4d4c37202f6..426462caef885 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/CalendarDaysWeekRow.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/CalendarDaysWeekRow.tsx @@ -14,7 +14,7 @@ const InnerCalendarDaysWeekRow = React.forwardRef(function CalendarDaysGrid( forwardedRef: React.ForwardedRef, ) { const { className, render, value, ctx, children, ...otherProps } = props; - const { getDaysWeekRowProps, calendarDayCellRefs } = useCalendarDaysWeekRow({ + const { getDaysWeekRowProps, dayCellRefs } = useCalendarDaysWeekRow({ value, ctx, children, @@ -30,7 +30,7 @@ const InnerCalendarDaysWeekRow = React.forwardRef(function CalendarDaysGrid( extraProps: otherProps, }); - return {renderElement()}; + return {renderElement()}; }); const MemoizedInnerCalendarDaysWeekRow = React.memo(InnerCalendarDaysWeekRow); @@ -39,24 +39,24 @@ const CalendarDaysWeekRow = React.forwardRef(function CalendarDaysWeekRow( props: CalendarDaysWeekRow.Props, forwardedRef: React.ForwardedRef, ) { - const calendarDaysGridContext = useCalendarDaysGridContext(); - const calendarDaysGridBodyContext = useCalendarDaysGridBodyContext(); + const daysGridContext = useCalendarDaysGridContext(); + const daysGridBodyContext = useCalendarDaysGridBodyContext(); const { ref: listItemRef, index: rowIndex } = useCompositeListItem(); const mergedRef = useForkRef(forwardedRef, listItemRef); // TODO: Improve how we pass the week to this component. const days = React.useMemo( - () => calendarDaysGridContext.daysGrid.find((week) => week[0] === props.value) ?? [], - [calendarDaysGridContext.daysGrid, props.value], + () => daysGridContext.daysGrid.find((week) => week[0] === props.value) ?? [], + [daysGridContext.daysGrid, props.value], ); const ctx = React.useMemo( () => ({ days, rowIndex, - registerWeekRowCells: calendarDaysGridBodyContext.registerWeekRowCells, + registerWeekRowCells: daysGridBodyContext.registerWeekRowCells, }), - [days, rowIndex, calendarDaysGridBodyContext.registerWeekRowCells], + [days, rowIndex, daysGridBodyContext.registerWeekRowCells], ); return ; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/useCalendarDaysWeekRow.ts b/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/useCalendarDaysWeekRow.ts index f7538db5c8b82..db17136e21987 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/useCalendarDaysWeekRow.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/useCalendarDaysWeekRow.ts @@ -7,7 +7,7 @@ import { CalendarDaysGridBodyContext } from '../days-grid-body/CalendarDaysGridB export function useCalendarDaysWeekRow(parameters: useCalendarDaysWeekRow.Parameters) { const { children, ctx } = parameters; const ref = React.useRef(null); - const calendarDayCellRefs = React.useRef<(HTMLElement | null)[]>([]); + const dayCellRefs = React.useRef<(HTMLElement | null)[]>([]); const getDaysWeekRowProps = React.useCallback( (externalProps: GenericHTMLProps) => { @@ -23,12 +23,12 @@ export function useCalendarDaysWeekRow(parameters: useCalendarDaysWeekRow.Parame const registerWeekRowCells = ctx.registerWeekRowCells; React.useEffect(() => { - return registerWeekRowCells(ref, calendarDayCellRefs); + return registerWeekRowCells(ref, dayCellRefs); }, [registerWeekRowCells]); return React.useMemo( - () => ({ getDaysWeekRowProps, calendarDayCellRefs }), - [getDaysWeekRowProps, calendarDayCellRefs], + () => ({ getDaysWeekRowProps, dayCellRefs }), + [getDaysWeekRowProps, dayCellRefs], ); } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/index.parts.ts b/packages/x-date-pickers/src/internals/base/Calendar/index.parts.ts index e72d5899cb0cf..2d854aebc5882 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/index.parts.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/index.parts.ts @@ -15,6 +15,7 @@ export { CalendarMonthsCell as MonthsCell } from './months-cell/CalendarMonthsCe // Years export { CalendarYearsList as YearsList } from './years-list/CalendarYearsList'; +export { CalendarYearsGrid as YearsGrid } from './years-grid/CalendarYearsGrid'; export { CalendarYearsCell as YearsCell } from './years-cell/CalendarYearsCell'; // Navigation diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx index 63e6f7f45327b..2571710de93d3 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx @@ -7,14 +7,14 @@ import { useCalendarRootContext } from '../root/CalendarRootContext'; import { useCalendarMonthsCell } from './useCalendarMonthsCell'; import { BaseUIComponentProps } from '../../utils/types'; import { useCompositeListItem } from '../../composite/list/useCompositeListItem'; -import { useCalendarMonthCellCollectionContext } from '../utils/month-cell-collection/CalendarMonthCellCollectionContext'; +import { useCalendarMonthsCellCollectionContext } from '../utils/months-cell-collection/CalendarMonthsCellCollectionContext'; const InnerCalendarMonthsCell = React.forwardRef(function InnerCalendarMonthsCell( props: InnerCalendarMonthsCellProps, forwardedRef: React.ForwardedRef, ) { const { className, render, value, format, ctx, ...otherProps } = props; - const { getMonthCellProps, isCurrent } = useCalendarMonthsCell({ value, format, ctx }); + const { getMonthsCellProps, isCurrent } = useCalendarMonthsCell({ value, format, ctx }); const state: CalendarMonthsCell.State = React.useMemo( () => ({ selected: ctx.isSelected, current: isCurrent }), @@ -22,7 +22,7 @@ const InnerCalendarMonthsCell = React.forwardRef(function InnerCalendarMonthsCel ); const { renderElement } = useComponentRenderer({ - propGetter: getMonthCellProps, + propGetter: getMonthsCellProps, render: render ?? 'button', ref: forwardedRef, className, @@ -39,38 +39,35 @@ const CalendarMonthsCell = React.forwardRef(function CalendarMonthsCell( props: CalendarMonthsCell.Props, forwardedRef: React.ForwardedRef, ) { - const calendarRootContext = useCalendarRootContext(); - const calendarMonthCellCollectionContext = useCalendarMonthCellCollectionContext(); + const rootContext = useCalendarRootContext(); + const monthsCellCollectionContext = useCalendarMonthsCellCollectionContext(); const { ref: listItemRef } = useCompositeListItem(); const utils = useUtils(); - const now = useNow(calendarRootContext.timezone); + const now = useNow(rootContext.timezone); const mergedRef = useForkRef(forwardedRef, listItemRef); const isSelected = React.useMemo( - () => - calendarRootContext.value == null - ? false - : utils.isSameMonth(calendarRootContext.value, props.value), - [calendarRootContext.value, props.value, utils], + () => (rootContext.value == null ? false : utils.isSameMonth(rootContext.value, props.value)), + [rootContext.value, props.value, utils], ); const isDisabled = React.useMemo(() => { - if (calendarRootContext.disabled) { + if (rootContext.disabled) { return true; } const firstEnabledMonth = utils.startOfMonth( - calendarRootContext.validationProps.disablePast && - utils.isAfter(now, calendarRootContext.validationProps.minDate) + rootContext.validationProps.disablePast && + utils.isAfter(now, rootContext.validationProps.minDate) ? now - : calendarRootContext.validationProps.minDate, + : rootContext.validationProps.minDate, ); const lastEnabledMonth = utils.startOfMonth( - calendarRootContext.validationProps.disableFuture && - utils.isBefore(now, calendarRootContext.validationProps.maxDate) + rootContext.validationProps.disableFuture && + utils.isBefore(now, rootContext.validationProps.maxDate) ? now - : calendarRootContext.validationProps.maxDate, + : rootContext.validationProps.maxDate, ); const monthToValidate = utils.startOfMonth(props.value); @@ -83,19 +80,19 @@ const CalendarMonthsCell = React.forwardRef(function CalendarMonthsCell( return true; } - if (!calendarRootContext.validationProps.shouldDisableMonth) { + if (!rootContext.validationProps.shouldDisableMonth) { return false; } - return calendarRootContext.validationProps.shouldDisableMonth(monthToValidate); - }, [calendarRootContext.disabled, calendarRootContext.validationProps, props.value, now, utils]); + return rootContext.validationProps.shouldDisableMonth(monthToValidate); + }, [rootContext.disabled, rootContext.validationProps, props.value, now, utils]); const isTabbable = React.useMemo( () => - utils.isValid(calendarRootContext.value) + utils.isValid(rootContext.value) ? isSelected - : utils.isSameMonth(calendarRootContext.referenceDate, props.value), - [utils, calendarRootContext.value, calendarRootContext.referenceDate, isSelected, props.value], + : utils.isSameMonth(rootContext.referenceDate, props.value), + [utils, rootContext.value, rootContext.referenceDate, isSelected, props.value], ); const ctx = React.useMemo( @@ -103,9 +100,9 @@ const CalendarMonthsCell = React.forwardRef(function CalendarMonthsCell( isSelected, isDisabled, isTabbable, - selectMonth: calendarMonthCellCollectionContext.selectMonth, + selectMonth: monthsCellCollectionContext.selectMonth, }), - [isSelected, isDisabled, isTabbable, calendarMonthCellCollectionContext.selectMonth], + [isSelected, isDisabled, isTabbable, monthsCellCollectionContext.selectMonth], ); return ; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/useCalendarMonthsCell.ts b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/useCalendarMonthsCell.ts index d7a54f723fda2..4696d4ecf1361 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/useCalendarMonthsCell.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/useCalendarMonthsCell.ts @@ -20,7 +20,7 @@ export function useCalendarMonthsCell(parameters: useCalendarMonthsCell.Paramete ctx.selectMonth(value); }); - const getMonthCellProps = React.useCallback( + const getMonthsCellProps = React.useCallback( (externalProps: GenericHTMLProps) => { return mergeReactProps(externalProps, { type: 'button' as const, @@ -36,7 +36,7 @@ export function useCalendarMonthsCell(parameters: useCalendarMonthsCell.Paramete [formattedValue, ctx.isSelected, ctx.isDisabled, ctx.isTabbable, onClick, isCurrent], ); - return React.useMemo(() => ({ getMonthCellProps, isCurrent }), [getMonthCellProps, isCurrent]); + return React.useMemo(() => ({ getMonthsCellProps, isCurrent }), [getMonthsCellProps, isCurrent]); } export namespace useCalendarMonthsCell { diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-grid/CalendarMonthsGrid.tsx b/packages/x-date-pickers/src/internals/base/Calendar/months-grid/CalendarMonthsGrid.tsx index 518d7ce62c1d3..29b22fea3074b 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-grid/CalendarMonthsGrid.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-grid/CalendarMonthsGrid.tsx @@ -4,21 +4,21 @@ import { useCalendarMonthsGrid } from './useCalendarMonthsGrid'; import { BaseUIComponentProps } from '../../utils/types'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; import { CompositeList } from '../../composite/list/CompositeList'; -import { CalendarMonthCellCollectionContext } from '../utils/month-cell-collection/CalendarMonthCellCollectionContext'; +import { CalendarMonthsCellCollectionContext } from '../utils/months-cell-collection/CalendarMonthsCellCollectionContext'; const CalendarMonthsGrid = React.forwardRef(function CalendarMonthsList( props: CalendarMonthsGrid.Props, forwardedRef: React.ForwardedRef, ) { const { className, render, children, cellsPerRow, ...otherProps } = props; - const { getMonthGridProps, context, calendarMonthsCellRefs } = useCalendarMonthsGrid({ + const { getMonthsGridProps, context, monthsCellRefs } = useCalendarMonthsGrid({ children, cellsPerRow, }); const state = React.useMemo(() => ({}), []); const { renderElement } = useComponentRenderer({ - propGetter: getMonthGridProps, + propGetter: getMonthsGridProps, render: render ?? 'div', ref: forwardedRef, className, @@ -27,9 +27,9 @@ const CalendarMonthsGrid = React.forwardRef(function CalendarMonthsList( }); return ( - - {renderElement()} - + + {renderElement()} + ); }); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-grid/useCalendarMonthsGrid.ts b/packages/x-date-pickers/src/internals/base/Calendar/months-grid/useCalendarMonthsGrid.ts index 0829f28b74651..18baa55fb32d6 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-grid/useCalendarMonthsGrid.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-grid/useCalendarMonthsGrid.ts @@ -11,26 +11,26 @@ import { NavigateInGridChangePage, PageNavigationTarget, } from '../utils/keyboardNavigation'; -import { useCalendarMonthCellCollection } from '../utils/month-cell-collection/useCalendarMonthCellCollection'; +import { useCalendarMonthsCellCollection } from '../utils/months-cell-collection/useCalendarMonthsCellCollection'; import { useCalendarRootContext } from '../root/CalendarRootContext'; import { getFirstEnabledYear, getLastEnabledYear } from '../utils/date'; export function useCalendarMonthsGrid(parameters: useCalendarMonthsGrid.Parameters) { const { children, cellsPerRow } = parameters; const utils = useUtils(); - const calendarRootContext = useCalendarRootContext(); - const calendarMonthsCellRefs = React.useRef<(HTMLElement | null)[]>([]); - const { months, context } = useCalendarMonthCellCollection(); + const rootContext = useCalendarRootContext(); + const monthsCellRefs = React.useRef<(HTMLElement | null)[]>([]); + const { months, context } = useCalendarMonthsCellCollection(); const pageNavigationTargetRef = React.useRef(null); const getCellsInCalendar = useEventCallback(() => { const grid: HTMLElement[][] = Array.from( { - length: Math.ceil(calendarMonthsCellRefs.current.length / cellsPerRow), + length: Math.ceil(monthsCellRefs.current.length / cellsPerRow), }, () => [], ); - calendarMonthsCellRefs.current.forEach((cell, index) => { + monthsCellRefs.current.forEach((cell, index) => { const rowIndex = Math.floor(index / cellsPerRow); if (cell != null) { grid[rowIndex].push(cell); @@ -49,7 +49,7 @@ export function useCalendarMonthsGrid(parameters: useCalendarMonthsGrid.Paramete applyInitialFocusInGrid({ cells, target }); }); } - }, [calendarRootContext.visibleDate, timeout, getCellsInCalendar]); + }, [rootContext.visibleDate, timeout, getCellsInCalendar]); // TODO: Add support for multiple months grids. const onKeyDown = useEventCallback((event: React.KeyboardEvent) => { @@ -57,40 +57,35 @@ export function useCalendarMonthsGrid(parameters: useCalendarMonthsGrid.Paramete // TODO: Jump over months with no valid date. if (params.direction === 'previous') { const targetDate = utils.addYears( - utils.startOfYear(calendarRootContext.visibleDate), - -calendarRootContext.yearPageSize, + utils.startOfYear(rootContext.visibleDate), + -rootContext.yearPageSize, ); - const lastYearInNewPage = utils.addYears(targetDate, calendarRootContext.yearPageSize - 1); + const lastYearInNewPage = utils.addYears(targetDate, rootContext.yearPageSize - 1); // All the years before the visible ones are fully disabled, we skip the navigation. if ( - utils.isAfter( - getFirstEnabledYear(utils, calendarRootContext.validationProps), - lastYearInNewPage, - ) + utils.isAfter(getFirstEnabledYear(utils, rootContext.validationProps), lastYearInNewPage) ) { return; } - calendarRootContext.setVisibleDate( - utils.addYears(calendarRootContext.visibleDate, -calendarRootContext.yearPageSize), + rootContext.setVisibleDate( + utils.addYears(rootContext.visibleDate, -rootContext.yearPageSize), false, ); } if (params.direction === 'next') { const targetDate = utils.addYears( - utils.startOfYear(calendarRootContext.visibleDate), - calendarRootContext.yearPageSize, + utils.startOfYear(rootContext.visibleDate), + rootContext.yearPageSize, ); // All the years after the visible ones are fully disabled, we skip the navigation. - if ( - utils.isBefore(getLastEnabledYear(utils, calendarRootContext.validationProps), targetDate) - ) { + if (utils.isBefore(getLastEnabledYear(utils, rootContext.validationProps), targetDate)) { return; } - calendarRootContext.setVisibleDate( - utils.addYears(calendarRootContext.visibleDate, calendarRootContext.yearPageSize), + rootContext.setVisibleDate( + utils.addYears(rootContext.visibleDate, rootContext.yearPageSize), false, ); } @@ -105,7 +100,7 @@ export function useCalendarMonthsGrid(parameters: useCalendarMonthsGrid.Paramete }); }); - const getMonthGridProps = React.useCallback( + const getMonthsGridProps = React.useCallback( (externalProps: GenericHTMLProps) => { return mergeReactProps(externalProps, { role: 'radiogroup', @@ -117,8 +112,8 @@ export function useCalendarMonthsGrid(parameters: useCalendarMonthsGrid.Paramete ); return React.useMemo( - () => ({ getMonthGridProps, context, calendarMonthsCellRefs }), - [getMonthGridProps, context, calendarMonthsCellRefs], + () => ({ getMonthsGridProps, context, monthsCellRefs }), + [getMonthsGridProps, context, monthsCellRefs], ); } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsList.tsx b/packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsList.tsx index a050af13dc5e1..0568c30390d69 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsList.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsList.tsx @@ -4,14 +4,14 @@ import { useCalendarMonthsList } from './useCalendarMonthsList'; import { BaseUIComponentProps } from '../../utils/types'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; import { CompositeList } from '../../composite/list/CompositeList'; -import { CalendarMonthCellCollectionContext } from '../utils/month-cell-collection/CalendarMonthCellCollectionContext'; +import { CalendarMonthsCellCollectionContext } from '../utils/months-cell-collection/CalendarMonthsCellCollectionContext'; const CalendarMonthsList = React.forwardRef(function CalendarMonthsList( props: CalendarMonthsList.Props, forwardedRef: React.ForwardedRef, ) { const { className, render, children, loop, ...otherProps } = props; - const { getMonthListProps, context, calendarMonthsCellRefs } = useCalendarMonthsList({ + const { getMonthListProps, context, monthsCellRefs } = useCalendarMonthsList({ children, loop, }); @@ -27,9 +27,9 @@ const CalendarMonthsList = React.forwardRef(function CalendarMonthsList( }); return ( - - {renderElement()} - + + {renderElement()} + ); }); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-list/useCalendarMonthsList.ts b/packages/x-date-pickers/src/internals/base/Calendar/months-list/useCalendarMonthsList.ts index d27f746798372..ba7cda0e7dc40 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-list/useCalendarMonthsList.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-list/useCalendarMonthsList.ts @@ -4,16 +4,16 @@ import { PickerValidDate } from '../../../../models'; import { GenericHTMLProps } from '../../utils/types'; import { mergeReactProps } from '../../utils/mergeReactProps'; import { navigateInList } from '../utils/keyboardNavigation'; -import { useCalendarMonthCellCollection } from '../utils/month-cell-collection/useCalendarMonthCellCollection'; +import { useCalendarMonthsCellCollection } from '../utils/months-cell-collection/useCalendarMonthsCellCollection'; export function useCalendarMonthsList(parameters: useCalendarMonthsList.Parameters) { const { children, loop = true } = parameters; - const calendarMonthsCellRefs = React.useRef<(HTMLElement | null)[]>([]); - const { months, context } = useCalendarMonthCellCollection(); + const monthsCellRefs = React.useRef<(HTMLElement | null)[]>([]); + const { months, context } = useCalendarMonthsCellCollection(); const onKeyDown = useEventCallback((event: React.KeyboardEvent) => { navigateInList({ - cells: calendarMonthsCellRefs.current, + cells: monthsCellRefs.current, event, loop, }); @@ -31,8 +31,8 @@ export function useCalendarMonthsList(parameters: useCalendarMonthsList.Paramete ); return React.useMemo( - () => ({ getMonthListProps, context, calendarMonthsCellRefs }), - [getMonthListProps, context, calendarMonthsCellRefs], + () => ({ getMonthListProps, context, monthsCellRefs }), + [getMonthListProps, context, monthsCellRefs], ); } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx index a9e84125fb760..71dc98ee55f54 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx @@ -35,44 +35,44 @@ const CalendarSetVisibleMonth = React.forwardRef(function CalendarSetVisibleMont props: CalendarSetVisibleMonth.Props, forwardedRef: React.ForwardedRef, ) { - const calendarRootContext = useCalendarRootContext(); + const rootContext = useCalendarRootContext(); const utils = useUtils(); const targetDate = React.useMemo(() => { if (props.target === 'previous') { return utils.startOfMonth( - utils.addMonths(calendarRootContext.visibleDate, -calendarRootContext.monthPageSize), + utils.addMonths(rootContext.visibleDate, -rootContext.monthPageSize), ); } - return utils.startOfMonth( - utils.addMonths(calendarRootContext.visibleDate, calendarRootContext.monthPageSize), - ); - }, [calendarRootContext.visibleDate, calendarRootContext.monthPageSize, utils, props.target]); + if (props.target === 'next') { + return utils.startOfMonth( + utils.addMonths(rootContext.visibleDate, rootContext.monthPageSize), + ); + } + + return utils.setMonth(rootContext.visibleDate, utils.getMonth(props.target)); + }, [rootContext.visibleDate, rootContext.monthPageSize, utils, props.target]); const isDisabled = React.useMemo(() => { - if (calendarRootContext.disabled) { + if (rootContext.disabled) { return true; } // TODO: Check if the logic below works correctly when multiple months are rendered at once. + const isMovingBefore = utils.isBefore(targetDate, rootContext.visibleDate); + // All the months before the visible ones are fully disabled, we skip the navigation. - if (props.target === 'previous') { - return utils.isAfter( - getFirstEnabledMonth(utils, calendarRootContext.validationProps), - targetDate, - ); + if (isMovingBefore) { + return utils.isAfter(getFirstEnabledMonth(utils, rootContext.validationProps), targetDate); } // All the months after the visible ones are fully disabled, we skip the navigation. - return utils.isBefore( - getLastEnabledMonth(utils, calendarRootContext.validationProps), - targetDate, - ); + return utils.isBefore(getLastEnabledMonth(utils, rootContext.validationProps), targetDate); }, [ - calendarRootContext.disabled, - calendarRootContext.validationProps, - props.target, + rootContext.disabled, + rootContext.validationProps, + rootContext.visibleDate, targetDate, utils, ]); @@ -81,7 +81,7 @@ const CalendarSetVisibleMonth = React.forwardRef(function CalendarSetVisibleMont if (isDisabled) { return; } - calendarRootContext.setVisibleDate(targetDate, false); + rootContext.setVisibleDate(targetDate, false); }); const ctx = React.useMemo( diff --git a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/useCalendarSetVisibleMonth.ts b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/useCalendarSetVisibleMonth.ts index 7ff31f2be0907..9074dc760258a 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/useCalendarSetVisibleMonth.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/useCalendarSetVisibleMonth.ts @@ -1,4 +1,5 @@ import * as React from 'react'; +import { PickerValidDate } from '../../../../models'; import { GenericHTMLProps } from '../../utils/types'; import { mergeReactProps } from '../../utils/mergeReactProps'; @@ -24,7 +25,7 @@ export namespace useCalendarSetVisibleMonth { /** * The month to navigate to. */ - target: 'previous' | 'next'; + target: 'previous' | 'next' | PickerValidDate; ctx: Context; } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx index b6ec59c71f24a..e74a4ad955de8 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx @@ -35,40 +35,39 @@ const CalendarSetVisibleYear = React.forwardRef(function CalendarSetVisibleYear( props: CalendarSetVisibleYear.Props, forwardedRef: React.ForwardedRef, ) { - const calendarRootContext = useCalendarRootContext(); + const rootContext = useCalendarRootContext(); const utils = useUtils(); const targetDate = React.useMemo(() => { if (props.target === 'previous') { - return utils.startOfMonth(utils.addYears(calendarRootContext.visibleDate, -1)); + return utils.startOfYear(utils.addYears(rootContext.visibleDate, -1)); } - return utils.startOfMonth(utils.addYears(calendarRootContext.visibleDate, 1)); - }, [calendarRootContext.visibleDate, utils, props.target]); + if (props.target === 'next') { + return utils.startOfYear(utils.addYears(rootContext.visibleDate, 1)); + } + + return utils.setYear(rootContext.visibleDate, utils.getYear(props.target)); + }, [rootContext.visibleDate, utils, props.target]); const isDisabled = React.useMemo(() => { - if (calendarRootContext.disabled) { + if (rootContext.disabled) { return true; } - // TODO: Check if the logic below works correctly when multiple months are rendered at once. - // All the months before the visible ones are fully disabled, we skip the navigation. - if (props.target === 'previous') { - return utils.isAfter( - getFirstEnabledYear(utils, calendarRootContext.validationProps), - targetDate, - ); + const isMovingBefore = utils.isBefore(targetDate, rootContext.visibleDate); + + // All the years before the visible ones are fully disabled, we skip the navigation. + if (isMovingBefore) { + return utils.isAfter(getFirstEnabledYear(utils, rootContext.validationProps), targetDate); } - // All the months after the visible ones are fully disabled, we skip the navigation. - return utils.isBefore( - getLastEnabledYear(utils, calendarRootContext.validationProps), - targetDate, - ); + // All the years after the visible ones are fully disabled, we skip the navigation. + return utils.isBefore(getLastEnabledYear(utils, rootContext.validationProps), targetDate); }, [ - calendarRootContext.disabled, - calendarRootContext.validationProps, - props.target, + rootContext.disabled, + rootContext.validationProps, + rootContext.visibleDate, targetDate, utils, ]); @@ -77,7 +76,7 @@ const CalendarSetVisibleYear = React.forwardRef(function CalendarSetVisibleYear( if (isDisabled) { return; } - calendarRootContext.setVisibleDate(targetDate, false); + rootContext.setVisibleDate(targetDate, false); }); const ctx = React.useMemo( diff --git a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/useCalendarSetVisibleYear.ts b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/useCalendarSetVisibleYear.ts index 0b8cf6e23178e..4ffb4409cbd8d 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/useCalendarSetVisibleYear.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/useCalendarSetVisibleYear.ts @@ -1,4 +1,5 @@ import * as React from 'react'; +import { PickerValidDate } from '../../../../models'; import { GenericHTMLProps } from '../../utils/types'; import { mergeReactProps } from '../../utils/mergeReactProps'; @@ -22,9 +23,9 @@ export function useCalendarSetVisibleYear(parameters: useCalendarSetVisibleYear. export namespace useCalendarSetVisibleYear { export interface Parameters { /** - * The month to navigate to. + * The year to navigate to. */ - target: 'previous' | 'next'; + target: 'previous' | 'next' | PickerValidDate; ctx: Context; } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/useCalendarContext/useCalendarContext.ts b/packages/x-date-pickers/src/internals/base/Calendar/useCalendarContext/useCalendarContext.ts index 162f46cb27c7e..4a6ed3b23891e 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/useCalendarContext/useCalendarContext.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/useCalendarContext/useCalendarContext.ts @@ -2,9 +2,9 @@ import { useCalendarRootContext } from '../root/CalendarRootContext'; // TODO: Use a dedicated context export function useCalendarContext() { - const calendarRootContext = useCalendarRootContext(); + const rootContext = useCalendarRootContext(); return { - visibleDate: calendarRootContext.visibleDate, + visibleDate: rootContext.visibleDate, }; } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/month-cell-collection/CalendarMonthCellCollectionContext.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/month-cell-collection/CalendarMonthCellCollectionContext.ts deleted file mode 100644 index 1e828ca11ba18..0000000000000 --- a/packages/x-date-pickers/src/internals/base/Calendar/utils/month-cell-collection/CalendarMonthCellCollectionContext.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as React from 'react'; -import { PickerValidDate } from '../../../../../models'; - -export interface CalendarMonthCellCollectionContext { - selectMonth: (value: PickerValidDate) => void; -} - -export const CalendarMonthCellCollectionContext = React.createContext< - CalendarMonthCellCollectionContext | undefined ->(undefined); - -if (process.env.NODE_ENV !== 'production') { - CalendarMonthCellCollectionContext.displayName = 'CalendarMonthCellCollectionContext'; -} - -export function useCalendarMonthCellCollectionContext() { - const context = React.useContext(CalendarMonthCellCollectionContext); - if (context === undefined) { - throw new Error( - 'Base UI X: CalendarMonthCellCollectionContext is missing. Calendar Month parts must be placed within or ``.', - ); - } - return context; -} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/month-cell-collection/useCalendarMonthCellCollection.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/month-cell-collection/useCalendarMonthCellCollection.ts deleted file mode 100644 index 25db60cd98d23..0000000000000 --- a/packages/x-date-pickers/src/internals/base/Calendar/utils/month-cell-collection/useCalendarMonthCellCollection.ts +++ /dev/null @@ -1,74 +0,0 @@ -import * as React from 'react'; -import useEventCallback from '@mui/utils/useEventCallback'; -import { PickerValidDate } from '../../../../../models'; -import { findClosestEnabledDate, getMonthsInYear } from '../../../../utils/date-utils'; -import { useUtils } from '../../../../hooks/useUtils'; -import { useCalendarRootContext } from '../../root/CalendarRootContext'; -import { CalendarMonthCellCollectionContext } from './CalendarMonthCellCollectionContext'; - -export function useCalendarMonthCellCollection(): useCalendarMonthCellCollection.ReturnValue { - const calendarRootContext = useCalendarRootContext(); - const utils = useUtils(); - - const currentYear = React.useMemo( - () => utils.startOfYear(calendarRootContext.visibleDate), - [utils, calendarRootContext.visibleDate], - ); - - const months = React.useMemo(() => getMonthsInYear(utils, currentYear), [utils, currentYear]); - - const selectMonth = useEventCallback((newValue: PickerValidDate) => { - if (calendarRootContext.readOnly) { - return; - } - - const newCleanValue = utils.setMonth( - calendarRootContext.value ?? calendarRootContext.referenceDate, - utils.getMonth(newValue), - ); - - const startOfMonth = utils.startOfMonth(newCleanValue); - const endOfMonth = utils.endOfMonth(newCleanValue); - - const closestEnabledDate = calendarRootContext.isDateDisabled(newCleanValue) - ? findClosestEnabledDate({ - utils, - date: newCleanValue, - minDate: utils.isBefore(calendarRootContext.validationProps.minDate, startOfMonth) - ? startOfMonth - : calendarRootContext.validationProps.minDate, - maxDate: utils.isAfter(calendarRootContext.validationProps.maxDate, endOfMonth) - ? endOfMonth - : calendarRootContext.validationProps.maxDate, - disablePast: calendarRootContext.validationProps.disablePast, - disableFuture: calendarRootContext.validationProps.disableFuture, - isDateDisabled: calendarRootContext.isDateDisabled, - timezone: calendarRootContext.timezone, - }) - : newCleanValue; - - if (closestEnabledDate) { - calendarRootContext.setVisibleDate(closestEnabledDate, true); - calendarRootContext.setValue(closestEnabledDate, { section: 'month' }); - } - }); - - const registerSection = calendarRootContext.registerSection; - React.useEffect(() => { - return registerSection({ type: 'month', value: currentYear }); - }, [registerSection, currentYear]); - - const context: CalendarMonthCellCollectionContext = React.useMemo( - () => ({ selectMonth }), - [selectMonth], - ); - - return { months, context }; -} - -export namespace useCalendarMonthCellCollection { - export interface ReturnValue { - months: PickerValidDate[]; - context: CalendarMonthCellCollectionContext; - } -} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/months-cell-collection/CalendarMonthsCellCollectionContext.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/months-cell-collection/CalendarMonthsCellCollectionContext.ts new file mode 100644 index 0000000000000..06e2273f27bb4 --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/Calendar/utils/months-cell-collection/CalendarMonthsCellCollectionContext.ts @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { PickerValidDate } from '../../../../../models'; + +export interface CalendarMonthsCellCollectionContext { + selectMonth: (value: PickerValidDate) => void; +} + +export const CalendarMonthsCellCollectionContext = React.createContext< + CalendarMonthsCellCollectionContext | undefined +>(undefined); + +if (process.env.NODE_ENV !== 'production') { + CalendarMonthsCellCollectionContext.displayName = 'CalendarMonthsCellCollectionContext'; +} + +export function useCalendarMonthsCellCollectionContext() { + const context = React.useContext(CalendarMonthsCellCollectionContext); + if (context === undefined) { + throw new Error( + 'Base UI X: CalendarMonthsCellCollectionContext is missing. Calendar Month parts must be placed within or ``.', + ); + } + return context; +} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/months-cell-collection/useCalendarMonthsCellCollection.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/months-cell-collection/useCalendarMonthsCellCollection.ts new file mode 100644 index 0000000000000..09ee18a661489 --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/Calendar/utils/months-cell-collection/useCalendarMonthsCellCollection.ts @@ -0,0 +1,74 @@ +import * as React from 'react'; +import useEventCallback from '@mui/utils/useEventCallback'; +import { PickerValidDate } from '../../../../../models'; +import { findClosestEnabledDate, getMonthsInYear } from '../../../../utils/date-utils'; +import { useUtils } from '../../../../hooks/useUtils'; +import { useCalendarRootContext } from '../../root/CalendarRootContext'; +import { CalendarMonthsCellCollectionContext } from './CalendarMonthsCellCollectionContext'; + +export function useCalendarMonthsCellCollection(): useCalendarMonthsCellCollection.ReturnValue { + const rootContext = useCalendarRootContext(); + const utils = useUtils(); + + const currentYear = React.useMemo( + () => utils.startOfYear(rootContext.visibleDate), + [utils, rootContext.visibleDate], + ); + + const months = React.useMemo(() => getMonthsInYear(utils, currentYear), [utils, currentYear]); + + const selectMonth = useEventCallback((newValue: PickerValidDate) => { + if (rootContext.readOnly) { + return; + } + + const newCleanValue = utils.setMonth( + rootContext.value ?? rootContext.referenceDate, + utils.getMonth(newValue), + ); + + const startOfMonth = utils.startOfMonth(newCleanValue); + const endOfMonth = utils.endOfMonth(newCleanValue); + + const closestEnabledDate = rootContext.isDateDisabled(newCleanValue) + ? findClosestEnabledDate({ + utils, + date: newCleanValue, + minDate: utils.isBefore(rootContext.validationProps.minDate, startOfMonth) + ? startOfMonth + : rootContext.validationProps.minDate, + maxDate: utils.isAfter(rootContext.validationProps.maxDate, endOfMonth) + ? endOfMonth + : rootContext.validationProps.maxDate, + disablePast: rootContext.validationProps.disablePast, + disableFuture: rootContext.validationProps.disableFuture, + isDateDisabled: rootContext.isDateDisabled, + timezone: rootContext.timezone, + }) + : newCleanValue; + + if (closestEnabledDate) { + rootContext.setVisibleDate(closestEnabledDate, true); + rootContext.setValue(closestEnabledDate, { section: 'month' }); + } + }); + + const registerSection = rootContext.registerSection; + React.useEffect(() => { + return registerSection({ type: 'month', value: currentYear }); + }, [registerSection, currentYear]); + + const context: CalendarMonthsCellCollectionContext = React.useMemo( + () => ({ selectMonth }), + [selectMonth], + ); + + return { months, context }; +} + +export namespace useCalendarMonthsCellCollection { + export interface ReturnValue { + months: PickerValidDate[]; + context: CalendarMonthsCellCollectionContext; + } +} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/years-cell-collection/CalendarYearsCellCollectionContext.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/years-cell-collection/CalendarYearsCellCollectionContext.ts new file mode 100644 index 0000000000000..d336f175fabc9 --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/Calendar/utils/years-cell-collection/CalendarYearsCellCollectionContext.ts @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { PickerValidDate } from '../../../../../models'; + +export interface CalendarYearsCellCollectionContext { + selectYear: (value: PickerValidDate) => void; +} + +export const CalendarYearsCellCollectionContext = React.createContext< + CalendarYearsCellCollectionContext | undefined +>(undefined); + +if (process.env.NODE_ENV !== 'production') { + CalendarYearsCellCollectionContext.displayName = 'CalendarYearsCellCollectionContext'; +} + +export function useCalendarYearsCellCollectionContext() { + const context = React.useContext(CalendarYearsCellCollectionContext); + if (context === undefined) { + throw new Error( + 'Base UI X: CalendarYearsCellCollectionContext is missing. Calendar Year parts must be placed within or ``.', + ); + } + return context; +} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/years-cell-collection/useCalendarYearsCellCollection.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/years-cell-collection/useCalendarYearsCellCollection.ts new file mode 100644 index 0000000000000..796f0cb0f1350 --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/Calendar/utils/years-cell-collection/useCalendarYearsCellCollection.ts @@ -0,0 +1,75 @@ +import * as React from 'react'; +import useEventCallback from '@mui/utils/useEventCallback'; +import { PickerValidDate } from '../../../../../models'; +import { findClosestEnabledDate } from '../../../../utils/date-utils'; +import { useUtils } from '../../../../hooks/useUtils'; +import { useCalendarRootContext } from '../../root/CalendarRootContext'; +import { CalendarYearsCellCollectionContext } from './CalendarYearsCellCollectionContext'; + +export function useCalendarYearsCellCollection(): useCalendarYearsCellCollection.ReturnValue { + const rootContext = useCalendarRootContext(); + const utils = useUtils(); + + const years = React.useMemo( + () => + utils.getYearRange([ + rootContext.validationProps.minDate, + rootContext.validationProps.maxDate, + ]), + [utils, rootContext.validationProps.minDate, rootContext.validationProps.maxDate], + ); + + const selectYear = useEventCallback((newValue: PickerValidDate) => { + if (rootContext.readOnly) { + return; + } + + const newCleanValue = utils.setYear( + rootContext.value ?? rootContext.referenceDate, + utils.getYear(newValue), + ); + + const startOfYear = utils.startOfYear(newCleanValue); + const endOfYear = utils.endOfYear(newCleanValue); + + const closestEnabledDate = rootContext.isDateDisabled(newCleanValue) + ? findClosestEnabledDate({ + utils, + date: newCleanValue, + minDate: utils.isBefore(rootContext.validationProps.minDate, startOfYear) + ? startOfYear + : rootContext.validationProps.minDate, + maxDate: utils.isAfter(rootContext.validationProps.maxDate, endOfYear) + ? endOfYear + : rootContext.validationProps.maxDate, + disablePast: rootContext.validationProps.disablePast, + disableFuture: rootContext.validationProps.disableFuture, + isDateDisabled: rootContext.isDateDisabled, + timezone: rootContext.timezone, + }) + : newCleanValue; + + if (closestEnabledDate) { + rootContext.setValue(closestEnabledDate, { section: 'year' }); + } + }); + + const registerSection = rootContext.registerSection; + React.useEffect(() => { + return registerSection({ type: 'month', value: rootContext.visibleDate }); + }, [registerSection, rootContext.visibleDate]); + + const context: CalendarYearsCellCollectionContext = React.useMemo( + () => ({ selectYear }), + [selectYear], + ); + + return { years, context }; +} + +export namespace useCalendarYearsCellCollection { + export interface ReturnValue { + years: PickerValidDate[]; + context: CalendarYearsCellCollectionContext; + } +} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx index 8e2c6fb8603ed..7144b43f7e5b4 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx @@ -7,7 +7,7 @@ import { useCalendarRootContext } from '../root/CalendarRootContext'; import { useCalendarYearsCell } from './useCalendarYearsCell'; import { BaseUIComponentProps } from '../../utils/types'; import { useCompositeListItem } from '../../composite/list/useCompositeListItem'; -import { useCalendarYearsListContext } from '../years-list/CalendarYearsListContext'; +import { useCalendarYearsCellCollectionContext } from '../utils/years-cell-collection/CalendarYearsCellCollectionContext'; const InnerCalendarYearsCell = React.forwardRef(function InnerCalendarYearsCell( props: InnerCalendarYearsCellProps, @@ -39,59 +39,56 @@ const CalendarYearsCell = React.forwardRef(function CalendarsYearCell( props: CalendarYearsCell.Props, forwardedRef: React.ForwardedRef, ) { - const calendarRootContext = useCalendarRootContext(); - const calendarYearsListContext = useCalendarYearsListContext(); + const rootContext = useCalendarRootContext(); + const yearsListContext = useCalendarYearsCellCollectionContext(); const { ref: listItemRef } = useCompositeListItem(); const utils = useUtils(); - const now = useNow(calendarRootContext.timezone); + const now = useNow(rootContext.timezone); const mergedRef = useForkRef(forwardedRef, listItemRef); const isSelected = React.useMemo( - () => - calendarRootContext.value == null - ? false - : utils.isSameYear(calendarRootContext.value, props.value), - [calendarRootContext.value, props.value, utils], + () => (rootContext.value == null ? false : utils.isSameYear(rootContext.value, props.value)), + [rootContext.value, props.value, utils], ); const isDisabled = React.useMemo(() => { - if (calendarRootContext.disabled) { + if (rootContext.disabled) { return true; } - if (calendarRootContext.validationProps.disablePast && utils.isBeforeYear(props.value, now)) { + if (rootContext.validationProps.disablePast && utils.isBeforeYear(props.value, now)) { return true; } - if (calendarRootContext.validationProps.disableFuture && utils.isAfterYear(props.value, now)) { + if (rootContext.validationProps.disableFuture && utils.isAfterYear(props.value, now)) { return true; } if ( - calendarRootContext.validationProps.minDate && - utils.isBeforeYear(props.value, calendarRootContext.validationProps.minDate) + rootContext.validationProps.minDate && + utils.isBeforeYear(props.value, rootContext.validationProps.minDate) ) { return true; } if ( - calendarRootContext.validationProps.maxDate && - utils.isAfterYear(props.value, calendarRootContext.validationProps.maxDate) + rootContext.validationProps.maxDate && + utils.isAfterYear(props.value, rootContext.validationProps.maxDate) ) { return true; } - if (!calendarRootContext.validationProps.shouldDisableYear) { + if (!rootContext.validationProps.shouldDisableYear) { return false; } const yearToValidate = utils.startOfYear(props.value); - return calendarRootContext.validationProps.shouldDisableYear(yearToValidate); - }, [calendarRootContext.disabled, calendarRootContext.validationProps, props.value, now, utils]); + return rootContext.validationProps.shouldDisableYear(yearToValidate); + }, [rootContext.disabled, rootContext.validationProps, props.value, now, utils]); const isTabbable = React.useMemo( () => - utils.isValid(calendarRootContext.value) + utils.isValid(rootContext.value) ? isSelected - : utils.isSameYear(calendarRootContext.referenceDate, props.value), - [utils, calendarRootContext.value, calendarRootContext.referenceDate, isSelected, props.value], + : utils.isSameYear(rootContext.referenceDate, props.value), + [utils, rootContext.value, rootContext.referenceDate, isSelected, props.value], ); const ctx = React.useMemo( @@ -99,9 +96,9 @@ const CalendarYearsCell = React.forwardRef(function CalendarsYearCell( isSelected, isDisabled, isTabbable, - selectYear: calendarYearsListContext.selectYear, + selectYear: yearsListContext.selectYear, }), - [isSelected, isDisabled, isTabbable, calendarYearsListContext.selectYear], + [isSelected, isDisabled, isTabbable, yearsListContext.selectYear], ); return ; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-grid/CalendarYearsGrid.tsx b/packages/x-date-pickers/src/internals/base/Calendar/years-grid/CalendarYearsGrid.tsx new file mode 100644 index 0000000000000..869cb56c13b51 --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-grid/CalendarYearsGrid.tsx @@ -0,0 +1,44 @@ +'use client'; +import * as React from 'react'; +import { useCalendarYearsGrid } from './useCalendarYearsGrid'; +import { BaseUIComponentProps } from '../../utils/types'; +import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import { CompositeList } from '../../composite/list/CompositeList'; +import { CalendarYearsCellCollectionContext } from '../utils/years-cell-collection/CalendarYearsCellCollectionContext'; + +const CalendarYearsGrid = React.forwardRef(function CalendarYearsList( + props: CalendarYearsGrid.Props, + forwardedRef: React.ForwardedRef, +) { + const { className, render, children, cellsPerRow, ...otherProps } = props; + const { getYearsGridProps, context, yearsCellRefs } = useCalendarYearsGrid({ + children, + cellsPerRow, + }); + const state = React.useMemo(() => ({}), []); + + const { renderElement } = useComponentRenderer({ + propGetter: getYearsGridProps, + render: render ?? 'div', + ref: forwardedRef, + className, + state, + extraProps: otherProps, + }); + + return ( + + {renderElement()} + + ); +}); + +export namespace CalendarYearsGrid { + export interface State {} + + export interface Props + extends Omit, 'children'>, + useCalendarYearsGrid.Parameters {} +} + +export { CalendarYearsGrid }; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-grid/useCalendarYearsGrid.ts b/packages/x-date-pickers/src/internals/base/Calendar/years-grid/useCalendarYearsGrid.ts new file mode 100644 index 0000000000000..3daf436474283 --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-grid/useCalendarYearsGrid.ts @@ -0,0 +1,70 @@ +import * as React from 'react'; +import useEventCallback from '@mui/utils/useEventCallback'; +import { PickerValidDate } from '../../../../models'; +import { GenericHTMLProps } from '../../utils/types'; +import { mergeReactProps } from '../../utils/mergeReactProps'; +import { navigateInGrid } from '../utils/keyboardNavigation'; +import { useCalendarYearsCellCollection } from '../utils/years-cell-collection/useCalendarYearsCellCollection'; + +export function useCalendarYearsGrid(parameters: useCalendarYearsGrid.Parameters) { + const { children, cellsPerRow } = parameters; + const yearsCellRefs = React.useRef<(HTMLElement | null)[]>([]); + const { years, context } = useCalendarYearsCellCollection(); + + const getCellsInCalendar = useEventCallback(() => { + const grid: HTMLElement[][] = Array.from( + { + length: Math.ceil(yearsCellRefs.current.length / cellsPerRow), + }, + () => [], + ); + yearsCellRefs.current.forEach((cell, index) => { + const rowIndex = Math.floor(index / cellsPerRow); + if (cell != null) { + grid[rowIndex].push(cell); + } + }); + + return [grid]; + }); + + // TODO: Add support for multiple years grids. + const onKeyDown = useEventCallback((event: React.KeyboardEvent) => { + navigateInGrid({ + cells: getCellsInCalendar(), + event, + changePage: undefined, + }); + }); + + const getYearsGridProps = React.useCallback( + (externalProps: GenericHTMLProps) => { + return mergeReactProps(externalProps, { + role: 'radiogroup', + children: children == null ? null : children({ years }), + onKeyDown, + }); + }, + [years, children, onKeyDown], + ); + + return React.useMemo( + () => ({ getYearsGridProps, context, yearsCellRefs }), + [getYearsGridProps, context, yearsCellRefs], + ); +} + +export namespace useCalendarYearsGrid { + export interface Parameters { + /** + * Cells rendered per row. + * This is used to make sure the keyboard navigation works correctly. + */ + cellsPerRow: number; + children?: (parameters: ChildrenParameters) => React.ReactNode; + } + + export interface ChildrenParameters { + years: PickerValidDate[]; + } +} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-list/CalendarYearsList.tsx b/packages/x-date-pickers/src/internals/base/Calendar/years-list/CalendarYearsList.tsx index ba7f6f0237f09..b2538e0ec16f5 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-list/CalendarYearsList.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-list/CalendarYearsList.tsx @@ -4,21 +4,21 @@ import { useCalendarYearsList } from './useCalendarYearsList'; import { BaseUIComponentProps } from '../../utils/types'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; import { CompositeList } from '../../composite/list/CompositeList'; -import { CalendarYearsListContext } from './CalendarYearsListContext'; +import { CalendarYearsCellCollectionContext } from '../utils/years-cell-collection/CalendarYearsCellCollectionContext'; const CalendarYearsList = React.forwardRef(function CalendarYearsList( props: CalendarYearsList.Props, forwardedRef: React.ForwardedRef, ) { const { className, render, children, loop, ...otherProps } = props; - const { getYearListProps, context, calendarYearsCellRefs } = useCalendarYearsList({ + const { getYearsListProps, context, yearsCellRefs } = useCalendarYearsList({ children, loop, }); const state = React.useMemo(() => ({}), []); const { renderElement } = useComponentRenderer({ - propGetter: getYearListProps, + propGetter: getYearsListProps, render: render ?? 'div', ref: forwardedRef, className, @@ -27,9 +27,9 @@ const CalendarYearsList = React.forwardRef(function CalendarYearsList( }); return ( - - {renderElement()} - + + {renderElement()} + ); }); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-list/CalendarYearsListContext.ts b/packages/x-date-pickers/src/internals/base/Calendar/years-list/CalendarYearsListContext.ts deleted file mode 100644 index 852f7e83a0d75..0000000000000 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-list/CalendarYearsListContext.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as React from 'react'; -import { PickerValidDate } from '../../../../models'; - -export interface CalendarYearsListContext { - selectYear: (value: PickerValidDate) => void; -} - -export const CalendarYearsListContext = React.createContext( - undefined, -); - -if (process.env.NODE_ENV !== 'production') { - CalendarYearsListContext.displayName = 'CalendarYearsListContext'; -} - -export function useCalendarYearsListContext() { - const context = React.useContext(CalendarYearsListContext); - if (context === undefined) { - throw new Error( - 'Base UI X: CalendarYearsListContext is missing. Calendar Year List parts must be placed within .', - ); - } - return context; -} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-list/useCalendarYearsList.ts b/packages/x-date-pickers/src/internals/base/Calendar/years-list/useCalendarYearsList.ts index cf39c01b527c0..216dba029f4fd 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-list/useCalendarYearsList.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-list/useCalendarYearsList.ts @@ -1,42 +1,25 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; import { PickerValidDate } from '../../../../models'; -import { findClosestEnabledDate } from '../../../utils/date-utils'; -import { useUtils } from '../../../hooks/useUtils'; -import { useCalendarRootContext } from '../root/CalendarRootContext'; import { GenericHTMLProps } from '../../utils/types'; import { mergeReactProps } from '../../utils/mergeReactProps'; -import { CalendarYearsListContext } from './CalendarYearsListContext'; import { navigateInList } from '../utils/keyboardNavigation'; +import { useCalendarYearsCellCollection } from '../utils/years-cell-collection/useCalendarYearsCellCollection'; export function useCalendarYearsList(parameters: useCalendarYearsList.Parameters) { const { children, loop = true } = parameters; - const utils = useUtils(); - const calendarRootContext = useCalendarRootContext(); - const calendarYearsCellRefs = React.useRef<(HTMLElement | null)[]>([]); - - const years = React.useMemo( - () => - utils.getYearRange([ - calendarRootContext.validationProps.minDate, - calendarRootContext.validationProps.maxDate, - ]), - [ - utils, - calendarRootContext.validationProps.minDate, - calendarRootContext.validationProps.maxDate, - ], - ); + const yearsCellRefs = React.useRef<(HTMLElement | null)[]>([]); + const { years, context } = useCalendarYearsCellCollection(); const onKeyDown = useEventCallback((event: React.KeyboardEvent) => { navigateInList({ - cells: calendarYearsCellRefs.current, + cells: yearsCellRefs.current, event, loop, }); }); - const getYearListProps = React.useCallback( + const getYearsListProps = React.useCallback( (externalProps: GenericHTMLProps) => { return mergeReactProps(externalProps, { role: 'radiogroup', @@ -47,51 +30,9 @@ export function useCalendarYearsList(parameters: useCalendarYearsList.Parameters [years, children, onKeyDown], ); - const selectYear = useEventCallback((newValue: PickerValidDate) => { - if (calendarRootContext.readOnly) { - return; - } - - const newCleanValue = utils.setYear( - calendarRootContext.value ?? calendarRootContext.referenceDate, - utils.getYear(newValue), - ); - - const startOfYear = utils.startOfYear(newCleanValue); - const endOfYear = utils.endOfYear(newCleanValue); - - const closestEnabledDate = calendarRootContext.isDateDisabled(newCleanValue) - ? findClosestEnabledDate({ - utils, - date: newCleanValue, - minDate: utils.isBefore(calendarRootContext.validationProps.minDate, startOfYear) - ? startOfYear - : calendarRootContext.validationProps.minDate, - maxDate: utils.isAfter(calendarRootContext.validationProps.maxDate, endOfYear) - ? endOfYear - : calendarRootContext.validationProps.maxDate, - disablePast: calendarRootContext.validationProps.disablePast, - disableFuture: calendarRootContext.validationProps.disableFuture, - isDateDisabled: calendarRootContext.isDateDisabled, - timezone: calendarRootContext.timezone, - }) - : newCleanValue; - - if (closestEnabledDate) { - calendarRootContext.setValue(closestEnabledDate, { section: 'year' }); - } - }); - - const registerSection = calendarRootContext.registerSection; - React.useEffect(() => { - return registerSection({ type: 'month', value: calendarRootContext.visibleDate }); - }, [registerSection, calendarRootContext.visibleDate]); - - const context: CalendarYearsListContext = React.useMemo(() => ({ selectYear }), [selectYear]); - return React.useMemo( - () => ({ getYearListProps, context, calendarYearsCellRefs }), - [getYearListProps, context, calendarYearsCellRefs], + () => ({ getYearsListProps, context, yearsCellRefs }), + [getYearsListProps, context, yearsCellRefs], ); } From 3e38cba08d147bf06e07e217e1621613ec3dc6a0 Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 6 Jan 2025 18:44:18 +0100 Subject: [PATCH 060/136] Improve demos --- .../base-calendar/calendar.module.css | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/data/date-pickers/base-calendar/calendar.module.css b/docs/data/date-pickers/base-calendar/calendar.module.css index 6cb51fff59b5e..510efececf827 100644 --- a/docs/data/date-pickers/base-calendar/calendar.module.css +++ b/docs/data/date-pickers/base-calendar/calendar.module.css @@ -163,12 +163,14 @@ } &[data-outsidemonth] { - opacity: 0.3; + color: #cbd5e1; pointer-events: none; } - &:disabled { + &:not([data-outsidemonth]):disabled { pointer-events: none; + text-decoration: line-through; + color: #64748b; } } @@ -223,6 +225,15 @@ &:not([data-outsidemonth])[data-selected] { background-color: #0369a1; } + + &[data-outsidemonth] { + color: #334155; + pointer-events: none; + } + + &:not([data-outsidemonth]):disabled { + color: #64748b; + } } :global(.mode-dark) From cfe40cf64c32ee4065aa2de66f8556dcefce5bd0 Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 6 Jan 2025 18:49:48 +0100 Subject: [PATCH 061/136] Fix --- .../date-pickers/base-calendar/DateCalendarMD2Demo.tsx | 4 ++-- docs/data/date-pickers/base-calendar/calendar.module.css | 5 +++++ .../set-visible-month/CalendarSetVisibleMonth.tsx | 8 ++------ .../Calendar/set-visible-year/CalendarSetVisibleYear.tsx | 4 ++-- .../base/Calendar/years-grid/CalendarYearsGrid.tsx | 6 ++++-- 5 files changed, 15 insertions(+), 12 deletions(-) diff --git a/docs/data/date-pickers/base-calendar/DateCalendarMD2Demo.tsx b/docs/data/date-pickers/base-calendar/DateCalendarMD2Demo.tsx index ff6bc46b5f6aa..e056b77865932 100644 --- a/docs/data/date-pickers/base-calendar/DateCalendarMD2Demo.tsx +++ b/docs/data/date-pickers/base-calendar/DateCalendarMD2Demo.tsx @@ -76,7 +76,7 @@ export default function DateCalendarMD2Demo() { onActiveSectionChange={setActiveSection} /> {activeSection === 'year' && ( - + {({ years }) => years.map((year) => ( )) } - + )} {activeSection === 'day' && ( diff --git a/docs/data/date-pickers/base-calendar/calendar.module.css b/docs/data/date-pickers/base-calendar/calendar.module.css index 510efececf827..29151ca37d888 100644 --- a/docs/data/date-pickers/base-calendar/calendar.module.css +++ b/docs/data/date-pickers/base-calendar/calendar.module.css @@ -104,6 +104,11 @@ column-gap: 4px; row-gap: 12px; overflow-y: auto; + + /* TODO: Use CSS variable instead */ + &[data-cellsperrow='3'] { + grid-template-columns: repeat(3, 1fr); + } } .MonthsCell, diff --git a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx index 71dc98ee55f54..7411761632dd2 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx @@ -40,15 +40,11 @@ const CalendarSetVisibleMonth = React.forwardRef(function CalendarSetVisibleMont const targetDate = React.useMemo(() => { if (props.target === 'previous') { - return utils.startOfMonth( - utils.addMonths(rootContext.visibleDate, -rootContext.monthPageSize), - ); + return utils.addMonths(rootContext.visibleDate, -rootContext.monthPageSize); } if (props.target === 'next') { - return utils.startOfMonth( - utils.addMonths(rootContext.visibleDate, rootContext.monthPageSize), - ); + return utils.addMonths(rootContext.visibleDate, rootContext.monthPageSize); } return utils.setMonth(rootContext.visibleDate, utils.getMonth(props.target)); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx index e74a4ad955de8..db12a50b9b264 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx @@ -40,11 +40,11 @@ const CalendarSetVisibleYear = React.forwardRef(function CalendarSetVisibleYear( const targetDate = React.useMemo(() => { if (props.target === 'previous') { - return utils.startOfYear(utils.addYears(rootContext.visibleDate, -1)); + return utils.addYears(rootContext.visibleDate, -1); } if (props.target === 'next') { - return utils.startOfYear(utils.addYears(rootContext.visibleDate, 1)); + return utils.addYears(rootContext.visibleDate, 1); } return utils.setYear(rootContext.visibleDate, utils.getYear(props.target)); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-grid/CalendarYearsGrid.tsx b/packages/x-date-pickers/src/internals/base/Calendar/years-grid/CalendarYearsGrid.tsx index 869cb56c13b51..8c6c464540062 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-grid/CalendarYearsGrid.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-grid/CalendarYearsGrid.tsx @@ -15,7 +15,7 @@ const CalendarYearsGrid = React.forwardRef(function CalendarYearsList( children, cellsPerRow, }); - const state = React.useMemo(() => ({}), []); + const state = React.useMemo(() => ({ cellsPerRow }), [cellsPerRow]); const { renderElement } = useComponentRenderer({ propGetter: getYearsGridProps, @@ -34,7 +34,9 @@ const CalendarYearsGrid = React.forwardRef(function CalendarYearsList( }); export namespace CalendarYearsGrid { - export interface State {} + export interface State { + cellsPerRow: number; + } export interface Props extends Omit, 'children'>, From 6564b18176c7ea1bcc4c9abfcd00a12dea21a66e Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 6 Jan 2025 21:59:44 +0100 Subject: [PATCH 062/136] Add data attrs and css vars files --- .../base-calendar/YearCalendarDemo.tsx | 2 +- .../date-pickers/base-calendar/calendar.module.css | 12 +++++++----- .../days-cell/CalendarDaysCellDataAttributes.ts | 14 ++++++++++++++ .../CalendarDaysGridBodyDataAttributes.ts | 1 + .../CalendarDaysGridHeaderCellDataAttributes.ts | 1 + .../CalendarDaysGridHeaderDataAttributes.ts | 1 + .../days-grid/CalendarDaysGridDataAttributes.ts | 1 + .../CalendarDaysWeekRowDataAttributes.ts | 1 + .../CalendarMonthsCellDataAttributes.ts | 10 ++++++++++ .../months-grid/CalendarMonthsGridCssVars.ts | 6 ++++++ .../CalendarMonthsGridDataAttributes.ts | 1 + .../Calendar/months-grid/useCalendarMonthsGrid.ts | 8 ++++++-- .../CalendarMonthsListDataAttributes.ts | 1 + .../Calendar/root/CalendarRootDataAttributes.ts | 1 + .../CalendarSetVisibleMonthDataAttributes.ts | 1 + .../CalendarSetVisibleYearDataAttributes.ts | 1 + .../years-cell/CalendarYearsCellDataAttributes.ts | 10 ++++++++++ .../years-grid/CalendarYearsGridCssVars.ts | 6 ++++++ .../years-grid/CalendarYearsGridDataAttributes.ts | 1 + .../Calendar/years-grid/useCalendarYearsGrid.ts | 6 +++++- .../years-list/CalendarYearsListDataAttributes.ts | 1 + 21 files changed, 77 insertions(+), 9 deletions(-) create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCellDataAttributes.ts create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/days-grid-body/CalendarDaysGridBodyDataAttributes.ts create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/days-grid-header-cell/CalendarDaysGridHeaderCellDataAttributes.ts create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/days-grid-header/CalendarDaysGridHeaderDataAttributes.ts create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/days-grid/CalendarDaysGridDataAttributes.ts create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/days-week-row/CalendarDaysWeekRowDataAttributes.ts create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCellDataAttributes.ts create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/months-grid/CalendarMonthsGridCssVars.ts create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/months-grid/CalendarMonthsGridDataAttributes.ts create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsListDataAttributes.ts create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootDataAttributes.ts create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonthDataAttributes.ts create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYearDataAttributes.ts create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCellDataAttributes.ts create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/years-grid/CalendarYearsGridCssVars.ts create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/years-grid/CalendarYearsGridDataAttributes.ts create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/years-list/CalendarYearsListDataAttributes.ts diff --git a/docs/data/date-pickers/base-calendar/YearCalendarDemo.tsx b/docs/data/date-pickers/base-calendar/YearCalendarDemo.tsx index 1b7dfc8b8ffef..a9d7d9b6ea8e3 100644 --- a/docs/data/date-pickers/base-calendar/YearCalendarDemo.tsx +++ b/docs/data/date-pickers/base-calendar/YearCalendarDemo.tsx @@ -12,7 +12,7 @@ export default function YearCalendarDemo() { return ( - + {({ years }) => years.map((year) => ( Date: Mon, 6 Jan 2025 22:08:02 +0100 Subject: [PATCH 063/136] Fix data attr name --- .../base-calendar/calendar.module.css | 12 +++++------ .../Calendar/days-cell/CalendarDaysCell.tsx | 21 ++++++++++++++++++- .../CalendarDaysCellDataAttributes.ts | 2 +- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/docs/data/date-pickers/base-calendar/calendar.module.css b/docs/data/date-pickers/base-calendar/calendar.module.css index dfc8ebd1e2527..f25b53691cfb5 100644 --- a/docs/data/date-pickers/base-calendar/calendar.module.css +++ b/docs/data/date-pickers/base-calendar/calendar.module.css @@ -169,12 +169,12 @@ outline: 1px solid #9ca3af; } - &[data-outsidemonth] { + &[data-outside-month] { color: #cbd5e1; pointer-events: none; } - &:not([data-outsidemonth]):disabled { + &:not([data-outside-month]):disabled { pointer-events: none; text-decoration: line-through; color: #64748b; @@ -210,7 +210,7 @@ background-color: #e0f2fe; } - &:not([data-outsidemonth])[data-selected] { + &:not([data-outside-month])[data-selected] { background-color: #7dd3fc; } @@ -229,16 +229,16 @@ background-color: #075985; } - &:not([data-outsidemonth])[data-selected] { + &:not([data-outside-month])[data-selected] { background-color: #0369a1; } - &[data-outsidemonth] { + &[data-outside-month] { color: #334155; pointer-events: none; } - &:not([data-outsidemonth]):disabled { + &:not([data-outside-month]):disabled { color: #64748b; } } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx index 8454109936188..a6aee9c0d6930 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx @@ -8,6 +8,20 @@ import { useCalendarDaysGridContext } from '../days-grid/CalendarDaysGridContext import { useCalendarDaysCell } from './useCalendarDaysCell'; import { useCalendarRootContext } from '../root/CalendarRootContext'; import { useCompositeListItem } from '../../composite/list/useCompositeListItem'; +import { CustomStyleHookMapping } from '../../utils/getStyleHookProps'; +import { CalendarDaysCellDataAttributes } from './CalendarDaysCellDataAttributes'; + +const customStyleHookMapping: CustomStyleHookMapping = { + selected(value) { + return value ? { [CalendarDaysCellDataAttributes.selected]: '' } : null; + }, + current(value) { + return value ? { [CalendarDaysCellDataAttributes.current]: '' } : null; + }, + outsideMonth(value) { + return value ? { [CalendarDaysCellDataAttributes.outsideMonth]: '' } : null; + }, +}; const InnerCalendarDaysCell = React.forwardRef(function CalendarDaysGrid( props: InnerCalendarDaysCellProps, @@ -32,6 +46,7 @@ const InnerCalendarDaysCell = React.forwardRef(function CalendarDaysGrid( className, state, extraProps: otherProps, + customStyleHookMapping, }); return renderElement(); @@ -102,7 +117,11 @@ const CalendarDaysCell = React.forwardRef(function CalendarDaysCell( }); export namespace CalendarDaysCell { - export interface State {} + export interface State { + selected: boolean; + current: boolean; + outsideMonth: boolean; + } export interface Props extends Omit, 'value'>, diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCellDataAttributes.ts b/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCellDataAttributes.ts index 92b209f49bd76..1a44944bc6d60 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCellDataAttributes.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCellDataAttributes.ts @@ -10,5 +10,5 @@ export enum CalendarDaysCellDataAttributes { /** * Present when the day is outside the month rendered by the day grid wrapping it. */ - outsideMonth = 'data-outsidemonth', + outsideMonth = 'data-outside-month', } From c9e77b2f2d73a9e601b06402fd2416bc9e5b5cfb Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 6 Jan 2025 22:14:55 +0100 Subject: [PATCH 064/136] Fix data attr name --- .../base/Calendar/days-cell/CalendarDaysCell.tsx | 9 +++++++++ .../base/Calendar/months-cell/CalendarMonthsCell.tsx | 3 +++ .../internals/base/Calendar/utils/keyboardNavigation.ts | 4 +--- .../base/Calendar/years-cell/CalendarYearsCell.tsx | 3 +++ .../base/Calendar/years-grid/CalendarYearsGrid.tsx | 6 ++---- 5 files changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx index a6aee9c0d6930..e1fef03ce9b35 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx @@ -118,8 +118,17 @@ const CalendarDaysCell = React.forwardRef(function CalendarDaysCell( export namespace CalendarDaysCell { export interface State { + /** + * Whether the day is selected. + */ selected: boolean; + /** + * Whether the day contains the current date. + */ current: boolean; + /** + * Whether the day is outside the month rendered by the day grid wrapping it. + */ outsideMonth: boolean; } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx index 2571710de93d3..71ab30023909b 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx @@ -110,6 +110,9 @@ const CalendarMonthsCell = React.forwardRef(function CalendarMonthsCell( export namespace CalendarMonthsCell { export interface State { + /** + * Whether the month is selected. + */ selected: boolean; } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts index 87ee8b25c0b07..ce144cec41006 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts @@ -236,11 +236,9 @@ export function navigateInGrid({ moveToRowOnTheLeft(); break; case 'ArrowDown': - // TODO: Add multi month navigation moveToRowBelow(); break; case 'ArrowUp': - // TODO: Add multi month navigation moveToRowAbove(); break; case 'Home': @@ -313,7 +311,7 @@ function isNavigable(element: HTMLElement | null): element is HTMLElement { return false; } - if (element.dataset.outsidemonth != null) { + if (element.dataset['outside-month'] != null) { return false; } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx index 7144b43f7e5b4..9008875c5b902 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx @@ -106,6 +106,9 @@ const CalendarYearsCell = React.forwardRef(function CalendarsYearCell( export namespace CalendarYearsCell { export interface State { + /** + * Whether the year is selected. + */ selected: boolean; } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-grid/CalendarYearsGrid.tsx b/packages/x-date-pickers/src/internals/base/Calendar/years-grid/CalendarYearsGrid.tsx index 8c6c464540062..869cb56c13b51 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-grid/CalendarYearsGrid.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-grid/CalendarYearsGrid.tsx @@ -15,7 +15,7 @@ const CalendarYearsGrid = React.forwardRef(function CalendarYearsList( children, cellsPerRow, }); - const state = React.useMemo(() => ({ cellsPerRow }), [cellsPerRow]); + const state = React.useMemo(() => ({}), []); const { renderElement } = useComponentRenderer({ propGetter: getYearsGridProps, @@ -34,9 +34,7 @@ const CalendarYearsGrid = React.forwardRef(function CalendarYearsList( }); export namespace CalendarYearsGrid { - export interface State { - cellsPerRow: number; - } + export interface State {} export interface Props extends Omit, 'children'>, From a9a29974e4b70d48165f43f34d416751cf79bd79 Mon Sep 17 00:00:00 2001 From: flavien Date: Tue, 7 Jan 2025 08:38:30 +0100 Subject: [PATCH 065/136] Remove useless contexts --- .../months-cell/CalendarMonthsCell.tsx | 45 ++++++++++- .../months-grid/CalendarMonthsGrid.tsx | 9 +-- .../months-grid/useCalendarMonthsGrid.ts | 8 +- .../months-list/CalendarMonthsList.tsx | 9 +-- .../months-list/useCalendarMonthsList.ts | 8 +- .../CalendarMonthsCellCollectionContext.ts | 24 ------ .../useCalendarMonthsCellCollection.ts | 74 ------------------ .../base/Calendar/utils/useMonthsCells.ts | 30 ++++++++ .../base/Calendar/utils/useYearsCells.ts | 31 ++++++++ .../CalendarYearsCellCollectionContext.ts | 24 ------ .../useCalendarYearsCellCollection.ts | 75 ------------------- .../Calendar/years-cell/CalendarYearsCell.tsx | 44 ++++++++++- .../Calendar/years-grid/CalendarYearsGrid.tsx | 9 +-- .../years-grid/useCalendarYearsGrid.ts | 8 +- .../Calendar/years-list/CalendarYearsList.tsx | 9 +-- .../years-list/useCalendarYearsList.ts | 8 +- 16 files changed, 166 insertions(+), 249 deletions(-) delete mode 100644 packages/x-date-pickers/src/internals/base/Calendar/utils/months-cell-collection/CalendarMonthsCellCollectionContext.ts delete mode 100644 packages/x-date-pickers/src/internals/base/Calendar/utils/months-cell-collection/useCalendarMonthsCellCollection.ts create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/utils/useMonthsCells.ts create mode 100644 packages/x-date-pickers/src/internals/base/Calendar/utils/useYearsCells.ts delete mode 100644 packages/x-date-pickers/src/internals/base/Calendar/utils/years-cell-collection/CalendarYearsCellCollectionContext.ts delete mode 100644 packages/x-date-pickers/src/internals/base/Calendar/utils/years-cell-collection/useCalendarYearsCellCollection.ts diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx index 71ab30023909b..30ae086e21715 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx @@ -1,13 +1,15 @@ 'use client'; import * as React from 'react'; +import useEventCallback from '@mui/utils/useEventCallback'; import useForkRef from '@mui/utils/useForkRef'; +import { PickerValidDate } from '../../../../models'; import { useNow, useUtils } from '../../../hooks/useUtils'; +import { findClosestEnabledDate } from '../../../utils/date-utils'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; import { useCalendarRootContext } from '../root/CalendarRootContext'; import { useCalendarMonthsCell } from './useCalendarMonthsCell'; import { BaseUIComponentProps } from '../../utils/types'; import { useCompositeListItem } from '../../composite/list/useCompositeListItem'; -import { useCalendarMonthsCellCollectionContext } from '../utils/months-cell-collection/CalendarMonthsCellCollectionContext'; const InnerCalendarMonthsCell = React.forwardRef(function InnerCalendarMonthsCell( props: InnerCalendarMonthsCellProps, @@ -40,7 +42,6 @@ const CalendarMonthsCell = React.forwardRef(function CalendarMonthsCell( forwardedRef: React.ForwardedRef, ) { const rootContext = useCalendarRootContext(); - const monthsCellCollectionContext = useCalendarMonthsCellCollectionContext(); const { ref: listItemRef } = useCompositeListItem(); const utils = useUtils(); const now = useNow(rootContext.timezone); @@ -95,14 +96,50 @@ const CalendarMonthsCell = React.forwardRef(function CalendarMonthsCell( [utils, rootContext.value, rootContext.referenceDate, isSelected, props.value], ); + const selectMonth = useEventCallback((newValue: PickerValidDate) => { + if (rootContext.readOnly) { + return; + } + + const newCleanValue = utils.setMonth( + rootContext.value ?? rootContext.referenceDate, + utils.getMonth(newValue), + ); + + const startOfMonth = utils.startOfMonth(newCleanValue); + const endOfMonth = utils.endOfMonth(newCleanValue); + + const closestEnabledDate = rootContext.isDateDisabled(newCleanValue) + ? findClosestEnabledDate({ + utils, + date: newCleanValue, + minDate: utils.isBefore(rootContext.validationProps.minDate, startOfMonth) + ? startOfMonth + : rootContext.validationProps.minDate, + maxDate: utils.isAfter(rootContext.validationProps.maxDate, endOfMonth) + ? endOfMonth + : rootContext.validationProps.maxDate, + disablePast: rootContext.validationProps.disablePast, + disableFuture: rootContext.validationProps.disableFuture, + isDateDisabled: rootContext.isDateDisabled, + timezone: rootContext.timezone, + }) + : newCleanValue; + + if (closestEnabledDate) { + rootContext.setVisibleDate(closestEnabledDate, true); + rootContext.setValue(closestEnabledDate, { section: 'month' }); + } + }); + const ctx = React.useMemo( () => ({ isSelected, isDisabled, isTabbable, - selectMonth: monthsCellCollectionContext.selectMonth, + selectMonth, }), - [isSelected, isDisabled, isTabbable, monthsCellCollectionContext.selectMonth], + [isSelected, isDisabled, isTabbable, selectMonth], ); return ; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-grid/CalendarMonthsGrid.tsx b/packages/x-date-pickers/src/internals/base/Calendar/months-grid/CalendarMonthsGrid.tsx index 29b22fea3074b..d7e3739fcb3be 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-grid/CalendarMonthsGrid.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-grid/CalendarMonthsGrid.tsx @@ -4,14 +4,13 @@ import { useCalendarMonthsGrid } from './useCalendarMonthsGrid'; import { BaseUIComponentProps } from '../../utils/types'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; import { CompositeList } from '../../composite/list/CompositeList'; -import { CalendarMonthsCellCollectionContext } from '../utils/months-cell-collection/CalendarMonthsCellCollectionContext'; const CalendarMonthsGrid = React.forwardRef(function CalendarMonthsList( props: CalendarMonthsGrid.Props, forwardedRef: React.ForwardedRef, ) { const { className, render, children, cellsPerRow, ...otherProps } = props; - const { getMonthsGridProps, context, monthsCellRefs } = useCalendarMonthsGrid({ + const { getMonthsGridProps, monthsCellRefs } = useCalendarMonthsGrid({ children, cellsPerRow, }); @@ -26,11 +25,7 @@ const CalendarMonthsGrid = React.forwardRef(function CalendarMonthsList( extraProps: otherProps, }); - return ( - - {renderElement()} - - ); + return {renderElement()}; }); export namespace CalendarMonthsGrid { diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-grid/useCalendarMonthsGrid.ts b/packages/x-date-pickers/src/internals/base/Calendar/months-grid/useCalendarMonthsGrid.ts index da890d7c484bd..d936920db5e26 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-grid/useCalendarMonthsGrid.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-grid/useCalendarMonthsGrid.ts @@ -11,7 +11,7 @@ import { NavigateInGridChangePage, PageNavigationTarget, } from '../utils/keyboardNavigation'; -import { useCalendarMonthsCellCollection } from '../utils/months-cell-collection/useCalendarMonthsCellCollection'; +import { useMonthsCells } from '../utils/useMonthsCells'; import { useCalendarRootContext } from '../root/CalendarRootContext'; import { getFirstEnabledYear, getLastEnabledYear } from '../utils/date'; import { CalendarMonthsGridCssVars } from './CalendarMonthsGridCssVars'; @@ -21,7 +21,7 @@ export function useCalendarMonthsGrid(parameters: useCalendarMonthsGrid.Paramete const utils = useUtils(); const rootContext = useCalendarRootContext(); const monthsCellRefs = React.useRef<(HTMLElement | null)[]>([]); - const { months, context } = useCalendarMonthsCellCollection(); + const { months } = useMonthsCells(); const pageNavigationTargetRef = React.useRef(null); const getCellsInCalendar = useEventCallback(() => { @@ -116,8 +116,8 @@ export function useCalendarMonthsGrid(parameters: useCalendarMonthsGrid.Paramete ); return React.useMemo( - () => ({ getMonthsGridProps, context, monthsCellRefs }), - [getMonthsGridProps, context, monthsCellRefs], + () => ({ getMonthsGridProps, monthsCellRefs }), + [getMonthsGridProps, monthsCellRefs], ); } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsList.tsx b/packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsList.tsx index 0568c30390d69..bb1a87ed60849 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsList.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsList.tsx @@ -4,14 +4,13 @@ import { useCalendarMonthsList } from './useCalendarMonthsList'; import { BaseUIComponentProps } from '../../utils/types'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; import { CompositeList } from '../../composite/list/CompositeList'; -import { CalendarMonthsCellCollectionContext } from '../utils/months-cell-collection/CalendarMonthsCellCollectionContext'; const CalendarMonthsList = React.forwardRef(function CalendarMonthsList( props: CalendarMonthsList.Props, forwardedRef: React.ForwardedRef, ) { const { className, render, children, loop, ...otherProps } = props; - const { getMonthListProps, context, monthsCellRefs } = useCalendarMonthsList({ + const { getMonthListProps, monthsCellRefs } = useCalendarMonthsList({ children, loop, }); @@ -26,11 +25,7 @@ const CalendarMonthsList = React.forwardRef(function CalendarMonthsList( extraProps: otherProps, }); - return ( - - {renderElement()} - - ); + return {renderElement()}; }); export namespace CalendarMonthsList { diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-list/useCalendarMonthsList.ts b/packages/x-date-pickers/src/internals/base/Calendar/months-list/useCalendarMonthsList.ts index ba7cda0e7dc40..32bdd7d1bd918 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-list/useCalendarMonthsList.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-list/useCalendarMonthsList.ts @@ -4,12 +4,12 @@ import { PickerValidDate } from '../../../../models'; import { GenericHTMLProps } from '../../utils/types'; import { mergeReactProps } from '../../utils/mergeReactProps'; import { navigateInList } from '../utils/keyboardNavigation'; -import { useCalendarMonthsCellCollection } from '../utils/months-cell-collection/useCalendarMonthsCellCollection'; +import { useMonthsCells } from '../utils/useMonthsCells'; export function useCalendarMonthsList(parameters: useCalendarMonthsList.Parameters) { const { children, loop = true } = parameters; const monthsCellRefs = React.useRef<(HTMLElement | null)[]>([]); - const { months, context } = useCalendarMonthsCellCollection(); + const { months } = useMonthsCells(); const onKeyDown = useEventCallback((event: React.KeyboardEvent) => { navigateInList({ @@ -31,8 +31,8 @@ export function useCalendarMonthsList(parameters: useCalendarMonthsList.Paramete ); return React.useMemo( - () => ({ getMonthListProps, context, monthsCellRefs }), - [getMonthListProps, context, monthsCellRefs], + () => ({ getMonthListProps, monthsCellRefs }), + [getMonthListProps, monthsCellRefs], ); } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/months-cell-collection/CalendarMonthsCellCollectionContext.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/months-cell-collection/CalendarMonthsCellCollectionContext.ts deleted file mode 100644 index 06e2273f27bb4..0000000000000 --- a/packages/x-date-pickers/src/internals/base/Calendar/utils/months-cell-collection/CalendarMonthsCellCollectionContext.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as React from 'react'; -import { PickerValidDate } from '../../../../../models'; - -export interface CalendarMonthsCellCollectionContext { - selectMonth: (value: PickerValidDate) => void; -} - -export const CalendarMonthsCellCollectionContext = React.createContext< - CalendarMonthsCellCollectionContext | undefined ->(undefined); - -if (process.env.NODE_ENV !== 'production') { - CalendarMonthsCellCollectionContext.displayName = 'CalendarMonthsCellCollectionContext'; -} - -export function useCalendarMonthsCellCollectionContext() { - const context = React.useContext(CalendarMonthsCellCollectionContext); - if (context === undefined) { - throw new Error( - 'Base UI X: CalendarMonthsCellCollectionContext is missing. Calendar Month parts must be placed within or ``.', - ); - } - return context; -} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/months-cell-collection/useCalendarMonthsCellCollection.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/months-cell-collection/useCalendarMonthsCellCollection.ts deleted file mode 100644 index 09ee18a661489..0000000000000 --- a/packages/x-date-pickers/src/internals/base/Calendar/utils/months-cell-collection/useCalendarMonthsCellCollection.ts +++ /dev/null @@ -1,74 +0,0 @@ -import * as React from 'react'; -import useEventCallback from '@mui/utils/useEventCallback'; -import { PickerValidDate } from '../../../../../models'; -import { findClosestEnabledDate, getMonthsInYear } from '../../../../utils/date-utils'; -import { useUtils } from '../../../../hooks/useUtils'; -import { useCalendarRootContext } from '../../root/CalendarRootContext'; -import { CalendarMonthsCellCollectionContext } from './CalendarMonthsCellCollectionContext'; - -export function useCalendarMonthsCellCollection(): useCalendarMonthsCellCollection.ReturnValue { - const rootContext = useCalendarRootContext(); - const utils = useUtils(); - - const currentYear = React.useMemo( - () => utils.startOfYear(rootContext.visibleDate), - [utils, rootContext.visibleDate], - ); - - const months = React.useMemo(() => getMonthsInYear(utils, currentYear), [utils, currentYear]); - - const selectMonth = useEventCallback((newValue: PickerValidDate) => { - if (rootContext.readOnly) { - return; - } - - const newCleanValue = utils.setMonth( - rootContext.value ?? rootContext.referenceDate, - utils.getMonth(newValue), - ); - - const startOfMonth = utils.startOfMonth(newCleanValue); - const endOfMonth = utils.endOfMonth(newCleanValue); - - const closestEnabledDate = rootContext.isDateDisabled(newCleanValue) - ? findClosestEnabledDate({ - utils, - date: newCleanValue, - minDate: utils.isBefore(rootContext.validationProps.minDate, startOfMonth) - ? startOfMonth - : rootContext.validationProps.minDate, - maxDate: utils.isAfter(rootContext.validationProps.maxDate, endOfMonth) - ? endOfMonth - : rootContext.validationProps.maxDate, - disablePast: rootContext.validationProps.disablePast, - disableFuture: rootContext.validationProps.disableFuture, - isDateDisabled: rootContext.isDateDisabled, - timezone: rootContext.timezone, - }) - : newCleanValue; - - if (closestEnabledDate) { - rootContext.setVisibleDate(closestEnabledDate, true); - rootContext.setValue(closestEnabledDate, { section: 'month' }); - } - }); - - const registerSection = rootContext.registerSection; - React.useEffect(() => { - return registerSection({ type: 'month', value: currentYear }); - }, [registerSection, currentYear]); - - const context: CalendarMonthsCellCollectionContext = React.useMemo( - () => ({ selectMonth }), - [selectMonth], - ); - - return { months, context }; -} - -export namespace useCalendarMonthsCellCollection { - export interface ReturnValue { - months: PickerValidDate[]; - context: CalendarMonthsCellCollectionContext; - } -} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/useMonthsCells.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/useMonthsCells.ts new file mode 100644 index 0000000000000..67a195de444c1 --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/Calendar/utils/useMonthsCells.ts @@ -0,0 +1,30 @@ +import * as React from 'react'; +import { PickerValidDate } from '../../../../models'; +import { getMonthsInYear } from '../../../utils/date-utils'; +import { useUtils } from '../../../hooks/useUtils'; +import { useCalendarRootContext } from '../root/CalendarRootContext'; + +export function useMonthsCells(): useMonthsCells.ReturnValue { + const rootContext = useCalendarRootContext(); + const utils = useUtils(); + + const currentYear = React.useMemo( + () => utils.startOfYear(rootContext.visibleDate), + [utils, rootContext.visibleDate], + ); + + const months = React.useMemo(() => getMonthsInYear(utils, currentYear), [utils, currentYear]); + + const registerSection = rootContext.registerSection; + React.useEffect(() => { + return registerSection({ type: 'month', value: currentYear }); + }, [registerSection, currentYear]); + + return { months }; +} + +export namespace useMonthsCells { + export interface ReturnValue { + months: PickerValidDate[]; + } +} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/useYearsCells.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/useYearsCells.ts new file mode 100644 index 0000000000000..b2ef70a3ed9f1 --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/Calendar/utils/useYearsCells.ts @@ -0,0 +1,31 @@ +import * as React from 'react'; +import { PickerValidDate } from '../../../../models'; +import { useUtils } from '../../../hooks/useUtils'; +import { useCalendarRootContext } from '../root/CalendarRootContext'; + +export function useYearsCells(): useYearsCells.ReturnValue { + const rootContext = useCalendarRootContext(); + const utils = useUtils(); + + const years = React.useMemo( + () => + utils.getYearRange([ + rootContext.validationProps.minDate, + rootContext.validationProps.maxDate, + ]), + [utils, rootContext.validationProps.minDate, rootContext.validationProps.maxDate], + ); + + const registerSection = rootContext.registerSection; + React.useEffect(() => { + return registerSection({ type: 'month', value: rootContext.visibleDate }); + }, [registerSection, rootContext.visibleDate]); + + return { years }; +} + +export namespace useYearsCells { + export interface ReturnValue { + years: PickerValidDate[]; + } +} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/years-cell-collection/CalendarYearsCellCollectionContext.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/years-cell-collection/CalendarYearsCellCollectionContext.ts deleted file mode 100644 index d336f175fabc9..0000000000000 --- a/packages/x-date-pickers/src/internals/base/Calendar/utils/years-cell-collection/CalendarYearsCellCollectionContext.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as React from 'react'; -import { PickerValidDate } from '../../../../../models'; - -export interface CalendarYearsCellCollectionContext { - selectYear: (value: PickerValidDate) => void; -} - -export const CalendarYearsCellCollectionContext = React.createContext< - CalendarYearsCellCollectionContext | undefined ->(undefined); - -if (process.env.NODE_ENV !== 'production') { - CalendarYearsCellCollectionContext.displayName = 'CalendarYearsCellCollectionContext'; -} - -export function useCalendarYearsCellCollectionContext() { - const context = React.useContext(CalendarYearsCellCollectionContext); - if (context === undefined) { - throw new Error( - 'Base UI X: CalendarYearsCellCollectionContext is missing. Calendar Year parts must be placed within or ``.', - ); - } - return context; -} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/years-cell-collection/useCalendarYearsCellCollection.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/years-cell-collection/useCalendarYearsCellCollection.ts deleted file mode 100644 index 796f0cb0f1350..0000000000000 --- a/packages/x-date-pickers/src/internals/base/Calendar/utils/years-cell-collection/useCalendarYearsCellCollection.ts +++ /dev/null @@ -1,75 +0,0 @@ -import * as React from 'react'; -import useEventCallback from '@mui/utils/useEventCallback'; -import { PickerValidDate } from '../../../../../models'; -import { findClosestEnabledDate } from '../../../../utils/date-utils'; -import { useUtils } from '../../../../hooks/useUtils'; -import { useCalendarRootContext } from '../../root/CalendarRootContext'; -import { CalendarYearsCellCollectionContext } from './CalendarYearsCellCollectionContext'; - -export function useCalendarYearsCellCollection(): useCalendarYearsCellCollection.ReturnValue { - const rootContext = useCalendarRootContext(); - const utils = useUtils(); - - const years = React.useMemo( - () => - utils.getYearRange([ - rootContext.validationProps.minDate, - rootContext.validationProps.maxDate, - ]), - [utils, rootContext.validationProps.minDate, rootContext.validationProps.maxDate], - ); - - const selectYear = useEventCallback((newValue: PickerValidDate) => { - if (rootContext.readOnly) { - return; - } - - const newCleanValue = utils.setYear( - rootContext.value ?? rootContext.referenceDate, - utils.getYear(newValue), - ); - - const startOfYear = utils.startOfYear(newCleanValue); - const endOfYear = utils.endOfYear(newCleanValue); - - const closestEnabledDate = rootContext.isDateDisabled(newCleanValue) - ? findClosestEnabledDate({ - utils, - date: newCleanValue, - minDate: utils.isBefore(rootContext.validationProps.minDate, startOfYear) - ? startOfYear - : rootContext.validationProps.minDate, - maxDate: utils.isAfter(rootContext.validationProps.maxDate, endOfYear) - ? endOfYear - : rootContext.validationProps.maxDate, - disablePast: rootContext.validationProps.disablePast, - disableFuture: rootContext.validationProps.disableFuture, - isDateDisabled: rootContext.isDateDisabled, - timezone: rootContext.timezone, - }) - : newCleanValue; - - if (closestEnabledDate) { - rootContext.setValue(closestEnabledDate, { section: 'year' }); - } - }); - - const registerSection = rootContext.registerSection; - React.useEffect(() => { - return registerSection({ type: 'month', value: rootContext.visibleDate }); - }, [registerSection, rootContext.visibleDate]); - - const context: CalendarYearsCellCollectionContext = React.useMemo( - () => ({ selectYear }), - [selectYear], - ); - - return { years, context }; -} - -export namespace useCalendarYearsCellCollection { - export interface ReturnValue { - years: PickerValidDate[]; - context: CalendarYearsCellCollectionContext; - } -} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx index 9008875c5b902..d79c01bab1cab 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx @@ -1,13 +1,15 @@ 'use client'; import * as React from 'react'; +import useEventCallback from '@mui/utils/useEventCallback'; import useForkRef from '@mui/utils/useForkRef'; +import { PickerValidDate } from '../../../../models'; import { useNow, useUtils } from '../../../hooks/useUtils'; +import { findClosestEnabledDate } from '../../../utils/date-utils'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; import { useCalendarRootContext } from '../root/CalendarRootContext'; import { useCalendarYearsCell } from './useCalendarYearsCell'; import { BaseUIComponentProps } from '../../utils/types'; import { useCompositeListItem } from '../../composite/list/useCompositeListItem'; -import { useCalendarYearsCellCollectionContext } from '../utils/years-cell-collection/CalendarYearsCellCollectionContext'; const InnerCalendarYearsCell = React.forwardRef(function InnerCalendarYearsCell( props: InnerCalendarYearsCellProps, @@ -40,7 +42,6 @@ const CalendarYearsCell = React.forwardRef(function CalendarsYearCell( forwardedRef: React.ForwardedRef, ) { const rootContext = useCalendarRootContext(); - const yearsListContext = useCalendarYearsCellCollectionContext(); const { ref: listItemRef } = useCompositeListItem(); const utils = useUtils(); const now = useNow(rootContext.timezone); @@ -91,14 +92,49 @@ const CalendarYearsCell = React.forwardRef(function CalendarsYearCell( [utils, rootContext.value, rootContext.referenceDate, isSelected, props.value], ); + const selectYear = useEventCallback((newValue: PickerValidDate) => { + if (rootContext.readOnly) { + return; + } + + const newCleanValue = utils.setYear( + rootContext.value ?? rootContext.referenceDate, + utils.getYear(newValue), + ); + + const startOfYear = utils.startOfYear(newCleanValue); + const endOfYear = utils.endOfYear(newCleanValue); + + const closestEnabledDate = rootContext.isDateDisabled(newCleanValue) + ? findClosestEnabledDate({ + utils, + date: newCleanValue, + minDate: utils.isBefore(rootContext.validationProps.minDate, startOfYear) + ? startOfYear + : rootContext.validationProps.minDate, + maxDate: utils.isAfter(rootContext.validationProps.maxDate, endOfYear) + ? endOfYear + : rootContext.validationProps.maxDate, + disablePast: rootContext.validationProps.disablePast, + disableFuture: rootContext.validationProps.disableFuture, + isDateDisabled: rootContext.isDateDisabled, + timezone: rootContext.timezone, + }) + : newCleanValue; + + if (closestEnabledDate) { + rootContext.setValue(closestEnabledDate, { section: 'year' }); + } + }); + const ctx = React.useMemo( () => ({ isSelected, isDisabled, isTabbable, - selectYear: yearsListContext.selectYear, + selectYear, }), - [isSelected, isDisabled, isTabbable, yearsListContext.selectYear], + [isSelected, isDisabled, isTabbable, selectYear], ); return ; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-grid/CalendarYearsGrid.tsx b/packages/x-date-pickers/src/internals/base/Calendar/years-grid/CalendarYearsGrid.tsx index 869cb56c13b51..1754b483ee092 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-grid/CalendarYearsGrid.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-grid/CalendarYearsGrid.tsx @@ -4,14 +4,13 @@ import { useCalendarYearsGrid } from './useCalendarYearsGrid'; import { BaseUIComponentProps } from '../../utils/types'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; import { CompositeList } from '../../composite/list/CompositeList'; -import { CalendarYearsCellCollectionContext } from '../utils/years-cell-collection/CalendarYearsCellCollectionContext'; const CalendarYearsGrid = React.forwardRef(function CalendarYearsList( props: CalendarYearsGrid.Props, forwardedRef: React.ForwardedRef, ) { const { className, render, children, cellsPerRow, ...otherProps } = props; - const { getYearsGridProps, context, yearsCellRefs } = useCalendarYearsGrid({ + const { getYearsGridProps, yearsCellRefs } = useCalendarYearsGrid({ children, cellsPerRow, }); @@ -26,11 +25,7 @@ const CalendarYearsGrid = React.forwardRef(function CalendarYearsList( extraProps: otherProps, }); - return ( - - {renderElement()} - - ); + return {renderElement()}; }); export namespace CalendarYearsGrid { diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-grid/useCalendarYearsGrid.ts b/packages/x-date-pickers/src/internals/base/Calendar/years-grid/useCalendarYearsGrid.ts index 1d81e478c59f0..911cc408c4576 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-grid/useCalendarYearsGrid.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-grid/useCalendarYearsGrid.ts @@ -4,13 +4,13 @@ import { PickerValidDate } from '../../../../models'; import { GenericHTMLProps } from '../../utils/types'; import { mergeReactProps } from '../../utils/mergeReactProps'; import { navigateInGrid } from '../utils/keyboardNavigation'; -import { useCalendarYearsCellCollection } from '../utils/years-cell-collection/useCalendarYearsCellCollection'; +import { useYearsCells } from '../utils/useYearsCells'; import { CalendarYearsGridCssVars } from './CalendarYearsGridCssVars'; export function useCalendarYearsGrid(parameters: useCalendarYearsGrid.Parameters) { const { children, cellsPerRow } = parameters; const yearsCellRefs = React.useRef<(HTMLElement | null)[]>([]); - const { years, context } = useCalendarYearsCellCollection(); + const { years } = useYearsCells(); const getCellsInCalendar = useEventCallback(() => { const grid: HTMLElement[][] = Array.from( @@ -53,8 +53,8 @@ export function useCalendarYearsGrid(parameters: useCalendarYearsGrid.Parameters ); return React.useMemo( - () => ({ getYearsGridProps, context, yearsCellRefs }), - [getYearsGridProps, context, yearsCellRefs], + () => ({ getYearsGridProps, yearsCellRefs }), + [getYearsGridProps, yearsCellRefs], ); } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-list/CalendarYearsList.tsx b/packages/x-date-pickers/src/internals/base/Calendar/years-list/CalendarYearsList.tsx index b2538e0ec16f5..f486d8057d7e3 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-list/CalendarYearsList.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-list/CalendarYearsList.tsx @@ -4,14 +4,13 @@ import { useCalendarYearsList } from './useCalendarYearsList'; import { BaseUIComponentProps } from '../../utils/types'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; import { CompositeList } from '../../composite/list/CompositeList'; -import { CalendarYearsCellCollectionContext } from '../utils/years-cell-collection/CalendarYearsCellCollectionContext'; const CalendarYearsList = React.forwardRef(function CalendarYearsList( props: CalendarYearsList.Props, forwardedRef: React.ForwardedRef, ) { const { className, render, children, loop, ...otherProps } = props; - const { getYearsListProps, context, yearsCellRefs } = useCalendarYearsList({ + const { getYearsListProps, yearsCellRefs } = useCalendarYearsList({ children, loop, }); @@ -26,11 +25,7 @@ const CalendarYearsList = React.forwardRef(function CalendarYearsList( extraProps: otherProps, }); - return ( - - {renderElement()} - - ); + return {renderElement()}; }); export namespace CalendarYearsList { diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-list/useCalendarYearsList.ts b/packages/x-date-pickers/src/internals/base/Calendar/years-list/useCalendarYearsList.ts index 216dba029f4fd..e311512780294 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-list/useCalendarYearsList.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-list/useCalendarYearsList.ts @@ -4,12 +4,12 @@ import { PickerValidDate } from '../../../../models'; import { GenericHTMLProps } from '../../utils/types'; import { mergeReactProps } from '../../utils/mergeReactProps'; import { navigateInList } from '../utils/keyboardNavigation'; -import { useCalendarYearsCellCollection } from '../utils/years-cell-collection/useCalendarYearsCellCollection'; +import { useYearsCells } from '../utils/useYearsCells'; export function useCalendarYearsList(parameters: useCalendarYearsList.Parameters) { const { children, loop = true } = parameters; const yearsCellRefs = React.useRef<(HTMLElement | null)[]>([]); - const { years, context } = useCalendarYearsCellCollection(); + const { years } = useYearsCells(); const onKeyDown = useEventCallback((event: React.KeyboardEvent) => { navigateInList({ @@ -31,8 +31,8 @@ export function useCalendarYearsList(parameters: useCalendarYearsList.Parameters ); return React.useMemo( - () => ({ getYearsListProps, context, yearsCellRefs }), - [getYearsListProps, context, yearsCellRefs], + () => ({ getYearsListProps, yearsCellRefs }), + [getYearsListProps, yearsCellRefs], ); } From f485808fd6317ac276fd7c89ef57a95fb1eb3e98 Mon Sep 17 00:00:00 2001 From: flavien Date: Tue, 7 Jan 2025 10:04:35 +0100 Subject: [PATCH 066/136] Work on demos --- .../base-calendar/DateCalendarMD2Demo.js | 4 +- .../base-calendar/DayCalendarTwoMonthsDemo.js | 8 ++- .../DayCalendarTwoMonthsDemo.tsx | 8 ++- .../MonthCalendarWithListLayoutDemo.js | 49 +++++++++++++++++ .../MonthCalendarWithListLayoutDemo.tsx | 49 +++++++++++++++++ ...onthCalendarWithListLayoutDemo.tsx.preview | 14 +++++ .../base-calendar/YearCalendarDemo.js | 2 +- .../YearCalendarDemo.tsx.preview | 2 +- .../YearCalendarWithListLayoutDemo.js | 29 ++++++++++ .../YearCalendarWithListLayoutDemo.tsx | 29 ++++++++++ ...YearCalendarWithListLayoutDemo.tsx.preview | 13 +++++ .../base-calendar/base-calendar.md | 18 ++++-- .../base-calendar/calendar.module.css | 12 +++- docs/package.json | 1 + .../months-grid/useCalendarMonthsGrid.ts | 49 ++--------------- .../months-list/CalendarMonthsList.tsx | 3 +- .../months-list/useCalendarMonthsList.ts | 39 ++++++++++++- .../root/useCalendarDaysGridsNavigation.ts | 4 +- .../base/Calendar/utils/keyboardNavigation.ts | 55 ++++++++++++++++--- .../base/Calendar/utils/useMonthsCells.ts | 43 ++++++++++++++- .../years-list/useCalendarYearsList.ts | 1 + pnpm-lock.yaml | 3 + 22 files changed, 361 insertions(+), 74 deletions(-) create mode 100644 docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.js create mode 100644 docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.tsx create mode 100644 docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.tsx.preview create mode 100644 docs/data/date-pickers/base-calendar/YearCalendarWithListLayoutDemo.js create mode 100644 docs/data/date-pickers/base-calendar/YearCalendarWithListLayoutDemo.tsx create mode 100644 docs/data/date-pickers/base-calendar/YearCalendarWithListLayoutDemo.tsx.preview diff --git a/docs/data/date-pickers/base-calendar/DateCalendarMD2Demo.js b/docs/data/date-pickers/base-calendar/DateCalendarMD2Demo.js index 630737371a112..ae7bf7defb08e 100644 --- a/docs/data/date-pickers/base-calendar/DateCalendarMD2Demo.js +++ b/docs/data/date-pickers/base-calendar/DateCalendarMD2Demo.js @@ -70,7 +70,7 @@ export default function DateCalendarMD2Demo() { onActiveSectionChange={setActiveSection} /> {activeSection === 'year' && ( - + {({ years }) => years.map((year) => ( )) } - + )} {activeSection === 'day' && ( diff --git a/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.js b/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.js index 402f9d876a3dd..624334c186992 100644 --- a/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.js +++ b/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.js @@ -1,6 +1,7 @@ import * as React from 'react'; import clsx from 'clsx'; +import { Separator } from '@base-ui-components/react/separator'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports @@ -35,7 +36,7 @@ function Header(props) { ); } -function DayGrid(props) { +function DaysGrid(props) { const { offset } = props; return (
@@ -86,8 +87,9 @@ function DayCalendar(props) { monthPageSize={2} className={clsx(styles.Root, styles.RootWithTwoPanels)} > - - + + + ); diff --git a/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.tsx b/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.tsx index feb44956b6066..3adb85cb1f179 100644 --- a/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.tsx +++ b/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import clsx from 'clsx'; import { Dayjs } from 'dayjs'; +import { Separator } from '@base-ui-components/react/separator'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports @@ -35,7 +36,7 @@ function Header(props: { offset: 0 | 1 }) { ); } -function DayGrid(props: { offset: 0 | 1 }) { +function DaysGrid(props: { offset: 0 | 1 }) { const { offset } = props; return (
@@ -86,8 +87,9 @@ function DayCalendar(props: Omit) { monthPageSize={2} className={clsx(styles.Root, styles.RootWithTwoPanels)} > - - + + + ); diff --git a/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.js b/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.js new file mode 100644 index 0000000000000..9f9c72e5f1695 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.js @@ -0,0 +1,49 @@ +import * as React from 'react'; + +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +// eslint-disable-next-line no-restricted-imports +import { + Calendar, + useCalendarContext, +} from '@mui/x-date-pickers/internals/base/Calendar'; +import styles from './calendar.module.css'; + +function Header() { + const { visibleDate } = useCalendarContext(); + + return ( +
+ + ◀ + + {visibleDate.format('YYYY')} + + ▶ + +
+ ); +} + +export default function MonthCalendarWithListLayoutDemo() { + const [value, setValue] = React.useState(null); + + return ( + + +
+ + {({ months }) => + months.map((month) => ( + + )) + } + + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.tsx b/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.tsx new file mode 100644 index 0000000000000..b64fea1a3de45 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import { Dayjs } from 'dayjs'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +// eslint-disable-next-line no-restricted-imports +import { + Calendar, + useCalendarContext, +} from '@mui/x-date-pickers/internals/base/Calendar'; +import styles from './calendar.module.css'; + +function Header() { + const { visibleDate } = useCalendarContext(); + + return ( +
+ + ◀ + + {visibleDate.format('YYYY')} + + ▶ + +
+ ); +} + +export default function MonthCalendarWithListLayoutDemo() { + const [value, setValue] = React.useState(null); + + return ( + + +
+ + {({ months }) => + months.map((month) => ( + + )) + } + + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.tsx.preview b/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.tsx.preview new file mode 100644 index 0000000000000..28bf2a9fcbb53 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.tsx.preview @@ -0,0 +1,14 @@ + +
+ + {({ months }) => + months.map((month) => ( + + )) + } + + \ No newline at end of file diff --git a/docs/data/date-pickers/base-calendar/YearCalendarDemo.js b/docs/data/date-pickers/base-calendar/YearCalendarDemo.js index 88856c14bca51..37840f2bcaf50 100644 --- a/docs/data/date-pickers/base-calendar/YearCalendarDemo.js +++ b/docs/data/date-pickers/base-calendar/YearCalendarDemo.js @@ -12,7 +12,7 @@ export default function YearCalendarDemo() { return ( - + {({ years }) => years.map((year) => ( - + {({ years }) => years.map((year) => ( + + + {({ years }) => + years.map((year) => ( + + )) + } + + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/YearCalendarWithListLayoutDemo.tsx b/docs/data/date-pickers/base-calendar/YearCalendarWithListLayoutDemo.tsx new file mode 100644 index 0000000000000..f166f2c102e54 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/YearCalendarWithListLayoutDemo.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; +import { Dayjs } from 'dayjs'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +// eslint-disable-next-line no-restricted-imports +import { Calendar } from '@mui/x-date-pickers/internals/base/Calendar'; +import styles from './calendar.module.css'; + +export default function YearCalendarWithListLayoutDemo() { + const [value, setValue] = React.useState(null); + + return ( + + + + {({ years }) => + years.map((year) => ( + + )) + } + + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/YearCalendarWithListLayoutDemo.tsx.preview b/docs/data/date-pickers/base-calendar/YearCalendarWithListLayoutDemo.tsx.preview new file mode 100644 index 0000000000000..bce391c7c37b9 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/YearCalendarWithListLayoutDemo.tsx.preview @@ -0,0 +1,13 @@ + + + {({ years }) => + years.map((year) => ( + + )) + } + + \ No newline at end of file diff --git a/docs/data/date-pickers/base-calendar/base-calendar.md b/docs/data/date-pickers/base-calendar/base-calendar.md index 0ed2e8378e980..62b1ba537f717 100644 --- a/docs/data/date-pickers/base-calendar/base-calendar.md +++ b/docs/data/date-pickers/base-calendar/base-calendar.md @@ -10,11 +10,11 @@ packageName: '@mui/x-date-pickers' ## Day Calendar -### One month +### Single visible month {{"demo": "DayCalendarDemo.js"}} -### Two months +### Multiple visible months {{"demo": "DayCalendarTwoMonthsDemo.js"}} @@ -28,20 +28,28 @@ packageName: '@mui/x-date-pickers' ## Month Calendar -### One year +### Grid layout {{"demo": "MonthCalendarDemo.js"}} -### Two years +### List layout + +{{"demo": "MonthCalendarWithListLayoutDemo.js"}} + +### Multiple visible years TODO ## Year Calendar -### Full list +### Grid layout {{"demo": "YearCalendarDemo.js"}} +### List layout + +{{"demo": "YearCalendarWithListLayoutDemo.js"}} + ### Grouped by decade {{"demo": "YearCalendarWithDecadeNavigationDemo.js"}} diff --git a/docs/data/date-pickers/base-calendar/calendar.module.css b/docs/data/date-pickers/base-calendar/calendar.module.css index f25b53691cfb5..38565d40ab4f5 100644 --- a/docs/data/date-pickers/base-calendar/calendar.module.css +++ b/docs/data/date-pickers/base-calendar/calendar.module.css @@ -13,7 +13,7 @@ } .RootWithTwoPanels { - width: 540px; + width: 552px; flex-direction: row; justify-content: space-between; } @@ -219,6 +219,12 @@ } } +.DaysGridSeparator { + background-color: #9ca3af; + margin: 24px 0; + width: 1px; +} + .Hidden { opacity: 0; pointer-events: none; @@ -255,3 +261,7 @@ background-color: #075985; } } + +:global(.mode-dark):where(.DaysGridSeparator) { + background-color: #4b5563; +} diff --git a/docs/package.json b/docs/package.json index efe046ca7cb89..0cd55c34a249d 100644 --- a/docs/package.json +++ b/docs/package.json @@ -22,6 +22,7 @@ "@babel/core": "^7.26.0", "@babel/runtime": "^7.26.0", "@babel/runtime-corejs2": "^7.26.0", + "@base-ui-components/react": "1.0.0-alpha.4", "@docsearch/react": "^3.8.2", "@emotion/cache": "^11.14.0", "@emotion/react": "^11.14.0", diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-grid/useCalendarMonthsGrid.ts b/packages/x-date-pickers/src/internals/base/Calendar/months-grid/useCalendarMonthsGrid.ts index d936920db5e26..14221bd724fbb 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-grid/useCalendarMonthsGrid.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-grid/useCalendarMonthsGrid.ts @@ -2,27 +2,24 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; import useTimeout from '@mui/utils/useTimeout'; import { PickerValidDate } from '../../../../models'; -import { useUtils } from '../../../hooks/useUtils'; import { GenericHTMLProps } from '../../utils/types'; import { mergeReactProps } from '../../utils/mergeReactProps'; import { applyInitialFocusInGrid, navigateInGrid, NavigateInGridChangePage, - PageNavigationTarget, + PageGridNavigationTarget, } from '../utils/keyboardNavigation'; import { useMonthsCells } from '../utils/useMonthsCells'; import { useCalendarRootContext } from '../root/CalendarRootContext'; -import { getFirstEnabledYear, getLastEnabledYear } from '../utils/date'; import { CalendarMonthsGridCssVars } from './CalendarMonthsGridCssVars'; export function useCalendarMonthsGrid(parameters: useCalendarMonthsGrid.Parameters) { const { children, cellsPerRow } = parameters; - const utils = useUtils(); const rootContext = useCalendarRootContext(); const monthsCellRefs = React.useRef<(HTMLElement | null)[]>([]); - const { months } = useMonthsCells(); - const pageNavigationTargetRef = React.useRef(null); + const { months, changePage } = useMonthsCells(); + const pageNavigationTargetRef = React.useRef(null); const getCellsInCalendar = useEventCallback(() => { const grid: HTMLElement[][] = Array.from( @@ -54,42 +51,8 @@ export function useCalendarMonthsGrid(parameters: useCalendarMonthsGrid.Paramete // TODO: Add support for multiple months grids. const onKeyDown = useEventCallback((event: React.KeyboardEvent) => { - const changePage: NavigateInGridChangePage = (params) => { - // TODO: Jump over months with no valid date. - if (params.direction === 'previous') { - const targetDate = utils.addYears( - utils.startOfYear(rootContext.visibleDate), - -rootContext.yearPageSize, - ); - const lastYearInNewPage = utils.addYears(targetDate, rootContext.yearPageSize - 1); - - // All the years before the visible ones are fully disabled, we skip the navigation. - if ( - utils.isAfter(getFirstEnabledYear(utils, rootContext.validationProps), lastYearInNewPage) - ) { - return; - } - - rootContext.setVisibleDate( - utils.addYears(rootContext.visibleDate, -rootContext.yearPageSize), - false, - ); - } - if (params.direction === 'next') { - const targetDate = utils.addYears( - utils.startOfYear(rootContext.visibleDate), - rootContext.yearPageSize, - ); - - // All the years after the visible ones are fully disabled, we skip the navigation. - if (utils.isBefore(getLastEnabledYear(utils, rootContext.validationProps), targetDate)) { - return; - } - rootContext.setVisibleDate( - utils.addYears(rootContext.visibleDate, rootContext.yearPageSize), - false, - ); - } + const changeGridPage: NavigateInGridChangePage = (params) => { + changePage(params.direction); pageNavigationTargetRef.current = params.target; }; @@ -97,7 +60,7 @@ export function useCalendarMonthsGrid(parameters: useCalendarMonthsGrid.Paramete navigateInGrid({ cells: getCellsInCalendar(), event, - changePage, + changePage: changeGridPage, }); }); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsList.tsx b/packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsList.tsx index bb1a87ed60849..88985037d867b 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsList.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsList.tsx @@ -9,10 +9,11 @@ const CalendarMonthsList = React.forwardRef(function CalendarMonthsList( props: CalendarMonthsList.Props, forwardedRef: React.ForwardedRef, ) { - const { className, render, children, loop, ...otherProps } = props; + const { className, render, children, loop, canChangeYear, ...otherProps } = props; const { getMonthListProps, monthsCellRefs } = useCalendarMonthsList({ children, loop, + canChangeYear, }); const state = React.useMemo(() => ({}), []); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-list/useCalendarMonthsList.ts b/packages/x-date-pickers/src/internals/base/Calendar/months-list/useCalendarMonthsList.ts index 32bdd7d1bd918..8dd9e0a402d06 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-list/useCalendarMonthsList.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-list/useCalendarMonthsList.ts @@ -1,21 +1,47 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; +import useTimeout from '@mui/utils/useTimeout'; import { PickerValidDate } from '../../../../models'; import { GenericHTMLProps } from '../../utils/types'; import { mergeReactProps } from '../../utils/mergeReactProps'; -import { navigateInList } from '../utils/keyboardNavigation'; +import { + applyInitialFocusInList, + navigateInList, + NavigateInListChangePage, + PageListNavigationTarget, +} from '../utils/keyboardNavigation'; import { useMonthsCells } from '../utils/useMonthsCells'; +import { useCalendarRootContext } from '../root/CalendarRootContext'; export function useCalendarMonthsList(parameters: useCalendarMonthsList.Parameters) { - const { children, loop = true } = parameters; + const { children, loop = true, canChangeYear = true } = parameters; + const rootContext = useCalendarRootContext(); const monthsCellRefs = React.useRef<(HTMLElement | null)[]>([]); - const { months } = useMonthsCells(); + const { months, changePage } = useMonthsCells(); + const pageNavigationTargetRef = React.useRef(null); + + const timeout = useTimeout(); + React.useEffect(() => { + if (pageNavigationTargetRef.current) { + const target = pageNavigationTargetRef.current; + timeout.start(0, () => { + applyInitialFocusInList({ cells: monthsCellRefs.current, target }); + }); + } + }, [rootContext.visibleDate, timeout]); const onKeyDown = useEventCallback((event: React.KeyboardEvent) => { + const changeListPage: NavigateInListChangePage = (params) => { + changePage(params.direction); + + pageNavigationTargetRef.current = params.target; + }; + navigateInList({ cells: monthsCellRefs.current, event, loop, + changePage: canChangeYear ? changeListPage : undefined, }); }); @@ -41,9 +67,16 @@ export namespace useCalendarMonthsList { /** * Whether to loop keyboard focus back to the first item * when the end of the list is reached while using the arrow keys. + * It is ignored when the `canChangeYear` prop is true. * @default true */ loop?: boolean; + /** + * Whether to go to the previous / next year + * when the end of the list is reached while using the arrow keys. + * @default true + */ + canChangeYear?: boolean; children?: (parameters: ChildrenParameters) => React.ReactNode; } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarDaysGridsNavigation.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarDaysGridsNavigation.ts index c2f5e59f33862..eb50303ce2ea2 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarDaysGridsNavigation.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarDaysGridsNavigation.ts @@ -9,7 +9,7 @@ import { applyInitialFocusInGrid, navigateInGrid, NavigateInGridChangePage, - PageNavigationTarget, + PageGridNavigationTarget, } from '../utils/keyboardNavigation'; import type { CalendarRootContext } from './CalendarRootContext'; import { getFirstEnabledMonth, getLastEnabledMonth } from '../utils/date'; @@ -26,7 +26,7 @@ export function useCalendarDaysGridNavigation( const gridsRef = React.useRef< { cells: useCalendarDaysGridBody.CellsRef; rows: useCalendarDaysGridBody.RowsRef }[] >([]); - const pageNavigationTargetRef = React.useRef(null); + const pageNavigationTargetRef = React.useRef(null); const timeout = useTimeout(); React.useEffect(() => { diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts index ce144cec41006..f7acbaf28c11c 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts @@ -4,10 +4,12 @@ export function navigateInList({ cells, event, loop, + changePage, }: { cells: (HTMLElement | null)[]; event: React.KeyboardEvent; loop: boolean; + changePage: NavigateInListChangePage | undefined; }) { if (!LIST_NAVIGATION_SUPPORTED_KEYS.includes(event.key)) { return; @@ -29,15 +31,23 @@ export function navigateInList({ switch (event.key) { case 'ArrowDown': - if (loop) { - nextIndex = currentIndex + 1 > lastIndex ? 0 : currentIndex + 1; + if (currentIndex === lastIndex) { + if (changePage) { + changePage({ direction: 'next', target: { type: 'first-cell' } }); + } else { + nextIndex = loop ? 0 : -1; + } } else { - nextIndex = Math.min(currentIndex + 1, lastIndex); + nextIndex = currentIndex + 1; } break; case 'ArrowUp': - if (loop) { - nextIndex = currentIndex === 0 ? lastIndex : currentIndex - 1; + if (currentIndex === 0) { + if (changePage) { + changePage({ direction: 'previous', target: { type: 'last-cell' } }); + } else { + nextIndex = loop ? lastIndex : -1; + } } else { nextIndex = currentIndex - 1; } @@ -57,6 +67,35 @@ export function navigateInList({ } } +export type PageListNavigationTarget = { type: 'first-cell' } | { type: 'last-cell' }; + +export type NavigateInListChangePage = (params: { + direction: 'next' | 'previous'; + target: PageListNavigationTarget; +}) => void; + +export function applyInitialFocusInList({ + cells, + target, +}: { + cells: (HTMLElement | null)[]; + target: PageListNavigationTarget; +}) { + let cell: HTMLElement | undefined; + + if (target.type === 'first-cell') { + cell = cells.flat(2).find(isNavigable); + } + + if (target.type === 'last-cell') { + cell = cells.flat(2).findLast(isNavigable); + } + + if (cell) { + cell.focus(); + } +} + const GRID_NAVIGATION_SUPPORTED_KEYS = [ 'ArrowDown', 'ArrowUp', @@ -252,7 +291,7 @@ export function navigateInGrid({ } } -export type PageNavigationTarget = +export type PageGridNavigationTarget = | { type: 'first-cell' } | { type: 'last-cell' } | { type: 'first-cell-in-col'; colIndex: number } @@ -262,7 +301,7 @@ export type PageNavigationTarget = export type NavigateInGridChangePage = (params: { direction: 'next' | 'previous'; - target: PageNavigationTarget; + target: PageGridNavigationTarget; }) => void; export function applyInitialFocusInGrid({ @@ -270,7 +309,7 @@ export function applyInitialFocusInGrid({ target, }: { cells: HTMLElement[][][]; - target: PageNavigationTarget; + target: PageGridNavigationTarget; }) { let cell: HTMLElement | undefined; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/useMonthsCells.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/useMonthsCells.ts index 67a195de444c1..54e66bab3ce8b 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/utils/useMonthsCells.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/utils/useMonthsCells.ts @@ -3,6 +3,8 @@ import { PickerValidDate } from '../../../../models'; import { getMonthsInYear } from '../../../utils/date-utils'; import { useUtils } from '../../../hooks/useUtils'; import { useCalendarRootContext } from '../root/CalendarRootContext'; +import { getFirstEnabledYear, getLastEnabledYear } from './date'; +import { PageGridNavigationTarget, PageListNavigationTarget } from './keyboardNavigation'; export function useMonthsCells(): useMonthsCells.ReturnValue { const rootContext = useCalendarRootContext(); @@ -15,16 +17,55 @@ export function useMonthsCells(): useMonthsCells.ReturnValue { const months = React.useMemo(() => getMonthsInYear(utils, currentYear), [utils, currentYear]); + const changePage = (direction: 'next' | 'previous') => { + // TODO: Jump over months with no valid date. + if (direction === 'previous') { + const targetDate = utils.addYears( + utils.startOfYear(rootContext.visibleDate), + -rootContext.yearPageSize, + ); + const lastYearInNewPage = utils.addYears(targetDate, rootContext.yearPageSize - 1); + + // All the years before the visible ones are fully disabled, we skip the navigation. + if ( + utils.isAfter(getFirstEnabledYear(utils, rootContext.validationProps), lastYearInNewPage) + ) { + return; + } + + rootContext.setVisibleDate( + utils.addYears(rootContext.visibleDate, -rootContext.yearPageSize), + false, + ); + } + if (direction === 'next') { + const targetDate = utils.addYears( + utils.startOfYear(rootContext.visibleDate), + rootContext.yearPageSize, + ); + + // All the years after the visible ones are fully disabled, we skip the navigation. + if (utils.isBefore(getLastEnabledYear(utils, rootContext.validationProps), targetDate)) { + return; + } + rootContext.setVisibleDate( + utils.addYears(rootContext.visibleDate, rootContext.yearPageSize), + false, + ); + } + }; + const registerSection = rootContext.registerSection; React.useEffect(() => { return registerSection({ type: 'month', value: currentYear }); }, [registerSection, currentYear]); - return { months }; + return { months, changePage }; } export namespace useMonthsCells { export interface ReturnValue { months: PickerValidDate[]; + changePage: (direction: 'next' | 'previous') => void; } } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-list/useCalendarYearsList.ts b/packages/x-date-pickers/src/internals/base/Calendar/years-list/useCalendarYearsList.ts index e311512780294..de648aac82919 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-list/useCalendarYearsList.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-list/useCalendarYearsList.ts @@ -16,6 +16,7 @@ export function useCalendarYearsList(parameters: useCalendarYearsList.Parameters cells: yearsCellRefs.current, event, loop, + changePage: undefined, }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1d209da1fd8fc..93b830286dca2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -410,6 +410,9 @@ importers: '@babel/runtime-corejs2': specifier: ^7.26.0 version: 7.26.0 + '@base-ui-components/react': + specifier: 1.0.0-alpha.4 + version: 1.0.0-alpha.4(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@docsearch/react': specifier: ^3.8.2 version: 3.8.2(@algolia/client-search@5.15.0)(@types/react@19.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(search-insights@2.17.3) From 0576bca3c935825151ed5b9d518c857a4b437b91 Mon Sep 17 00:00:00 2001 From: flavien Date: Tue, 7 Jan 2025 10:19:30 +0100 Subject: [PATCH 067/136] Work --- .../MonthCalendarWithCustomCellFormatDemo.js | 55 +++++++++++++++++++ .../MonthCalendarWithCustomCellFormatDemo.tsx | 55 +++++++++++++++++++ .../base-calendar/base-calendar.md | 4 ++ .../base-calendar/calendar.module.css | 4 ++ .../base/Calendar/utils/keyboardNavigation.ts | 2 +- 5 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 docs/data/date-pickers/base-calendar/MonthCalendarWithCustomCellFormatDemo.js create mode 100644 docs/data/date-pickers/base-calendar/MonthCalendarWithCustomCellFormatDemo.tsx diff --git a/docs/data/date-pickers/base-calendar/MonthCalendarWithCustomCellFormatDemo.js b/docs/data/date-pickers/base-calendar/MonthCalendarWithCustomCellFormatDemo.js new file mode 100644 index 0000000000000..1d22853691634 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/MonthCalendarWithCustomCellFormatDemo.js @@ -0,0 +1,55 @@ +import * as React from 'react'; +import clsx from 'clsx'; + +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +// eslint-disable-next-line no-restricted-imports +import { + Calendar, + useCalendarContext, +} from '@mui/x-date-pickers/internals/base/Calendar'; +import styles from './calendar.module.css'; + +function Header() { + const { visibleDate } = useCalendarContext(); + + return ( +
+ + ◀ + + {visibleDate.format('YYYY')} + + ▶ + +
+ ); +} + +export default function MonthCalendarWithCustomCellFormatDemo() { + const [value, setValue] = React.useState(null); + + return ( + + +
+ + {({ months }) => + months.map((month) => ( + + )) + } + + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/MonthCalendarWithCustomCellFormatDemo.tsx b/docs/data/date-pickers/base-calendar/MonthCalendarWithCustomCellFormatDemo.tsx new file mode 100644 index 0000000000000..e4f45d97071fb --- /dev/null +++ b/docs/data/date-pickers/base-calendar/MonthCalendarWithCustomCellFormatDemo.tsx @@ -0,0 +1,55 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import { Dayjs } from 'dayjs'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +// eslint-disable-next-line no-restricted-imports +import { + Calendar, + useCalendarContext, +} from '@mui/x-date-pickers/internals/base/Calendar'; +import styles from './calendar.module.css'; + +function Header() { + const { visibleDate } = useCalendarContext(); + + return ( +
+ + ◀ + + {visibleDate.format('YYYY')} + + ▶ + +
+ ); +} + +export default function MonthCalendarWithCustomCellFormatDemo() { + const [value, setValue] = React.useState(null); + + return ( + + +
+ + {({ months }) => + months.map((month) => ( + + )) + } + + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/base-calendar.md b/docs/data/date-pickers/base-calendar/base-calendar.md index 62b1ba537f717..60f6eef1292f8 100644 --- a/docs/data/date-pickers/base-calendar/base-calendar.md +++ b/docs/data/date-pickers/base-calendar/base-calendar.md @@ -36,6 +36,10 @@ packageName: '@mui/x-date-pickers' {{"demo": "MonthCalendarWithListLayoutDemo.js"}} +### Custom cell format + +{{"demo": "MonthCalendarWithCustomCellFormatDemo.js"}} + ### Multiple visible years TODO diff --git a/docs/data/date-pickers/base-calendar/calendar.module.css b/docs/data/date-pickers/base-calendar/calendar.module.css index 38565d40ab4f5..0dcac2ea30521 100644 --- a/docs/data/date-pickers/base-calendar/calendar.module.css +++ b/docs/data/date-pickers/base-calendar/calendar.module.css @@ -12,6 +12,10 @@ width: 312px; } +.RootShort { + height: 204px; +} + .RootWithTwoPanels { width: 552px; flex-direction: row; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts index f7acbaf28c11c..3e1f4773dddb8 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts @@ -350,7 +350,7 @@ function isNavigable(element: HTMLElement | null): element is HTMLElement { return false; } - if (element.dataset['outside-month'] != null) { + if (element.dataset.outsideMonth != null) { return false; } From 3ca7af1be9adac353495a9dbeaa04f2552c52b6f Mon Sep 17 00:00:00 2001 From: flavien Date: Tue, 7 Jan 2025 10:21:08 +0100 Subject: [PATCH 068/136] Fix --- .../src/internals/base/Calendar/utils/keyboardNavigation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts index 3e1f4773dddb8..7bc17ac9c9b3c 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts @@ -350,7 +350,7 @@ function isNavigable(element: HTMLElement | null): element is HTMLElement { return false; } - if (element.dataset.outsideMonth != null) { + if (element.getAttribute('data-outside-month') != null) { return false; } From 9bf48704eb5de2309244585fd8a6f49f60bcc93c Mon Sep 17 00:00:00 2001 From: flavien Date: Tue, 7 Jan 2025 11:02:53 +0100 Subject: [PATCH 069/136] Work --- .../base/Calendar/months-grid/CalendarMonthsGrid.tsx | 3 ++- .../base/Calendar/months-grid/useCalendarMonthsGrid.ts | 10 ++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-grid/CalendarMonthsGrid.tsx b/packages/x-date-pickers/src/internals/base/Calendar/months-grid/CalendarMonthsGrid.tsx index d7e3739fcb3be..120fb24525f74 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-grid/CalendarMonthsGrid.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-grid/CalendarMonthsGrid.tsx @@ -9,10 +9,11 @@ const CalendarMonthsGrid = React.forwardRef(function CalendarMonthsList( props: CalendarMonthsGrid.Props, forwardedRef: React.ForwardedRef, ) { - const { className, render, children, cellsPerRow, ...otherProps } = props; + const { className, render, children, cellsPerRow, canChangeYear, ...otherProps } = props; const { getMonthsGridProps, monthsCellRefs } = useCalendarMonthsGrid({ children, cellsPerRow, + canChangeYear, }); const state = React.useMemo(() => ({}), []); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-grid/useCalendarMonthsGrid.ts b/packages/x-date-pickers/src/internals/base/Calendar/months-grid/useCalendarMonthsGrid.ts index 14221bd724fbb..5a1b4e979f9df 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-grid/useCalendarMonthsGrid.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-grid/useCalendarMonthsGrid.ts @@ -15,7 +15,7 @@ import { useCalendarRootContext } from '../root/CalendarRootContext'; import { CalendarMonthsGridCssVars } from './CalendarMonthsGridCssVars'; export function useCalendarMonthsGrid(parameters: useCalendarMonthsGrid.Parameters) { - const { children, cellsPerRow } = parameters; + const { children, cellsPerRow, canChangeYear = true } = parameters; const rootContext = useCalendarRootContext(); const monthsCellRefs = React.useRef<(HTMLElement | null)[]>([]); const { months, changePage } = useMonthsCells(); @@ -60,7 +60,7 @@ export function useCalendarMonthsGrid(parameters: useCalendarMonthsGrid.Paramete navigateInGrid({ cells: getCellsInCalendar(), event, - changePage: changeGridPage, + changePage: canChangeYear ? changeGridPage : undefined, }); }); @@ -91,6 +91,12 @@ export namespace useCalendarMonthsGrid { * This is used to make sure the keyboard navigation works correctly. */ cellsPerRow: number; + /** + * Whether to go to the previous / next year + * when the end of the list is reached while using the arrow keys. + * @default true + */ + canChangeYear?: boolean; children?: (parameters: ChildrenParameters) => React.ReactNode; } From c3f8e0f831e0640201c918b59f9365645b50b4be Mon Sep 17 00:00:00 2001 From: flavien Date: Tue, 7 Jan 2025 11:29:49 +0100 Subject: [PATCH 070/136] Work --- .../base/Calendar/days-cell/CalendarDaysCell.tsx | 10 +++++++++- .../days-cell/CalendarDaysCellDataAttributes.ts | 4 ++++ .../base/Calendar/months-cell/CalendarMonthsCell.tsx | 12 ++++++++++-- .../months-cell/CalendarMonthsCellDataAttributes.ts | 4 ++++ .../base/Calendar/years-cell/CalendarYearsCell.tsx | 12 ++++++++++-- .../years-cell/CalendarYearsCellDataAttributes.ts | 4 ++++ 6 files changed, 41 insertions(+), 5 deletions(-) diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx index e1fef03ce9b35..cf2d971a1c73e 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx @@ -15,6 +15,9 @@ const customStyleHookMapping: CustomStyleHookMapping = { selected(value) { return value ? { [CalendarDaysCellDataAttributes.selected]: '' } : null; }, + disabled(value) { + return value ? { [CalendarDaysCellDataAttributes.disabled]: '' } : null; + }, current(value) { return value ? { [CalendarDaysCellDataAttributes.current]: '' } : null; }, @@ -33,10 +36,11 @@ const InnerCalendarDaysCell = React.forwardRef(function CalendarDaysGrid( const state: CalendarDaysCell.State = React.useMemo( () => ({ selected: ctx.isSelected, + disabled: ctx.isDisabled, outsideMonth: ctx.isOutsideCurrentMonth, current: isCurrent, }), - [ctx.isSelected, ctx.isOutsideCurrentMonth, isCurrent], + [ctx.isSelected, ctx.isDisabled, ctx.isOutsideCurrentMonth, isCurrent], ); const { renderElement } = useComponentRenderer({ @@ -122,6 +126,10 @@ export namespace CalendarDaysCell { * Whether the day is selected. */ selected: boolean; + /** + * Whether the day is disabled. + */ + disabled: boolean; /** * Whether the day contains the current date. */ diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCellDataAttributes.ts b/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCellDataAttributes.ts index 1a44944bc6d60..6bdbd3616a2d5 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCellDataAttributes.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCellDataAttributes.ts @@ -3,6 +3,10 @@ export enum CalendarDaysCellDataAttributes { * Present when the day is selected. */ selected = 'data-selected', + /** + * Present when the day is disabled. + */ + disabled = 'data-disabled', /** * Present when the day contains the current date. */ diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx index 30ae086e21715..606f311e080c0 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx @@ -19,8 +19,8 @@ const InnerCalendarMonthsCell = React.forwardRef(function InnerCalendarMonthsCel const { getMonthsCellProps, isCurrent } = useCalendarMonthsCell({ value, format, ctx }); const state: CalendarMonthsCell.State = React.useMemo( - () => ({ selected: ctx.isSelected, current: isCurrent }), - [ctx.isSelected, isCurrent], + () => ({ selected: ctx.isSelected, disabled: ctx.isDisabled, current: isCurrent }), + [ctx.isSelected, ctx.isDisabled, isCurrent], ); const { renderElement } = useComponentRenderer({ @@ -151,6 +151,14 @@ export namespace CalendarMonthsCell { * Whether the month is selected. */ selected: boolean; + /** + * Whether the month is disabled. + */ + disabled: boolean; + /** + * Whether the month contains the current date. + */ + current: boolean; } export interface Props diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCellDataAttributes.ts b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCellDataAttributes.ts index 69d2c63983d10..379deb6332fbb 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCellDataAttributes.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCellDataAttributes.ts @@ -3,6 +3,10 @@ export enum CalendarMonthsCellDataAttributes { * Present when the month is selected. */ selected = 'data-selected', + /** + * Present when the month is disabled. + */ + disabled = 'data-disabled', /** * Present when the month contains the current date. */ diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx index d79c01bab1cab..2ab1a3d7a06d7 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx @@ -19,8 +19,8 @@ const InnerCalendarYearsCell = React.forwardRef(function InnerCalendarYearsCell( const { getYearCellProps, isCurrent } = useCalendarYearsCell({ value, format, ctx }); const state: CalendarYearsCell.State = React.useMemo( - () => ({ selected: ctx.isSelected, current: isCurrent }), - [ctx.isSelected, isCurrent], + () => ({ selected: ctx.isSelected, disabled: ctx.isDisabled, current: isCurrent }), + [ctx.isSelected, ctx.isDisabled, isCurrent], ); const { renderElement } = useComponentRenderer({ @@ -146,6 +146,14 @@ export namespace CalendarYearsCell { * Whether the year is selected. */ selected: boolean; + /** + * Whether the year is disabled. + */ + disabled: boolean; + /** + * Whether the year contains the current date. + */ + current: boolean; } export interface Props diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCellDataAttributes.ts b/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCellDataAttributes.ts index 306aef5ec9002..cb79a217d442d 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCellDataAttributes.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCellDataAttributes.ts @@ -3,6 +3,10 @@ export enum CalendarYearsCellDataAttributes { * Present when the year is selected. */ selected = 'data-selected', + /** + * Present when the year is disabled. + */ + disabled = 'data-disabled', /** * Present when the year contains the current date. */ From 3487e5b977f7ba61efd9091f50e34231b7f084d3 Mon Sep 17 00:00:00 2001 From: flavien Date: Tue, 7 Jan 2025 11:33:33 +0100 Subject: [PATCH 071/136] Work --- .../base/Calendar/days-cell/CalendarDaysCell.tsx | 10 +++++++--- .../days-cell/CalendarDaysCellDataAttributes.ts | 6 +++++- .../base/Calendar/days-cell/useCalendarDaysCell.ts | 1 + .../base/Calendar/months-cell/CalendarMonthsCell.tsx | 4 ++-- .../base/Calendar/root/CalendarRootContext.ts | 2 +- .../internals/base/Calendar/root/useCalendarRoot.ts | 7 ++++--- .../base/Calendar/years-cell/CalendarYearsCell.tsx | 4 ++-- 7 files changed, 22 insertions(+), 12 deletions(-) diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx index cf2d971a1c73e..e93b36800c5b6 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx @@ -81,14 +81,16 @@ const CalendarDaysCell = React.forwardRef(function CalendarDaysCell( [monthsListContext.currentMonth, props.value, utils], ); - const isDateDisabled = rootContext.isDateDisabled; + const isDateInvalid = rootContext.isDateInvalid; + const isInvalid = React.useMemo(() => isDateInvalid(props.value), [props.value, isDateInvalid]); + const isDisabled = React.useMemo(() => { if (rootContext.disabled) { return true; } - return isDateDisabled(props.value); - }, [rootContext.disabled, isDateDisabled, props.value]); + return isInvalid; + }, [rootContext.disabled, isInvalid]); const isTabbable = React.useMemo( () => @@ -103,6 +105,7 @@ const CalendarDaysCell = React.forwardRef(function CalendarDaysCell( colIndex, isSelected, isDisabled, + isInvalid, isTabbable, isOutsideCurrentMonth, selectDay: monthsListContext.selectDay, @@ -110,6 +113,7 @@ const CalendarDaysCell = React.forwardRef(function CalendarDaysCell( [ isSelected, isDisabled, + isInvalid, isTabbable, isOutsideCurrentMonth, monthsListContext.selectDay, diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCellDataAttributes.ts b/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCellDataAttributes.ts index 6bdbd3616a2d5..85ca9259ac7bb 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCellDataAttributes.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCellDataAttributes.ts @@ -8,7 +8,11 @@ export enum CalendarDaysCellDataAttributes { */ disabled = 'data-disabled', /** - * Present when the day contains the current date. + * Present when the day is invalid. + */ + invalid = 'data-invalid', + /** + * Present when the day is the current date. */ current = 'data-current', /** diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-cell/useCalendarDaysCell.ts b/packages/x-date-pickers/src/internals/base/Calendar/days-cell/useCalendarDaysCell.ts index d3484f1f7dd98..60af135e92a66 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-cell/useCalendarDaysCell.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-cell/useCalendarDaysCell.ts @@ -62,6 +62,7 @@ export namespace useCalendarDaysCell { colIndex: number; isSelected: boolean; isDisabled: boolean; + isInvalid: boolean; isTabbable: boolean; isOutsideCurrentMonth: boolean; selectDay: (value: PickerValidDate) => void; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx index 606f311e080c0..42d2b47d836b3 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx @@ -109,7 +109,7 @@ const CalendarMonthsCell = React.forwardRef(function CalendarMonthsCell( const startOfMonth = utils.startOfMonth(newCleanValue); const endOfMonth = utils.endOfMonth(newCleanValue); - const closestEnabledDate = rootContext.isDateDisabled(newCleanValue) + const closestEnabledDate = rootContext.isDateInvalid(newCleanValue) ? findClosestEnabledDate({ utils, date: newCleanValue, @@ -121,7 +121,7 @@ const CalendarMonthsCell = React.forwardRef(function CalendarMonthsCell( : rootContext.validationProps.maxDate, disablePast: rootContext.validationProps.disablePast, disableFuture: rootContext.validationProps.disableFuture, - isDateDisabled: rootContext.isDateDisabled, + isDateDisabled: rootContext.isDateInvalid, timezone: rootContext.timezone, }) : newCleanValue; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts index f8c223d68cd46..46cc34948f58f 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts @@ -27,7 +27,7 @@ export interface CalendarRootContext { disabled: boolean; readOnly: boolean; autoFocus: boolean; - isDateDisabled: (day: PickerValidDate | null) => boolean; + isDateInvalid: (day: PickerValidDate | null) => boolean; validationProps: ValidateDateProps; visibleDate: PickerValidDate; setVisibleDate: (visibleDate: PickerValidDate, skipIfAlreadyVisible: boolean) => void; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts index ff50a7cb3c040..9ab747f09a005 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts @@ -115,7 +115,8 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { validator: validateDate, }); - const isDateDisabled = useIsDateDisabled({ + // TODO: Rename this hook (if we keep it for Base UI X) + const isDateInvalid = useIsDateDisabled({ ...validationProps, timezone, }); @@ -192,7 +193,7 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { disabled, readOnly, autoFocus, - isDateDisabled, + isDateInvalid, validationProps, visibleDate, setVisibleDate: handleVisibleDateChange, @@ -210,7 +211,7 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { disabled, readOnly, autoFocus, - isDateDisabled, + isDateInvalid, validationProps, visibleDate, handleVisibleDateChange, diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx index 2ab1a3d7a06d7..5a2c3390cb5b7 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx @@ -105,7 +105,7 @@ const CalendarYearsCell = React.forwardRef(function CalendarsYearCell( const startOfYear = utils.startOfYear(newCleanValue); const endOfYear = utils.endOfYear(newCleanValue); - const closestEnabledDate = rootContext.isDateDisabled(newCleanValue) + const closestEnabledDate = rootContext.isDateInvalid(newCleanValue) ? findClosestEnabledDate({ utils, date: newCleanValue, @@ -117,7 +117,7 @@ const CalendarYearsCell = React.forwardRef(function CalendarsYearCell( : rootContext.validationProps.maxDate, disablePast: rootContext.validationProps.disablePast, disableFuture: rootContext.validationProps.disableFuture, - isDateDisabled: rootContext.isDateDisabled, + isDateDisabled: rootContext.isDateInvalid, timezone: rootContext.timezone, }) : newCleanValue; From 5f455feac6330413a13c734ee9af2866e7857049 Mon Sep 17 00:00:00 2001 From: flavien Date: Tue, 7 Jan 2025 12:52:11 +0100 Subject: [PATCH 072/136] Add isInvalid state and data attr --- .../Calendar/days-cell/CalendarDaysCell.tsx | 7 +++- .../months-cell/CalendarMonthsCell.tsx | 32 +++++++++++++----- .../CalendarMonthsCellDataAttributes.ts | 4 +++ .../months-cell/useCalendarMonthsCell.ts | 1 + .../Calendar/years-cell/CalendarYearsCell.tsx | 33 ++++++++++++++----- .../CalendarYearsCellDataAttributes.ts | 4 +++ .../years-cell/useCalendarYearsCell.ts | 1 + 7 files changed, 63 insertions(+), 19 deletions(-) diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx index e93b36800c5b6..970d7a97e89c3 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx @@ -37,10 +37,11 @@ const InnerCalendarDaysCell = React.forwardRef(function CalendarDaysGrid( () => ({ selected: ctx.isSelected, disabled: ctx.isDisabled, + invalid: ctx.isInvalid, outsideMonth: ctx.isOutsideCurrentMonth, current: isCurrent, }), - [ctx.isSelected, ctx.isDisabled, ctx.isOutsideCurrentMonth, isCurrent], + [ctx.isSelected, ctx.isDisabled, ctx.isInvalid, ctx.isOutsideCurrentMonth, isCurrent], ); const { renderElement } = useComponentRenderer({ @@ -134,6 +135,10 @@ export namespace CalendarDaysCell { * Whether the day is disabled. */ disabled: boolean; + /** + * Whether the day is invalid. + */ + invalid: boolean; /** * Whether the day contains the current date. */ diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx index 42d2b47d836b3..ecd0dc3a5a05d 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx @@ -19,8 +19,13 @@ const InnerCalendarMonthsCell = React.forwardRef(function InnerCalendarMonthsCel const { getMonthsCellProps, isCurrent } = useCalendarMonthsCell({ value, format, ctx }); const state: CalendarMonthsCell.State = React.useMemo( - () => ({ selected: ctx.isSelected, disabled: ctx.isDisabled, current: isCurrent }), - [ctx.isSelected, ctx.isDisabled, isCurrent], + () => ({ + selected: ctx.isSelected, + disabled: ctx.isDisabled, + invlid: ctx.isInvalid, + current: isCurrent, + }), + [ctx.isSelected, ctx.isDisabled, ctx.isInvalid, isCurrent], ); const { renderElement } = useComponentRenderer({ @@ -52,11 +57,7 @@ const CalendarMonthsCell = React.forwardRef(function CalendarMonthsCell( [rootContext.value, props.value, utils], ); - const isDisabled = React.useMemo(() => { - if (rootContext.disabled) { - return true; - } - + const isInvalid = React.useMemo(() => { const firstEnabledMonth = utils.startOfMonth( rootContext.validationProps.disablePast && utils.isAfter(now, rootContext.validationProps.minDate) @@ -86,7 +87,15 @@ const CalendarMonthsCell = React.forwardRef(function CalendarMonthsCell( } return rootContext.validationProps.shouldDisableMonth(monthToValidate); - }, [rootContext.disabled, rootContext.validationProps, props.value, now, utils]); + }, [rootContext.validationProps, props.value, now, utils]); + + const isDisabled = React.useMemo(() => { + if (rootContext.disabled) { + return true; + } + + return isInvalid; + }, [rootContext.disabled, isInvalid]); const isTabbable = React.useMemo( () => @@ -136,10 +145,11 @@ const CalendarMonthsCell = React.forwardRef(function CalendarMonthsCell( () => ({ isSelected, isDisabled, + isInvalid, isTabbable, selectMonth, }), - [isSelected, isDisabled, isTabbable, selectMonth], + [isSelected, isDisabled, isInvalid, isTabbable, selectMonth], ); return ; @@ -155,6 +165,10 @@ export namespace CalendarMonthsCell { * Whether the month is disabled. */ disabled: boolean; + /** + * Whether the month is invalid. + */ + invalid: boolean; /** * Whether the month contains the current date. */ diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCellDataAttributes.ts b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCellDataAttributes.ts index 379deb6332fbb..c9a4229fa99cc 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCellDataAttributes.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCellDataAttributes.ts @@ -7,6 +7,10 @@ export enum CalendarMonthsCellDataAttributes { * Present when the month is disabled. */ disabled = 'data-disabled', + /** + * Present when the month is invalid. + */ + invalid = 'data-invalid', /** * Present when the month contains the current date. */ diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/useCalendarMonthsCell.ts b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/useCalendarMonthsCell.ts index 4696d4ecf1361..2d7ecac3476e9 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/useCalendarMonthsCell.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/useCalendarMonthsCell.ts @@ -53,6 +53,7 @@ export namespace useCalendarMonthsCell { export interface Context { isSelected: boolean; isDisabled: boolean; + isInvalid: boolean; isTabbable: boolean; selectMonth: (value: PickerValidDate) => void; } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx index 5a2c3390cb5b7..5494d4ccbaad3 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx @@ -19,8 +19,13 @@ const InnerCalendarYearsCell = React.forwardRef(function InnerCalendarYearsCell( const { getYearCellProps, isCurrent } = useCalendarYearsCell({ value, format, ctx }); const state: CalendarYearsCell.State = React.useMemo( - () => ({ selected: ctx.isSelected, disabled: ctx.isDisabled, current: isCurrent }), - [ctx.isSelected, ctx.isDisabled, isCurrent], + () => ({ + selected: ctx.isSelected, + disabled: ctx.isDisabled, + invalid: ctx.isInvalid, + current: isCurrent, + }), + [ctx.isSelected, ctx.isDisabled, ctx.isInvalid, isCurrent], ); const { renderElement } = useComponentRenderer({ @@ -52,11 +57,7 @@ const CalendarYearsCell = React.forwardRef(function CalendarsYearCell( [rootContext.value, props.value, utils], ); - const isDisabled = React.useMemo(() => { - if (rootContext.disabled) { - return true; - } - + const isInvalid = React.useMemo(() => { if (rootContext.validationProps.disablePast && utils.isBeforeYear(props.value, now)) { return true; } @@ -81,8 +82,17 @@ const CalendarYearsCell = React.forwardRef(function CalendarsYearCell( } const yearToValidate = utils.startOfYear(props.value); + return rootContext.validationProps.shouldDisableYear(yearToValidate); - }, [rootContext.disabled, rootContext.validationProps, props.value, now, utils]); + }, [rootContext.validationProps, props.value, now, utils]); + + const isDisabled = React.useMemo(() => { + if (rootContext.disabled) { + return true; + } + + return isInvalid; + }, [rootContext.disabled, isInvalid]); const isTabbable = React.useMemo( () => @@ -131,10 +141,11 @@ const CalendarYearsCell = React.forwardRef(function CalendarsYearCell( () => ({ isSelected, isDisabled, + isInvalid, isTabbable, selectYear, }), - [isSelected, isDisabled, isTabbable, selectYear], + [isSelected, isDisabled, isInvalid, isTabbable, selectYear], ); return ; @@ -150,6 +161,10 @@ export namespace CalendarYearsCell { * Whether the year is disabled. */ disabled: boolean; + /** + * Whether the year is invalid. + */ + invalid: boolean; /** * Whether the year contains the current date. */ diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCellDataAttributes.ts b/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCellDataAttributes.ts index cb79a217d442d..784444f4795da 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCellDataAttributes.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCellDataAttributes.ts @@ -7,6 +7,10 @@ export enum CalendarYearsCellDataAttributes { * Present when the year is disabled. */ disabled = 'data-disabled', + /** + * Present when the year is invalid. + */ + invalid = 'data-invalid', /** * Present when the year contains the current date. */ diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-cell/useCalendarYearsCell.ts b/packages/x-date-pickers/src/internals/base/Calendar/years-cell/useCalendarYearsCell.ts index 0dd6845e8d6c8..4eccc7801b2de 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-cell/useCalendarYearsCell.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-cell/useCalendarYearsCell.ts @@ -53,6 +53,7 @@ export namespace useCalendarYearsCell { export interface Context { isSelected: boolean; isDisabled: boolean; + isInvalid: boolean; isTabbable: boolean; selectYear: (value: PickerValidDate) => void; } From d236ff88242ec317ca3ace332ae0c4409ed51596 Mon Sep 17 00:00:00 2001 From: flavien Date: Tue, 7 Jan 2025 14:46:41 +0100 Subject: [PATCH 073/136] Start working on the range calendar --- .../base/RangeCalendar/index.parts.ts | 23 ++ .../src/internals/base/RangeCalendar/index.ts | 2 + .../RangeCalendar/root/RangeCalendarRoot.tsx | 92 ++++++ .../root/RangeCalendarRootContext.ts | 41 +++ .../root/useRangeCalendarRoot.tsx | 83 +++++ .../useRangeCalendarContext/index.ts | 1 + .../useRangeCalendarContext.ts | 11 + .../Calendar/days-cell/CalendarDaysCell.tsx | 18 +- .../Calendar/days-cell/useCalendarDaysCell.ts | 4 +- .../days-grid-body/CalendarDaysGridBody.tsx | 16 +- .../CalendarDaysGridBodyContext.ts | 26 -- .../CalendarDaysGridHeaderCell.tsx | 4 +- .../useCalendarDaysGridHeaderCell.ts | 4 +- .../CalendarDaysGridHeader.tsx | 4 +- .../useCalendarDaysGridHeader.ts | 8 +- .../Calendar/days-grid/CalendarDaysGrid.tsx | 16 +- .../days-grid/CalendarDaysGridContext.ts | 27 -- .../days-week-row/CalendarDaysWeekRow.tsx | 20 +- .../days-week-row/useCalendarDaysWeekRow.ts | 6 +- .../months-cell/CalendarMonthsCell.tsx | 30 +- .../months-cell/useCalendarMonthsCell.ts | 4 +- .../months-grid/CalendarMonthsGrid.tsx | 4 +- .../months-grid/useCalendarMonthsGrid.ts | 10 +- .../months-list/CalendarMonthsList.tsx | 4 +- .../months-list/useCalendarMonthsList.ts | 10 +- .../base/Calendar/root/CalendarRoot.tsx | 16 +- .../base/Calendar/root/CalendarRootContext.ts | 31 +- .../base/Calendar/root/useCalendarRoot.ts | 304 +++--------------- .../CalendarSetVisibleMonth.tsx | 35 +- .../useCalendarSetVisibleMonth.ts | 4 +- .../CalendarSetVisibleYear.tsx | 32 +- .../useCalendarSetVisibleYear.ts | 4 +- .../useCalendarContext/useCalendarContext.ts | 6 +- .../src/internals/base/Calendar/utils/date.ts | 9 +- .../base/Calendar/utils/useMonthsCells.ts | 36 ++- .../base/Calendar/utils/useYearsCells.ts | 16 +- .../Calendar/years-cell/CalendarYearsCell.tsx | 28 +- .../years-cell/useCalendarYearsCell.ts | 4 +- .../Calendar/years-grid/CalendarYearsGrid.tsx | 4 +- .../years-grid/useCalendarYearsGrid.ts | 4 +- .../Calendar/years-list/CalendarYearsList.tsx | 4 +- .../years-list/useCalendarYearsList.ts | 4 +- .../defaultRenderFunctions.tsx | 0 .../evaluateRenderProp.ts | 0 .../fastObjectShallowCompare.ts | 0 .../getStyleHookProps.ts | 0 .../{utils => base-utils}/mergeReactProps.ts | 0 .../{utils => base-utils}/reactVersion.ts | 0 .../{utils => base-utils}/resolveClassName.ts | 0 .../base/{utils => base-utils}/types.ts | 0 .../useComponentRenderer.ts | 0 .../useEnhancedEffect.ts | 0 .../{utils => base-utils}/useEventCallback.ts | 0 .../base/{utils => base-utils}/useForkRef.ts | 0 .../useRenderPropForkRef.ts | 0 .../base/composite/list/CompositeList.tsx | 4 +- .../BaseCalendarDaysGridBodyContext.ts | 29 ++ .../useBaseCalendarDaysGridBody.ts} | 34 +- .../days-grid/BaseCalendarDaysGridContext.ts | 30 ++ .../days-grid/useBaseCalendarDaysGrid.ts} | 38 +-- .../root/BaseCalendarRootContext.ts | 45 +++ .../useBaseCalendarDaysGridsNavigation.ts} | 25 +- .../base-calendar/root/useBaseCalendarRoot.ts | 288 +++++++++++++++++ 63 files changed, 935 insertions(+), 567 deletions(-) create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/index.parts.ts create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/index.ts create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRoot.tsx create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/useRangeCalendarContext/index.ts create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/useRangeCalendarContext/useRangeCalendarContext.ts delete mode 100644 packages/x-date-pickers/src/internals/base/Calendar/days-grid-body/CalendarDaysGridBodyContext.ts delete mode 100644 packages/x-date-pickers/src/internals/base/Calendar/days-grid/CalendarDaysGridContext.ts rename packages/x-date-pickers/src/internals/base/{utils => base-utils}/defaultRenderFunctions.tsx (100%) rename packages/x-date-pickers/src/internals/base/{utils => base-utils}/evaluateRenderProp.ts (100%) rename packages/x-date-pickers/src/internals/base/{utils => base-utils}/fastObjectShallowCompare.ts (100%) rename packages/x-date-pickers/src/internals/base/{utils => base-utils}/getStyleHookProps.ts (100%) rename packages/x-date-pickers/src/internals/base/{utils => base-utils}/mergeReactProps.ts (100%) rename packages/x-date-pickers/src/internals/base/{utils => base-utils}/reactVersion.ts (100%) rename packages/x-date-pickers/src/internals/base/{utils => base-utils}/resolveClassName.ts (100%) rename packages/x-date-pickers/src/internals/base/{utils => base-utils}/types.ts (100%) rename packages/x-date-pickers/src/internals/base/{utils => base-utils}/useComponentRenderer.ts (100%) rename packages/x-date-pickers/src/internals/base/{utils => base-utils}/useEnhancedEffect.ts (100%) rename packages/x-date-pickers/src/internals/base/{utils => base-utils}/useEventCallback.ts (100%) rename packages/x-date-pickers/src/internals/base/{utils => base-utils}/useForkRef.ts (100%) rename packages/x-date-pickers/src/internals/base/{utils => base-utils}/useRenderPropForkRef.ts (100%) create mode 100644 packages/x-date-pickers/src/internals/base/utils/base-calendar/days-grid-body/BaseCalendarDaysGridBodyContext.ts rename packages/x-date-pickers/src/internals/base/{Calendar/days-grid-body/useCalendarDaysGridBody.ts => utils/base-calendar/days-grid-body/useBaseCalendarDaysGridBody.ts} (55%) create mode 100644 packages/x-date-pickers/src/internals/base/utils/base-calendar/days-grid/BaseCalendarDaysGridContext.ts rename packages/x-date-pickers/src/internals/base/{Calendar/days-grid/useCalendarDaysGrid.ts => utils/base-calendar/days-grid/useBaseCalendarDaysGrid.ts} (69%) create mode 100644 packages/x-date-pickers/src/internals/base/utils/base-calendar/root/BaseCalendarRootContext.ts rename packages/x-date-pickers/src/internals/base/{Calendar/root/useCalendarDaysGridsNavigation.ts => utils/base-calendar/root/useBaseCalendarDaysGridsNavigation.ts} (84%) create mode 100644 packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarRoot.ts diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/index.parts.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/index.parts.ts new file mode 100644 index 0000000000000..c0427819f848b --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/index.parts.ts @@ -0,0 +1,23 @@ +export { RangeCalendarRoot as Root } from './root/RangeCalendarRoot'; + +// // Days +// export { CalendarDaysGrid as DaysGrid } from './days-grid/CalendarDaysGrid'; +// export { CalendarDaysGridHeader as DaysGridHeader } from './days-grid-header/CalendarDaysGridHeader'; +// export { CalendarDaysGridHeaderCell as DaysGridHeaderCell } from './days-grid-header-cell/CalendarDaysGridHeaderCell'; +// export { CalendarDaysGridBody as DaysGridBody } from './days-grid-body/CalendarDaysGridBody'; +// export { CalendarDaysWeekRow as DaysWeekRow } from './days-week-row/CalendarDaysWeekRow'; +// export { CalendarDaysCell as DaysCell } from './days-cell/CalendarDaysCell'; + +// // Months +// export { CalendarMonthsList as MonthsList } from './months-list/CalendarMonthsList'; +// export { CalendarMonthsGrid as MonthsGrid } from './months-grid/CalendarMonthsGrid'; +// export { CalendarMonthsCell as MonthsCell } from './months-cell/CalendarMonthsCell'; + +// // Years +// export { CalendarYearsList as YearsList } from './years-list/CalendarYearsList'; +// export { CalendarYearsGrid as YearsGrid } from './years-grid/CalendarYearsGrid'; +// export { CalendarYearsCell as YearsCell } from './years-cell/CalendarYearsCell'; + +// // Navigation +// export { CalendarSetVisibleMonth as SetVisibleMonth } from './set-visible-month/CalendarSetVisibleMonth'; +// export { CalendarSetVisibleYear as SetVisibleYear } from './set-visible-year/CalendarSetVisibleYear'; diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/index.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/index.ts new file mode 100644 index 0000000000000..b1f6dcb2f7811 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/index.ts @@ -0,0 +1,2 @@ +export * as RangeCalendar from './index.parts'; +export { useRangeCalendarContext } from './useRangeCalendarContext'; diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRoot.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRoot.tsx new file mode 100644 index 0000000000000..3e225198b6b83 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRoot.tsx @@ -0,0 +1,92 @@ +'use client'; +import * as React from 'react'; +// eslint-disable-next-line no-restricted-imports +import { BaseCalendarRootContext } from '@mui/x-date-pickers/internals/base/utils/base-calendar/root/BaseCalendarRootContext'; +// eslint-disable-next-line no-restricted-imports +import { useBaseCalendarRoot } from '@mui/x-date-pickers/internals/base/utils/base-calendar/root/useBaseCalendarRoot'; +// eslint-disable-next-line no-restricted-imports +import { BaseUIComponentProps } from '@mui/x-date-pickers/internals/base/base-utils/types'; +// eslint-disable-next-line no-restricted-imports +import { useComponentRenderer } from '@mui/x-date-pickers/internals/base/base-utils/useComponentRenderer'; +import { DateRangeValidationError } from '../../../../models'; +import { RangeCalendarRootContext } from './RangeCalendarRootContext'; +import { useRangeCalendarRoot } from './useRangeCalendarRoot'; + +const RangeCalendarRoot = React.forwardRef(function RangeCalendarRoot( + props: RangeCalendarRoot.Props, + forwardedRef: React.ForwardedRef, +) { + const { + className, + render, + readOnly, + disabled, + autoFocus, + onError, + defaultValue, + onValueChange, + value, + timezone, + referenceDate, + monthPageSize, + yearPageSize, + shouldDisableDate, + disablePast, + disableFuture, + minDate, + maxDate, + ...otherProps + } = props; + const { getRootProps, context, baseContext } = useRangeCalendarRoot({ + readOnly, + disabled, + autoFocus, + onError, + defaultValue, + onValueChange, + value, + timezone, + referenceDate, + monthPageSize, + yearPageSize, + shouldDisableDate, + disablePast, + disableFuture, + minDate, + maxDate, + }); + + const state: RangeCalendarRoot.State = React.useMemo(() => ({}), []); + + const { renderElement } = useComponentRenderer({ + propGetter: getRootProps, + render: render ?? 'div', + ref: forwardedRef, + className, + state, + extraProps: otherProps, + }); + + return ( + + + {renderElement()} + + + ); +}); + +export namespace RangeCalendarRoot { + export interface State {} + + export interface Props + extends useRangeCalendarRoot.Parameters, + Omit, 'value' | 'defaultValue' | 'onError'> { + children: React.ReactNode; + } + + export interface ValueChangeHandlerContext + extends useBaseCalendarRoot.ValueChangeHandlerContext {} +} + +export { RangeCalendarRoot }; diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts new file mode 100644 index 0000000000000..b78b765c8cea8 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts @@ -0,0 +1,41 @@ +import * as React from 'react'; +import { PickerNonNullableRangeValue, PickerRangeValue } from '@mui/x-date-pickers/internals'; +// eslint-disable-next-line no-restricted-imports +import { useBaseCalendarRoot } from '@mui/x-date-pickers/internals/base/utils/base-calendar/root/useBaseCalendarRoot'; +import { ValidateDateRangeProps } from '../../../../validation'; + +export interface RangeCalendarRootContext { + /** + * The current value of the calendar. + */ + value: PickerRangeValue; + /** + * Set the current value of the calendar. + * @param {PickerRangeValue} value The new value of the calendar. + * @param {Pick, 'section'>} options The options to customize the behavior of this update. + */ + setValue: ( + value: PickerRangeValue, + options: Pick, 'section'>, + ) => void; + referenceValue: PickerNonNullableRangeValue; + validationProps: ValidateDateRangeProps; +} + +export const RangeCalendarRootContext = React.createContext( + undefined, +); + +if (process.env.NODE_ENV !== 'production') { + RangeCalendarRootContext.displayName = 'RangeCalendarRootContext'; +} + +export function useCalendarRootContext() { + const context = React.useContext(RangeCalendarRootContext); + if (context === undefined) { + throw new Error( + 'Base UI X: RangeCalendarRootContext is missing. Range Calendar parts must be placed within .', + ); + } + return context; +} diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx new file mode 100644 index 0000000000000..75cd1777cd846 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx @@ -0,0 +1,83 @@ +import * as React from 'react'; +import { PickerValidDate } from '@mui/x-date-pickers/models'; +import { PickerRangeValue, useUtils } from '@mui/x-date-pickers/internals'; +// eslint-disable-next-line no-restricted-imports +import { useBaseCalendarRoot } from '@mui/x-date-pickers/internals/base/utils/base-calendar/root/useBaseCalendarRoot'; +// eslint-disable-next-line no-restricted-imports +import { mergeReactProps } from '@mui/x-date-pickers/internals/base/base-utils/mergeReactProps'; +// eslint-disable-next-line no-restricted-imports +import { GenericHTMLProps } from '@mui/x-date-pickers/internals/base/base-utils/types'; +import { DateRangeValidationError } from '../../../../models'; +import { useDateRangeManager } from '../../../../managers'; +import { + ValidateDateRangeProps, + ExportedValidateDateRangeProps, +} from '../../../../validation/validateDateRange'; +import { RangeCalendarRootContext } from './RangeCalendarRootContext'; + +export function useRangeCalendarRoot(parameters: useRangeCalendarRoot.Parameters) { + const { shouldDisableDate, ...baseParameters } = parameters; + const utils = useUtils(); + const manager = useDateRangeManager(); + const { + value, + setValue, + referenceValue, + setVisibleDate, + isDateCellVisible, + context: baseContext, + validationProps: baseValidationProps, + } = useBaseCalendarRoot({ + ...baseParameters, + manager, + getInitialVisibleDate: (referenceValueParam) => referenceValueParam[0], + }); + + const validationProps = React.useMemo( + () => ({ ...baseValidationProps, shouldDisableDate }), + [baseValidationProps, shouldDisableDate], + ); + + // TODO: Apply some logic based on the range position. + const [prevValue, setPrevValue] = React.useState(value); + if (value !== prevValue) { + setPrevValue(value); + let targetDate: PickerValidDate | null = null; + if (utils.isValid(value[0])) { + targetDate = value[0]; + } else if (utils.isValid(value[1])) { + targetDate = value[1]; + } + if (targetDate != null && isDateCellVisible(targetDate)) { + setVisibleDate(targetDate); + } + } + + const context: RangeCalendarRootContext = React.useMemo( + () => ({ + value, + setValue, + referenceValue, + validationProps, + }), + [value, setValue, referenceValue, validationProps], + ); + + const getRootProps = React.useCallback((externalProps: GenericHTMLProps) => { + return mergeReactProps(externalProps, {}); + }, []); + + return React.useMemo( + () => ({ getRootProps, context, baseContext }), + [getRootProps, context, baseContext], + ); +} + +export namespace useRangeCalendarRoot { + export interface Parameters + extends Omit< + useBaseCalendarRoot.Parameters, + 'manager' | 'getInitialVisibleDate' + >, + ExportedValidateDateRangeProps {} +} diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/useRangeCalendarContext/index.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/useRangeCalendarContext/index.ts new file mode 100644 index 0000000000000..a43c5e480479a --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/useRangeCalendarContext/index.ts @@ -0,0 +1 @@ +export { useRangeCalendarContext } from './useRangeCalendarContext'; diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/useRangeCalendarContext/useRangeCalendarContext.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/useRangeCalendarContext/useRangeCalendarContext.ts new file mode 100644 index 0000000000000..b24fd325f13c0 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/useRangeCalendarContext/useRangeCalendarContext.ts @@ -0,0 +1,11 @@ +// eslint-disable-next-line no-restricted-imports +import { useBaseCalendarRootContext } from '@mui/x-date-pickers/internals/base/utils/base-calendar/root/BaseCalendarRootContext'; + +// TODO: Use a dedicated context +export function useRangeCalendarContext() { + const baseRootContext = useBaseCalendarRootContext(); + + return { + visibleDate: baseRootContext.visibleDate, + }; +} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx index 970d7a97e89c3..cf8f9a3d34f73 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx @@ -2,13 +2,14 @@ import * as React from 'react'; import useForkRef from '@mui/utils/useForkRef'; import { useUtils } from '../../../hooks/useUtils'; -import { BaseUIComponentProps } from '../../utils/types'; -import { useComponentRenderer } from '../../utils/useComponentRenderer'; -import { useCalendarDaysGridContext } from '../days-grid/CalendarDaysGridContext'; +import { BaseUIComponentProps } from '../../base-utils/types'; +import { useComponentRenderer } from '../../base-utils/useComponentRenderer'; +import { useBaseCalendarDaysGridContext } from '../../utils/base-calendar/days-grid/BaseCalendarDaysGridContext'; import { useCalendarDaysCell } from './useCalendarDaysCell'; import { useCalendarRootContext } from '../root/CalendarRootContext'; import { useCompositeListItem } from '../../composite/list/useCompositeListItem'; -import { CustomStyleHookMapping } from '../../utils/getStyleHookProps'; +import { CustomStyleHookMapping } from '../../base-utils/getStyleHookProps'; +import { useBaseCalendarRootContext } from '../../utils/base-calendar/root/BaseCalendarRootContext'; import { CalendarDaysCellDataAttributes } from './CalendarDaysCellDataAttributes'; const customStyleHookMapping: CustomStyleHookMapping = { @@ -64,7 +65,8 @@ const CalendarDaysCell = React.forwardRef(function CalendarDaysCell( forwardedRef: React.ForwardedRef, ) { const rootContext = useCalendarRootContext(); - const monthsListContext = useCalendarDaysGridContext(); + const baseRootContext = useBaseCalendarRootContext(); + const monthsListContext = useBaseCalendarDaysGridContext(); const { ref: listItemRef, index: colIndex } = useCompositeListItem(); const utils = useUtils(); const mergedRef = useForkRef(forwardedRef, listItemRef); @@ -82,16 +84,16 @@ const CalendarDaysCell = React.forwardRef(function CalendarDaysCell( [monthsListContext.currentMonth, props.value, utils], ); - const isDateInvalid = rootContext.isDateInvalid; + const isDateInvalid = baseRootContext.isDateInvalid; const isInvalid = React.useMemo(() => isDateInvalid(props.value), [props.value, isDateInvalid]); const isDisabled = React.useMemo(() => { - if (rootContext.disabled) { + if (baseRootContext.disabled) { return true; } return isInvalid; - }, [rootContext.disabled, isInvalid]); + }, [baseRootContext.disabled, isInvalid]); const isTabbable = React.useMemo( () => diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-cell/useCalendarDaysCell.ts b/packages/x-date-pickers/src/internals/base/Calendar/days-cell/useCalendarDaysCell.ts index 60af135e92a66..3a9c02bec2205 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-cell/useCalendarDaysCell.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-cell/useCalendarDaysCell.ts @@ -1,8 +1,8 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; import { PickerValidDate } from '../../../../models'; -import { GenericHTMLProps } from '../../utils/types'; -import { mergeReactProps } from '../../utils/mergeReactProps'; +import { GenericHTMLProps } from '../../base-utils/types'; +import { mergeReactProps } from '../../base-utils/mergeReactProps'; import { useUtils } from '../../../hooks/useUtils'; export function useCalendarDaysCell(parameters: useCalendarDaysCell.Parameters) { diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-grid-body/CalendarDaysGridBody.tsx b/packages/x-date-pickers/src/internals/base/Calendar/days-grid-body/CalendarDaysGridBody.tsx index 2ac0b271710ef..2280cfc5c85d5 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-grid-body/CalendarDaysGridBody.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-grid-body/CalendarDaysGridBody.tsx @@ -1,17 +1,17 @@ 'use client'; import * as React from 'react'; -import { useCalendarDaysGridBody } from './useCalendarDaysGridBody'; -import { BaseUIComponentProps } from '../../utils/types'; -import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import { BaseUIComponentProps } from '../../base-utils/types'; +import { useComponentRenderer } from '../../base-utils/useComponentRenderer'; import { CompositeList } from '../../composite/list/CompositeList'; -import { CalendarDaysGridBodyContext } from './CalendarDaysGridBodyContext'; +import { BaseCalendarDaysGridBodyContext } from '../../utils/base-calendar/days-grid-body/BaseCalendarDaysGridBodyContext'; +import { useBaseCalendarDaysGridBody } from '../../utils/base-calendar/days-grid-body/useBaseCalendarDaysGridBody'; const CalendarDaysGridBody = React.forwardRef(function CalendarDaysGrid( props: CalendarDaysGridBody.Props, forwardedRef: React.ForwardedRef, ) { const { className, render, children, ...otherProps } = props; - const { getDaysGridBodyProps, context, calendarWeekRowRefs } = useCalendarDaysGridBody({ + const { getDaysGridBodyProps, context, calendarWeekRowRefs } = useBaseCalendarDaysGridBody({ children, }); const state = React.useMemo(() => ({}), []); @@ -26,9 +26,9 @@ const CalendarDaysGridBody = React.forwardRef(function CalendarDaysGrid( }); return ( - + {renderElement()} - + ); }); @@ -37,7 +37,7 @@ export namespace CalendarDaysGridBody { export interface Props extends Omit, 'children'>, - useCalendarDaysGridBody.Parameters {} + useBaseCalendarDaysGridBody.Parameters {} } export { CalendarDaysGridBody }; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-grid-body/CalendarDaysGridBodyContext.ts b/packages/x-date-pickers/src/internals/base/Calendar/days-grid-body/CalendarDaysGridBodyContext.ts deleted file mode 100644 index 1197380a6f592..0000000000000 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-grid-body/CalendarDaysGridBodyContext.ts +++ /dev/null @@ -1,26 +0,0 @@ -import * as React from 'react'; - -export interface CalendarDaysGridBodyContext { - registerWeekRowCells: ( - weekRowRef: React.RefObject, - cellsRef: React.RefObject<(HTMLElement | null)[]>, - ) => () => void; -} - -export const CalendarDaysGridBodyContext = React.createContext< - CalendarDaysGridBodyContext | undefined ->(undefined); - -if (process.env.NODE_ENV !== 'production') { - CalendarDaysGridBodyContext.displayName = 'CalendarDaysGridBodyContext'; -} - -export function useCalendarDaysGridBodyContext() { - const context = React.useContext(CalendarDaysGridBodyContext); - if (context === undefined) { - throw new Error( - 'Base UI X: CalendarDaysGridBodyContext is missing. Calendar Days Grid parts must be placed within .', - ); - } - return context; -} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-grid-header-cell/CalendarDaysGridHeaderCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/days-grid-header-cell/CalendarDaysGridHeaderCell.tsx index 41d363538536a..772c3adaa9eb3 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-grid-header-cell/CalendarDaysGridHeaderCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-grid-header-cell/CalendarDaysGridHeaderCell.tsx @@ -1,8 +1,8 @@ 'use client'; import * as React from 'react'; -import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import { useComponentRenderer } from '../../base-utils/useComponentRenderer'; import { useCalendarDaysGridHeaderCell } from './useCalendarDaysGridHeaderCell'; -import { BaseUIComponentProps } from '../../utils/types'; +import { BaseUIComponentProps } from '../../base-utils/types'; const CalendarDaysGridHeaderCell = React.forwardRef(function CalendarDaysGridHeaderCell( props: CalendarDaysGridHeaderCell.Props, diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-grid-header-cell/useCalendarDaysGridHeaderCell.ts b/packages/x-date-pickers/src/internals/base/Calendar/days-grid-header-cell/useCalendarDaysGridHeaderCell.ts index a6b6ac0dc107d..32ad3c39a052c 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-grid-header-cell/useCalendarDaysGridHeaderCell.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-grid-header-cell/useCalendarDaysGridHeaderCell.ts @@ -1,7 +1,7 @@ import * as React from 'react'; import { PickerValidDate } from '../../../../models'; -import { GenericHTMLProps } from '../../utils/types'; -import { mergeReactProps } from '../../utils/mergeReactProps'; +import { GenericHTMLProps } from '../../base-utils/types'; +import { mergeReactProps } from '../../base-utils/mergeReactProps'; import { useUtils } from '../../../hooks/useUtils'; export function useCalendarDaysGridHeaderCell( diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-grid-header/CalendarDaysGridHeader.tsx b/packages/x-date-pickers/src/internals/base/Calendar/days-grid-header/CalendarDaysGridHeader.tsx index 8dbf9d59c9645..9bd030952d4d8 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-grid-header/CalendarDaysGridHeader.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-grid-header/CalendarDaysGridHeader.tsx @@ -1,8 +1,8 @@ 'use client'; import * as React from 'react'; import { useCalendarDaysGridHeader } from './useCalendarDaysGridHeader'; -import { BaseUIComponentProps } from '../../utils/types'; -import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import { BaseUIComponentProps } from '../../base-utils/types'; +import { useComponentRenderer } from '../../base-utils/useComponentRenderer'; const CalendarDaysGridHeader = React.forwardRef(function CalendarDaysGridHeader( props: CalendarDaysGridHeader.Props, diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-grid-header/useCalendarDaysGridHeader.ts b/packages/x-date-pickers/src/internals/base/Calendar/days-grid-header/useCalendarDaysGridHeader.ts index 50a92f6b6eec4..580986ad9af28 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-grid-header/useCalendarDaysGridHeader.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-grid-header/useCalendarDaysGridHeader.ts @@ -3,8 +3,8 @@ import { PickerValidDate } from '../../../../models'; import { getWeekdays } from '../../../utils/date-utils'; import { useUtils } from '../../../hooks/useUtils'; import { useCalendarRootContext } from '../root/CalendarRootContext'; -import { GenericHTMLProps } from '../../utils/types'; -import { mergeReactProps } from '../../utils/mergeReactProps'; +import { GenericHTMLProps } from '../../base-utils/types'; +import { mergeReactProps } from '../../base-utils/mergeReactProps'; export function useCalendarDaysGridHeader(parameters: useCalendarDaysGridHeader.Parameters) { const { children } = parameters; @@ -12,8 +12,8 @@ export function useCalendarDaysGridHeader(parameters: useCalendarDaysGridHeader. const rootContext = useCalendarRootContext(); const days = React.useMemo( - () => getWeekdays(utils, rootContext.value ?? rootContext.referenceDate), - [utils, rootContext.value, rootContext.referenceDate], + () => getWeekdays(utils, rootContext.value ?? rootContext.referenceValue), + [utils, rootContext.value, rootContext.referenceValue], ); const getDaysGridHeaderProps = React.useCallback( diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-grid/CalendarDaysGrid.tsx b/packages/x-date-pickers/src/internals/base/Calendar/days-grid/CalendarDaysGrid.tsx index d6d43a7b1b80c..c9b942aecfcf1 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-grid/CalendarDaysGrid.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-grid/CalendarDaysGrid.tsx @@ -1,16 +1,16 @@ 'use client'; import * as React from 'react'; -import { useCalendarDaysGrid } from './useCalendarDaysGrid'; -import { BaseUIComponentProps } from '../../utils/types'; -import { useComponentRenderer } from '../../utils/useComponentRenderer'; -import { CalendarDaysGridContext } from './CalendarDaysGridContext'; +import { useBaseCalendarDaysGrid } from '../../utils/base-calendar/days-grid/useBaseCalendarDaysGrid'; +import { BaseUIComponentProps } from '../../base-utils/types'; +import { useComponentRenderer } from '../../base-utils/useComponentRenderer'; +import { BaseCalendarDaysGridContext } from '../../utils/base-calendar/days-grid/BaseCalendarDaysGridContext'; const CalendarDaysGrid = React.forwardRef(function CalendarDaysGrid( props: CalendarDaysGrid.Props, forwardedRef: React.ForwardedRef, ) { const { className, render, fixedWeekNumber, offset, ...otherProps } = props; - const { getDaysGridProps, context } = useCalendarDaysGrid({ + const { getDaysGridProps, context } = useBaseCalendarDaysGrid({ fixedWeekNumber, offset, }); @@ -26,9 +26,9 @@ const CalendarDaysGrid = React.forwardRef(function CalendarDaysGrid( }); return ( - + {renderElement()} - + ); }); @@ -37,7 +37,7 @@ export namespace CalendarDaysGrid { export interface Props extends BaseUIComponentProps<'div', State>, - useCalendarDaysGrid.Parameters {} + useBaseCalendarDaysGrid.Parameters {} } export { CalendarDaysGrid }; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-grid/CalendarDaysGridContext.ts b/packages/x-date-pickers/src/internals/base/Calendar/days-grid/CalendarDaysGridContext.ts deleted file mode 100644 index e1c6cfd65153f..0000000000000 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-grid/CalendarDaysGridContext.ts +++ /dev/null @@ -1,27 +0,0 @@ -import * as React from 'react'; -import { PickerValidDate } from '../../../../models'; - -export interface CalendarDaysGridContext { - selectDay: (value: PickerValidDate) => void; - currentMonth: PickerValidDate; - tabbableDay: PickerValidDate | null; - daysGrid: PickerValidDate[][]; -} - -export const CalendarDaysGridContext = React.createContext( - undefined, -); - -if (process.env.NODE_ENV !== 'production') { - CalendarDaysGridContext.displayName = 'CalendarDaysGridContext'; -} - -export function useCalendarDaysGridContext() { - const context = React.useContext(CalendarDaysGridContext); - if (context === undefined) { - throw new Error( - 'Base UI X: CalendarDaysGridContext is missing. Calendar Days Grid parts must be placed within .', - ); - } - return context; -} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/CalendarDaysWeekRow.tsx b/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/CalendarDaysWeekRow.tsx index 426462caef885..0932687258761 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/CalendarDaysWeekRow.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/CalendarDaysWeekRow.tsx @@ -2,12 +2,12 @@ import * as React from 'react'; import useForkRef from '@mui/utils/useForkRef'; import { useCalendarDaysWeekRow } from './useCalendarDaysWeekRow'; -import { BaseUIComponentProps } from '../../utils/types'; -import { useComponentRenderer } from '../../utils/useComponentRenderer'; -import { useCalendarDaysGridContext } from '../days-grid/CalendarDaysGridContext'; +import { BaseUIComponentProps } from '../../base-utils/types'; +import { useComponentRenderer } from '../../base-utils/useComponentRenderer'; +import { useBaseCalendarDaysGridContext } from '../../utils/base-calendar/days-grid/BaseCalendarDaysGridContext'; import { CompositeList } from '../../composite/list/CompositeList'; import { useCompositeListItem } from '../../composite/list/useCompositeListItem'; -import { useCalendarDaysGridBodyContext } from '../days-grid-body/CalendarDaysGridBodyContext'; +import { useBaseCalendarDaysGridBodyContext } from '../../utils/base-calendar/days-grid-body/BaseCalendarDaysGridBodyContext'; const InnerCalendarDaysWeekRow = React.forwardRef(function CalendarDaysGrid( props: InnerCalendarDaysWeekRowProps, @@ -39,24 +39,24 @@ const CalendarDaysWeekRow = React.forwardRef(function CalendarDaysWeekRow( props: CalendarDaysWeekRow.Props, forwardedRef: React.ForwardedRef, ) { - const daysGridContext = useCalendarDaysGridContext(); - const daysGridBodyContext = useCalendarDaysGridBodyContext(); + const baseDaysGridContext = useBaseCalendarDaysGridContext(); + const baseDaysGridBodyContext = useBaseCalendarDaysGridBodyContext(); const { ref: listItemRef, index: rowIndex } = useCompositeListItem(); const mergedRef = useForkRef(forwardedRef, listItemRef); // TODO: Improve how we pass the week to this component. const days = React.useMemo( - () => daysGridContext.daysGrid.find((week) => week[0] === props.value) ?? [], - [daysGridContext.daysGrid, props.value], + () => baseDaysGridContext.daysGrid.find((week) => week[0] === props.value) ?? [], + [baseDaysGridContext.daysGrid, props.value], ); const ctx = React.useMemo( () => ({ days, rowIndex, - registerWeekRowCells: daysGridBodyContext.registerWeekRowCells, + registerWeekRowCells: baseDaysGridBodyContext.registerWeekRowCells, }), - [days, rowIndex, daysGridBodyContext.registerWeekRowCells], + [days, rowIndex, baseDaysGridBodyContext.registerWeekRowCells], ); return ; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/useCalendarDaysWeekRow.ts b/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/useCalendarDaysWeekRow.ts index db17136e21987..0f7c737297eeb 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/useCalendarDaysWeekRow.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/useCalendarDaysWeekRow.ts @@ -1,8 +1,8 @@ import * as React from 'react'; import { PickerValidDate } from '../../../../models'; -import { GenericHTMLProps } from '../../utils/types'; -import { mergeReactProps } from '../../utils/mergeReactProps'; -import { CalendarDaysGridBodyContext } from '../days-grid-body/CalendarDaysGridBodyContext'; +import { GenericHTMLProps } from '../../base-utils/types'; +import { mergeReactProps } from '../../base-utils/mergeReactProps'; +import { CalendarDaysGridBodyContext } from '../../utils/base-calendar/days-grid-body/BaseCalendarDaysGridBodyContext'; export function useCalendarDaysWeekRow(parameters: useCalendarDaysWeekRow.Parameters) { const { children, ctx } = parameters; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx index ecd0dc3a5a05d..5dada65215ec5 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx @@ -5,11 +5,12 @@ import useForkRef from '@mui/utils/useForkRef'; import { PickerValidDate } from '../../../../models'; import { useNow, useUtils } from '../../../hooks/useUtils'; import { findClosestEnabledDate } from '../../../utils/date-utils'; -import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import { useComponentRenderer } from '../../base-utils/useComponentRenderer'; +import { BaseUIComponentProps } from '../../base-utils/types'; +import { useCompositeListItem } from '../../composite/list/useCompositeListItem'; +import { useBaseCalendarRootContext } from '../../utils/base-calendar/root/BaseCalendarRootContext'; import { useCalendarRootContext } from '../root/CalendarRootContext'; import { useCalendarMonthsCell } from './useCalendarMonthsCell'; -import { BaseUIComponentProps } from '../../utils/types'; -import { useCompositeListItem } from '../../composite/list/useCompositeListItem'; const InnerCalendarMonthsCell = React.forwardRef(function InnerCalendarMonthsCell( props: InnerCalendarMonthsCellProps, @@ -47,9 +48,10 @@ const CalendarMonthsCell = React.forwardRef(function CalendarMonthsCell( forwardedRef: React.ForwardedRef, ) { const rootContext = useCalendarRootContext(); + const baseRootContext = useBaseCalendarRootContext(); const { ref: listItemRef } = useCompositeListItem(); const utils = useUtils(); - const now = useNow(rootContext.timezone); + const now = useNow(baseRootContext.timezone); const mergedRef = useForkRef(forwardedRef, listItemRef); const isSelected = React.useMemo( @@ -90,35 +92,35 @@ const CalendarMonthsCell = React.forwardRef(function CalendarMonthsCell( }, [rootContext.validationProps, props.value, now, utils]); const isDisabled = React.useMemo(() => { - if (rootContext.disabled) { + if (baseRootContext.disabled) { return true; } return isInvalid; - }, [rootContext.disabled, isInvalid]); + }, [baseRootContext.disabled, isInvalid]); const isTabbable = React.useMemo( () => utils.isValid(rootContext.value) ? isSelected - : utils.isSameMonth(rootContext.referenceDate, props.value), - [utils, rootContext.value, rootContext.referenceDate, isSelected, props.value], + : utils.isSameMonth(rootContext.referenceValue, props.value), + [utils, rootContext.value, rootContext.referenceValue, isSelected, props.value], ); const selectMonth = useEventCallback((newValue: PickerValidDate) => { - if (rootContext.readOnly) { + if (baseRootContext.readOnly) { return; } const newCleanValue = utils.setMonth( - rootContext.value ?? rootContext.referenceDate, + rootContext.value ?? rootContext.referenceValue, utils.getMonth(newValue), ); const startOfMonth = utils.startOfMonth(newCleanValue); const endOfMonth = utils.endOfMonth(newCleanValue); - const closestEnabledDate = rootContext.isDateInvalid(newCleanValue) + const closestEnabledDate = baseRootContext.isDateInvalid(newCleanValue) ? findClosestEnabledDate({ utils, date: newCleanValue, @@ -130,13 +132,13 @@ const CalendarMonthsCell = React.forwardRef(function CalendarMonthsCell( : rootContext.validationProps.maxDate, disablePast: rootContext.validationProps.disablePast, disableFuture: rootContext.validationProps.disableFuture, - isDateDisabled: rootContext.isDateInvalid, - timezone: rootContext.timezone, + isDateDisabled: baseRootContext.isDateInvalid, + timezone: baseRootContext.timezone, }) : newCleanValue; if (closestEnabledDate) { - rootContext.setVisibleDate(closestEnabledDate, true); + baseRootContext.setVisibleDate(closestEnabledDate, true); rootContext.setValue(closestEnabledDate, { section: 'month' }); } }); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/useCalendarMonthsCell.ts b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/useCalendarMonthsCell.ts index 2d7ecac3476e9..be23aacd0fcb1 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/useCalendarMonthsCell.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/useCalendarMonthsCell.ts @@ -2,8 +2,8 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; import { PickerValidDate } from '../../../../models'; import { useUtils } from '../../../hooks/useUtils'; -import { GenericHTMLProps } from '../../utils/types'; -import { mergeReactProps } from '../../utils/mergeReactProps'; +import { GenericHTMLProps } from '../../base-utils/types'; +import { mergeReactProps } from '../../base-utils/mergeReactProps'; export function useCalendarMonthsCell(parameters: useCalendarMonthsCell.Parameters) { const utils = useUtils(); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-grid/CalendarMonthsGrid.tsx b/packages/x-date-pickers/src/internals/base/Calendar/months-grid/CalendarMonthsGrid.tsx index 120fb24525f74..51fc7b68de4b0 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-grid/CalendarMonthsGrid.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-grid/CalendarMonthsGrid.tsx @@ -1,8 +1,8 @@ 'use client'; import * as React from 'react'; import { useCalendarMonthsGrid } from './useCalendarMonthsGrid'; -import { BaseUIComponentProps } from '../../utils/types'; -import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import { BaseUIComponentProps } from '../../base-utils/types'; +import { useComponentRenderer } from '../../base-utils/useComponentRenderer'; import { CompositeList } from '../../composite/list/CompositeList'; const CalendarMonthsGrid = React.forwardRef(function CalendarMonthsList( diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-grid/useCalendarMonthsGrid.ts b/packages/x-date-pickers/src/internals/base/Calendar/months-grid/useCalendarMonthsGrid.ts index 5a1b4e979f9df..71590df123473 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-grid/useCalendarMonthsGrid.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-grid/useCalendarMonthsGrid.ts @@ -2,8 +2,9 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; import useTimeout from '@mui/utils/useTimeout'; import { PickerValidDate } from '../../../../models'; -import { GenericHTMLProps } from '../../utils/types'; -import { mergeReactProps } from '../../utils/mergeReactProps'; +import { GenericHTMLProps } from '../../base-utils/types'; +import { mergeReactProps } from '../../base-utils/mergeReactProps'; +import { useBaseCalendarRootContext } from '../../utils/base-calendar/root/BaseCalendarRootContext'; import { applyInitialFocusInGrid, navigateInGrid, @@ -11,12 +12,11 @@ import { PageGridNavigationTarget, } from '../utils/keyboardNavigation'; import { useMonthsCells } from '../utils/useMonthsCells'; -import { useCalendarRootContext } from '../root/CalendarRootContext'; import { CalendarMonthsGridCssVars } from './CalendarMonthsGridCssVars'; export function useCalendarMonthsGrid(parameters: useCalendarMonthsGrid.Parameters) { const { children, cellsPerRow, canChangeYear = true } = parameters; - const rootContext = useCalendarRootContext(); + const baseRootContext = useBaseCalendarRootContext(); const monthsCellRefs = React.useRef<(HTMLElement | null)[]>([]); const { months, changePage } = useMonthsCells(); const pageNavigationTargetRef = React.useRef(null); @@ -47,7 +47,7 @@ export function useCalendarMonthsGrid(parameters: useCalendarMonthsGrid.Paramete applyInitialFocusInGrid({ cells, target }); }); } - }, [rootContext.visibleDate, timeout, getCellsInCalendar]); + }, [baseRootContext.visibleDate, timeout, getCellsInCalendar]); // TODO: Add support for multiple months grids. const onKeyDown = useEventCallback((event: React.KeyboardEvent) => { diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsList.tsx b/packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsList.tsx index 88985037d867b..2c115b4312b0e 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsList.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsList.tsx @@ -1,8 +1,8 @@ 'use client'; import * as React from 'react'; import { useCalendarMonthsList } from './useCalendarMonthsList'; -import { BaseUIComponentProps } from '../../utils/types'; -import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import { BaseUIComponentProps } from '../../base-utils/types'; +import { useComponentRenderer } from '../../base-utils/useComponentRenderer'; import { CompositeList } from '../../composite/list/CompositeList'; const CalendarMonthsList = React.forwardRef(function CalendarMonthsList( diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-list/useCalendarMonthsList.ts b/packages/x-date-pickers/src/internals/base/Calendar/months-list/useCalendarMonthsList.ts index 8dd9e0a402d06..8b818d23cc3a0 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-list/useCalendarMonthsList.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-list/useCalendarMonthsList.ts @@ -2,8 +2,9 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; import useTimeout from '@mui/utils/useTimeout'; import { PickerValidDate } from '../../../../models'; -import { GenericHTMLProps } from '../../utils/types'; -import { mergeReactProps } from '../../utils/mergeReactProps'; +import { GenericHTMLProps } from '../../base-utils/types'; +import { mergeReactProps } from '../../base-utils/mergeReactProps'; +import { useBaseCalendarRootContext } from '../../utils/base-calendar/root/BaseCalendarRootContext'; import { applyInitialFocusInList, navigateInList, @@ -11,11 +12,10 @@ import { PageListNavigationTarget, } from '../utils/keyboardNavigation'; import { useMonthsCells } from '../utils/useMonthsCells'; -import { useCalendarRootContext } from '../root/CalendarRootContext'; export function useCalendarMonthsList(parameters: useCalendarMonthsList.Parameters) { const { children, loop = true, canChangeYear = true } = parameters; - const rootContext = useCalendarRootContext(); + const baseRootContext = useBaseCalendarRootContext(); const monthsCellRefs = React.useRef<(HTMLElement | null)[]>([]); const { months, changePage } = useMonthsCells(); const pageNavigationTargetRef = React.useRef(null); @@ -28,7 +28,7 @@ export function useCalendarMonthsList(parameters: useCalendarMonthsList.Paramete applyInitialFocusInList({ cells: monthsCellRefs.current, target }); }); } - }, [rootContext.visibleDate, timeout]); + }, [baseRootContext.visibleDate, timeout]); const onKeyDown = useEventCallback((event: React.KeyboardEvent) => { const changeListPage: NavigateInListChangePage = (params) => { diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRoot.tsx b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRoot.tsx index 10580929d35c4..9260875ae18dc 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRoot.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRoot.tsx @@ -1,9 +1,12 @@ 'use client'; import * as React from 'react'; +import { DateValidationError } from '../../../../models'; +import { useComponentRenderer } from '../../base-utils/useComponentRenderer'; +import { BaseUIComponentProps } from '../../base-utils/types'; +import { BaseCalendarRootContext } from '../../utils/base-calendar/root/BaseCalendarRootContext'; +import { useBaseCalendarRoot } from '../../utils/base-calendar/root/useBaseCalendarRoot'; import { CalendarRootContext } from './CalendarRootContext'; import { useCalendarRoot } from './useCalendarRoot'; -import { useComponentRenderer } from '../../utils/useComponentRenderer'; -import { BaseUIComponentProps } from '../../utils/types'; const CalendarRoot = React.forwardRef(function CalendarRoot( props: CalendarRoot.Props, @@ -32,7 +35,7 @@ const CalendarRoot = React.forwardRef(function CalendarRoot( maxDate, ...otherProps } = props; - const { context, getRootProps } = useCalendarRoot({ + const { getRootProps, context, baseContext } = useCalendarRoot({ readOnly, disabled, autoFocus, @@ -65,7 +68,9 @@ const CalendarRoot = React.forwardRef(function CalendarRoot( }); return ( - {renderElement()} + + {renderElement()} + ); }); @@ -78,7 +83,8 @@ export namespace CalendarRoot { children: React.ReactNode; } - export interface ValueChangeHandlerContext extends useCalendarRoot.ValueChangeHandlerContext {} + export interface ValueChangeHandlerContext + extends useBaseCalendarRoot.ValueChangeHandlerContext {} } export { CalendarRoot }; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts index 46cc34948f58f..92adab14ebb71 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts @@ -1,9 +1,8 @@ import * as React from 'react'; -import { PickersTimezone, PickerValidDate } from '../../../../models'; -import { ValidateDateProps } from '../../../../validation'; +import { PickerValidDate } from '../../../../models'; import { PickerValue } from '../../../models'; -import type { useCalendarRoot } from './useCalendarRoot'; -import type { useCalendarDaysGridBody } from '../days-grid-body/useCalendarDaysGridBody'; +import { useBaseCalendarRoot } from '../../utils/base-calendar/root/useBaseCalendarRoot'; +import { ValidateDateProps } from '../../../../validation'; export interface CalendarRootContext { /** @@ -13,32 +12,14 @@ export interface CalendarRootContext { /** * Set the current value of the calendar. * @param {PickerValue} value The new value of the calendar. - * @param {Pick} options The options to customize the behavior of this update. + * @param {Pick, 'section'>} options The options to customize the behavior of this update. */ setValue: ( value: PickerValue, - options: Pick, + options: Pick, 'section'>, ) => void; - /** - * The reference date of the calendar. - */ - referenceDate: PickerValidDate; - timezone: PickersTimezone; - disabled: boolean; - readOnly: boolean; - autoFocus: boolean; - isDateInvalid: (day: PickerValidDate | null) => boolean; + referenceValue: PickerValidDate; validationProps: ValidateDateProps; - visibleDate: PickerValidDate; - setVisibleDate: (visibleDate: PickerValidDate, skipIfAlreadyVisible: boolean) => void; - monthPageSize: number; - yearPageSize: number; - applyDayGridKeyboardNavigation: (event: React.KeyboardEvent) => void; - registerDaysGridCells: ( - cellsRef: useCalendarDaysGridBody.CellsRef, - rowsRef: useCalendarDaysGridBody.RowsRef, - ) => () => void; - registerSection: (parameters: useCalendarRoot.RegisterSectionParameters) => () => void; } export const CalendarRootContext = React.createContext(undefined); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts index 9ab747f09a005..1d6b7d9cdfbb2 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts @@ -1,165 +1,39 @@ import * as React from 'react'; -import useEventCallback from '@mui/utils/useEventCallback'; -import { - DateValidationError, - OnErrorProps, - PickerValidDate, - TimezoneProps, -} from '../../../../models'; -import { useIsDateDisabled } from '../../../../DateCalendar/useIsDateDisabled'; -import { - ExportedValidateDateProps, - validateDate, - ValidateDateProps, -} from '../../../../validation/validateDate'; -import { useControlledValueWithTimezone } from '../../../hooks/useValueWithTimezone'; -import { useDefaultDates, useUtils } from '../../../hooks/useUtils'; -import { SECTION_TYPE_GRANULARITY } from '../../../utils/getDefaultReferenceDate'; -import { singleItemValueManager } from '../../../utils/valueManagers'; -import { applyDefaultDate } from '../../../utils/date-utils'; -import { FormProps, PickerValue } from '../../../models'; +import { DateValidationError } from '../../../../models'; +import { useDateManager } from '../../../../managers'; +import { ExportedValidateDateProps, ValidateDateProps } from '../../../../validation/validateDate'; +import { useUtils } from '../../../hooks/useUtils'; +import { PickerValue } from '../../../models'; import { CalendarRootContext } from './CalendarRootContext'; -import { useValidation } from '../../../../validation'; -import { mergeReactProps } from '../../utils/mergeReactProps'; -import { GenericHTMLProps } from '../../utils/types'; -import { useCalendarDaysGridNavigation } from './useCalendarDaysGridsNavigation'; - -function useAddDefaultsToValidateDateProps( - validationDate: ExportedValidateDateProps, -): ValidateDateProps { - const utils = useUtils(); - const defaultDates = useDefaultDates(); - - const { - shouldDisableDate, - shouldDisableMonth, - shouldDisableYear, - disablePast, - disableFuture, - minDate, - maxDate, - } = validationDate; - - return React.useMemo( - () => ({ - shouldDisableDate, - shouldDisableMonth, - shouldDisableYear, - disablePast: disablePast ?? false, - disableFuture: disableFuture ?? false, - minDate: applyDefaultDate(utils, minDate, defaultDates.minDate), - maxDate: applyDefaultDate(utils, maxDate, defaultDates.maxDate), - }), - [ - shouldDisableDate, - shouldDisableMonth, - shouldDisableYear, - disablePast, - disableFuture, - minDate, - maxDate, - utils, - defaultDates, - ], - ); -} +import { mergeReactProps } from '../../base-utils/mergeReactProps'; +import { GenericHTMLProps } from '../../base-utils/types'; +import { useBaseCalendarRoot } from '../../utils/base-calendar/root/useBaseCalendarRoot'; export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { - const { - readOnly = false, - disabled = false, - autoFocus = false, - onError, - defaultValue, - onValueChange, - value: valueProp, - timezone: timezoneProp, - referenceDate: referenceDateProp, - monthPageSize = 1, - yearPageSize = 1, - } = parameters; - + const { shouldDisableDate, shouldDisableMonth, shouldDisableYear, ...baseParameters } = + parameters; const utils = useUtils(); - const validationProps = useAddDefaultsToValidateDateProps(parameters); - - const { value, handleValueChange, timezone } = useControlledValueWithTimezone({ - name: 'CalendarRoot', - timezone: timezoneProp, - value: valueProp, - defaultValue, - referenceDate: referenceDateProp, - onChange: onValueChange, - valueManager: singleItemValueManager, - }); - - const referenceDate = React.useMemo( - () => { - return singleItemValueManager.getInitialReferenceValue({ - value, - utils, - timezone, - props: validationProps, - referenceDate: referenceDateProp, - granularity: SECTION_TYPE_GRANULARITY.day, - }); - }, - // We want the `referenceDate` to update on prop and `timezone` change (https://github.com/mui/mui-x/issues/10804) - // eslint-disable-next-line react-hooks/exhaustive-deps - [referenceDateProp, timezone], - ); - - const { getValidationErrorForNewValue } = useValidation({ - props: { ...validationProps, onError }, + const manager = useDateManager(); + const { value, - timezone, - validator: validateDate, - }); - - // TODO: Rename this hook (if we keep it for Base UI X) - const isDateInvalid = useIsDateDisabled({ - ...validationProps, - timezone, - }); - - const setValue = useEventCallback((newValue, context) => { - handleValueChange(newValue, { - ...context, - validationError: getValidationErrorForNewValue(newValue), - }); - }); - - const sectionsRef = React.useRef< - Record<'day' | 'month' | 'year', Record> - >({ - day: {}, - month: {}, - year: {}, - }); - const registerSection = useEventCallback((section: useCalendarRoot.RegisterSectionParameters) => { - const id = Math.random(); - sectionsRef.current[section.type][id] = section.value; - return () => { - delete sectionsRef.current[section.type][id]; - }; + setValue, + referenceValue, + setVisibleDate, + isDateCellVisible, + context: baseContext, + validationProps: baseValidationProps, + } = useBaseCalendarRoot({ + ...baseParameters, + manager, + getInitialVisibleDate: (referenceValueParam) => referenceValueParam, }); - const [visibleDate, setVisibleDate] = React.useState(referenceDate); - const [prevValue, setPrevValue] = React.useState(value); - - const isDateCellVisible = (date: PickerValidDate) => { - if (Object.values(sectionsRef.current.day).length > 0) { - return Object.values(sectionsRef.current.day).every( - (month) => !utils.isSameMonth(date, month), - ); - } - if (Object.values(sectionsRef.current.month).length > 0) { - return Object.values(sectionsRef.current.month).every( - (year) => !utils.isSameYear(date, year), - ); - } - return true; - }; + const validationProps = React.useMemo( + () => ({ ...baseValidationProps, shouldDisableDate, shouldDisableMonth, shouldDisableYear }), + [baseValidationProps, shouldDisableDate, shouldDisableMonth, shouldDisableYear], + ); + const [prevValue, setPrevValue] = React.useState(value); if (value !== prevValue && utils.isValid(value)) { setPrevValue(value); if (isDateCellVisible(value)) { @@ -167,133 +41,31 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { } } - const { applyDayGridKeyboardNavigation, registerDaysGridCells } = useCalendarDaysGridNavigation({ - visibleDate, - setVisibleDate, - monthPageSize, - validationProps, - }); - - const handleVisibleDateChange = useEventCallback( - (newVisibleDate: PickerValidDate, skipIfAlreadyVisible: boolean) => { - if (skipIfAlreadyVisible && isDateCellVisible(newVisibleDate)) { - return; - } - - setVisibleDate(newVisibleDate); - }, - ); - const context: CalendarRootContext = React.useMemo( () => ({ value, setValue, - referenceDate, - timezone, - disabled, - readOnly, - autoFocus, - isDateInvalid, + referenceValue, validationProps, - visibleDate, - setVisibleDate: handleVisibleDateChange, - monthPageSize, - yearPageSize, - applyDayGridKeyboardNavigation, - registerDaysGridCells, - registerSection, }), - [ - value, - setValue, - referenceDate, - timezone, - disabled, - readOnly, - autoFocus, - isDateInvalid, - validationProps, - visibleDate, - handleVisibleDateChange, - monthPageSize, - yearPageSize, - applyDayGridKeyboardNavigation, - registerDaysGridCells, - registerSection, - ], + [value, setValue, referenceValue, validationProps], ); const getRootProps = React.useCallback((externalProps: GenericHTMLProps) => { return mergeReactProps(externalProps, {}); }, []); - return React.useMemo(() => ({ getRootProps, context }), [getRootProps, context]); + return React.useMemo( + () => ({ getRootProps, context, baseContext }), + [getRootProps, context, baseContext], + ); } export namespace useCalendarRoot { export interface Parameters - extends TimezoneProps, - FormProps, - OnErrorProps, - ExportedValidateDateProps { - /** - * The controlled value that should be selected. - * To render an uncontrolled Date Calendar, use the `defaultValue` prop instead. - */ - value?: PickerValidDate | null; - /** - * The uncontrolled value that should be initially selected. - * To render a controlled accordion, use the `value` prop instead. - */ - defaultValue?: PickerValidDate | null; - /** - * Event handler called when the selected value changes. - * Provides the new value as an argument. - * @param {PickerValidDate | null} value The new selected value. - * @param {useCalendarRoot.ValueChangeHandlerContext} context Additional context information. - */ - onValueChange?: ( - value: PickerValidDate | null, - context: useCalendarRoot.ValueChangeHandlerContext, - ) => void; - /** - * The date used to generate the new value when both `value` and `defaultValue` are empty. - * @default The closest valid date using the validation props, except callbacks such as `shouldDisableDate`. - */ - referenceDate?: PickerValidDate; - /** - * If `true`, one of the cells will be automatically focused when the component is mounted. - * If a value or a default value is provided, the focused cell will be the one corresponding to the selected date. - * @default false - */ - autoFocus?: boolean; - /** - * The amount of months to navigate by when pressing or when using keyboard navigation in the day grid. - * This is mostly useful when displaying multiple day grids. - * @default 1 - */ - monthPageSize?: number; - /** - * The amount of months to navigate by when pressing or when using keyboard navigation in the month grid or the month list. - * This is mostly useful when displaying multiple month grids or month lists. - * @default 1 - */ - yearPageSize?: number; - } - - export interface ValueChangeHandlerContext { - /** - * The section handled by the UI that triggered the change. - */ - section: 'day' | 'month' | 'year'; - /** - * The validation error associated to the new value. - */ - validationError: DateValidationError; - } - - export interface RegisterSectionParameters { - type: 'day' | 'month' | 'year'; - value: PickerValidDate; - } + extends Omit< + useBaseCalendarRoot.Parameters, + 'manager' | 'getInitialVisibleDate' + >, + ExportedValidateDateProps {} } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx index 7411761632dd2..ecf5f96a2ad08 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx @@ -2,10 +2,10 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; import { useUtils } from '../../../hooks/useUtils'; -import { useComponentRenderer } from '../../utils/useComponentRenderer'; -import { useCalendarRootContext } from '../root/CalendarRootContext'; +import { useComponentRenderer } from '../../base-utils/useComponentRenderer'; +import { useBaseCalendarRootContext } from '../../utils/base-calendar/root/BaseCalendarRootContext'; import { useCalendarSetVisibleMonth } from './useCalendarSetVisibleMonth'; -import { BaseUIComponentProps } from '../../utils/types'; +import { BaseUIComponentProps } from '../../base-utils/types'; import { getFirstEnabledMonth, getLastEnabledMonth } from '../utils/date'; const InnerCalendarSetVisibleMonth = React.forwardRef(function InnerCalendarSetVisibleMonth( @@ -35,40 +35,43 @@ const CalendarSetVisibleMonth = React.forwardRef(function CalendarSetVisibleMont props: CalendarSetVisibleMonth.Props, forwardedRef: React.ForwardedRef, ) { - const rootContext = useCalendarRootContext(); + const baseRootContext = useBaseCalendarRootContext(); const utils = useUtils(); const targetDate = React.useMemo(() => { if (props.target === 'previous') { - return utils.addMonths(rootContext.visibleDate, -rootContext.monthPageSize); + return utils.addMonths(baseRootContext.visibleDate, -baseRootContext.monthPageSize); } if (props.target === 'next') { - return utils.addMonths(rootContext.visibleDate, rootContext.monthPageSize); + return utils.addMonths(baseRootContext.visibleDate, baseRootContext.monthPageSize); } - return utils.setMonth(rootContext.visibleDate, utils.getMonth(props.target)); - }, [rootContext.visibleDate, rootContext.monthPageSize, utils, props.target]); + return utils.setMonth(baseRootContext.visibleDate, utils.getMonth(props.target)); + }, [baseRootContext.visibleDate, baseRootContext.monthPageSize, utils, props.target]); const isDisabled = React.useMemo(() => { - if (rootContext.disabled) { + if (baseRootContext.disabled) { return true; } // TODO: Check if the logic below works correctly when multiple months are rendered at once. - const isMovingBefore = utils.isBefore(targetDate, rootContext.visibleDate); + const isMovingBefore = utils.isBefore(targetDate, baseRootContext.visibleDate); // All the months before the visible ones are fully disabled, we skip the navigation. if (isMovingBefore) { - return utils.isAfter(getFirstEnabledMonth(utils, rootContext.validationProps), targetDate); + return utils.isAfter( + getFirstEnabledMonth(utils, baseRootContext.validationProps), + targetDate, + ); } // All the months after the visible ones are fully disabled, we skip the navigation. - return utils.isBefore(getLastEnabledMonth(utils, rootContext.validationProps), targetDate); + return utils.isBefore(getLastEnabledMonth(utils, baseRootContext.validationProps), targetDate); }, [ - rootContext.disabled, - rootContext.validationProps, - rootContext.visibleDate, + baseRootContext.disabled, + baseRootContext.validationProps, + baseRootContext.visibleDate, targetDate, utils, ]); @@ -77,7 +80,7 @@ const CalendarSetVisibleMonth = React.forwardRef(function CalendarSetVisibleMont if (isDisabled) { return; } - rootContext.setVisibleDate(targetDate, false); + baseRootContext.setVisibleDate(targetDate, false); }); const ctx = React.useMemo( diff --git a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/useCalendarSetVisibleMonth.ts b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/useCalendarSetVisibleMonth.ts index 9074dc760258a..a39598b6638f0 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/useCalendarSetVisibleMonth.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/useCalendarSetVisibleMonth.ts @@ -1,7 +1,7 @@ import * as React from 'react'; import { PickerValidDate } from '../../../../models'; -import { GenericHTMLProps } from '../../utils/types'; -import { mergeReactProps } from '../../utils/mergeReactProps'; +import { GenericHTMLProps } from '../../base-utils/types'; +import { mergeReactProps } from '../../base-utils/mergeReactProps'; export function useCalendarSetVisibleMonth(parameters: useCalendarSetVisibleMonth.Parameters) { const { ctx } = parameters; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx index db12a50b9b264..58af16af4ab4d 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx @@ -2,10 +2,10 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; import { useUtils } from '../../../hooks/useUtils'; -import { useComponentRenderer } from '../../utils/useComponentRenderer'; -import { useCalendarRootContext } from '../root/CalendarRootContext'; +import { useComponentRenderer } from '../../base-utils/useComponentRenderer'; +import { useBaseCalendarRootContext } from '../../utils/base-calendar/root/BaseCalendarRootContext'; import { useCalendarSetVisibleYear } from './useCalendarSetVisibleYear'; -import { BaseUIComponentProps } from '../../utils/types'; +import { BaseUIComponentProps } from '../../base-utils/types'; import { getFirstEnabledYear, getLastEnabledYear } from '../utils/date'; const InnerCalendarSetVisibleYear = React.forwardRef(function InnerCalendarSetVisibleYear( @@ -35,39 +35,39 @@ const CalendarSetVisibleYear = React.forwardRef(function CalendarSetVisibleYear( props: CalendarSetVisibleYear.Props, forwardedRef: React.ForwardedRef, ) { - const rootContext = useCalendarRootContext(); + const baseRootContext = useBaseCalendarRootContext(); const utils = useUtils(); const targetDate = React.useMemo(() => { if (props.target === 'previous') { - return utils.addYears(rootContext.visibleDate, -1); + return utils.addYears(baseRootContext.visibleDate, -1); } if (props.target === 'next') { - return utils.addYears(rootContext.visibleDate, 1); + return utils.addYears(baseRootContext.visibleDate, 1); } - return utils.setYear(rootContext.visibleDate, utils.getYear(props.target)); - }, [rootContext.visibleDate, utils, props.target]); + return utils.setYear(baseRootContext.visibleDate, utils.getYear(props.target)); + }, [baseRootContext.visibleDate, utils, props.target]); const isDisabled = React.useMemo(() => { - if (rootContext.disabled) { + if (baseRootContext.disabled) { return true; } - const isMovingBefore = utils.isBefore(targetDate, rootContext.visibleDate); + const isMovingBefore = utils.isBefore(targetDate, baseRootContext.visibleDate); // All the years before the visible ones are fully disabled, we skip the navigation. if (isMovingBefore) { - return utils.isAfter(getFirstEnabledYear(utils, rootContext.validationProps), targetDate); + return utils.isAfter(getFirstEnabledYear(utils, baseRootContext.validationProps), targetDate); } // All the years after the visible ones are fully disabled, we skip the navigation. - return utils.isBefore(getLastEnabledYear(utils, rootContext.validationProps), targetDate); + return utils.isBefore(getLastEnabledYear(utils, baseRootContext.validationProps), targetDate); }, [ - rootContext.disabled, - rootContext.validationProps, - rootContext.visibleDate, + baseRootContext.disabled, + baseRootContext.validationProps, + baseRootContext.visibleDate, targetDate, utils, ]); @@ -76,7 +76,7 @@ const CalendarSetVisibleYear = React.forwardRef(function CalendarSetVisibleYear( if (isDisabled) { return; } - rootContext.setVisibleDate(targetDate, false); + baseRootContext.setVisibleDate(targetDate, false); }); const ctx = React.useMemo( diff --git a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/useCalendarSetVisibleYear.ts b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/useCalendarSetVisibleYear.ts index 4ffb4409cbd8d..f7f188792571f 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/useCalendarSetVisibleYear.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/useCalendarSetVisibleYear.ts @@ -1,7 +1,7 @@ import * as React from 'react'; import { PickerValidDate } from '../../../../models'; -import { GenericHTMLProps } from '../../utils/types'; -import { mergeReactProps } from '../../utils/mergeReactProps'; +import { GenericHTMLProps } from '../../base-utils/types'; +import { mergeReactProps } from '../../base-utils/mergeReactProps'; export function useCalendarSetVisibleYear(parameters: useCalendarSetVisibleYear.Parameters) { const { ctx } = parameters; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/useCalendarContext/useCalendarContext.ts b/packages/x-date-pickers/src/internals/base/Calendar/useCalendarContext/useCalendarContext.ts index 4a6ed3b23891e..6434e24b3439c 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/useCalendarContext/useCalendarContext.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/useCalendarContext/useCalendarContext.ts @@ -1,10 +1,10 @@ -import { useCalendarRootContext } from '../root/CalendarRootContext'; +import { useBaseCalendarRootContext } from '../../utils/base-calendar/root/BaseCalendarRootContext'; // TODO: Use a dedicated context export function useCalendarContext() { - const rootContext = useCalendarRootContext(); + const baseRootContext = useBaseCalendarRootContext(); return { - visibleDate: rootContext.visibleDate, + visibleDate: baseRootContext.visibleDate, }; } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/date.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/date.ts index 2363c9fefbba6..a8930deccf1ee 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/utils/date.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/utils/date.ts @@ -1,9 +1,10 @@ import { ValidateDateProps } from '../../../../validation'; import { MuiPickersAdapter, PickerValidDate } from '../../../../models'; +import { BaseDateValidationProps } from '../../../models/validation'; export function getFirstEnabledMonth( utils: MuiPickersAdapter, - validationProps: ValidateDateProps, + validationProps: Required, ): PickerValidDate { const now = utils.date(); return utils.startOfMonth( @@ -15,7 +16,7 @@ export function getFirstEnabledMonth( export function getLastEnabledMonth( utils: MuiPickersAdapter, - validationProps: ValidateDateProps, + validationProps: Required, ): PickerValidDate { const now = utils.date(); return utils.startOfMonth( @@ -27,7 +28,7 @@ export function getLastEnabledMonth( export function getFirstEnabledYear( utils: MuiPickersAdapter, - validationProps: ValidateDateProps, + validationProps: Required, ): PickerValidDate { const now = utils.date(); return utils.startOfYear( @@ -39,7 +40,7 @@ export function getFirstEnabledYear( export function getLastEnabledYear( utils: MuiPickersAdapter, - validationProps: ValidateDateProps, + validationProps: Required, ): PickerValidDate { const now = utils.date(); return utils.startOfYear( diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/useMonthsCells.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/useMonthsCells.ts index 54e66bab3ce8b..da712179f8d18 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/utils/useMonthsCells.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/utils/useMonthsCells.ts @@ -2,17 +2,16 @@ import * as React from 'react'; import { PickerValidDate } from '../../../../models'; import { getMonthsInYear } from '../../../utils/date-utils'; import { useUtils } from '../../../hooks/useUtils'; -import { useCalendarRootContext } from '../root/CalendarRootContext'; import { getFirstEnabledYear, getLastEnabledYear } from './date'; -import { PageGridNavigationTarget, PageListNavigationTarget } from './keyboardNavigation'; +import { useBaseCalendarRootContext } from '../../utils/base-calendar/root/BaseCalendarRootContext'; export function useMonthsCells(): useMonthsCells.ReturnValue { - const rootContext = useCalendarRootContext(); + const baseRootContext = useBaseCalendarRootContext(); const utils = useUtils(); const currentYear = React.useMemo( - () => utils.startOfYear(rootContext.visibleDate), - [utils, rootContext.visibleDate], + () => utils.startOfYear(baseRootContext.visibleDate), + [utils, baseRootContext.visibleDate], ); const months = React.useMemo(() => getMonthsInYear(utils, currentYear), [utils, currentYear]); @@ -21,41 +20,44 @@ export function useMonthsCells(): useMonthsCells.ReturnValue { // TODO: Jump over months with no valid date. if (direction === 'previous') { const targetDate = utils.addYears( - utils.startOfYear(rootContext.visibleDate), - -rootContext.yearPageSize, + utils.startOfYear(baseRootContext.visibleDate), + -baseRootContext.yearPageSize, ); - const lastYearInNewPage = utils.addYears(targetDate, rootContext.yearPageSize - 1); + const lastYearInNewPage = utils.addYears(targetDate, baseRootContext.yearPageSize - 1); // All the years before the visible ones are fully disabled, we skip the navigation. if ( - utils.isAfter(getFirstEnabledYear(utils, rootContext.validationProps), lastYearInNewPage) + utils.isAfter( + getFirstEnabledYear(utils, baseRootContext.validationProps), + lastYearInNewPage, + ) ) { return; } - rootContext.setVisibleDate( - utils.addYears(rootContext.visibleDate, -rootContext.yearPageSize), + baseRootContext.setVisibleDate( + utils.addYears(baseRootContext.visibleDate, -baseRootContext.yearPageSize), false, ); } if (direction === 'next') { const targetDate = utils.addYears( - utils.startOfYear(rootContext.visibleDate), - rootContext.yearPageSize, + utils.startOfYear(baseRootContext.visibleDate), + baseRootContext.yearPageSize, ); // All the years after the visible ones are fully disabled, we skip the navigation. - if (utils.isBefore(getLastEnabledYear(utils, rootContext.validationProps), targetDate)) { + if (utils.isBefore(getLastEnabledYear(utils, baseRootContext.validationProps), targetDate)) { return; } - rootContext.setVisibleDate( - utils.addYears(rootContext.visibleDate, rootContext.yearPageSize), + baseRootContext.setVisibleDate( + utils.addYears(baseRootContext.visibleDate, baseRootContext.yearPageSize), false, ); } }; - const registerSection = rootContext.registerSection; + const registerSection = baseRootContext.registerSection; React.useEffect(() => { return registerSection({ type: 'month', value: currentYear }); }, [registerSection, currentYear]); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/useYearsCells.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/useYearsCells.ts index b2ef70a3ed9f1..0700dedbce313 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/utils/useYearsCells.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/utils/useYearsCells.ts @@ -1,25 +1,25 @@ import * as React from 'react'; import { PickerValidDate } from '../../../../models'; import { useUtils } from '../../../hooks/useUtils'; -import { useCalendarRootContext } from '../root/CalendarRootContext'; +import { useBaseCalendarRootContext } from '../../utils/base-calendar/root/BaseCalendarRootContext'; export function useYearsCells(): useYearsCells.ReturnValue { - const rootContext = useCalendarRootContext(); + const baseRootContext = useBaseCalendarRootContext(); const utils = useUtils(); const years = React.useMemo( () => utils.getYearRange([ - rootContext.validationProps.minDate, - rootContext.validationProps.maxDate, + baseRootContext.validationProps.minDate, + baseRootContext.validationProps.maxDate, ]), - [utils, rootContext.validationProps.minDate, rootContext.validationProps.maxDate], + [utils, baseRootContext.validationProps.minDate, baseRootContext.validationProps.maxDate], ); - const registerSection = rootContext.registerSection; + const registerSection = baseRootContext.registerSection; React.useEffect(() => { - return registerSection({ type: 'month', value: rootContext.visibleDate }); - }, [registerSection, rootContext.visibleDate]); + return registerSection({ type: 'month', value: baseRootContext.visibleDate }); + }, [registerSection, baseRootContext.visibleDate]); return { years }; } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx index 5494d4ccbaad3..2f548c59ffc79 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx @@ -5,11 +5,12 @@ import useForkRef from '@mui/utils/useForkRef'; import { PickerValidDate } from '../../../../models'; import { useNow, useUtils } from '../../../hooks/useUtils'; import { findClosestEnabledDate } from '../../../utils/date-utils'; -import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import { useComponentRenderer } from '../../base-utils/useComponentRenderer'; +import { BaseUIComponentProps } from '../../base-utils/types'; +import { useCompositeListItem } from '../../composite/list/useCompositeListItem'; +import { useBaseCalendarRootContext } from '../../utils/base-calendar/root/BaseCalendarRootContext'; import { useCalendarRootContext } from '../root/CalendarRootContext'; import { useCalendarYearsCell } from './useCalendarYearsCell'; -import { BaseUIComponentProps } from '../../utils/types'; -import { useCompositeListItem } from '../../composite/list/useCompositeListItem'; const InnerCalendarYearsCell = React.forwardRef(function InnerCalendarYearsCell( props: InnerCalendarYearsCellProps, @@ -47,9 +48,10 @@ const CalendarYearsCell = React.forwardRef(function CalendarsYearCell( forwardedRef: React.ForwardedRef, ) { const rootContext = useCalendarRootContext(); + const baseRootContext = useBaseCalendarRootContext(); const { ref: listItemRef } = useCompositeListItem(); const utils = useUtils(); - const now = useNow(rootContext.timezone); + const now = useNow(baseRootContext.timezone); const mergedRef = useForkRef(forwardedRef, listItemRef); const isSelected = React.useMemo( @@ -87,35 +89,35 @@ const CalendarYearsCell = React.forwardRef(function CalendarsYearCell( }, [rootContext.validationProps, props.value, now, utils]); const isDisabled = React.useMemo(() => { - if (rootContext.disabled) { + if (baseRootContext.disabled) { return true; } return isInvalid; - }, [rootContext.disabled, isInvalid]); + }, [baseRootContext.disabled, isInvalid]); const isTabbable = React.useMemo( () => utils.isValid(rootContext.value) ? isSelected - : utils.isSameYear(rootContext.referenceDate, props.value), - [utils, rootContext.value, rootContext.referenceDate, isSelected, props.value], + : utils.isSameYear(rootContext.referenceValue, props.value), + [utils, rootContext.value, rootContext.referenceValue, isSelected, props.value], ); const selectYear = useEventCallback((newValue: PickerValidDate) => { - if (rootContext.readOnly) { + if (baseRootContext.readOnly) { return; } const newCleanValue = utils.setYear( - rootContext.value ?? rootContext.referenceDate, + rootContext.value ?? rootContext.referenceValue, utils.getYear(newValue), ); const startOfYear = utils.startOfYear(newCleanValue); const endOfYear = utils.endOfYear(newCleanValue); - const closestEnabledDate = rootContext.isDateInvalid(newCleanValue) + const closestEnabledDate = baseRootContext.isDateInvalid(newCleanValue) ? findClosestEnabledDate({ utils, date: newCleanValue, @@ -127,8 +129,8 @@ const CalendarYearsCell = React.forwardRef(function CalendarsYearCell( : rootContext.validationProps.maxDate, disablePast: rootContext.validationProps.disablePast, disableFuture: rootContext.validationProps.disableFuture, - isDateDisabled: rootContext.isDateInvalid, - timezone: rootContext.timezone, + isDateDisabled: baseRootContext.isDateInvalid, + timezone: baseRootContext.timezone, }) : newCleanValue; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-cell/useCalendarYearsCell.ts b/packages/x-date-pickers/src/internals/base/Calendar/years-cell/useCalendarYearsCell.ts index 4eccc7801b2de..027f6648be686 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-cell/useCalendarYearsCell.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-cell/useCalendarYearsCell.ts @@ -2,8 +2,8 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; import { PickerValidDate } from '../../../../models'; import { useUtils } from '../../../hooks/useUtils'; -import { GenericHTMLProps } from '../../utils/types'; -import { mergeReactProps } from '../../utils/mergeReactProps'; +import { GenericHTMLProps } from '../../base-utils/types'; +import { mergeReactProps } from '../../base-utils/mergeReactProps'; export function useCalendarYearsCell(parameters: useCalendarYearsCell.Parameters) { const utils = useUtils(); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-grid/CalendarYearsGrid.tsx b/packages/x-date-pickers/src/internals/base/Calendar/years-grid/CalendarYearsGrid.tsx index 1754b483ee092..2827242ce2438 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-grid/CalendarYearsGrid.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-grid/CalendarYearsGrid.tsx @@ -1,8 +1,8 @@ 'use client'; import * as React from 'react'; import { useCalendarYearsGrid } from './useCalendarYearsGrid'; -import { BaseUIComponentProps } from '../../utils/types'; -import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import { BaseUIComponentProps } from '../../base-utils/types'; +import { useComponentRenderer } from '../../base-utils/useComponentRenderer'; import { CompositeList } from '../../composite/list/CompositeList'; const CalendarYearsGrid = React.forwardRef(function CalendarYearsList( diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-grid/useCalendarYearsGrid.ts b/packages/x-date-pickers/src/internals/base/Calendar/years-grid/useCalendarYearsGrid.ts index 911cc408c4576..00719a4f7bfad 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-grid/useCalendarYearsGrid.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-grid/useCalendarYearsGrid.ts @@ -1,8 +1,8 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; import { PickerValidDate } from '../../../../models'; -import { GenericHTMLProps } from '../../utils/types'; -import { mergeReactProps } from '../../utils/mergeReactProps'; +import { GenericHTMLProps } from '../../base-utils/types'; +import { mergeReactProps } from '../../base-utils/mergeReactProps'; import { navigateInGrid } from '../utils/keyboardNavigation'; import { useYearsCells } from '../utils/useYearsCells'; import { CalendarYearsGridCssVars } from './CalendarYearsGridCssVars'; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-list/CalendarYearsList.tsx b/packages/x-date-pickers/src/internals/base/Calendar/years-list/CalendarYearsList.tsx index f486d8057d7e3..439f9835de5db 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-list/CalendarYearsList.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-list/CalendarYearsList.tsx @@ -1,8 +1,8 @@ 'use client'; import * as React from 'react'; import { useCalendarYearsList } from './useCalendarYearsList'; -import { BaseUIComponentProps } from '../../utils/types'; -import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import { BaseUIComponentProps } from '../../base-utils/types'; +import { useComponentRenderer } from '../../base-utils/useComponentRenderer'; import { CompositeList } from '../../composite/list/CompositeList'; const CalendarYearsList = React.forwardRef(function CalendarYearsList( diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-list/useCalendarYearsList.ts b/packages/x-date-pickers/src/internals/base/Calendar/years-list/useCalendarYearsList.ts index de648aac82919..ae3bfa77bf18e 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-list/useCalendarYearsList.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-list/useCalendarYearsList.ts @@ -1,8 +1,8 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; import { PickerValidDate } from '../../../../models'; -import { GenericHTMLProps } from '../../utils/types'; -import { mergeReactProps } from '../../utils/mergeReactProps'; +import { GenericHTMLProps } from '../../base-utils/types'; +import { mergeReactProps } from '../../base-utils/mergeReactProps'; import { navigateInList } from '../utils/keyboardNavigation'; import { useYearsCells } from '../utils/useYearsCells'; diff --git a/packages/x-date-pickers/src/internals/base/utils/defaultRenderFunctions.tsx b/packages/x-date-pickers/src/internals/base/base-utils/defaultRenderFunctions.tsx similarity index 100% rename from packages/x-date-pickers/src/internals/base/utils/defaultRenderFunctions.tsx rename to packages/x-date-pickers/src/internals/base/base-utils/defaultRenderFunctions.tsx diff --git a/packages/x-date-pickers/src/internals/base/utils/evaluateRenderProp.ts b/packages/x-date-pickers/src/internals/base/base-utils/evaluateRenderProp.ts similarity index 100% rename from packages/x-date-pickers/src/internals/base/utils/evaluateRenderProp.ts rename to packages/x-date-pickers/src/internals/base/base-utils/evaluateRenderProp.ts diff --git a/packages/x-date-pickers/src/internals/base/utils/fastObjectShallowCompare.ts b/packages/x-date-pickers/src/internals/base/base-utils/fastObjectShallowCompare.ts similarity index 100% rename from packages/x-date-pickers/src/internals/base/utils/fastObjectShallowCompare.ts rename to packages/x-date-pickers/src/internals/base/base-utils/fastObjectShallowCompare.ts diff --git a/packages/x-date-pickers/src/internals/base/utils/getStyleHookProps.ts b/packages/x-date-pickers/src/internals/base/base-utils/getStyleHookProps.ts similarity index 100% rename from packages/x-date-pickers/src/internals/base/utils/getStyleHookProps.ts rename to packages/x-date-pickers/src/internals/base/base-utils/getStyleHookProps.ts diff --git a/packages/x-date-pickers/src/internals/base/utils/mergeReactProps.ts b/packages/x-date-pickers/src/internals/base/base-utils/mergeReactProps.ts similarity index 100% rename from packages/x-date-pickers/src/internals/base/utils/mergeReactProps.ts rename to packages/x-date-pickers/src/internals/base/base-utils/mergeReactProps.ts diff --git a/packages/x-date-pickers/src/internals/base/utils/reactVersion.ts b/packages/x-date-pickers/src/internals/base/base-utils/reactVersion.ts similarity index 100% rename from packages/x-date-pickers/src/internals/base/utils/reactVersion.ts rename to packages/x-date-pickers/src/internals/base/base-utils/reactVersion.ts diff --git a/packages/x-date-pickers/src/internals/base/utils/resolveClassName.ts b/packages/x-date-pickers/src/internals/base/base-utils/resolveClassName.ts similarity index 100% rename from packages/x-date-pickers/src/internals/base/utils/resolveClassName.ts rename to packages/x-date-pickers/src/internals/base/base-utils/resolveClassName.ts diff --git a/packages/x-date-pickers/src/internals/base/utils/types.ts b/packages/x-date-pickers/src/internals/base/base-utils/types.ts similarity index 100% rename from packages/x-date-pickers/src/internals/base/utils/types.ts rename to packages/x-date-pickers/src/internals/base/base-utils/types.ts diff --git a/packages/x-date-pickers/src/internals/base/utils/useComponentRenderer.ts b/packages/x-date-pickers/src/internals/base/base-utils/useComponentRenderer.ts similarity index 100% rename from packages/x-date-pickers/src/internals/base/utils/useComponentRenderer.ts rename to packages/x-date-pickers/src/internals/base/base-utils/useComponentRenderer.ts diff --git a/packages/x-date-pickers/src/internals/base/utils/useEnhancedEffect.ts b/packages/x-date-pickers/src/internals/base/base-utils/useEnhancedEffect.ts similarity index 100% rename from packages/x-date-pickers/src/internals/base/utils/useEnhancedEffect.ts rename to packages/x-date-pickers/src/internals/base/base-utils/useEnhancedEffect.ts diff --git a/packages/x-date-pickers/src/internals/base/utils/useEventCallback.ts b/packages/x-date-pickers/src/internals/base/base-utils/useEventCallback.ts similarity index 100% rename from packages/x-date-pickers/src/internals/base/utils/useEventCallback.ts rename to packages/x-date-pickers/src/internals/base/base-utils/useEventCallback.ts diff --git a/packages/x-date-pickers/src/internals/base/utils/useForkRef.ts b/packages/x-date-pickers/src/internals/base/base-utils/useForkRef.ts similarity index 100% rename from packages/x-date-pickers/src/internals/base/utils/useForkRef.ts rename to packages/x-date-pickers/src/internals/base/base-utils/useForkRef.ts diff --git a/packages/x-date-pickers/src/internals/base/utils/useRenderPropForkRef.ts b/packages/x-date-pickers/src/internals/base/base-utils/useRenderPropForkRef.ts similarity index 100% rename from packages/x-date-pickers/src/internals/base/utils/useRenderPropForkRef.ts rename to packages/x-date-pickers/src/internals/base/base-utils/useRenderPropForkRef.ts diff --git a/packages/x-date-pickers/src/internals/base/composite/list/CompositeList.tsx b/packages/x-date-pickers/src/internals/base/composite/list/CompositeList.tsx index be2ed3827cf73..09c44edcee9e5 100644 --- a/packages/x-date-pickers/src/internals/base/composite/list/CompositeList.tsx +++ b/packages/x-date-pickers/src/internals/base/composite/list/CompositeList.tsx @@ -2,8 +2,8 @@ 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; -import { fastObjectShallowCompare } from '../../utils/fastObjectShallowCompare'; -import { useEnhancedEffect } from '../../utils/useEnhancedEffect'; +import { fastObjectShallowCompare } from '../../base-utils/fastObjectShallowCompare'; +import { useEnhancedEffect } from '../../base-utils/useEnhancedEffect'; import { CompositeListContext } from './CompositeListContext'; function sortByDocumentPosition(a: Node, b: Node) { diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-grid-body/BaseCalendarDaysGridBodyContext.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-grid-body/BaseCalendarDaysGridBodyContext.ts new file mode 100644 index 0000000000000..a3f0486c15190 --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-grid-body/BaseCalendarDaysGridBodyContext.ts @@ -0,0 +1,29 @@ +import * as React from 'react'; + +export interface BaseCalendarDaysGridBodyContext { + registerWeekRowCells: ( + weekRowRef: React.RefObject, + cellsRef: React.RefObject<(HTMLElement | null)[]>, + ) => () => void; +} + +export const BaseCalendarDaysGridBodyContext = React.createContext< + BaseCalendarDaysGridBodyContext | undefined +>(undefined); + +if (process.env.NODE_ENV !== 'production') { + BaseCalendarDaysGridBodyContext.displayName = 'BaseCalendarDaysGridBodyContext'; +} + +export function useBaseCalendarDaysGridBodyContext() { + const context = React.useContext(BaseCalendarDaysGridBodyContext); + if (context === undefined) { + throw new Error( + [ + 'Base UI X: BaseCalendarDaysGridBodyContext is missing.', + ' must be placed within and must be placed within .', + ].join('\n'), + ); + } + return context; +} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-grid-body/useCalendarDaysGridBody.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-grid-body/useBaseCalendarDaysGridBody.ts similarity index 55% rename from packages/x-date-pickers/src/internals/base/Calendar/days-grid-body/useCalendarDaysGridBody.ts rename to packages/x-date-pickers/src/internals/base/utils/base-calendar/days-grid-body/useBaseCalendarDaysGridBody.ts index a16fa0376e207..e0ef9cf95c2cc 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-grid-body/useCalendarDaysGridBody.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-grid-body/useBaseCalendarDaysGridBody.ts @@ -1,18 +1,18 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; -import { PickerValidDate } from '../../../../models'; -import { useCalendarDaysGridContext } from '../days-grid/CalendarDaysGridContext'; -import { mergeReactProps } from '../../utils/mergeReactProps'; -import { GenericHTMLProps } from '../../utils/types'; -import { CalendarDaysGridBodyContext } from './CalendarDaysGridBodyContext'; -import { useCalendarRootContext } from '../root/CalendarRootContext'; +import { PickerValidDate } from '../../../../../models'; +import { useBaseCalendarDaysGridContext } from '../days-grid/BaseCalendarDaysGridContext'; +import { mergeReactProps } from '../../../base-utils/mergeReactProps'; +import { GenericHTMLProps } from '../../../base-utils/types'; +import { useBaseCalendarRootContext } from '../root/BaseCalendarRootContext'; +import { BaseCalendarDaysGridBodyContext } from './BaseCalendarDaysGridBodyContext'; -export function useCalendarDaysGridBody(parameters: useCalendarDaysGridBody.Parameters) { +export function useBaseCalendarDaysGridBody(parameters: useBaseCalendarDaysGridBody.Parameters) { const { children } = parameters; - const rootContext = useCalendarRootContext(); - const daysGridContext = useCalendarDaysGridContext(); - const rowsRef: useCalendarDaysGridBody.RowsRef = React.useRef([]); - const cellsRef: useCalendarDaysGridBody.CellsRef = React.useRef([]); + const baseRootContext = useBaseCalendarRootContext(); + const baseDaysGridContext = useBaseCalendarDaysGridContext(); + const rowsRef: useBaseCalendarDaysGridBody.RowsRef = React.useRef([]); + const cellsRef: useBaseCalendarDaysGridBody.CellsRef = React.useRef([]); const getDaysGridBodyProps = React.useCallback( (externalProps: GenericHTMLProps) => { @@ -21,11 +21,11 @@ export function useCalendarDaysGridBody(parameters: useCalendarDaysGridBody.Para children: children == null ? null - : children({ weeks: daysGridContext.daysGrid.map((week) => week[0]) }), - onKeyDown: rootContext.applyDayGridKeyboardNavigation, + : children({ weeks: baseDaysGridContext.daysGrid.map((week) => week[0]) }), + onKeyDown: baseRootContext.applyDayGridKeyboardNavigation, }); }, - [daysGridContext.daysGrid, rootContext.applyDayGridKeyboardNavigation, children], + [baseDaysGridContext.daysGrid, baseRootContext.applyDayGridKeyboardNavigation, children], ); const registerWeekRowCells = useEventCallback( @@ -41,12 +41,12 @@ export function useCalendarDaysGridBody(parameters: useCalendarDaysGridBody.Para }, ); - const registerDaysGridCells = rootContext.registerDaysGridCells; + const registerDaysGridCells = baseRootContext.registerDaysGridCells; React.useEffect(() => { return registerDaysGridCells(cellsRef, rowsRef); }, [registerDaysGridCells]); - const context: CalendarDaysGridBodyContext = React.useMemo( + const context: BaseCalendarDaysGridBodyContext = React.useMemo( () => ({ registerWeekRowCells }), [registerWeekRowCells], ); @@ -57,7 +57,7 @@ export function useCalendarDaysGridBody(parameters: useCalendarDaysGridBody.Para ); } -export namespace useCalendarDaysGridBody { +export namespace useBaseCalendarDaysGridBody { export interface Parameters { children?: (parameters: ChildrenParameters) => React.ReactNode; } diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-grid/BaseCalendarDaysGridContext.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-grid/BaseCalendarDaysGridContext.ts new file mode 100644 index 0000000000000..e41d53025ca3c --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-grid/BaseCalendarDaysGridContext.ts @@ -0,0 +1,30 @@ +import * as React from 'react'; +import { PickerValidDate } from '../../../../../models'; + +export interface BaseCalendarDaysGridContext { + selectDay: (value: PickerValidDate) => void; + currentMonth: PickerValidDate; + tabbableDay: PickerValidDate | null; + daysGrid: PickerValidDate[][]; +} + +export const BaseCalendarDaysGridContext = React.createContext< + BaseCalendarDaysGridContext | undefined +>(undefined); + +if (process.env.NODE_ENV !== 'production') { + BaseCalendarDaysGridContext.displayName = 'BaseCalendarDaysGridContext'; +} + +export function useBaseCalendarDaysGridContext() { + const context = React.useContext(BaseCalendarDaysGridContext); + if (context === undefined) { + throw new Error( + [ + 'Base UI X: BaseCalendarDaysGridContext is missing.', + ' must be placed within and must be placed within .', + ].join('\n'), + ); + } + return context; +} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-grid/useCalendarDaysGrid.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-grid/useBaseCalendarDaysGrid.ts similarity index 69% rename from packages/x-date-pickers/src/internals/base/Calendar/days-grid/useCalendarDaysGrid.ts rename to packages/x-date-pickers/src/internals/base/utils/base-calendar/days-grid/useBaseCalendarDaysGrid.ts index a11f056b08687..a2d35a9a17283 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-grid/useCalendarDaysGrid.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-grid/useBaseCalendarDaysGrid.ts @@ -1,22 +1,24 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; -import { PickerValidDate } from '../../../../models'; -import { useUtils } from '../../../hooks/useUtils'; -import { mergeDateAndTime } from '../../../utils/date-utils'; -import { useCalendarRootContext } from '../root/CalendarRootContext'; -import { GenericHTMLProps } from '../../utils/types'; -import { mergeReactProps } from '../../utils/mergeReactProps'; -import { CalendarDaysGridContext } from './CalendarDaysGridContext'; - -export function useCalendarDaysGrid(parameters: useCalendarDaysGrid.Parameters) { +import { PickerValidDate } from '../../../../../models'; +import { useUtils } from '../../../../hooks/useUtils'; +import { mergeDateAndTime } from '../../../../utils/date-utils'; +import { useCalendarRootContext } from '../../../Calendar/root/CalendarRootContext'; +import { GenericHTMLProps } from '../../../base-utils/types'; +import { mergeReactProps } from '../../../base-utils/mergeReactProps'; +import { useBaseCalendarRootContext } from '../root/BaseCalendarRootContext'; +import { BaseCalendarDaysGridContext } from './BaseCalendarDaysGridContext'; + +export function useBaseCalendarDaysGrid(parameters: useBaseCalendarDaysGrid.Parameters) { const { fixedWeekNumber, offset = 0 } = parameters; const utils = useUtils(); const rootContext = useCalendarRootContext(); + const baseRootContext = useBaseCalendarRootContext(); const currentMonth = React.useMemo(() => { - const cleanVisibleDate = utils.startOfMonth(rootContext.visibleDate); + const cleanVisibleDate = utils.startOfMonth(baseRootContext.visibleDate); return offset === 0 ? cleanVisibleDate : utils.addMonths(cleanVisibleDate, offset); - }, [utils, rootContext.visibleDate, offset]); + }, [utils, baseRootContext.visibleDate, offset]); const daysGrid = React.useMemo(() => { const toDisplay = utils.getWeekArray(currentMonth); @@ -47,14 +49,14 @@ export function useCalendarDaysGrid(parameters: useCalendarDaysGrid.Parameters) }, []); const selectDay = useEventCallback((newValue: PickerValidDate) => { - if (rootContext.readOnly) { + if (baseRootContext.readOnly) { return; } const newCleanValue = mergeDateAndTime( utils, newValue, - rootContext.value ?? rootContext.referenceDate, + rootContext.value ?? rootContext.referenceValue, ); rootContext.setValue(newCleanValue, { section: 'day' }); @@ -62,20 +64,20 @@ export function useCalendarDaysGrid(parameters: useCalendarDaysGrid.Parameters) const tabbableDay = React.useMemo(() => { const flatDays = daysGrid.flat(); - const tempTabbableDay = rootContext.value ?? rootContext.referenceDate; + const tempTabbableDay = rootContext.value ?? rootContext.referenceValue; if (flatDays.some((day) => utils.isSameDay(day, tempTabbableDay))) { return tempTabbableDay; } return flatDays.find((day) => utils.isSameMonth(day, currentMonth)) ?? null; - }, [rootContext.value, rootContext.referenceDate, daysGrid, utils, currentMonth]); + }, [rootContext.value, rootContext.referenceValue, daysGrid, utils, currentMonth]); - const registerSection = rootContext.registerSection; + const registerSection = baseRootContext.registerSection; React.useEffect(() => { return registerSection({ type: 'day', value: currentMonth }); }, [registerSection, currentMonth]); - const context: CalendarDaysGridContext = React.useMemo( + const context: BaseCalendarDaysGridContext = React.useMemo( () => ({ selectDay, daysGrid, currentMonth, tabbableDay }), [selectDay, daysGrid, currentMonth, tabbableDay], ); @@ -83,7 +85,7 @@ export function useCalendarDaysGrid(parameters: useCalendarDaysGrid.Parameters) return React.useMemo(() => ({ getDaysGridProps, context }), [getDaysGridProps, context]); } -export namespace useCalendarDaysGrid { +export namespace useBaseCalendarDaysGrid { export interface Parameters { /** * The day view will show as many weeks as needed after the end of the current month to match this value. diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/BaseCalendarRootContext.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/BaseCalendarRootContext.ts new file mode 100644 index 0000000000000..cde97cf52c617 --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/BaseCalendarRootContext.ts @@ -0,0 +1,45 @@ +import * as React from 'react'; +import { PickersTimezone, PickerValidDate } from '../../../../../models'; +import type { useBaseCalendarRoot } from './useBaseCalendarRoot'; +import { BaseDateValidationProps } from '../../../../models/validation'; +import type { useBaseCalendarDaysGridBody } from '../days-grid-body/useBaseCalendarDaysGridBody'; + +export interface BaseCalendarRootContext { + timezone: PickersTimezone; + disabled: boolean; + readOnly: boolean; + autoFocus: boolean; + isDateInvalid: (day: PickerValidDate | null) => boolean; + validationProps: Required; + visibleDate: PickerValidDate; + setVisibleDate: (visibleDate: PickerValidDate, skipIfAlreadyVisible: boolean) => void; + monthPageSize: number; + yearPageSize: number; + applyDayGridKeyboardNavigation: (event: React.KeyboardEvent) => void; + registerDaysGridCells: ( + cellsRef: useBaseCalendarDaysGridBody.CellsRef, + rowsRef: useBaseCalendarDaysGridBody.RowsRef, + ) => () => void; + registerSection: (parameters: useBaseCalendarRoot.RegisterSectionParameters) => () => void; +} + +export const BaseCalendarRootContext = React.createContext( + undefined, +); + +if (process.env.NODE_ENV !== 'production') { + BaseCalendarRootContext.displayName = 'BaseCalendarRootContext'; +} + +export function useBaseCalendarRootContext() { + const context = React.useContext(BaseCalendarRootContext); + if (context === undefined) { + throw new Error( + [ + 'Base UI X: BaseCalendarRootContext is missing.', + 'Calendar parts must be placed within and Range Calendar parts must be placed within .', + ].join('\n'), + ); + } + return context; +} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarDaysGridsNavigation.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarDaysGridsNavigation.ts similarity index 84% rename from packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarDaysGridsNavigation.ts rename to packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarDaysGridsNavigation.ts index eb50303ce2ea2..eec360c7420b8 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarDaysGridsNavigation.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarDaysGridsNavigation.ts @@ -1,25 +1,25 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; import useTimeout from '@mui/utils/useTimeout'; -import { PickerValidDate } from '../../../../models'; -import { ValidateDateProps } from '../../../../validation'; -import { useUtils } from '../../../hooks/useUtils'; -import type { useCalendarDaysGridBody } from '../days-grid-body/useCalendarDaysGridBody'; +import { PickerValidDate } from '../../../../../models'; +import { ValidateDateProps } from '../../../../../validation'; +import { useUtils } from '../../../../hooks/useUtils'; +import type { useCalendarDaysGridBody } from '../../../Calendar/days-grid-body/useCalendarDaysGridBody'; import { applyInitialFocusInGrid, navigateInGrid, NavigateInGridChangePage, PageGridNavigationTarget, -} from '../utils/keyboardNavigation'; -import type { CalendarRootContext } from './CalendarRootContext'; -import { getFirstEnabledMonth, getLastEnabledMonth } from '../utils/date'; +} from '../../../Calendar/utils/keyboardNavigation'; +import { getFirstEnabledMonth, getLastEnabledMonth } from '../../../Calendar/utils/date'; +import { BaseCalendarRootContext } from './BaseCalendarRootContext'; /** * This logic needs to be in Calendar.Root to support multiple Calendar.DaysGrid. * We could introduce a Calendar.MultipleDaysGrid component that would handle this logic if we want to avoid having it in Calendar.Root. */ -export function useCalendarDaysGridNavigation( - parameters: useCalendarDaysGridNavigation.Parameters, +export function useBaseCalendarDaysGridNavigation( + parameters: useBaseCalendarDaysGridNavigation.Parameters, ) { const { visibleDate, setVisibleDate, monthPageSize, validationProps } = parameters; const utils = useUtils(); @@ -89,7 +89,7 @@ export function useCalendarDaysGridNavigation( }; } -export namespace useCalendarDaysGridNavigation { +export namespace useBaseCalendarDaysGridNavigation { export interface Parameters { visibleDate: PickerValidDate; setVisibleDate: (visibleDate: PickerValidDate) => void; @@ -98,7 +98,10 @@ export namespace useCalendarDaysGridNavigation { } export interface ReturnValue - extends Pick {} + extends Pick< + BaseCalendarRootContext, + 'registerDaysGridCells' | 'applyDayGridKeyboardNavigation' + > {} } /* eslint-disable no-bitwise */ diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarRoot.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarRoot.ts new file mode 100644 index 0000000000000..3ba521ed1e639 --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarRoot.ts @@ -0,0 +1,288 @@ +import * as React from 'react'; +import useEventCallback from '@mui/utils/useEventCallback'; +import { OnErrorProps, PickerManager, PickerValidDate, TimezoneProps } from '../../../../../models'; +import { useValidation, ValidateDateProps } from '../../../../../validation'; +import { useIsDateDisabled } from '../../../../../DateCalendar/useIsDateDisabled'; +import { FormProps, InferNonNullablePickerValue, PickerValidValue } from '../../../../models'; +import { SECTION_TYPE_GRANULARITY } from '../../../../utils/getDefaultReferenceDate'; +import { applyDefaultDate } from '../../../../utils/date-utils'; +import { useDefaultDates, useUtils } from '../../../../hooks/useUtils'; +import { BaseDateValidationProps } from '../../../../models/validation'; +import { useControlledValueWithTimezone } from '../../../../hooks/useValueWithTimezone'; +import { useBaseCalendarDaysGridNavigation } from './useBaseCalendarDaysGridsNavigation'; +import { BaseCalendarRootContext } from './BaseCalendarRootContext'; + +export function useBaseCalendarRoot( + parameters: useBaseCalendarRoot.Parameters, +) { + const { + readOnly = false, + disabled = false, + autoFocus = false, + onError, + defaultValue, + onValueChange, + value: valueProp, + timezone: timezoneProp, + referenceDate: referenceDateProp, + monthPageSize = 1, + yearPageSize = 1, + manager, + minDate, + maxDate, + disablePast, + disableFuture, + getInitialVisibleDate, + } = parameters; + + const utils = useUtils(); + const validationProps = useAddDefaultsToBaseDateValidationProps({ + minDate, + maxDate, + disablePast, + disableFuture, + }); + + const { value, handleValueChange, timezone } = useControlledValueWithTimezone({ + name: 'CalendarRoot', + timezone: timezoneProp, + value: valueProp, + defaultValue, + referenceDate: referenceDateProp, + onChange: onValueChange, + valueManager: manager.internal_valueManager, + }); + + const referenceValue = React.useMemo( + () => { + return manager.internal_valueManager.getInitialReferenceValue({ + value, + utils, + timezone, + props: validationProps, + referenceDate: referenceDateProp, + granularity: SECTION_TYPE_GRANULARITY.day, + }); + }, + // We want the `referenceDate` to update on prop and `timezone` change (https://github.com/mui/mui-x/issues/10804) + // eslint-disable-next-line react-hooks/exhaustive-deps + [referenceDateProp, timezone], + ); + + const { getValidationErrorForNewValue } = useValidation({ + props: { ...validationProps, onError }, + value, + timezone, + validator: manager.validator, + }); + + const setValue = useEventCallback( + ( + newValue: TValue, + context: Pick, 'section'>, + ) => { + handleValueChange(newValue, { + ...context, + validationError: getValidationErrorForNewValue(newValue), + }); + }, + ); + + // TODO: Rename this hook (if we keep it for Base UI X) + const isDateInvalid = useIsDateDisabled({ + ...validationProps, + timezone, + }); + + const sectionsRef = React.useRef< + Record<'day' | 'month' | 'year', Record> + >({ + day: {}, + month: {}, + year: {}, + }); + const registerSection = useEventCallback( + (section: useBaseCalendarRoot.RegisterSectionParameters) => { + const id = Math.random(); + sectionsRef.current[section.type][id] = section.value; + return () => { + delete sectionsRef.current[section.type][id]; + }; + }, + ); + + const isDateCellVisible = (date: PickerValidDate) => { + if (Object.values(sectionsRef.current.day).length > 0) { + return Object.values(sectionsRef.current.day).every( + (month) => !utils.isSameMonth(date, month), + ); + } + if (Object.values(sectionsRef.current.month).length > 0) { + return Object.values(sectionsRef.current.month).every( + (year) => !utils.isSameYear(date, year), + ); + } + return true; + }; + + const [visibleDate, setVisibleDate] = React.useState(() => + getInitialVisibleDate(referenceValue), + ); + const handleVisibleDateChange = useEventCallback( + (newVisibleDate: PickerValidDate, skipIfAlreadyVisible: boolean) => { + if (skipIfAlreadyVisible && isDateCellVisible(newVisibleDate)) { + return; + } + + setVisibleDate(newVisibleDate); + }, + ); + + const { applyDayGridKeyboardNavigation, registerDaysGridCells } = + useBaseCalendarDaysGridNavigation({ + visibleDate, + setVisibleDate, + monthPageSize, + validationProps, + }); + + const context: BaseCalendarRootContext = React.useMemo( + () => ({ + timezone, + disabled, + readOnly, + autoFocus, + isDateInvalid, + visibleDate, + setVisibleDate: handleVisibleDateChange, + monthPageSize, + yearPageSize, + applyDayGridKeyboardNavigation, + registerDaysGridCells, + registerSection, + validationProps, + }), + [ + timezone, + disabled, + readOnly, + autoFocus, + isDateInvalid, + visibleDate, + handleVisibleDateChange, + monthPageSize, + yearPageSize, + applyDayGridKeyboardNavigation, + registerDaysGridCells, + registerSection, + validationProps, + ], + ); + + return { + value, + setValue, + referenceValue, + setVisibleDate, + isDateCellVisible, + context, + validationProps, + }; +} + +export namespace useBaseCalendarRoot { + export interface Parameters + extends TimezoneProps, + FormProps, + OnErrorProps, + BaseDateValidationProps { + /** + * The manager of the calendar (uses `useDateManager` for Calendar and `useDateRangeManager` for RangeCalendar). + */ + manager: PickerManager; + /** + * TODO: Write description + * @param {InferNonNullablePickerValue} referenceValue The reference value to get the initial visible date from. + * @returns {PickerValidDate | null} The initial visible date. + */ + getInitialVisibleDate: (referenceValue: InferNonNullablePickerValue) => PickerValidDate; + /** + * The controlled value that should be selected. + * To render an uncontrolled Date Calendar, use the `defaultValue` prop instead. + */ + value?: TValue; + /** + * The uncontrolled value that should be initially selected. + * To render a controlled accordion, use the `value` prop instead. + */ + defaultValue?: TValue; + /** + * Event handler called when the selected value changes. + * Provides the new value as an argument. + * @param {TValue} value The new selected value. + * @param {useBaseCalendarRoot.ValueChangeHandlerContext} context Additional context information. + */ + onValueChange?: ( + value: TValue, + context: useBaseCalendarRoot.ValueChangeHandlerContext, + ) => void; + /** + * The date used to generate the new value when both `value` and `defaultValue` are empty. + * @default The closest valid date using the validation props, except callbacks such as `shouldDisableDate`. + */ + referenceDate?: PickerValidDate; + /** + * If `true`, one of the cells will be automatically focused when the component is mounted. + * If a value or a default value is provided, the focused cell will be the one corresponding to the selected date. + * @default false + */ + autoFocus?: boolean; + /** + * The amount of months to navigate by when pressing or when using keyboard navigation in the day grid. + * This is mostly useful when displaying multiple day grids. + * @default 1 + */ + monthPageSize?: number; + /** + * The amount of months to navigate by when pressing or when using keyboard navigation in the month grid or the month list. + * This is mostly useful when displaying multiple month grids or month lists. + * @default 1 + */ + yearPageSize?: number; + } + + export interface ValueChangeHandlerContext { + /** + * The section handled by the UI that triggered the change. + */ + section: 'day' | 'month' | 'year'; + /** + * The validation error associated to the new value. + */ + validationError: TError; + } + + export interface RegisterSectionParameters { + type: 'day' | 'month' | 'year'; + value: PickerValidDate; + } +} + +function useAddDefaultsToBaseDateValidationProps( + validationDate: BaseDateValidationProps, +): ValidateDateProps { + const utils = useUtils(); + const defaultDates = useDefaultDates(); + + const { disablePast, disableFuture, minDate, maxDate } = validationDate; + + return React.useMemo( + () => ({ + disablePast: disablePast ?? false, + disableFuture: disableFuture ?? false, + minDate: applyDefaultDate(utils, minDate, defaultDates.minDate), + maxDate: applyDefaultDate(utils, maxDate, defaultDates.maxDate), + }), + [disablePast, disableFuture, minDate, maxDate, utils, defaultDates], + ); +} From 2075c83f31288a5f777804f2168e324e675fccce Mon Sep 17 00:00:00 2001 From: flavien Date: Tue, 7 Jan 2025 14:52:15 +0100 Subject: [PATCH 074/136] Work on range calendar --- .../RangeCalendarDaysGridBody.tsx | 48 +++++++++++++++++++ ...RangeCalendarDaysGridBodyDataAttributes.ts | 1 + .../days-grid/RangeCalendarDaysGrid.tsx | 47 ++++++++++++++++++ .../RangeCalendarDaysGridDataAttributes.ts | 1 + .../base/RangeCalendar/index.parts.ts | 4 +- .../root/RangeCalendarRootDataAttributes.ts | 1 + 6 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid-body/RangeCalendarDaysGridBody.tsx create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid-body/RangeCalendarDaysGridBodyDataAttributes.ts create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid/RangeCalendarDaysGrid.tsx create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid/RangeCalendarDaysGridDataAttributes.ts create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootDataAttributes.ts diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid-body/RangeCalendarDaysGridBody.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid-body/RangeCalendarDaysGridBody.tsx new file mode 100644 index 0000000000000..4fca7ba1822cf --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid-body/RangeCalendarDaysGridBody.tsx @@ -0,0 +1,48 @@ +'use client'; +import * as React from 'react'; +// eslint-disable-next-line no-restricted-imports +import { BaseUIComponentProps } from '@mui/x-date-pickers/internals/base/base-utils/types'; +// eslint-disable-next-line no-restricted-imports +import { useComponentRenderer } from '@mui/x-date-pickers/internals/base/base-utils/useComponentRenderer'; +// eslint-disable-next-line no-restricted-imports +import { CompositeList } from '@mui/x-date-pickers/internals/base/composite/list/CompositeList'; +// eslint-disable-next-line no-restricted-imports +import { useBaseCalendarDaysGridBody } from '@mui/x-date-pickers/internals/base/utils/base-calendar/days-grid-body/useBaseCalendarDaysGridBody'; +// eslint-disable-next-line no-restricted-imports +import { BaseCalendarDaysGridBodyContext } from '@mui/x-date-pickers/internals/base/utils/base-calendar/days-grid-body/BaseCalendarDaysGridBodyContext'; + +const CalendarDaysGridBody = React.forwardRef(function CalendarDaysGrid( + props: CalendarDaysGridBody.Props, + forwardedRef: React.ForwardedRef, +) { + const { className, render, children, ...otherProps } = props; + const { getDaysGridBodyProps, context, calendarWeekRowRefs } = useBaseCalendarDaysGridBody({ + children, + }); + const state = React.useMemo(() => ({}), []); + + const { renderElement } = useComponentRenderer({ + propGetter: getDaysGridBodyProps, + render: render ?? 'div', + ref: forwardedRef, + className, + state, + extraProps: otherProps, + }); + + return ( + + {renderElement()} + + ); +}); + +export namespace CalendarDaysGridBody { + export interface State {} + + export interface Props + extends Omit, 'children'>, + useBaseCalendarDaysGridBody.Parameters {} +} + +export { CalendarDaysGridBody }; diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid-body/RangeCalendarDaysGridBodyDataAttributes.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid-body/RangeCalendarDaysGridBodyDataAttributes.ts new file mode 100644 index 0000000000000..c0af763a666b2 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid-body/RangeCalendarDaysGridBodyDataAttributes.ts @@ -0,0 +1 @@ +export enum CalendarDaysGridBodyDataAttributes {} diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid/RangeCalendarDaysGrid.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid/RangeCalendarDaysGrid.tsx new file mode 100644 index 0000000000000..cbf5ba5ca95b0 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid/RangeCalendarDaysGrid.tsx @@ -0,0 +1,47 @@ +'use client'; +import * as React from 'react'; +// eslint-disable-next-line no-restricted-imports +import { BaseCalendarDaysGridContext } from '@mui/x-date-pickers/internals/base/utils/base-calendar/days-grid/BaseCalendarDaysGridContext'; +// eslint-disable-next-line no-restricted-imports +import { useBaseCalendarDaysGrid } from '@mui/x-date-pickers/internals/base/utils/base-calendar/days-grid/useBaseCalendarDaysGrid'; +// eslint-disable-next-line no-restricted-imports +import { BaseUIComponentProps } from '@mui/x-date-pickers/internals/base/base-utils/types'; +// eslint-disable-next-line no-restricted-imports +import { useComponentRenderer } from '@mui/x-date-pickers/internals/base/base-utils/useComponentRenderer'; + +const RangeCalendarDaysGrid = React.forwardRef(function RangeCalendarDaysGrid( + props: RangeCalendarDaysGrid.Props, + forwardedRef: React.ForwardedRef, +) { + const { className, render, fixedWeekNumber, offset, ...otherProps } = props; + const { getDaysGridProps, context } = useBaseCalendarDaysGrid({ + fixedWeekNumber, + offset, + }); + const state = React.useMemo(() => ({}), []); + + const { renderElement } = useComponentRenderer({ + propGetter: getDaysGridProps, + render: render ?? 'div', + ref: forwardedRef, + className, + state, + extraProps: otherProps, + }); + + return ( + + {renderElement()} + + ); +}); + +export namespace RangeCalendarDaysGrid { + export interface State {} + + export interface Props + extends BaseUIComponentProps<'div', State>, + useBaseCalendarDaysGrid.Parameters {} +} + +export { RangeCalendarDaysGrid }; diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid/RangeCalendarDaysGridDataAttributes.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid/RangeCalendarDaysGridDataAttributes.ts new file mode 100644 index 0000000000000..f82799fe31225 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid/RangeCalendarDaysGridDataAttributes.ts @@ -0,0 +1 @@ +export enum RangeCalendarDaysGridDataAttributes {} diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/index.parts.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/index.parts.ts index c0427819f848b..efeee5170d520 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/index.parts.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/index.parts.ts @@ -1,10 +1,10 @@ export { RangeCalendarRoot as Root } from './root/RangeCalendarRoot'; // // Days -// export { CalendarDaysGrid as DaysGrid } from './days-grid/CalendarDaysGrid'; +export { RangeCalendarDaysGrid as DaysGrid } from './days-grid/RangeCalendarDaysGrid'; // export { CalendarDaysGridHeader as DaysGridHeader } from './days-grid-header/CalendarDaysGridHeader'; // export { CalendarDaysGridHeaderCell as DaysGridHeaderCell } from './days-grid-header-cell/CalendarDaysGridHeaderCell'; -// export { CalendarDaysGridBody as DaysGridBody } from './days-grid-body/CalendarDaysGridBody'; +export { CalendarDaysGridBody as DaysGridBody } from './days-grid-body/RangeCalendarDaysGridBody'; // export { CalendarDaysWeekRow as DaysWeekRow } from './days-week-row/CalendarDaysWeekRow'; // export { CalendarDaysCell as DaysCell } from './days-cell/CalendarDaysCell'; diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootDataAttributes.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootDataAttributes.ts new file mode 100644 index 0000000000000..74c45a5a14500 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootDataAttributes.ts @@ -0,0 +1 @@ +export enum RangeCalendarRootDataAttributes {} From 70b871ec7f5dd8cef0bf72ba5584d265dd5e3bde Mon Sep 17 00:00:00 2001 From: flavien Date: Tue, 7 Jan 2025 15:31:33 +0100 Subject: [PATCH 075/136] Add week row on range calendar --- .../RangeCalendarDaysWeekRow.tsx | 60 +++++++++++++++++++ .../RangeCalendarDaysWeekRowDataAttributes.ts | 1 + .../days-week-row/CalendarDaysWeekRow.tsx | 36 +++-------- .../useBaseCalendarDaysWeekRow.ts} | 16 ++--- .../useBaseCalendarDaysWeekRowWrapper.ts | 40 +++++++++++++ 5 files changed, 116 insertions(+), 37 deletions(-) create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-week-row/RangeCalendarDaysWeekRow.tsx create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-week-row/RangeCalendarDaysWeekRowDataAttributes.ts rename packages/x-date-pickers/src/internals/base/{Calendar/days-week-row/useCalendarDaysWeekRow.ts => utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRow.ts} (65%) create mode 100644 packages/x-date-pickers/src/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRowWrapper.ts diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-week-row/RangeCalendarDaysWeekRow.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-week-row/RangeCalendarDaysWeekRow.tsx new file mode 100644 index 0000000000000..251fe03440a5c --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-week-row/RangeCalendarDaysWeekRow.tsx @@ -0,0 +1,60 @@ +'use client'; +import * as React from 'react'; +// eslint-disable-next-line no-restricted-imports +import { useBaseCalendarDaysWeekRow } from '@mui/x-date-pickers/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRow'; +// eslint-disable-next-line no-restricted-imports +import { useBaseCalendarDaysWeekRowWrapper } from '@mui/x-date-pickers/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRowWrapper'; +// eslint-disable-next-line no-restricted-imports +import { BaseUIComponentProps } from '@mui/x-date-pickers/internals/base/base-utils/types'; +// eslint-disable-next-line no-restricted-imports +import { useComponentRenderer } from '@mui/x-date-pickers/internals/base/base-utils/useComponentRenderer'; +// eslint-disable-next-line no-restricted-imports +import { CompositeList } from '@mui/x-date-pickers/internals/base/composite/list/CompositeList'; + +const InnerCalendarDaysWeekRow = React.forwardRef(function CalendarDaysGrid( + props: InnerCalendarDaysWeekRowProps, + forwardedRef: React.ForwardedRef, +) { + const { className, render, value, ctx, children, ...otherProps } = props; + const { getDaysWeekRowProps, dayCellRefs } = useBaseCalendarDaysWeekRow({ + value, + ctx, + children, + }); + const state = React.useMemo(() => ({}), []); + + const { renderElement } = useComponentRenderer({ + propGetter: getDaysWeekRowProps, + render: render ?? 'div', + ref: forwardedRef, + className, + state, + extraProps: otherProps, + }); + + return {renderElement()}; +}); + +const MemoizedInnerCalendarDaysWeekRow = React.memo(InnerCalendarDaysWeekRow); + +const CalendarDaysWeekRow = React.forwardRef(function CalendarDaysWeekRow( + props: CalendarDaysWeekRow.Props, + forwardedRef: React.ForwardedRef, +) { + const { ref, ctx } = useBaseCalendarDaysWeekRowWrapper({ forwardedRef, value: props.value }); + return ; +}); + +export namespace CalendarDaysWeekRow { + export interface State {} + + export interface Props + extends Omit, 'children'>, + Omit {} +} + +interface InnerCalendarDaysWeekRowProps + extends Omit, 'children'>, + useBaseCalendarDaysWeekRow.Parameters {} + +export { CalendarDaysWeekRow }; diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-week-row/RangeCalendarDaysWeekRowDataAttributes.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-week-row/RangeCalendarDaysWeekRowDataAttributes.ts new file mode 100644 index 0000000000000..18db71e3264d0 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-week-row/RangeCalendarDaysWeekRowDataAttributes.ts @@ -0,0 +1 @@ +export enum RangeCalendarDaysWeekRowDataAttributes {} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/CalendarDaysWeekRow.tsx b/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/CalendarDaysWeekRow.tsx index 0932687258761..a6e693d068a82 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/CalendarDaysWeekRow.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/CalendarDaysWeekRow.tsx @@ -1,20 +1,17 @@ 'use client'; import * as React from 'react'; -import useForkRef from '@mui/utils/useForkRef'; -import { useCalendarDaysWeekRow } from './useCalendarDaysWeekRow'; +import { useBaseCalendarDaysWeekRow } from '../../utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRow'; import { BaseUIComponentProps } from '../../base-utils/types'; import { useComponentRenderer } from '../../base-utils/useComponentRenderer'; -import { useBaseCalendarDaysGridContext } from '../../utils/base-calendar/days-grid/BaseCalendarDaysGridContext'; import { CompositeList } from '../../composite/list/CompositeList'; -import { useCompositeListItem } from '../../composite/list/useCompositeListItem'; -import { useBaseCalendarDaysGridBodyContext } from '../../utils/base-calendar/days-grid-body/BaseCalendarDaysGridBodyContext'; +import { useBaseCalendarDaysWeekRowWrapper } from '../../utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRowWrapper'; const InnerCalendarDaysWeekRow = React.forwardRef(function CalendarDaysGrid( props: InnerCalendarDaysWeekRowProps, forwardedRef: React.ForwardedRef, ) { const { className, render, value, ctx, children, ...otherProps } = props; - const { getDaysWeekRowProps, dayCellRefs } = useCalendarDaysWeekRow({ + const { getDaysWeekRowProps, dayCellRefs } = useBaseCalendarDaysWeekRow({ value, ctx, children, @@ -39,27 +36,8 @@ const CalendarDaysWeekRow = React.forwardRef(function CalendarDaysWeekRow( props: CalendarDaysWeekRow.Props, forwardedRef: React.ForwardedRef, ) { - const baseDaysGridContext = useBaseCalendarDaysGridContext(); - const baseDaysGridBodyContext = useBaseCalendarDaysGridBodyContext(); - const { ref: listItemRef, index: rowIndex } = useCompositeListItem(); - const mergedRef = useForkRef(forwardedRef, listItemRef); - - // TODO: Improve how we pass the week to this component. - const days = React.useMemo( - () => baseDaysGridContext.daysGrid.find((week) => week[0] === props.value) ?? [], - [baseDaysGridContext.daysGrid, props.value], - ); - - const ctx = React.useMemo( - () => ({ - days, - rowIndex, - registerWeekRowCells: baseDaysGridBodyContext.registerWeekRowCells, - }), - [days, rowIndex, baseDaysGridBodyContext.registerWeekRowCells], - ); - - return ; + const { ref, ctx } = useBaseCalendarDaysWeekRowWrapper({ forwardedRef, value: props.value }); + return ; }); export namespace CalendarDaysWeekRow { @@ -67,11 +45,11 @@ export namespace CalendarDaysWeekRow { export interface Props extends Omit, 'children'>, - Omit {} + Omit {} } interface InnerCalendarDaysWeekRowProps extends Omit, 'children'>, - useCalendarDaysWeekRow.Parameters {} + useBaseCalendarDaysWeekRow.Parameters {} export { CalendarDaysWeekRow }; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/useCalendarDaysWeekRow.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRow.ts similarity index 65% rename from packages/x-date-pickers/src/internals/base/Calendar/days-week-row/useCalendarDaysWeekRow.ts rename to packages/x-date-pickers/src/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRow.ts index 0f7c737297eeb..b0079700a0a9e 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/useCalendarDaysWeekRow.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRow.ts @@ -1,10 +1,10 @@ import * as React from 'react'; -import { PickerValidDate } from '../../../../models'; -import { GenericHTMLProps } from '../../base-utils/types'; -import { mergeReactProps } from '../../base-utils/mergeReactProps'; -import { CalendarDaysGridBodyContext } from '../../utils/base-calendar/days-grid-body/BaseCalendarDaysGridBodyContext'; +import { PickerValidDate } from '../../../../../models'; +import { GenericHTMLProps } from '../../../base-utils/types'; +import { mergeReactProps } from '../../../base-utils/mergeReactProps'; +import { BaseCalendarDaysGridBodyContext } from '../days-grid-body/BaseCalendarDaysGridBodyContext'; -export function useCalendarDaysWeekRow(parameters: useCalendarDaysWeekRow.Parameters) { +export function useBaseCalendarDaysWeekRow(parameters: useBaseCalendarDaysWeekRow.Parameters) { const { children, ctx } = parameters; const ref = React.useRef(null); const dayCellRefs = React.useRef<(HTMLElement | null)[]>([]); @@ -32,10 +32,10 @@ export function useCalendarDaysWeekRow(parameters: useCalendarDaysWeekRow.Parame ); } -export namespace useCalendarDaysWeekRow { +export namespace useBaseCalendarDaysWeekRow { export interface Parameters { value: PickerValidDate; - ctx: useCalendarDaysWeekRow.Context; + ctx: useBaseCalendarDaysWeekRow.Context; children?: (parameters: ChildrenParameters) => React.ReactNode; } @@ -43,7 +43,7 @@ export namespace useCalendarDaysWeekRow { days: PickerValidDate[]; } - export interface Context extends Pick { + export interface Context extends Pick { days: PickerValidDate[]; rowIndex: number; } diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRowWrapper.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRowWrapper.ts new file mode 100644 index 0000000000000..a142634a62153 --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRowWrapper.ts @@ -0,0 +1,40 @@ +import * as React from 'react'; +import useForkRef from '@mui/utils/useForkRef'; +import { PickerValidDate } from '../../../../../models'; +import { useCompositeListItem } from '../../../composite/list/useCompositeListItem'; +import { useBaseCalendarDaysGridBodyContext } from '../days-grid-body/BaseCalendarDaysGridBodyContext'; +import { useBaseCalendarDaysGridContext } from '../days-grid/BaseCalendarDaysGridContext'; + +export function useBaseCalendarDaysWeekRowWrapper({ + forwardedRef, + value, +}: useBaseCalendarDaysWeekRowWrapper.Parameters) { + const baseDaysGridContext = useBaseCalendarDaysGridContext(); + const baseDaysGridBodyContext = useBaseCalendarDaysGridBodyContext(); + const { ref: listItemRef, index: rowIndex } = useCompositeListItem(); + const mergedRef = useForkRef(forwardedRef, listItemRef); + + // TODO: Improve how we pass the week to the week row components. + const days = React.useMemo( + () => baseDaysGridContext.daysGrid.find((week) => week[0] === value) ?? [], + [baseDaysGridContext.daysGrid, value], + ); + + const ctx = React.useMemo( + () => ({ + days, + rowIndex, + registerWeekRowCells: baseDaysGridBodyContext.registerWeekRowCells, + }), + [days, rowIndex, baseDaysGridBodyContext.registerWeekRowCells], + ); + + return { ref: mergedRef, ctx }; +} + +export namespace useBaseCalendarDaysWeekRowWrapper { + export interface Parameters { + forwardedRef: React.ForwardedRef; + value: PickerValidDate; + } +} From 3713e80a521c66a5b2ebb82532d66e324d2de7e2 Mon Sep 17 00:00:00 2001 From: flavien Date: Tue, 7 Jan 2025 15:39:51 +0100 Subject: [PATCH 076/136] Work --- .../base-calendar/DayCalendarDemo.tsx | 2 +- .../base-calendar/DayRangeCalendarDemo.js | 91 +++++++++++++++++++ .../base-calendar/DayRangeCalendarDemo.tsx | 91 +++++++++++++++++++ .../DayRangeCalendarDemo.tsx.preview | 1 + .../base-calendar/base-calendar.md | 4 + .../RangeCalendarDaysGridBody.tsx | 8 +- .../RangeCalendarDaysWeekRow.tsx | 16 ++-- .../base/RangeCalendar/index.parts.ts | 4 +- .../days-week-row/CalendarDaysWeekRow.tsx | 2 +- 9 files changed, 203 insertions(+), 16 deletions(-) create mode 100644 docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.js create mode 100644 docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx create mode 100644 docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx.preview diff --git a/docs/data/date-pickers/base-calendar/DayCalendarDemo.tsx b/docs/data/date-pickers/base-calendar/DayCalendarDemo.tsx index f3540af494627..8f83700f9e1bf 100644 --- a/docs/data/date-pickers/base-calendar/DayCalendarDemo.tsx +++ b/docs/data/date-pickers/base-calendar/DayCalendarDemo.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import dayjs, { Dayjs } from 'dayjs'; +import { Dayjs } from 'dayjs'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports diff --git a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.js b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.js new file mode 100644 index 0000000000000..a8105dc490635 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.js @@ -0,0 +1,91 @@ +import * as React from 'react'; + +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +// eslint-disable-next-line no-restricted-imports +import { + RangeCalendar, + // useRangeCalendarContext, +} from '@mui/x-date-pickers-pro/internals/base/RangeCalendar'; +import styles from './calendar.module.css'; + +function Header() { + // const { visibleDate } = useRangeCalendarContext(); + + return ( +
+ {/* + ◀ + + {visibleDate.format('MMMM YYYY')} + + ▶ + */} +
+ ); +} + +function DayCalendar(props) { + return ( + + +
+ + {/* + {({ days }) => + days.map((day) => ( + + )) + } + */} + + {({ weeks }) => + weeks.map((week) => ( + + {/* {({ days }) => + days.map((day) => ( + + )) + } */} + {({ days }) => + days.map((day) => ( + + )) + } + + )) + } + + + + + ); +} + +export default function DayRangeCalendarDemo() { + const [value, setValue] = React.useState(null); + + const handleValueChange = React.useCallback((newValue) => { + setValue(newValue); + }, []); + + return ( + + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx new file mode 100644 index 0000000000000..853a8e939783e --- /dev/null +++ b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx @@ -0,0 +1,91 @@ +import * as React from 'react'; +import { Dayjs } from 'dayjs'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +// eslint-disable-next-line no-restricted-imports +import { + RangeCalendar, + // useRangeCalendarContext, +} from '@mui/x-date-pickers-pro/internals/base/RangeCalendar'; +import styles from './calendar.module.css'; + +function Header() { + // const { visibleDate } = useRangeCalendarContext(); + + return ( +
+ {/* + ◀ + + {visibleDate.format('MMMM YYYY')} + + ▶ + */} +
+ ); +} + +function DayCalendar(props: Omit) { + return ( + + +
+ + {/* + {({ days }) => + days.map((day) => ( + + )) + } + */} + + {({ weeks }) => + weeks.map((week) => ( + + {/* {({ days }) => + days.map((day) => ( + + )) + } */} + {({ days }) => + days.map((day) => ( + + )) + } + + )) + } + + + + + ); +} + +export default function DayRangeCalendarDemo() { + const [value, setValue] = React.useState(null); + + const handleValueChange = React.useCallback((newValue: Dayjs | null) => { + setValue(newValue); + }, []); + + return ( + + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx.preview b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx.preview new file mode 100644 index 0000000000000..72b23c81a5a56 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx.preview @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/data/date-pickers/base-calendar/base-calendar.md b/docs/data/date-pickers/base-calendar/base-calendar.md index 60f6eef1292f8..5ed6fa11f7995 100644 --- a/docs/data/date-pickers/base-calendar/base-calendar.md +++ b/docs/data/date-pickers/base-calendar/base-calendar.md @@ -67,3 +67,7 @@ TODO ### MD3-ish layout {{"demo": "DateCalendarDemo.js"}} + +## Range calendar (TODO: move) + +{{"demo": "DayRangeCalendarDemo.js"}} diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid-body/RangeCalendarDaysGridBody.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid-body/RangeCalendarDaysGridBody.tsx index 4fca7ba1822cf..83c01c532aba5 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid-body/RangeCalendarDaysGridBody.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid-body/RangeCalendarDaysGridBody.tsx @@ -11,8 +11,8 @@ import { useBaseCalendarDaysGridBody } from '@mui/x-date-pickers/internals/base/ // eslint-disable-next-line no-restricted-imports import { BaseCalendarDaysGridBodyContext } from '@mui/x-date-pickers/internals/base/utils/base-calendar/days-grid-body/BaseCalendarDaysGridBodyContext'; -const CalendarDaysGridBody = React.forwardRef(function CalendarDaysGrid( - props: CalendarDaysGridBody.Props, +const RangeCalendarDaysGridBody = React.forwardRef(function CalendarDaysGrid( + props: RangeCalendarDaysGridBody.Props, forwardedRef: React.ForwardedRef, ) { const { className, render, children, ...otherProps } = props; @@ -37,7 +37,7 @@ const CalendarDaysGridBody = React.forwardRef(function CalendarDaysGrid( ); }); -export namespace CalendarDaysGridBody { +export namespace RangeCalendarDaysGridBody { export interface State {} export interface Props @@ -45,4 +45,4 @@ export namespace CalendarDaysGridBody { useBaseCalendarDaysGridBody.Parameters {} } -export { CalendarDaysGridBody }; +export { RangeCalendarDaysGridBody }; diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-week-row/RangeCalendarDaysWeekRow.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-week-row/RangeCalendarDaysWeekRow.tsx index 251fe03440a5c..a4373d5d41d1a 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-week-row/RangeCalendarDaysWeekRow.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-week-row/RangeCalendarDaysWeekRow.tsx @@ -11,8 +11,8 @@ import { useComponentRenderer } from '@mui/x-date-pickers/internals/base/base-ut // eslint-disable-next-line no-restricted-imports import { CompositeList } from '@mui/x-date-pickers/internals/base/composite/list/CompositeList'; -const InnerCalendarDaysWeekRow = React.forwardRef(function CalendarDaysGrid( - props: InnerCalendarDaysWeekRowProps, +const InnerRangeCalendarDaysWeekRow = React.forwardRef(function InnerRangeCalendarDaysWeekRow( + props: InnerRangeCalendarDaysWeekRowProps, forwardedRef: React.ForwardedRef, ) { const { className, render, value, ctx, children, ...otherProps } = props; @@ -35,17 +35,17 @@ const InnerCalendarDaysWeekRow = React.forwardRef(function CalendarDaysGrid( return {renderElement()}; }); -const MemoizedInnerCalendarDaysWeekRow = React.memo(InnerCalendarDaysWeekRow); +const MemoizedInnerRangeCalendarDaysWeekRow = React.memo(InnerRangeCalendarDaysWeekRow); const CalendarDaysWeekRow = React.forwardRef(function CalendarDaysWeekRow( - props: CalendarDaysWeekRow.Props, + props: RangeCalendarDaysWeekRow.Props, forwardedRef: React.ForwardedRef, ) { const { ref, ctx } = useBaseCalendarDaysWeekRowWrapper({ forwardedRef, value: props.value }); - return ; + return ; }); -export namespace CalendarDaysWeekRow { +export namespace RangeCalendarDaysWeekRow { export interface State {} export interface Props @@ -53,8 +53,8 @@ export namespace CalendarDaysWeekRow { Omit {} } -interface InnerCalendarDaysWeekRowProps - extends Omit, 'children'>, +interface InnerRangeCalendarDaysWeekRowProps + extends Omit, 'children'>, useBaseCalendarDaysWeekRow.Parameters {} export { CalendarDaysWeekRow }; diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/index.parts.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/index.parts.ts index efeee5170d520..9ebdcca9108e7 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/index.parts.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/index.parts.ts @@ -4,8 +4,8 @@ export { RangeCalendarRoot as Root } from './root/RangeCalendarRoot'; export { RangeCalendarDaysGrid as DaysGrid } from './days-grid/RangeCalendarDaysGrid'; // export { CalendarDaysGridHeader as DaysGridHeader } from './days-grid-header/CalendarDaysGridHeader'; // export { CalendarDaysGridHeaderCell as DaysGridHeaderCell } from './days-grid-header-cell/CalendarDaysGridHeaderCell'; -export { CalendarDaysGridBody as DaysGridBody } from './days-grid-body/RangeCalendarDaysGridBody'; -// export { CalendarDaysWeekRow as DaysWeekRow } from './days-week-row/CalendarDaysWeekRow'; +export { RangeCalendarDaysGridBody as DaysGridBody } from './days-grid-body/RangeCalendarDaysGridBody'; +export { RangeCalendarDaysWeekRow as DaysWeekRow } from './days-week-row/RangeCalendarDaysWeekRow'; // export { CalendarDaysCell as DaysCell } from './days-cell/CalendarDaysCell'; // // Months diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/CalendarDaysWeekRow.tsx b/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/CalendarDaysWeekRow.tsx index a6e693d068a82..bfd221665bdaf 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/CalendarDaysWeekRow.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/CalendarDaysWeekRow.tsx @@ -6,7 +6,7 @@ import { useComponentRenderer } from '../../base-utils/useComponentRenderer'; import { CompositeList } from '../../composite/list/CompositeList'; import { useBaseCalendarDaysWeekRowWrapper } from '../../utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRowWrapper'; -const InnerCalendarDaysWeekRow = React.forwardRef(function CalendarDaysGrid( +const InnerCalendarDaysWeekRow = React.forwardRef(function InnerCalendarDaysWeekRow( props: InnerCalendarDaysWeekRowProps, forwardedRef: React.ForwardedRef, ) { From 2ee4b70f289f74677a3a92574fc4b62ff372f179 Mon Sep 17 00:00:00 2001 From: flavien Date: Thu, 9 Jan 2025 08:49:35 +0100 Subject: [PATCH 077/136] Work --- .../months-cell/CalendarMonthsCell.tsx | 32 ++++---- .../base/Calendar/root/CalendarRootContext.ts | 2 - .../base/Calendar/root/useCalendarRoot.ts | 15 +--- .../CalendarSetVisibleMonth.tsx | 9 ++- .../CalendarSetVisibleYear.tsx | 12 ++- .../src/internals/base/Calendar/utils/date.ts | 1 - .../base/Calendar/utils/useMonthsCells.ts | 6 +- .../base/Calendar/utils/useYearsCells.ts | 10 ++- .../Calendar/years-cell/CalendarYearsCell.tsx | 30 +++---- .../root/BaseCalendarRootContext.ts | 4 +- .../useBaseCalendarDaysGridsNavigation.ts | 21 ++--- .../base-calendar/root/useBaseCalendarRoot.ts | 80 ++++++++++++++----- .../root/useBaseCalendarValidation.ts | 27 +++++++ 13 files changed, 160 insertions(+), 89 deletions(-) create mode 100644 packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarValidation.ts diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx index 5dada65215ec5..33a80327fcd5a 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx @@ -23,7 +23,7 @@ const InnerCalendarMonthsCell = React.forwardRef(function InnerCalendarMonthsCel () => ({ selected: ctx.isSelected, disabled: ctx.isDisabled, - invlid: ctx.isInvalid, + invalid: ctx.isInvalid, current: isCurrent, }), [ctx.isSelected, ctx.isDisabled, ctx.isInvalid, isCurrent], @@ -61,17 +61,17 @@ const CalendarMonthsCell = React.forwardRef(function CalendarMonthsCell( const isInvalid = React.useMemo(() => { const firstEnabledMonth = utils.startOfMonth( - rootContext.validationProps.disablePast && - utils.isAfter(now, rootContext.validationProps.minDate) + baseRootContext.dateValidationProps.disablePast && + utils.isAfter(now, baseRootContext.dateValidationProps.minDate) ? now - : rootContext.validationProps.minDate, + : baseRootContext.dateValidationProps.minDate, ); const lastEnabledMonth = utils.startOfMonth( - rootContext.validationProps.disableFuture && - utils.isBefore(now, rootContext.validationProps.maxDate) + baseRootContext.dateValidationProps.disableFuture && + utils.isBefore(now, baseRootContext.dateValidationProps.maxDate) ? now - : rootContext.validationProps.maxDate, + : baseRootContext.dateValidationProps.maxDate, ); const monthToValidate = utils.startOfMonth(props.value); @@ -84,12 +84,12 @@ const CalendarMonthsCell = React.forwardRef(function CalendarMonthsCell( return true; } - if (!rootContext.validationProps.shouldDisableMonth) { + if (!baseRootContext.dateValidationProps.shouldDisableMonth) { return false; } - return rootContext.validationProps.shouldDisableMonth(monthToValidate); - }, [rootContext.validationProps, props.value, now, utils]); + return baseRootContext.dateValidationProps.shouldDisableMonth(monthToValidate); + }, [baseRootContext.dateValidationProps, props.value, now, utils]); const isDisabled = React.useMemo(() => { if (baseRootContext.disabled) { @@ -124,14 +124,14 @@ const CalendarMonthsCell = React.forwardRef(function CalendarMonthsCell( ? findClosestEnabledDate({ utils, date: newCleanValue, - minDate: utils.isBefore(rootContext.validationProps.minDate, startOfMonth) + minDate: utils.isBefore(baseRootContext.dateValidationProps.minDate, startOfMonth) ? startOfMonth - : rootContext.validationProps.minDate, - maxDate: utils.isAfter(rootContext.validationProps.maxDate, endOfMonth) + : baseRootContext.dateValidationProps.minDate, + maxDate: utils.isAfter(baseRootContext.dateValidationProps.maxDate, endOfMonth) ? endOfMonth - : rootContext.validationProps.maxDate, - disablePast: rootContext.validationProps.disablePast, - disableFuture: rootContext.validationProps.disableFuture, + : baseRootContext.dateValidationProps.maxDate, + disablePast: baseRootContext.dateValidationProps.disablePast, + disableFuture: baseRootContext.dateValidationProps.disableFuture, isDateDisabled: baseRootContext.isDateInvalid, timezone: baseRootContext.timezone, }) diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts index 92adab14ebb71..46c03f3ba5472 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts @@ -2,7 +2,6 @@ import * as React from 'react'; import { PickerValidDate } from '../../../../models'; import { PickerValue } from '../../../models'; import { useBaseCalendarRoot } from '../../utils/base-calendar/root/useBaseCalendarRoot'; -import { ValidateDateProps } from '../../../../validation'; export interface CalendarRootContext { /** @@ -19,7 +18,6 @@ export interface CalendarRootContext { options: Pick, 'section'>, ) => void; referenceValue: PickerValidDate; - validationProps: ValidateDateProps; } export const CalendarRootContext = React.createContext(undefined); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts index 1d6b7d9cdfbb2..16eb2c9430ab6 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts @@ -1,7 +1,7 @@ import * as React from 'react'; import { DateValidationError } from '../../../../models'; import { useDateManager } from '../../../../managers'; -import { ExportedValidateDateProps, ValidateDateProps } from '../../../../validation/validateDate'; +import { ExportedValidateDateProps } from '../../../../validation/validateDate'; import { useUtils } from '../../../hooks/useUtils'; import { PickerValue } from '../../../models'; import { CalendarRootContext } from './CalendarRootContext'; @@ -10,10 +10,10 @@ import { GenericHTMLProps } from '../../base-utils/types'; import { useBaseCalendarRoot } from '../../utils/base-calendar/root/useBaseCalendarRoot'; export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { - const { shouldDisableDate, shouldDisableMonth, shouldDisableYear, ...baseParameters } = - parameters; + const { ...baseParameters } = parameters; const utils = useUtils(); const manager = useDateManager(); + const { value, setValue, @@ -21,18 +21,12 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { setVisibleDate, isDateCellVisible, context: baseContext, - validationProps: baseValidationProps, } = useBaseCalendarRoot({ ...baseParameters, manager, getInitialVisibleDate: (referenceValueParam) => referenceValueParam, }); - const validationProps = React.useMemo( - () => ({ ...baseValidationProps, shouldDisableDate, shouldDisableMonth, shouldDisableYear }), - [baseValidationProps, shouldDisableDate, shouldDisableMonth, shouldDisableYear], - ); - const [prevValue, setPrevValue] = React.useState(value); if (value !== prevValue && utils.isValid(value)) { setPrevValue(value); @@ -46,9 +40,8 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { value, setValue, referenceValue, - validationProps, }), - [value, setValue, referenceValue, validationProps], + [value, setValue, referenceValue], ); const getRootProps = React.useCallback((externalProps: GenericHTMLProps) => { diff --git a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx index ecf5f96a2ad08..60abd030e45c5 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx @@ -61,16 +61,19 @@ const CalendarSetVisibleMonth = React.forwardRef(function CalendarSetVisibleMont // All the months before the visible ones are fully disabled, we skip the navigation. if (isMovingBefore) { return utils.isAfter( - getFirstEnabledMonth(utils, baseRootContext.validationProps), + getFirstEnabledMonth(utils, baseRootContext.dateValidationProps), targetDate, ); } // All the months after the visible ones are fully disabled, we skip the navigation. - return utils.isBefore(getLastEnabledMonth(utils, baseRootContext.validationProps), targetDate); + return utils.isBefore( + getLastEnabledMonth(utils, baseRootContext.dateValidationProps), + targetDate, + ); }, [ baseRootContext.disabled, - baseRootContext.validationProps, + baseRootContext.dateValidationProps, baseRootContext.visibleDate, targetDate, utils, diff --git a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx index 58af16af4ab4d..9cdb2326f8642 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx @@ -59,14 +59,20 @@ const CalendarSetVisibleYear = React.forwardRef(function CalendarSetVisibleYear( // All the years before the visible ones are fully disabled, we skip the navigation. if (isMovingBefore) { - return utils.isAfter(getFirstEnabledYear(utils, baseRootContext.validationProps), targetDate); + return utils.isAfter( + getFirstEnabledYear(utils, baseRootContext.dateValidationProps), + targetDate, + ); } // All the years after the visible ones are fully disabled, we skip the navigation. - return utils.isBefore(getLastEnabledYear(utils, baseRootContext.validationProps), targetDate); + return utils.isBefore( + getLastEnabledYear(utils, baseRootContext.dateValidationProps), + targetDate, + ); }, [ baseRootContext.disabled, - baseRootContext.validationProps, + baseRootContext.dateValidationProps, baseRootContext.visibleDate, targetDate, utils, diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/date.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/date.ts index a8930deccf1ee..a2668a8514132 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/utils/date.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/utils/date.ts @@ -1,4 +1,3 @@ -import { ValidateDateProps } from '../../../../validation'; import { MuiPickersAdapter, PickerValidDate } from '../../../../models'; import { BaseDateValidationProps } from '../../../models/validation'; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/useMonthsCells.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/useMonthsCells.ts index da712179f8d18..6e2afcc6dce6b 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/utils/useMonthsCells.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/utils/useMonthsCells.ts @@ -28,7 +28,7 @@ export function useMonthsCells(): useMonthsCells.ReturnValue { // All the years before the visible ones are fully disabled, we skip the navigation. if ( utils.isAfter( - getFirstEnabledYear(utils, baseRootContext.validationProps), + getFirstEnabledYear(utils, baseRootContext.dateValidationProps), lastYearInNewPage, ) ) { @@ -47,7 +47,9 @@ export function useMonthsCells(): useMonthsCells.ReturnValue { ); // All the years after the visible ones are fully disabled, we skip the navigation. - if (utils.isBefore(getLastEnabledYear(utils, baseRootContext.validationProps), targetDate)) { + if ( + utils.isBefore(getLastEnabledYear(utils, baseRootContext.dateValidationProps), targetDate) + ) { return; } baseRootContext.setVisibleDate( diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/useYearsCells.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/useYearsCells.ts index 0700dedbce313..ff57f0abb25b7 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/utils/useYearsCells.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/utils/useYearsCells.ts @@ -10,10 +10,14 @@ export function useYearsCells(): useYearsCells.ReturnValue { const years = React.useMemo( () => utils.getYearRange([ - baseRootContext.validationProps.minDate, - baseRootContext.validationProps.maxDate, + baseRootContext.dateValidationProps.minDate, + baseRootContext.dateValidationProps.maxDate, ]), - [utils, baseRootContext.validationProps.minDate, baseRootContext.validationProps.maxDate], + [ + utils, + baseRootContext.dateValidationProps.minDate, + baseRootContext.dateValidationProps.maxDate, + ], ); const registerSection = baseRootContext.registerSection; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx index 2f548c59ffc79..cbd29cead34f3 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx @@ -60,33 +60,33 @@ const CalendarYearsCell = React.forwardRef(function CalendarsYearCell( ); const isInvalid = React.useMemo(() => { - if (rootContext.validationProps.disablePast && utils.isBeforeYear(props.value, now)) { + if (baseRootContext.dateValidationProps.disablePast && utils.isBeforeYear(props.value, now)) { return true; } - if (rootContext.validationProps.disableFuture && utils.isAfterYear(props.value, now)) { + if (baseRootContext.dateValidationProps.disableFuture && utils.isAfterYear(props.value, now)) { return true; } if ( - rootContext.validationProps.minDate && - utils.isBeforeYear(props.value, rootContext.validationProps.minDate) + baseRootContext.dateValidationProps.minDate && + utils.isBeforeYear(props.value, baseRootContext.dateValidationProps.minDate) ) { return true; } if ( - rootContext.validationProps.maxDate && - utils.isAfterYear(props.value, rootContext.validationProps.maxDate) + baseRootContext.dateValidationProps.maxDate && + utils.isAfterYear(props.value, baseRootContext.dateValidationProps.maxDate) ) { return true; } - if (!rootContext.validationProps.shouldDisableYear) { + if (!baseRootContext.dateValidationProps.shouldDisableYear) { return false; } const yearToValidate = utils.startOfYear(props.value); - return rootContext.validationProps.shouldDisableYear(yearToValidate); - }, [rootContext.validationProps, props.value, now, utils]); + return baseRootContext.dateValidationProps.shouldDisableYear(yearToValidate); + }, [baseRootContext.dateValidationProps, props.value, now, utils]); const isDisabled = React.useMemo(() => { if (baseRootContext.disabled) { @@ -121,14 +121,14 @@ const CalendarYearsCell = React.forwardRef(function CalendarsYearCell( ? findClosestEnabledDate({ utils, date: newCleanValue, - minDate: utils.isBefore(rootContext.validationProps.minDate, startOfYear) + minDate: utils.isBefore(baseRootContext.dateValidationProps.minDate, startOfYear) ? startOfYear - : rootContext.validationProps.minDate, - maxDate: utils.isAfter(rootContext.validationProps.maxDate, endOfYear) + : baseRootContext.dateValidationProps.minDate, + maxDate: utils.isAfter(baseRootContext.dateValidationProps.maxDate, endOfYear) ? endOfYear - : rootContext.validationProps.maxDate, - disablePast: rootContext.validationProps.disablePast, - disableFuture: rootContext.validationProps.disableFuture, + : baseRootContext.dateValidationProps.maxDate, + disablePast: baseRootContext.dateValidationProps.disablePast, + disableFuture: baseRootContext.dateValidationProps.disableFuture, isDateDisabled: baseRootContext.isDateInvalid, timezone: baseRootContext.timezone, }) diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/BaseCalendarRootContext.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/BaseCalendarRootContext.ts index cde97cf52c617..5b7bba7ae41dd 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/BaseCalendarRootContext.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/BaseCalendarRootContext.ts @@ -1,7 +1,7 @@ import * as React from 'react'; import { PickersTimezone, PickerValidDate } from '../../../../../models'; +import { ValidateDateProps } from '../../../../../validation'; import type { useBaseCalendarRoot } from './useBaseCalendarRoot'; -import { BaseDateValidationProps } from '../../../../models/validation'; import type { useBaseCalendarDaysGridBody } from '../days-grid-body/useBaseCalendarDaysGridBody'; export interface BaseCalendarRootContext { @@ -10,7 +10,7 @@ export interface BaseCalendarRootContext { readOnly: boolean; autoFocus: boolean; isDateInvalid: (day: PickerValidDate | null) => boolean; - validationProps: Required; + dateValidationProps: ValidateDateProps; visibleDate: PickerValidDate; setVisibleDate: (visibleDate: PickerValidDate, skipIfAlreadyVisible: boolean) => void; monthPageSize: number; diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarDaysGridsNavigation.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarDaysGridsNavigation.ts index eec360c7420b8..f4b5a53ee8222 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarDaysGridsNavigation.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarDaysGridsNavigation.ts @@ -4,7 +4,7 @@ import useTimeout from '@mui/utils/useTimeout'; import { PickerValidDate } from '../../../../../models'; import { ValidateDateProps } from '../../../../../validation'; import { useUtils } from '../../../../hooks/useUtils'; -import type { useCalendarDaysGridBody } from '../../../Calendar/days-grid-body/useCalendarDaysGridBody'; +import type { useBaseCalendarDaysGridBody } from '../days-grid-body/useBaseCalendarDaysGridBody'; import { applyInitialFocusInGrid, navigateInGrid, @@ -21,10 +21,10 @@ import { BaseCalendarRootContext } from './BaseCalendarRootContext'; export function useBaseCalendarDaysGridNavigation( parameters: useBaseCalendarDaysGridNavigation.Parameters, ) { - const { visibleDate, setVisibleDate, monthPageSize, validationProps } = parameters; + const { visibleDate, setVisibleDate, monthPageSize, dateValidationProps } = parameters; const utils = useUtils(); const gridsRef = React.useRef< - { cells: useCalendarDaysGridBody.CellsRef; rows: useCalendarDaysGridBody.RowsRef }[] + { cells: useBaseCalendarDaysGridBody.CellsRef; rows: useBaseCalendarDaysGridBody.RowsRef }[] >([]); const pageNavigationTargetRef = React.useRef(null); @@ -47,7 +47,7 @@ export function useBaseCalendarDaysGridNavigation( const lastMonthInNewPage = utils.addMonths(targetDate, monthPageSize - 1); // All the months before the visible ones are fully disabled, we skip the navigation. - if (utils.isAfter(getFirstEnabledMonth(utils, validationProps), lastMonthInNewPage)) { + if (utils.isAfter(getFirstEnabledMonth(utils, dateValidationProps), lastMonthInNewPage)) { return; } @@ -57,7 +57,7 @@ export function useBaseCalendarDaysGridNavigation( const targetDate = utils.addMonths(utils.startOfMonth(visibleDate), monthPageSize); // All the months after the visible ones are fully disabled, we skip the navigation. - if (utils.isBefore(getLastEnabledMonth(utils, validationProps), targetDate)) { + if (utils.isBefore(getLastEnabledMonth(utils, dateValidationProps), targetDate)) { return; } setVisibleDate(utils.addMonths(visibleDate, monthPageSize)); @@ -72,8 +72,8 @@ export function useBaseCalendarDaysGridNavigation( const registerDaysGridCells = useEventCallback( ( - gridCellsRef: useCalendarDaysGridBody.CellsRef, - gridRowsRef: useCalendarDaysGridBody.RowsRef, + gridCellsRef: useBaseCalendarDaysGridBody.CellsRef, + gridRowsRef: useBaseCalendarDaysGridBody.RowsRef, ) => { gridsRef.current.push({ cells: gridCellsRef, rows: gridRowsRef }); @@ -94,7 +94,7 @@ export namespace useBaseCalendarDaysGridNavigation { visibleDate: PickerValidDate; setVisibleDate: (visibleDate: PickerValidDate) => void; monthPageSize: number; - validationProps: ValidateDateProps; + dateValidationProps: ValidateDateProps; } export interface ReturnValue @@ -124,7 +124,10 @@ function sortGridByDocumentPosition(a: HTMLElement[][], b: HTMLElement[][]) { /* eslint-enable no-bitwise */ function getCellsInCalendar( - grids: { cells: useCalendarDaysGridBody.CellsRef; rows: useCalendarDaysGridBody.RowsRef }[], + grids: { + cells: useBaseCalendarDaysGridBody.CellsRef; + rows: useBaseCalendarDaysGridBody.RowsRef; + }[], ) { const cells: HTMLElement[][][] = []; diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarRoot.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarRoot.ts index 3ba521ed1e639..2ef0c3e045eab 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarRoot.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarRoot.ts @@ -1,13 +1,16 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; import { OnErrorProps, PickerManager, PickerValidDate, TimezoneProps } from '../../../../../models'; -import { useValidation, ValidateDateProps } from '../../../../../validation'; -import { useIsDateDisabled } from '../../../../../DateCalendar/useIsDateDisabled'; +import { useValidation } from '../../../../../validation'; +import { + ValidateDateProps, + ExportedValidateDateProps, + validateDate, +} from '../../../../../validation/validateDate'; import { FormProps, InferNonNullablePickerValue, PickerValidValue } from '../../../../models'; import { SECTION_TYPE_GRANULARITY } from '../../../../utils/getDefaultReferenceDate'; import { applyDefaultDate } from '../../../../utils/date-utils'; -import { useDefaultDates, useUtils } from '../../../../hooks/useUtils'; -import { BaseDateValidationProps } from '../../../../models/validation'; +import { useDefaultDates, useLocalizationContext, useUtils } from '../../../../hooks/useUtils'; import { useControlledValueWithTimezone } from '../../../../hooks/useValueWithTimezone'; import { useBaseCalendarDaysGridNavigation } from './useBaseCalendarDaysGridsNavigation'; import { BaseCalendarRootContext } from './BaseCalendarRootContext'; @@ -32,15 +35,22 @@ export function useBaseCalendarRoot( maxDate, disablePast, disableFuture, + shouldDisableDate, + shouldDisableMonth, + shouldDisableYear, getInitialVisibleDate, } = parameters; const utils = useUtils(); - const validationProps = useAddDefaultsToBaseDateValidationProps({ + const adapter = useLocalizationContext(); + const dateValidationProps = useAddDefaultsToDateValidationProps({ minDate, maxDate, disablePast, disableFuture, + shouldDisableDate, + shouldDisableMonth, + shouldDisableYear, }); const { value, handleValueChange, timezone } = useControlledValueWithTimezone({ @@ -59,7 +69,7 @@ export function useBaseCalendarRoot( value, utils, timezone, - props: validationProps, + props: dateValidationProps, referenceDate: referenceDateProp, granularity: SECTION_TYPE_GRANULARITY.day, }); @@ -70,7 +80,8 @@ export function useBaseCalendarRoot( ); const { getValidationErrorForNewValue } = useValidation({ - props: { ...validationProps, onError }, + // TODO: This is incorrect + props: { ...dateValidationProps, onError }, value, timezone, validator: manager.validator, @@ -88,12 +99,6 @@ export function useBaseCalendarRoot( }, ); - // TODO: Rename this hook (if we keep it for Base UI X) - const isDateInvalid = useIsDateDisabled({ - ...validationProps, - timezone, - }); - const sectionsRef = React.useRef< Record<'day' | 'month' | 'year', Record> >({ @@ -143,9 +148,20 @@ export function useBaseCalendarRoot( visibleDate, setVisibleDate, monthPageSize, - validationProps, + dateValidationProps, }); + const isDateInvalid = React.useCallback( + (day: PickerValidDate | null) => + validateDate({ + adapter, + value: day, + timezone, + props: dateValidationProps, + }) !== null, + [adapter, dateValidationProps, timezone], + ); + const context: BaseCalendarRootContext = React.useMemo( () => ({ timezone, @@ -160,7 +176,7 @@ export function useBaseCalendarRoot( applyDayGridKeyboardNavigation, registerDaysGridCells, registerSection, - validationProps, + dateValidationProps, }), [ timezone, @@ -175,7 +191,7 @@ export function useBaseCalendarRoot( applyDayGridKeyboardNavigation, registerDaysGridCells, registerSection, - validationProps, + dateValidationProps, ], ); @@ -186,7 +202,6 @@ export function useBaseCalendarRoot( setVisibleDate, isDateCellVisible, context, - validationProps, }; } @@ -195,7 +210,7 @@ export namespace useBaseCalendarRoot { extends TimezoneProps, FormProps, OnErrorProps, - BaseDateValidationProps { + ExportedValidateDateProps { /** * The manager of the calendar (uses `useDateManager` for Calendar and `useDateRangeManager` for RangeCalendar). */ @@ -268,13 +283,21 @@ export namespace useBaseCalendarRoot { } } -function useAddDefaultsToBaseDateValidationProps( - validationDate: BaseDateValidationProps, +export function useAddDefaultsToDateValidationProps( + validationDate: ExportedValidateDateProps, ): ValidateDateProps { const utils = useUtils(); const defaultDates = useDefaultDates(); - const { disablePast, disableFuture, minDate, maxDate } = validationDate; + const { + disablePast, + disableFuture, + minDate, + maxDate, + shouldDisableDate, + shouldDisableMonth, + shouldDisableYear, + } = validationDate; return React.useMemo( () => ({ @@ -282,7 +305,20 @@ function useAddDefaultsToBaseDateValidationProps( disableFuture: disableFuture ?? false, minDate: applyDefaultDate(utils, minDate, defaultDates.minDate), maxDate: applyDefaultDate(utils, maxDate, defaultDates.maxDate), + shouldDisableDate, + shouldDisableMonth, + shouldDisableYear, }), - [disablePast, disableFuture, minDate, maxDate, utils, defaultDates], + [ + disablePast, + disableFuture, + minDate, + maxDate, + shouldDisableDate, + shouldDisableMonth, + shouldDisableYear, + utils, + defaultDates, + ], ); } diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarValidation.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarValidation.ts new file mode 100644 index 0000000000000..29e60d67a3a45 --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarValidation.ts @@ -0,0 +1,27 @@ +import * as React from 'react'; +import { PickersTimezone, PickerValidDate } from '../../../../../models'; +import { useLocalizationContext } from '../../../../hooks/useUtils'; +import { useValidation, validateDate, ValidateDateProps } from '../../../../../validation'; + +export function useBaseCalendarValidation(parameters: useBaseCalendarValidation.Parameters) { + const { timezone, validationProps } = parameters; + const adapter = useLocalizationContext(); + + return React.useCallback( + (day: PickerValidDate | null) => + validateDate({ + adapter, + value: day, + timezone, + props: validationProps, + }) !== null, + [adapter, validationProps, timezone], + ); +} + +export namespace useBaseCalendarValidation { + export interface Parameters { + timezone: PickersTimezone; + validationProps: any; + } +} From 9ae7954d3645200a15b90a186d2e2ab7fb4bcba9 Mon Sep 17 00:00:00 2001 From: flavien Date: Thu, 9 Jan 2025 10:49:39 +0100 Subject: [PATCH 078/136] Working on range calendar --- .../base-calendar/DayRangeCalendarDemo.js | 57 +++-- .../base-calendar/DayRangeCalendarDemo.tsx | 43 ++-- .../DayRangeCalendarDemo.tsx.preview | 6 +- .../RangeCalendarDaysGridHeaderCell.tsx | 41 ++++ ...alendarDaysGridHeaderCellDataAttributes.ts | 1 + .../RangeCalendarDaysGridHeader.tsx | 40 ++++ ...ngeCalendarDaysGridHeaderDataAttributes.ts | 1 + .../RangeCalendarDaysWeekRow.tsx | 4 +- .../base/RangeCalendar/index.parts.ts | 10 +- .../root/RangeCalendarRootContext.ts | 19 +- .../root/useRangeCalendarRoot.tsx | 133 +++++++++-- .../RangeCalendarSetVisibleMonth.tsx | 57 +++++ ...geCalendarSetVisibleMonthDataAttributes.ts | 1 + .../RangeCalendarSetVisibleYear.tsx | 56 +++++ ...ngeCalendarSetVisibleYearDataAttributes.ts | 1 + .../Calendar/days-cell/CalendarDaysCell.tsx | 19 +- .../CalendarDaysGridHeaderCell.tsx | 6 +- .../CalendarDaysGridHeader.tsx | 6 +- .../useCalendarDaysGridHeader.ts | 40 ---- .../months-cell/CalendarMonthsCell.tsx | 11 +- .../base/Calendar/root/CalendarRootContext.ts | 15 -- .../base/Calendar/root/useCalendarRoot.ts | 64 +++-- .../CalendarSetVisibleMonth.tsx | 74 +----- .../CalendarSetVisibleYear.tsx | 71 +----- .../CalendarSetVisibleYearDataAttributes.ts | 2 +- .../base/Calendar/utils/useMonthsCells.ts | 2 +- .../Calendar/years-cell/CalendarYearsCell.tsx | 11 +- .../useBaseCalendarDaysGridHeaderCell.ts} | 14 +- .../useBaseCalendarDaysGridHeader.ts | 42 ++++ .../days-grid/BaseCalendarDaysGridContext.ts | 2 +- .../days-grid/useBaseCalendarDaysGrid.ts | 40 ++-- .../useBaseCalendarDaysWeekRowWrapper.ts | 20 +- .../root/BaseCalendarRootContext.ts | 52 +++- .../useBaseCalendarDaysGridsNavigation.ts | 2 +- .../base-calendar/root/useBaseCalendarRoot.ts | 226 +++++++++++------- .../useBaseCalendarSetVisibleMonth.ts} | 12 +- .../useBaseCalendarSetVisibleMonthWrapper.ts | 83 +++++++ .../useBaseCalendarSetVisibleYear.ts} | 12 +- .../useBaseCalendarSetVisibleYearWrapper.ts | 82 +++++++ .../base-calendar}/utils/date.ts | 4 +- 40 files changed, 932 insertions(+), 450 deletions(-) create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid-header-cell/RangeCalendarDaysGridHeaderCell.tsx create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid-header-cell/RangeCalendarDaysGridHeaderCellDataAttributes.ts create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid-header/RangeCalendarDaysGridHeader.tsx create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid-header/RangeCalendarDaysGridHeaderDataAttributes.ts create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/set-visible-month/RangeCalendarSetVisibleMonth.tsx create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/set-visible-month/RangeCalendarSetVisibleMonthDataAttributes.ts create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/set-visible-year/RangeCalendarSetVisibleYear.tsx create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/set-visible-year/RangeCalendarSetVisibleYearDataAttributes.ts delete mode 100644 packages/x-date-pickers/src/internals/base/Calendar/days-grid-header/useCalendarDaysGridHeader.ts rename packages/x-date-pickers/src/internals/base/{Calendar/days-grid-header-cell/useCalendarDaysGridHeaderCell.ts => utils/base-calendar/days-grid-header-cell/useBaseCalendarDaysGridHeaderCell.ts} (75%) create mode 100644 packages/x-date-pickers/src/internals/base/utils/base-calendar/days-grid-header/useBaseCalendarDaysGridHeader.ts rename packages/x-date-pickers/src/internals/base/{Calendar/set-visible-month/useCalendarSetVisibleMonth.ts => utils/base-calendar/set-visible-month/useBaseCalendarSetVisibleMonth.ts} (66%) create mode 100644 packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-month/useBaseCalendarSetVisibleMonthWrapper.ts rename packages/x-date-pickers/src/internals/base/{Calendar/set-visible-year/useCalendarSetVisibleYear.ts => utils/base-calendar/set-visible-year/useBaseCalendarSetVisibleYear.ts} (66%) create mode 100644 packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-year/useBaseCalendarSetVisibleYearWrapper.ts rename packages/x-date-pickers/src/internals/base/{Calendar => utils/base-calendar}/utils/date.ts (94%) diff --git a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.js b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.js index a8105dc490635..d42667a0e3d37 100644 --- a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.js +++ b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.js @@ -1,26 +1,33 @@ import * as React from 'react'; +import NoSsr from '@mui/material/NoSsr'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports import { RangeCalendar, - // useRangeCalendarContext, + useRangeCalendarContext, } from '@mui/x-date-pickers-pro/internals/base/RangeCalendar'; import styles from './calendar.module.css'; function Header() { - // const { visibleDate } = useRangeCalendarContext(); + const { visibleDate } = useRangeCalendarContext(); return (
- {/* - ◀ - - {visibleDate.format('MMMM YYYY')} - - ▶ - */} + + ◀ + + {visibleDate.format('MMMM YYYY')} + + ▶ +
); } @@ -31,17 +38,17 @@ function DayCalendar(props) {
- {/* - {({ days }) => - days.map((day) => ( - - )) - } - */} + + {({ days }) => + days.map((day) => ( + + )) + } + {({ weeks }) => weeks.map((week) => ( @@ -77,15 +84,17 @@ function DayCalendar(props) { } export default function DayRangeCalendarDemo() { - const [value, setValue] = React.useState(null); + const [value, setValue] = React.useState([null, null]); const handleValueChange = React.useCallback((newValue) => { setValue(newValue); }, []); return ( - - - + + + + + ); } diff --git a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx index 853a8e939783e..d087c00972ddb 100644 --- a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx +++ b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx @@ -1,26 +1,33 @@ import * as React from 'react'; import { Dayjs } from 'dayjs'; +import NoSsr from '@mui/material/NoSsr'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports import { RangeCalendar, - // useRangeCalendarContext, + useRangeCalendarContext, } from '@mui/x-date-pickers-pro/internals/base/RangeCalendar'; import styles from './calendar.module.css'; function Header() { - // const { visibleDate } = useRangeCalendarContext(); + const { visibleDate } = useRangeCalendarContext(); return (
- {/* + {visibleDate.format('MMMM YYYY')} - + ▶ - */} +
); } @@ -31,7 +38,7 @@ function DayCalendar(props: Omit) {
- {/* + {({ days }) => days.map((day) => ( ) { /> )) } - */} + {({ weeks }) => weeks.map((week) => ( @@ -77,15 +84,23 @@ function DayCalendar(props: Omit) { } export default function DayRangeCalendarDemo() { - const [value, setValue] = React.useState(null); + const [value, setValue] = React.useState<[Dayjs | null, Dayjs | null]>([ + null, + null, + ]); - const handleValueChange = React.useCallback((newValue: Dayjs | null) => { - setValue(newValue); - }, []); + const handleValueChange = React.useCallback( + (newValue: [Dayjs | null, Dayjs | null]) => { + setValue(newValue); + }, + [], + ); return ( - - - + + + + + ); } diff --git a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx.preview b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx.preview index 72b23c81a5a56..57d95c16e0caf 100644 --- a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx.preview +++ b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx.preview @@ -1 +1,5 @@ - \ No newline at end of file + + + + + \ No newline at end of file diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid-header-cell/RangeCalendarDaysGridHeaderCell.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid-header-cell/RangeCalendarDaysGridHeaderCell.tsx new file mode 100644 index 0000000000000..71011ac744f47 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid-header-cell/RangeCalendarDaysGridHeaderCell.tsx @@ -0,0 +1,41 @@ +'use client'; +import * as React from 'react'; +// eslint-disable-next-line no-restricted-imports +import { BaseUIComponentProps } from '@mui/x-date-pickers/internals/base/base-utils/types'; +// eslint-disable-next-line no-restricted-imports +import { useComponentRenderer } from '@mui/x-date-pickers/internals/base/base-utils/useComponentRenderer'; +// eslint-disable-next-line no-restricted-imports +import { useBaseCalendarDaysGridHeaderCell } from '@mui/x-date-pickers/internals/base/utils/base-calendar/days-grid-header-cell/useBaseCalendarDaysGridHeaderCell'; + +const RangeCalendarDaysGridHeaderCell = React.forwardRef(function RangeCalendarDaysGridHeaderCell( + props: CalendarDaysGridHeaderCell.Props, + forwardedRef: React.ForwardedRef, +) { + const { className, render, value, formatter, ...otherProps } = props; + const { getDaysGridHeaderCellProps } = useBaseCalendarDaysGridHeaderCell({ value, formatter }); + + const state: CalendarDaysGridHeaderCell.State = React.useMemo(() => ({}), []); + + const { renderElement } = useComponentRenderer({ + propGetter: getDaysGridHeaderCellProps, + render: render ?? 'span', + ref: forwardedRef, + className, + state, + extraProps: otherProps, + }); + + return renderElement(); +}); + +export namespace CalendarDaysGridHeaderCell { + export interface State {} + + export interface Props + extends useBaseCalendarDaysGridHeaderCell.Parameters, + BaseUIComponentProps<'span', State> {} +} + +const MemoizedRangeCalendarDaysGridHeaderCell = React.memo(RangeCalendarDaysGridHeaderCell); + +export { MemoizedRangeCalendarDaysGridHeaderCell as RangeCalendarDaysGridHeaderCell }; diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid-header-cell/RangeCalendarDaysGridHeaderCellDataAttributes.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid-header-cell/RangeCalendarDaysGridHeaderCellDataAttributes.ts new file mode 100644 index 0000000000000..ef4830f48d863 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid-header-cell/RangeCalendarDaysGridHeaderCellDataAttributes.ts @@ -0,0 +1 @@ +export enum RangeCalendarDaysGridHeaderCell {} diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid-header/RangeCalendarDaysGridHeader.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid-header/RangeCalendarDaysGridHeader.tsx new file mode 100644 index 0000000000000..8dd12f75c16c9 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid-header/RangeCalendarDaysGridHeader.tsx @@ -0,0 +1,40 @@ +'use client'; +import * as React from 'react'; +// eslint-disable-next-line no-restricted-imports +import { BaseUIComponentProps } from '@mui/x-date-pickers/internals/base/base-utils/types'; +// eslint-disable-next-line no-restricted-imports +import { useComponentRenderer } from '@mui/x-date-pickers/internals/base/base-utils/useComponentRenderer'; +// eslint-disable-next-line no-restricted-imports +import { useBaseCalendarDaysGridHeader } from '@mui/x-date-pickers/internals/base/utils/base-calendar/days-grid-header/useBaseCalendarDaysGridHeader'; + +const RangeCalendarDaysGridHeader = React.forwardRef(function CalendarDaysGridHeader( + props: RangeCalendarDaysGridHeader.Props, + forwardedRef: React.ForwardedRef, +) { + const { className, render, children, ...otherProps } = props; + const { getDaysGridHeaderProps } = useBaseCalendarDaysGridHeader({ + children, + }); + const state = React.useMemo(() => ({}), []); + + const { renderElement } = useComponentRenderer({ + propGetter: getDaysGridHeaderProps, + render: render ?? 'div', + ref: forwardedRef, + className, + state, + extraProps: otherProps, + }); + + return renderElement(); +}); + +export namespace RangeCalendarDaysGridHeader { + export interface State {} + + export interface Props + extends Omit, 'children'>, + useBaseCalendarDaysGridHeader.Parameters {} +} + +export { RangeCalendarDaysGridHeader }; diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid-header/RangeCalendarDaysGridHeaderDataAttributes.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid-header/RangeCalendarDaysGridHeaderDataAttributes.ts new file mode 100644 index 0000000000000..279ebc5d7e52a --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid-header/RangeCalendarDaysGridHeaderDataAttributes.ts @@ -0,0 +1 @@ +export enum RangeCalendarDaysGridHeader {} diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-week-row/RangeCalendarDaysWeekRow.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-week-row/RangeCalendarDaysWeekRow.tsx index a4373d5d41d1a..2a1bd2a9f9f75 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-week-row/RangeCalendarDaysWeekRow.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-week-row/RangeCalendarDaysWeekRow.tsx @@ -37,7 +37,7 @@ const InnerRangeCalendarDaysWeekRow = React.forwardRef(function InnerRangeCalend const MemoizedInnerRangeCalendarDaysWeekRow = React.memo(InnerRangeCalendarDaysWeekRow); -const CalendarDaysWeekRow = React.forwardRef(function CalendarDaysWeekRow( +const RangeCalendarDaysWeekRow = React.forwardRef(function CalendarDaysWeekRow( props: RangeCalendarDaysWeekRow.Props, forwardedRef: React.ForwardedRef, ) { @@ -57,4 +57,4 @@ interface InnerRangeCalendarDaysWeekRowProps extends Omit, 'children'>, useBaseCalendarDaysWeekRow.Parameters {} -export { CalendarDaysWeekRow }; +export { RangeCalendarDaysWeekRow }; diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/index.parts.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/index.parts.ts index 9ebdcca9108e7..1b32d29fb9799 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/index.parts.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/index.parts.ts @@ -2,8 +2,8 @@ export { RangeCalendarRoot as Root } from './root/RangeCalendarRoot'; // // Days export { RangeCalendarDaysGrid as DaysGrid } from './days-grid/RangeCalendarDaysGrid'; -// export { CalendarDaysGridHeader as DaysGridHeader } from './days-grid-header/CalendarDaysGridHeader'; -// export { CalendarDaysGridHeaderCell as DaysGridHeaderCell } from './days-grid-header-cell/CalendarDaysGridHeaderCell'; +export { RangeCalendarDaysGridHeader as DaysGridHeader } from './days-grid-header/RangeCalendarDaysGridHeader'; +export { RangeCalendarDaysGridHeaderCell as DaysGridHeaderCell } from './days-grid-header-cell/RangeCalendarDaysGridHeaderCell'; export { RangeCalendarDaysGridBody as DaysGridBody } from './days-grid-body/RangeCalendarDaysGridBody'; export { RangeCalendarDaysWeekRow as DaysWeekRow } from './days-week-row/RangeCalendarDaysWeekRow'; // export { CalendarDaysCell as DaysCell } from './days-cell/CalendarDaysCell'; @@ -18,6 +18,6 @@ export { RangeCalendarDaysWeekRow as DaysWeekRow } from './days-week-row/RangeCa // export { CalendarYearsGrid as YearsGrid } from './years-grid/CalendarYearsGrid'; // export { CalendarYearsCell as YearsCell } from './years-cell/CalendarYearsCell'; -// // Navigation -// export { CalendarSetVisibleMonth as SetVisibleMonth } from './set-visible-month/CalendarSetVisibleMonth'; -// export { CalendarSetVisibleYear as SetVisibleYear } from './set-visible-year/CalendarSetVisibleYear'; +// Navigation +export { RangeCalendarSetVisibleMonth as SetVisibleMonth } from './set-visible-month/RangeCalendarSetVisibleMonth'; +export { RangeCalendarSetVisibleYear as SetVisibleYear } from './set-visible-year/RangeCalendarSetVisibleYear'; diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts index b78b765c8cea8..6e0053a508ff0 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts @@ -1,25 +1,8 @@ import * as React from 'react'; -import { PickerNonNullableRangeValue, PickerRangeValue } from '@mui/x-date-pickers/internals'; -// eslint-disable-next-line no-restricted-imports -import { useBaseCalendarRoot } from '@mui/x-date-pickers/internals/base/utils/base-calendar/root/useBaseCalendarRoot'; -import { ValidateDateRangeProps } from '../../../../validation'; +import { PickerRangeValue } from '@mui/x-date-pickers/internals'; export interface RangeCalendarRootContext { - /** - * The current value of the calendar. - */ value: PickerRangeValue; - /** - * Set the current value of the calendar. - * @param {PickerRangeValue} value The new value of the calendar. - * @param {Pick, 'section'>} options The options to customize the behavior of this update. - */ - setValue: ( - value: PickerRangeValue, - options: Pick, 'section'>, - ) => void; - referenceValue: PickerNonNullableRangeValue; - validationProps: ValidateDateRangeProps; } export const RangeCalendarRootContext = React.createContext( diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx index 75cd1777cd846..9603f9d16ac63 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx @@ -1,8 +1,12 @@ import * as React from 'react'; import { PickerValidDate } from '@mui/x-date-pickers/models'; -import { PickerRangeValue, useUtils } from '@mui/x-date-pickers/internals'; +import { ValidateDateProps } from '@mui/x-date-pickers/validation'; +import { PickerRangeValue, RangePosition, useUtils } from '@mui/x-date-pickers/internals'; // eslint-disable-next-line no-restricted-imports -import { useBaseCalendarRoot } from '@mui/x-date-pickers/internals/base/utils/base-calendar/root/useBaseCalendarRoot'; +import { + useAddDefaultsToBaseDateValidationProps, + useBaseCalendarRoot, +} from '@mui/x-date-pickers/internals/base/utils/base-calendar/root/useBaseCalendarRoot'; // eslint-disable-next-line no-restricted-imports import { mergeReactProps } from '@mui/x-date-pickers/internals/base/base-utils/mergeReactProps'; // eslint-disable-next-line no-restricted-imports @@ -13,31 +17,119 @@ import { ValidateDateRangeProps, ExportedValidateDateRangeProps, } from '../../../../validation/validateDateRange'; +import { calculateRangeChange } from '../../../utils/date-range-manager'; +import { isRangeValid } from '../../../utils/date-utils'; +import { useRangePosition, UseRangePositionProps } from '../../../hooks/useRangePosition'; import { RangeCalendarRootContext } from './RangeCalendarRootContext'; export function useRangeCalendarRoot(parameters: useRangeCalendarRoot.Parameters) { - const { shouldDisableDate, ...baseParameters } = parameters; + const { + // Validation props + minDate, + maxDate, + disablePast, + disableFuture, + shouldDisableDate, + // Range position props + rangePosition: rangePositionProp, + defaultRangePosition: defaultRangePositionProp, + onRangePositionChange: onRangePositionChangeProp, + // Parameters forwarded to `useBaseCalendarRoot` + ...baseParameters + } = parameters; const utils = useUtils(); const manager = useDateRangeManager(); + + const availableRangePositions: RangePosition[] = ['start', 'end']; + + // TODO: Add support for range position from the context when implementing the Picker Base UI X component. + const { rangePosition, onRangePositionChange } = useRangePosition({ + rangePosition: rangePositionProp, + defaultRangePosition: defaultRangePositionProp, + onRangePositionChange: onRangePositionChangeProp, + }); + + const baseDateValidationProps = useAddDefaultsToBaseDateValidationProps({ + minDate, + maxDate, + disablePast, + disableFuture, + }); + + const valueValidationProps = React.useMemo( + () => ({ + ...baseDateValidationProps, + shouldDisableDate, + }), + [baseDateValidationProps, shouldDisableDate], + ); + + const shouldDisableDateForSingleDateValidation = React.useMemo(() => { + if (!shouldDisableDate) { + return undefined; + } + + return (dayToTest: PickerValidDate) => + // TODO: Add correct range position. + shouldDisableDate(dayToTest, rangePosition /* draggingDatePosition || rangePosition */); + }, [shouldDisableDate, rangePosition]); + + const dateValidationProps = React.useMemo( + () => ({ + ...baseDateValidationProps, + shouldDisableDate: shouldDisableDateForSingleDateValidation, + }), + [baseDateValidationProps, shouldDisableDateForSingleDateValidation], + ); + + const getNewValueFromNewSelectedDate = ({ + prevValue, + selectedDate, + referenceDate, + allowRangeFlip, + }: useBaseCalendarRoot.GetNewValueFromNewSelectedDateParameters & { + allowRangeFlip?: boolean; + }): useBaseCalendarRoot.GetNewValueFromNewSelectedDateReturnValue => { + const { nextSelection, newRange } = calculateRangeChange({ + newDate: selectedDate, + utils, + range: prevValue, + rangePosition, + allowRangeFlip, + shouldMergeDateAndTime: true, + referenceDate, + }); + + const isNextSectionAvailable = availableRangePositions.includes(nextSelection); + if (isNextSectionAvailable) { + onRangePositionChange(nextSelection); + } + + const isFullRangeSelected = rangePosition === 'end' && isRangeValid(utils, newRange); + + return { + value: newRange, + changeImportance: isFullRangeSelected || !isNextSectionAvailable ? 'set' : 'accept', + }; + }; + const { value, - setValue, - referenceValue, setVisibleDate, isDateCellVisible, context: baseContext, - validationProps: baseValidationProps, } = useBaseCalendarRoot({ ...baseParameters, manager, - getInitialVisibleDate: (referenceValueParam) => referenceValueParam[0], + valueValidationProps, + dateValidationProps, + getDateToUseForReferenceDate: (initialValue) => initialValue[0] ?? initialValue[1], + getCurrentDateFromValue: (currentValue) => + rangePosition === 'start' ? currentValue[0] : currentValue[1], + getNewValueFromNewSelectedDate, + getSelectedDatesFromValue, }); - const validationProps = React.useMemo( - () => ({ ...baseValidationProps, shouldDisableDate }), - [baseValidationProps, shouldDisableDate], - ); - // TODO: Apply some logic based on the range position. const [prevValue, setPrevValue] = React.useState(value); if (value !== prevValue) { @@ -56,11 +148,8 @@ export function useRangeCalendarRoot(parameters: useRangeCalendarRoot.Parameters const context: RangeCalendarRootContext = React.useMemo( () => ({ value, - setValue, - referenceValue, - validationProps, }), - [value, setValue, referenceValue, validationProps], + [value], ); const getRootProps = React.useCallback((externalProps: GenericHTMLProps) => { @@ -75,9 +164,11 @@ export function useRangeCalendarRoot(parameters: useRangeCalendarRoot.Parameters export namespace useRangeCalendarRoot { export interface Parameters - extends Omit< - useBaseCalendarRoot.Parameters, - 'manager' | 'getInitialVisibleDate' - >, - ExportedValidateDateRangeProps {} + extends useBaseCalendarRoot.PublicParameters, + ExportedValidateDateRangeProps, + UseRangePositionProps {} +} + +function getSelectedDatesFromValue(value: PickerRangeValue): PickerValidDate[] { + return value.filter((date) => date != null); } diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/set-visible-month/RangeCalendarSetVisibleMonth.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/set-visible-month/RangeCalendarSetVisibleMonth.tsx new file mode 100644 index 0000000000000..0c16156bd495a --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/set-visible-month/RangeCalendarSetVisibleMonth.tsx @@ -0,0 +1,57 @@ +'use client'; +import * as React from 'react'; +// eslint-disable-next-line no-restricted-imports +import { useBaseCalendarSetVisibleMonth } from '@mui/x-date-pickers/internals/base/utils/base-calendar/set-visible-month/useBaseCalendarSetVisibleMonth'; +// eslint-disable-next-line no-restricted-imports +import { useBaseCalendarSetVisibleMonthWrapper } from '@mui/x-date-pickers/internals/base/utils/base-calendar/set-visible-month/useBaseCalendarSetVisibleMonthWrapper'; +// eslint-disable-next-line no-restricted-imports +import { BaseUIComponentProps } from '@mui/x-date-pickers/internals/base/base-utils/types'; +// eslint-disable-next-line no-restricted-imports +import { useComponentRenderer } from '@mui/x-date-pickers/internals/base/base-utils/useComponentRenderer'; + +const InnerRangeCalendarSetVisibleMonth = React.forwardRef( + function InnerRangeCalendarSetVisibleMonth( + props: InnerRangeCalendarSetVisibleMonthProps, + forwardedRef: React.ForwardedRef, + ) { + const { className, render, ctx, target, ...otherProps } = props; + const { getSetVisibleMonthProps } = useBaseCalendarSetVisibleMonth({ ctx, target }); + + const state: RangeCalendarSetVisibleMonth.State = React.useMemo(() => ({}), []); + + const { renderElement } = useComponentRenderer({ + propGetter: getSetVisibleMonthProps, + render: render ?? 'button', + ref: forwardedRef, + className, + state, + extraProps: otherProps, + }); + + return renderElement(); + }, +); + +const MemoizedInnerRangeCalendarSetVisibleMonth = React.memo(InnerRangeCalendarSetVisibleMonth); + +const RangeCalendarSetVisibleMonth = React.forwardRef(function RangeCalendarSetVisibleMonth( + props: RangeCalendarSetVisibleMonth.Props, + forwardedRef: React.ForwardedRef, +) { + const { ctx } = useBaseCalendarSetVisibleMonthWrapper({ target: props.target }); + return ; +}); + +export namespace RangeCalendarSetVisibleMonth { + export interface State {} + + export interface Props + extends Omit, + BaseUIComponentProps<'button', State> {} +} + +interface InnerRangeCalendarSetVisibleMonthProps + extends useBaseCalendarSetVisibleMonth.Parameters, + BaseUIComponentProps<'button', RangeCalendarSetVisibleMonth.State> {} + +export { RangeCalendarSetVisibleMonth }; diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/set-visible-month/RangeCalendarSetVisibleMonthDataAttributes.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/set-visible-month/RangeCalendarSetVisibleMonthDataAttributes.ts new file mode 100644 index 0000000000000..75640ce829b70 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/set-visible-month/RangeCalendarSetVisibleMonthDataAttributes.ts @@ -0,0 +1 @@ +export enum RangeCalendarSetVisibleMonthDataAttributes {} diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/set-visible-year/RangeCalendarSetVisibleYear.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/set-visible-year/RangeCalendarSetVisibleYear.tsx new file mode 100644 index 0000000000000..baa3d8660d8b4 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/set-visible-year/RangeCalendarSetVisibleYear.tsx @@ -0,0 +1,56 @@ +'use client'; +import * as React from 'react'; +// eslint-disable-next-line no-restricted-imports +import { useBaseCalendarSetVisibleYear } from '@mui/x-date-pickers/internals/base/utils/base-calendar/set-visible-year/useBaseCalendarSetVisibleYear'; +// eslint-disable-next-line no-restricted-imports +import { useBaseCalendarSetVisibleYearWrapper } from '@mui/x-date-pickers/internals/base/utils/base-calendar/set-visible-year/useBaseCalendarSetVisibleYearWrapper'; +// eslint-disable-next-line no-restricted-imports +import { BaseUIComponentProps } from '@mui/x-date-pickers/internals/base/base-utils/types'; +// eslint-disable-next-line no-restricted-imports +import { useComponentRenderer } from '@mui/x-date-pickers/internals/base/base-utils/useComponentRenderer'; + +const InnerRangeCalendarSetVisibleYear = React.forwardRef(function InnerRangeCalendarSetVisibleYear( + props: InnerRangeCalendarSetVisibleYearProps, + forwardedRef: React.ForwardedRef, +) { + const { className, render, ctx, target, ...otherProps } = props; + const { getSetVisibleYearProps } = useBaseCalendarSetVisibleYear({ ctx, target }); + + const state: RangeCalendarSetVisibleYear.State = React.useMemo(() => ({}), []); + + const { renderElement } = useComponentRenderer({ + propGetter: getSetVisibleYearProps, + render: render ?? 'button', + ref: forwardedRef, + className, + state, + extraProps: otherProps, + }); + + return renderElement(); +}); + +const MemoizedInnerRangeCalendarSetVisibleYear = React.memo(InnerRangeCalendarSetVisibleYear); + +const RangeCalendarSetVisibleYear = React.forwardRef(function RangeCalendarSetVisibleYear( + props: RangeCalendarSetVisibleYear.Props, + forwardedRef: React.ForwardedRef, +) { + const { ctx } = useBaseCalendarSetVisibleYearWrapper({ target: props.target }); + + return ; +}); + +export namespace RangeCalendarSetVisibleYear { + export interface State {} + + export interface Props + extends Omit, + BaseUIComponentProps<'button', State> {} +} + +interface InnerRangeCalendarSetVisibleYearProps + extends useBaseCalendarSetVisibleYear.Parameters, + BaseUIComponentProps<'button', RangeCalendarSetVisibleYear.State> {} + +export { RangeCalendarSetVisibleYear }; diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/set-visible-year/RangeCalendarSetVisibleYearDataAttributes.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/set-visible-year/RangeCalendarSetVisibleYearDataAttributes.ts new file mode 100644 index 0000000000000..4c18b7a6afc32 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/set-visible-year/RangeCalendarSetVisibleYearDataAttributes.ts @@ -0,0 +1 @@ +export enum RangeCalendarSetVisibleYearDataAttributes {} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx index cf8f9a3d34f73..2ba096ecc48b2 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx @@ -66,7 +66,7 @@ const CalendarDaysCell = React.forwardRef(function CalendarDaysCell( ) { const rootContext = useCalendarRootContext(); const baseRootContext = useBaseCalendarRootContext(); - const monthsListContext = useBaseCalendarDaysGridContext(); + const baseDaysGridContext = useBaseCalendarDaysGridContext(); const { ref: listItemRef, index: colIndex } = useCompositeListItem(); const utils = useUtils(); const mergedRef = useForkRef(forwardedRef, listItemRef); @@ -78,10 +78,10 @@ const CalendarDaysCell = React.forwardRef(function CalendarDaysCell( const isOutsideCurrentMonth = React.useMemo( () => - monthsListContext.currentMonth == null + baseDaysGridContext.currentMonth == null ? false - : !utils.isSameMonth(monthsListContext.currentMonth, props.value), - [monthsListContext.currentMonth, props.value, utils], + : !utils.isSameMonth(baseDaysGridContext.currentMonth, props.value), + [baseDaysGridContext.currentMonth, props.value, utils], ); const isDateInvalid = baseRootContext.isDateInvalid; @@ -96,11 +96,8 @@ const CalendarDaysCell = React.forwardRef(function CalendarDaysCell( }, [baseRootContext.disabled, isInvalid]); const isTabbable = React.useMemo( - () => - monthsListContext.tabbableDay == null - ? false - : utils.isSameDay(monthsListContext.tabbableDay, props.value), - [utils, monthsListContext.tabbableDay, props.value], + () => baseDaysGridContext.tabbableDays.some((day) => utils.isSameDay(day, props.value)), + [utils, baseDaysGridContext.tabbableDays, props.value], ); const ctx = React.useMemo( @@ -111,7 +108,7 @@ const CalendarDaysCell = React.forwardRef(function CalendarDaysCell( isInvalid, isTabbable, isOutsideCurrentMonth, - selectDay: monthsListContext.selectDay, + selectDay: baseDaysGridContext.selectDay, }), [ isSelected, @@ -119,7 +116,7 @@ const CalendarDaysCell = React.forwardRef(function CalendarDaysCell( isInvalid, isTabbable, isOutsideCurrentMonth, - monthsListContext.selectDay, + baseDaysGridContext.selectDay, colIndex, ], ); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-grid-header-cell/CalendarDaysGridHeaderCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/days-grid-header-cell/CalendarDaysGridHeaderCell.tsx index 772c3adaa9eb3..9308605c2604f 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-grid-header-cell/CalendarDaysGridHeaderCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-grid-header-cell/CalendarDaysGridHeaderCell.tsx @@ -1,7 +1,7 @@ 'use client'; import * as React from 'react'; import { useComponentRenderer } from '../../base-utils/useComponentRenderer'; -import { useCalendarDaysGridHeaderCell } from './useCalendarDaysGridHeaderCell'; +import { useBaseCalendarDaysGridHeaderCell } from '../../utils/base-calendar/days-grid-header-cell/useBaseCalendarDaysGridHeaderCell'; import { BaseUIComponentProps } from '../../base-utils/types'; const CalendarDaysGridHeaderCell = React.forwardRef(function CalendarDaysGridHeaderCell( @@ -9,7 +9,7 @@ const CalendarDaysGridHeaderCell = React.forwardRef(function CalendarDaysGridHea forwardedRef: React.ForwardedRef, ) { const { className, render, value, formatter, ...otherProps } = props; - const { getDaysGridHeaderCellProps } = useCalendarDaysGridHeaderCell({ value, formatter }); + const { getDaysGridHeaderCellProps } = useBaseCalendarDaysGridHeaderCell({ value, formatter }); const state: CalendarDaysGridHeaderCell.State = React.useMemo(() => ({}), []); @@ -29,7 +29,7 @@ export namespace CalendarDaysGridHeaderCell { export interface State {} export interface Props - extends useCalendarDaysGridHeaderCell.Parameters, + extends useBaseCalendarDaysGridHeaderCell.Parameters, BaseUIComponentProps<'span', State> {} } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-grid-header/CalendarDaysGridHeader.tsx b/packages/x-date-pickers/src/internals/base/Calendar/days-grid-header/CalendarDaysGridHeader.tsx index 9bd030952d4d8..9bbc1607be542 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-grid-header/CalendarDaysGridHeader.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-grid-header/CalendarDaysGridHeader.tsx @@ -1,15 +1,15 @@ 'use client'; import * as React from 'react'; -import { useCalendarDaysGridHeader } from './useCalendarDaysGridHeader'; import { BaseUIComponentProps } from '../../base-utils/types'; import { useComponentRenderer } from '../../base-utils/useComponentRenderer'; +import { useBaseCalendarDaysGridHeader } from '../../utils/base-calendar/days-grid-header/useBaseCalendarDaysGridHeader'; const CalendarDaysGridHeader = React.forwardRef(function CalendarDaysGridHeader( props: CalendarDaysGridHeader.Props, forwardedRef: React.ForwardedRef, ) { const { className, render, children, ...otherProps } = props; - const { getDaysGridHeaderProps } = useCalendarDaysGridHeader({ + const { getDaysGridHeaderProps } = useBaseCalendarDaysGridHeader({ children, }); const state = React.useMemo(() => ({}), []); @@ -31,7 +31,7 @@ export namespace CalendarDaysGridHeader { export interface Props extends Omit, 'children'>, - useCalendarDaysGridHeader.Parameters {} + useBaseCalendarDaysGridHeader.Parameters {} } export { CalendarDaysGridHeader }; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-grid-header/useCalendarDaysGridHeader.ts b/packages/x-date-pickers/src/internals/base/Calendar/days-grid-header/useCalendarDaysGridHeader.ts deleted file mode 100644 index 580986ad9af28..0000000000000 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-grid-header/useCalendarDaysGridHeader.ts +++ /dev/null @@ -1,40 +0,0 @@ -import * as React from 'react'; -import { PickerValidDate } from '../../../../models'; -import { getWeekdays } from '../../../utils/date-utils'; -import { useUtils } from '../../../hooks/useUtils'; -import { useCalendarRootContext } from '../root/CalendarRootContext'; -import { GenericHTMLProps } from '../../base-utils/types'; -import { mergeReactProps } from '../../base-utils/mergeReactProps'; - -export function useCalendarDaysGridHeader(parameters: useCalendarDaysGridHeader.Parameters) { - const { children } = parameters; - const utils = useUtils(); - const rootContext = useCalendarRootContext(); - - const days = React.useMemo( - () => getWeekdays(utils, rootContext.value ?? rootContext.referenceValue), - [utils, rootContext.value, rootContext.referenceValue], - ); - - const getDaysGridHeaderProps = React.useCallback( - (externalProps: GenericHTMLProps) => { - return mergeReactProps(externalProps, { - role: 'row', - children: children == null ? null : children({ days }), - }); - }, - [days, children], - ); - - return React.useMemo(() => ({ getDaysGridHeaderProps }), [getDaysGridHeaderProps]); -} - -export namespace useCalendarDaysGridHeader { - export interface Parameters { - children?: (parameters: ChildrenParameters) => React.ReactNode; - } - - export interface ChildrenParameters { - days: PickerValidDate[]; - } -} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx index 33a80327fcd5a..0d5a17dc3d41b 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx @@ -103,8 +103,8 @@ const CalendarMonthsCell = React.forwardRef(function CalendarMonthsCell( () => utils.isValid(rootContext.value) ? isSelected - : utils.isSameMonth(rootContext.referenceValue, props.value), - [utils, rootContext.value, rootContext.referenceValue, isSelected, props.value], + : utils.isSameMonth(baseRootContext.currentDate, props.value), + [utils, rootContext.value, baseRootContext.currentDate, isSelected, props.value], ); const selectMonth = useEventCallback((newValue: PickerValidDate) => { @@ -112,10 +112,7 @@ const CalendarMonthsCell = React.forwardRef(function CalendarMonthsCell( return; } - const newCleanValue = utils.setMonth( - rootContext.value ?? rootContext.referenceValue, - utils.getMonth(newValue), - ); + const newCleanValue = utils.setMonth(baseRootContext.currentDate, utils.getMonth(newValue)); const startOfMonth = utils.startOfMonth(newCleanValue); const endOfMonth = utils.endOfMonth(newCleanValue); @@ -139,7 +136,7 @@ const CalendarMonthsCell = React.forwardRef(function CalendarMonthsCell( if (closestEnabledDate) { baseRootContext.setVisibleDate(closestEnabledDate, true); - rootContext.setValue(closestEnabledDate, { section: 'month' }); + baseRootContext.selectDate(closestEnabledDate, { section: 'month' }); } }); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts index 46c03f3ba5472..5a45e98589dde 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts @@ -1,23 +1,8 @@ import * as React from 'react'; -import { PickerValidDate } from '../../../../models'; import { PickerValue } from '../../../models'; -import { useBaseCalendarRoot } from '../../utils/base-calendar/root/useBaseCalendarRoot'; export interface CalendarRootContext { - /** - * The current value of the calendar. - */ value: PickerValue; - /** - * Set the current value of the calendar. - * @param {PickerValue} value The new value of the calendar. - * @param {Pick, 'section'>} options The options to customize the behavior of this update. - */ - setValue: ( - value: PickerValue, - options: Pick, 'section'>, - ) => void; - referenceValue: PickerValidDate; } export const CalendarRootContext = React.createContext(undefined); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts index 16eb2c9430ab6..1a13890e008e1 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts @@ -1,30 +1,67 @@ import * as React from 'react'; -import { DateValidationError } from '../../../../models'; +import { DateValidationError, PickerValidDate } from '../../../../models'; import { useDateManager } from '../../../../managers'; -import { ExportedValidateDateProps } from '../../../../validation/validateDate'; +import { ExportedValidateDateProps, ValidateDateProps } from '../../../../validation/validateDate'; import { useUtils } from '../../../hooks/useUtils'; import { PickerValue } from '../../../models'; import { CalendarRootContext } from './CalendarRootContext'; import { mergeReactProps } from '../../base-utils/mergeReactProps'; import { GenericHTMLProps } from '../../base-utils/types'; -import { useBaseCalendarRoot } from '../../utils/base-calendar/root/useBaseCalendarRoot'; +import { + useAddDefaultsToBaseDateValidationProps, + useBaseCalendarRoot, +} from '../../utils/base-calendar/root/useBaseCalendarRoot'; export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { - const { ...baseParameters } = parameters; + const { + // Validation props + minDate, + maxDate, + disablePast, + disableFuture, + shouldDisableDate, + shouldDisableMonth, + shouldDisableYear, + // Parameters forwarded to `useBaseCalendarRoot` + ...baseParameters + } = parameters; const utils = useUtils(); const manager = useDateManager(); + const baseDateValidationProps = useAddDefaultsToBaseDateValidationProps({ + minDate, + maxDate, + disablePast, + disableFuture, + }); + + const validationProps = React.useMemo( + () => ({ + ...baseDateValidationProps, + shouldDisableDate, + shouldDisableMonth, + shouldDisableYear, + }), + [baseDateValidationProps, shouldDisableDate, shouldDisableMonth, shouldDisableYear], + ); + const { value, - setValue, - referenceValue, setVisibleDate, isDateCellVisible, context: baseContext, } = useBaseCalendarRoot({ ...baseParameters, manager, - getInitialVisibleDate: (referenceValueParam) => referenceValueParam, + dateValidationProps: validationProps, + valueValidationProps: validationProps, + getDateToUseForReferenceDate: (initialValue) => initialValue, + getNewValueFromNewSelectedDate: ({ selectedDate }) => ({ + value: selectedDate, + changeImportance: 'accept', + }), + getCurrentDateFromValue: (currentValue) => currentValue, + getSelectedDatesFromValue, }); const [prevValue, setPrevValue] = React.useState(value); @@ -38,10 +75,8 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { const context: CalendarRootContext = React.useMemo( () => ({ value, - setValue, - referenceValue, }), - [value, setValue, referenceValue], + [value], ); const getRootProps = React.useCallback((externalProps: GenericHTMLProps) => { @@ -56,9 +91,10 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { export namespace useCalendarRoot { export interface Parameters - extends Omit< - useBaseCalendarRoot.Parameters, - 'manager' | 'getInitialVisibleDate' - >, + extends useBaseCalendarRoot.PublicParameters, ExportedValidateDateProps {} } + +function getSelectedDatesFromValue(value: PickerValue): PickerValidDate[] { + return value == null ? [] : [value]; +} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx index 60abd030e45c5..0a6b621049ee0 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx @@ -1,19 +1,17 @@ 'use client'; import * as React from 'react'; -import useEventCallback from '@mui/utils/useEventCallback'; -import { useUtils } from '../../../hooks/useUtils'; + import { useComponentRenderer } from '../../base-utils/useComponentRenderer'; -import { useBaseCalendarRootContext } from '../../utils/base-calendar/root/BaseCalendarRootContext'; -import { useCalendarSetVisibleMonth } from './useCalendarSetVisibleMonth'; +import { useBaseCalendarSetVisibleMonth } from '../../utils/base-calendar/set-visible-month/useBaseCalendarSetVisibleMonth'; +import { useBaseCalendarSetVisibleMonthWrapper } from '../../utils/base-calendar/set-visible-month/useBaseCalendarSetVisibleMonthWrapper'; import { BaseUIComponentProps } from '../../base-utils/types'; -import { getFirstEnabledMonth, getLastEnabledMonth } from '../utils/date'; const InnerCalendarSetVisibleMonth = React.forwardRef(function InnerCalendarSetVisibleMonth( props: InnerCalendarSetVisibleMonthProps, forwardedRef: React.ForwardedRef, ) { const { className, render, ctx, target, ...otherProps } = props; - const { getSetVisibleMonthProps } = useCalendarSetVisibleMonth({ ctx, target }); + const { getSetVisibleMonthProps } = useBaseCalendarSetVisibleMonth({ ctx, target }); const state: CalendarSetVisibleMonth.State = React.useMemo(() => ({}), []); @@ -35,65 +33,7 @@ const CalendarSetVisibleMonth = React.forwardRef(function CalendarSetVisibleMont props: CalendarSetVisibleMonth.Props, forwardedRef: React.ForwardedRef, ) { - const baseRootContext = useBaseCalendarRootContext(); - const utils = useUtils(); - - const targetDate = React.useMemo(() => { - if (props.target === 'previous') { - return utils.addMonths(baseRootContext.visibleDate, -baseRootContext.monthPageSize); - } - - if (props.target === 'next') { - return utils.addMonths(baseRootContext.visibleDate, baseRootContext.monthPageSize); - } - - return utils.setMonth(baseRootContext.visibleDate, utils.getMonth(props.target)); - }, [baseRootContext.visibleDate, baseRootContext.monthPageSize, utils, props.target]); - - const isDisabled = React.useMemo(() => { - if (baseRootContext.disabled) { - return true; - } - - // TODO: Check if the logic below works correctly when multiple months are rendered at once. - const isMovingBefore = utils.isBefore(targetDate, baseRootContext.visibleDate); - - // All the months before the visible ones are fully disabled, we skip the navigation. - if (isMovingBefore) { - return utils.isAfter( - getFirstEnabledMonth(utils, baseRootContext.dateValidationProps), - targetDate, - ); - } - - // All the months after the visible ones are fully disabled, we skip the navigation. - return utils.isBefore( - getLastEnabledMonth(utils, baseRootContext.dateValidationProps), - targetDate, - ); - }, [ - baseRootContext.disabled, - baseRootContext.dateValidationProps, - baseRootContext.visibleDate, - targetDate, - utils, - ]); - - const setTarget = useEventCallback(() => { - if (isDisabled) { - return; - } - baseRootContext.setVisibleDate(targetDate, false); - }); - - const ctx = React.useMemo( - () => ({ - setTarget, - isDisabled, - }), - [setTarget, isDisabled], - ); - + const { ctx } = useBaseCalendarSetVisibleMonthWrapper({ target: props.target }); return ; }); @@ -101,12 +41,12 @@ export namespace CalendarSetVisibleMonth { export interface State {} export interface Props - extends Omit, + extends Omit, BaseUIComponentProps<'button', State> {} } interface InnerCalendarSetVisibleMonthProps - extends useCalendarSetVisibleMonth.Parameters, + extends useBaseCalendarSetVisibleMonth.Parameters, BaseUIComponentProps<'button', CalendarSetVisibleMonth.State> {} export { CalendarSetVisibleMonth }; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx index 9cdb2326f8642..493efaca96bd9 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx @@ -1,19 +1,16 @@ 'use client'; import * as React from 'react'; -import useEventCallback from '@mui/utils/useEventCallback'; -import { useUtils } from '../../../hooks/useUtils'; import { useComponentRenderer } from '../../base-utils/useComponentRenderer'; -import { useBaseCalendarRootContext } from '../../utils/base-calendar/root/BaseCalendarRootContext'; -import { useCalendarSetVisibleYear } from './useCalendarSetVisibleYear'; +import { useBaseCalendarSetVisibleYear } from '../../utils/base-calendar/set-visible-year/useBaseCalendarSetVisibleYear'; import { BaseUIComponentProps } from '../../base-utils/types'; -import { getFirstEnabledYear, getLastEnabledYear } from '../utils/date'; +import { useBaseCalendarSetVisibleYearWrapper } from '../../utils/base-calendar/set-visible-year/useBaseCalendarSetVisibleYearWrapper'; const InnerCalendarSetVisibleYear = React.forwardRef(function InnerCalendarSetVisibleYear( props: InnerCalendarSetVisibleYearProps, forwardedRef: React.ForwardedRef, ) { const { className, render, ctx, target, ...otherProps } = props; - const { getSetVisibleYearProps } = useCalendarSetVisibleYear({ ctx, target }); + const { getSetVisibleYearProps } = useBaseCalendarSetVisibleYear({ ctx, target }); const state: CalendarSetVisibleYear.State = React.useMemo(() => ({}), []); @@ -35,63 +32,7 @@ const CalendarSetVisibleYear = React.forwardRef(function CalendarSetVisibleYear( props: CalendarSetVisibleYear.Props, forwardedRef: React.ForwardedRef, ) { - const baseRootContext = useBaseCalendarRootContext(); - const utils = useUtils(); - - const targetDate = React.useMemo(() => { - if (props.target === 'previous') { - return utils.addYears(baseRootContext.visibleDate, -1); - } - - if (props.target === 'next') { - return utils.addYears(baseRootContext.visibleDate, 1); - } - - return utils.setYear(baseRootContext.visibleDate, utils.getYear(props.target)); - }, [baseRootContext.visibleDate, utils, props.target]); - - const isDisabled = React.useMemo(() => { - if (baseRootContext.disabled) { - return true; - } - - const isMovingBefore = utils.isBefore(targetDate, baseRootContext.visibleDate); - - // All the years before the visible ones are fully disabled, we skip the navigation. - if (isMovingBefore) { - return utils.isAfter( - getFirstEnabledYear(utils, baseRootContext.dateValidationProps), - targetDate, - ); - } - - // All the years after the visible ones are fully disabled, we skip the navigation. - return utils.isBefore( - getLastEnabledYear(utils, baseRootContext.dateValidationProps), - targetDate, - ); - }, [ - baseRootContext.disabled, - baseRootContext.dateValidationProps, - baseRootContext.visibleDate, - targetDate, - utils, - ]); - - const setTarget = useEventCallback(() => { - if (isDisabled) { - return; - } - baseRootContext.setVisibleDate(targetDate, false); - }); - - const ctx = React.useMemo( - () => ({ - setTarget, - isDisabled, - }), - [setTarget, isDisabled], - ); + const { ctx } = useBaseCalendarSetVisibleYearWrapper({ target: props.target }); return ; }); @@ -100,12 +41,12 @@ export namespace CalendarSetVisibleYear { export interface State {} export interface Props - extends Omit, + extends Omit, BaseUIComponentProps<'button', State> {} } interface InnerCalendarSetVisibleYearProps - extends useCalendarSetVisibleYear.Parameters, + extends useBaseCalendarSetVisibleYear.Parameters, BaseUIComponentProps<'button', CalendarSetVisibleYear.State> {} export { CalendarSetVisibleYear }; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYearDataAttributes.ts b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYearDataAttributes.ts index 6e13ae3149e8f..3ee945e138d86 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYearDataAttributes.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYearDataAttributes.ts @@ -1 +1 @@ -export enum CalendarSetVisCalendarSetVisibleYearDataAttributesibleMonthDataAttributes {} +export enum CalendarSetVisibleYearDataAttributes {} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/useMonthsCells.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/useMonthsCells.ts index 6e2afcc6dce6b..eb1535d349f17 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/utils/useMonthsCells.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/utils/useMonthsCells.ts @@ -2,7 +2,7 @@ import * as React from 'react'; import { PickerValidDate } from '../../../../models'; import { getMonthsInYear } from '../../../utils/date-utils'; import { useUtils } from '../../../hooks/useUtils'; -import { getFirstEnabledYear, getLastEnabledYear } from './date'; +import { getFirstEnabledYear, getLastEnabledYear } from '../../utils/base-calendar/utils/date'; import { useBaseCalendarRootContext } from '../../utils/base-calendar/root/BaseCalendarRootContext'; export function useMonthsCells(): useMonthsCells.ReturnValue { diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx index cbd29cead34f3..2418baf4e26e9 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx @@ -100,8 +100,8 @@ const CalendarYearsCell = React.forwardRef(function CalendarsYearCell( () => utils.isValid(rootContext.value) ? isSelected - : utils.isSameYear(rootContext.referenceValue, props.value), - [utils, rootContext.value, rootContext.referenceValue, isSelected, props.value], + : utils.isSameYear(baseRootContext.currentDate, props.value), + [utils, rootContext.value, baseRootContext.currentDate, isSelected, props.value], ); const selectYear = useEventCallback((newValue: PickerValidDate) => { @@ -109,10 +109,7 @@ const CalendarYearsCell = React.forwardRef(function CalendarsYearCell( return; } - const newCleanValue = utils.setYear( - rootContext.value ?? rootContext.referenceValue, - utils.getYear(newValue), - ); + const newCleanValue = utils.setYear(baseRootContext.currentDate, utils.getYear(newValue)); const startOfYear = utils.startOfYear(newCleanValue); const endOfYear = utils.endOfYear(newCleanValue); @@ -135,7 +132,7 @@ const CalendarYearsCell = React.forwardRef(function CalendarsYearCell( : newCleanValue; if (closestEnabledDate) { - rootContext.setValue(closestEnabledDate, { section: 'year' }); + baseRootContext.selectDate(closestEnabledDate, { section: 'year' }); } }); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-grid-header-cell/useCalendarDaysGridHeaderCell.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-grid-header-cell/useBaseCalendarDaysGridHeaderCell.ts similarity index 75% rename from packages/x-date-pickers/src/internals/base/Calendar/days-grid-header-cell/useCalendarDaysGridHeaderCell.ts rename to packages/x-date-pickers/src/internals/base/utils/base-calendar/days-grid-header-cell/useBaseCalendarDaysGridHeaderCell.ts index 32ad3c39a052c..7154332bb3f82 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-grid-header-cell/useCalendarDaysGridHeaderCell.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-grid-header-cell/useBaseCalendarDaysGridHeaderCell.ts @@ -1,11 +1,11 @@ import * as React from 'react'; -import { PickerValidDate } from '../../../../models'; -import { GenericHTMLProps } from '../../base-utils/types'; -import { mergeReactProps } from '../../base-utils/mergeReactProps'; -import { useUtils } from '../../../hooks/useUtils'; +import { PickerValidDate } from '../../../../../models'; +import { GenericHTMLProps } from '../../../base-utils/types'; +import { mergeReactProps } from '../../../base-utils/mergeReactProps'; +import { useUtils } from '../../../../hooks/useUtils'; -export function useCalendarDaysGridHeaderCell( - parameters: useCalendarDaysGridHeaderCell.Parameters, +export function useBaseCalendarDaysGridHeaderCell( + parameters: useBaseCalendarDaysGridHeaderCell.Parameters, ) { const utils = useUtils(); @@ -33,7 +33,7 @@ export function useCalendarDaysGridHeaderCell( return React.useMemo(() => ({ getDaysGridHeaderCellProps }), [getDaysGridHeaderCellProps]); } -export namespace useCalendarDaysGridHeaderCell { +export namespace useBaseCalendarDaysGridHeaderCell { export interface Parameters { value: PickerValidDate; /** diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-grid-header/useBaseCalendarDaysGridHeader.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-grid-header/useBaseCalendarDaysGridHeader.ts new file mode 100644 index 0000000000000..f9f8c598de30a --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-grid-header/useBaseCalendarDaysGridHeader.ts @@ -0,0 +1,42 @@ +import * as React from 'react'; +import { PickerValidDate } from '../../../../../models'; +import { getWeekdays } from '../../../../utils/date-utils'; +import { useUtils } from '../../../../hooks/useUtils'; +import { GenericHTMLProps } from '../../../base-utils/types'; +import { mergeReactProps } from '../../../base-utils/mergeReactProps'; +import { useBaseCalendarRootContext } from '../root/BaseCalendarRootContext'; + +export function useBaseCalendarDaysGridHeader( + parameters: useBaseCalendarDaysGridHeader.Parameters, +) { + const { children } = parameters; + const utils = useUtils(); + const baseRootContext = useBaseCalendarRootContext(); + + const days = React.useMemo( + () => getWeekdays(utils, baseRootContext.currentDate), + [utils, baseRootContext.currentDate], + ); + + const getDaysGridHeaderProps = React.useCallback( + (externalProps: GenericHTMLProps) => { + return mergeReactProps(externalProps, { + role: 'row', + children: children == null ? null : children({ days }), + }); + }, + [days, children], + ); + + return React.useMemo(() => ({ getDaysGridHeaderProps }), [getDaysGridHeaderProps]); +} + +export namespace useBaseCalendarDaysGridHeader { + export interface Parameters { + children?: (parameters: ChildrenParameters) => React.ReactNode; + } + + export interface ChildrenParameters { + days: PickerValidDate[]; + } +} diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-grid/BaseCalendarDaysGridContext.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-grid/BaseCalendarDaysGridContext.ts index e41d53025ca3c..5fd6a05d0d202 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-grid/BaseCalendarDaysGridContext.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-grid/BaseCalendarDaysGridContext.ts @@ -4,7 +4,7 @@ import { PickerValidDate } from '../../../../../models'; export interface BaseCalendarDaysGridContext { selectDay: (value: PickerValidDate) => void; currentMonth: PickerValidDate; - tabbableDay: PickerValidDate | null; + tabbableDays: PickerValidDate[]; daysGrid: PickerValidDate[][]; } diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-grid/useBaseCalendarDaysGrid.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-grid/useBaseCalendarDaysGrid.ts index a2d35a9a17283..4f57b38c55b79 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-grid/useBaseCalendarDaysGrid.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-grid/useBaseCalendarDaysGrid.ts @@ -3,7 +3,6 @@ import useEventCallback from '@mui/utils/useEventCallback'; import { PickerValidDate } from '../../../../../models'; import { useUtils } from '../../../../hooks/useUtils'; import { mergeDateAndTime } from '../../../../utils/date-utils'; -import { useCalendarRootContext } from '../../../Calendar/root/CalendarRootContext'; import { GenericHTMLProps } from '../../../base-utils/types'; import { mergeReactProps } from '../../../base-utils/mergeReactProps'; import { useBaseCalendarRootContext } from '../root/BaseCalendarRootContext'; @@ -12,7 +11,6 @@ import { BaseCalendarDaysGridContext } from './BaseCalendarDaysGridContext'; export function useBaseCalendarDaysGrid(parameters: useBaseCalendarDaysGrid.Parameters) { const { fixedWeekNumber, offset = 0 } = parameters; const utils = useUtils(); - const rootContext = useCalendarRootContext(); const baseRootContext = useBaseCalendarRootContext(); const currentMonth = React.useMemo(() => { @@ -53,24 +51,34 @@ export function useBaseCalendarDaysGrid(parameters: useBaseCalendarDaysGrid.Para return; } - const newCleanValue = mergeDateAndTime( - utils, - newValue, - rootContext.value ?? rootContext.referenceValue, - ); + const newCleanValue = mergeDateAndTime(utils, newValue, baseRootContext.currentDate); - rootContext.setValue(newCleanValue, { section: 'day' }); + baseRootContext.selectDate(newCleanValue, { section: 'day' }); }); - const tabbableDay = React.useMemo(() => { + const tabbableDays = React.useMemo(() => { const flatDays = daysGrid.flat(); - const tempTabbableDay = rootContext.value ?? rootContext.referenceValue; - if (flatDays.some((day) => utils.isSameDay(day, tempTabbableDay))) { - return tempTabbableDay; + + let tempTabbableDays: PickerValidDate[] = []; + tempTabbableDays = flatDays.filter((day) => + baseRootContext.selectedDates.some((selectedDay) => utils.isSameDay(day, selectedDay)), + ); + + if (tempTabbableDays.length === 0) { + tempTabbableDays = flatDays.filter((day) => + utils.isSameDay(day, baseRootContext.currentDate), + ); + } + + if (tempTabbableDays.length === 0) { + const firstDayInMonth = flatDays.find((day) => utils.isSameMonth(day, currentMonth)); + if (firstDayInMonth != null) { + tempTabbableDays = [firstDayInMonth]; + } } - return flatDays.find((day) => utils.isSameMonth(day, currentMonth)) ?? null; - }, [rootContext.value, rootContext.referenceValue, daysGrid, utils, currentMonth]); + return tempTabbableDays; + }, [baseRootContext.currentDate, baseRootContext.selectedDates, daysGrid, utils, currentMonth]); const registerSection = baseRootContext.registerSection; React.useEffect(() => { @@ -78,8 +86,8 @@ export function useBaseCalendarDaysGrid(parameters: useBaseCalendarDaysGrid.Para }, [registerSection, currentMonth]); const context: BaseCalendarDaysGridContext = React.useMemo( - () => ({ selectDay, daysGrid, currentMonth, tabbableDay }), - [selectDay, daysGrid, currentMonth, tabbableDay], + () => ({ selectDay, daysGrid, currentMonth, tabbableDays }), + [selectDay, daysGrid, currentMonth, tabbableDays], ); return React.useMemo(() => ({ getDaysGridProps, context }), [getDaysGridProps, context]); diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRowWrapper.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRowWrapper.ts index a142634a62153..03e03c5417976 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRowWrapper.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRowWrapper.ts @@ -4,11 +4,12 @@ import { PickerValidDate } from '../../../../../models'; import { useCompositeListItem } from '../../../composite/list/useCompositeListItem'; import { useBaseCalendarDaysGridBodyContext } from '../days-grid-body/BaseCalendarDaysGridBodyContext'; import { useBaseCalendarDaysGridContext } from '../days-grid/BaseCalendarDaysGridContext'; +import type { useBaseCalendarDaysWeekRow } from './useBaseCalendarDaysWeekRow'; -export function useBaseCalendarDaysWeekRowWrapper({ - forwardedRef, - value, -}: useBaseCalendarDaysWeekRowWrapper.Parameters) { +export function useBaseCalendarDaysWeekRowWrapper( + parameters: useBaseCalendarDaysWeekRowWrapper.Parameters, +) { + const { forwardedRef, value } = parameters; const baseDaysGridContext = useBaseCalendarDaysGridContext(); const baseDaysGridBodyContext = useBaseCalendarDaysGridBodyContext(); const { ref: listItemRef, index: rowIndex } = useCompositeListItem(); @@ -37,4 +38,15 @@ export namespace useBaseCalendarDaysWeekRowWrapper { forwardedRef: React.ForwardedRef; value: PickerValidDate; } + + export interface ReturnValue { + /** + * The ref to forward to the component. + */ + ref: React.RefObject; + /** + * The memoized context to forward to the memoized component so that it does not need to subscribe to any context. + */ + ctx: useBaseCalendarDaysWeekRow.Context; + } } diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/BaseCalendarRootContext.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/BaseCalendarRootContext.ts index 5b7bba7ae41dd..2e49a275a2b1e 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/BaseCalendarRootContext.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/BaseCalendarRootContext.ts @@ -5,13 +5,61 @@ import type { useBaseCalendarRoot } from './useBaseCalendarRoot'; import type { useBaseCalendarDaysGridBody } from '../days-grid-body/useBaseCalendarDaysGridBody'; export interface BaseCalendarRootContext { + /** + * The timezone to use when rendering or interactive with the dates. + */ timezone: PickersTimezone; + /** + * Whether the calendar is disabled. + */ disabled: boolean; + /** + * Whether the calendar is read-only. + */ readOnly: boolean; + // TODO: Implement the behavior. + /** + * Whether the calendar should auto-focus on mount. + */ autoFocus: boolean; - isDateInvalid: (day: PickerValidDate | null) => boolean; - dateValidationProps: ValidateDateProps; + /** + * The date currently visible. + * It is used to determine: + * - which month to render in Calendar.DaysGrid and RangeCalendar.DaysGrid + * - which year to render in Calendar.YearsGrid, Calendar.YearsList, RangeCalendar.YearsGrid, and RangeCalendar.YearsList + */ visibleDate: PickerValidDate; + /** + * The current date. + * It is used to determine: + * - if the rendered cells should be disabled or not + * - which date to apply when clicking on a cell + */ + currentDate: PickerValidDate; + /** + * The list of currently selected dates. + * When used inside the Calendar component, it contains the current value if not null. + * When used inside the RangeCalendar component, it contains the selected start and/or end dates if not null. + */ + selectedDates: PickerValidDate[]; + /** + * Selects a date. + * @param {PickerValidDate} date The date to select. + * @param {object} options The options to select the date. + * @param {'day' | 'month' | 'year'} options.section The section handled by the UI that triggered the change. + */ + selectDate: (date: PickerValidDate, options: { section: 'day' | 'month' | 'year' }) => void; + /** + * Determines if the given date is invalid. + * @param {PickerValidDate} date The date to check. + * @returns {boolean} Whether the date is invalid. + */ + isDateInvalid: (date: PickerValidDate) => boolean; + /** + * The props to check if a date is valid or not. + * Warning: Even when used inside the RangeCalendar component, this is still equal to the validation props for a single date. + */ + dateValidationProps: ValidateDateProps; setVisibleDate: (visibleDate: PickerValidDate, skipIfAlreadyVisible: boolean) => void; monthPageSize: number; yearPageSize: number; diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarDaysGridsNavigation.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarDaysGridsNavigation.ts index f4b5a53ee8222..4674105692f9c 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarDaysGridsNavigation.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarDaysGridsNavigation.ts @@ -11,7 +11,7 @@ import { NavigateInGridChangePage, PageGridNavigationTarget, } from '../../../Calendar/utils/keyboardNavigation'; -import { getFirstEnabledMonth, getLastEnabledMonth } from '../../../Calendar/utils/date'; +import { getFirstEnabledMonth, getLastEnabledMonth } from '../utils/date'; import { BaseCalendarRootContext } from './BaseCalendarRootContext'; /** diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarRoot.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarRoot.ts index 2ef0c3e045eab..bab74a0eaab9e 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarRoot.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarRoot.ts @@ -1,23 +1,29 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; -import { OnErrorProps, PickerManager, PickerValidDate, TimezoneProps } from '../../../../../models'; -import { useValidation } from '../../../../../validation'; import { - ValidateDateProps, - ExportedValidateDateProps, - validateDate, -} from '../../../../../validation/validateDate'; -import { FormProps, InferNonNullablePickerValue, PickerValidValue } from '../../../../models'; + OnErrorProps, + PickerChangeImportance, + PickerManager, + PickerValidDate, + TimezoneProps, +} from '../../../../../models'; +import { useValidation } from '../../../../../validation'; +import { ValidateDateProps, validateDate } from '../../../../../validation/validateDate'; +import { FormProps, PickerValidValue } from '../../../../models'; import { SECTION_TYPE_GRANULARITY } from '../../../../utils/getDefaultReferenceDate'; +import { singleItemValueManager } from '../../../../utils/valueManagers'; import { applyDefaultDate } from '../../../../utils/date-utils'; import { useDefaultDates, useLocalizationContext, useUtils } from '../../../../hooks/useUtils'; import { useControlledValueWithTimezone } from '../../../../hooks/useValueWithTimezone'; +import { BaseDateValidationProps } from '../../../../models/validation'; import { useBaseCalendarDaysGridNavigation } from './useBaseCalendarDaysGridsNavigation'; import { BaseCalendarRootContext } from './BaseCalendarRootContext'; -export function useBaseCalendarRoot( - parameters: useBaseCalendarRoot.Parameters, -) { +export function useBaseCalendarRoot< + TValue extends PickerValidValue, + TError, + TValidationProps extends Required, +>(parameters: useBaseCalendarRoot.Parameters) { const { readOnly = false, disabled = false, @@ -31,27 +37,16 @@ export function useBaseCalendarRoot( monthPageSize = 1, yearPageSize = 1, manager, - minDate, - maxDate, - disablePast, - disableFuture, - shouldDisableDate, - shouldDisableMonth, - shouldDisableYear, - getInitialVisibleDate, + getDateToUseForReferenceDate, + getNewValueFromNewSelectedDate, + getCurrentDateFromValue, + getSelectedDatesFromValue, + dateValidationProps, + valueValidationProps, } = parameters; const utils = useUtils(); const adapter = useLocalizationContext(); - const dateValidationProps = useAddDefaultsToDateValidationProps({ - minDate, - maxDate, - disablePast, - disableFuture, - shouldDisableDate, - shouldDisableMonth, - shouldDisableYear, - }); const { value, handleValueChange, timezone } = useControlledValueWithTimezone({ name: 'CalendarRoot', @@ -63,10 +58,10 @@ export function useBaseCalendarRoot( valueManager: manager.internal_valueManager, }); - const referenceValue = React.useMemo( + const referenceDate = React.useMemo( () => { - return manager.internal_valueManager.getInitialReferenceValue({ - value, + return singleItemValueManager.getInitialReferenceValue({ + value: getDateToUseForReferenceDate(value), utils, timezone, props: dateValidationProps, @@ -80,25 +75,12 @@ export function useBaseCalendarRoot( ); const { getValidationErrorForNewValue } = useValidation({ - // TODO: This is incorrect - props: { ...dateValidationProps, onError }, + props: { ...valueValidationProps, onError }, value, timezone, validator: manager.validator, }); - const setValue = useEventCallback( - ( - newValue: TValue, - context: Pick, 'section'>, - ) => { - handleValueChange(newValue, { - ...context, - validationError: getValidationErrorForNewValue(newValue), - }); - }, - ); - const sectionsRef = React.useRef< Record<'day' | 'month' | 'year', Record> >({ @@ -130,9 +112,7 @@ export function useBaseCalendarRoot( return true; }; - const [visibleDate, setVisibleDate] = React.useState(() => - getInitialVisibleDate(referenceValue), - ); + const [visibleDate, setVisibleDate] = React.useState(referenceDate); const handleVisibleDateChange = useEventCallback( (newVisibleDate: PickerValidDate, skipIfAlreadyVisible: boolean) => { if (skipIfAlreadyVisible && isDateCellVisible(newVisibleDate)) { @@ -162,6 +142,29 @@ export function useBaseCalendarRoot( [adapter, dateValidationProps, timezone], ); + const selectDate = useEventCallback( + (selectedDate: PickerValidDate, options) => { + const response = getNewValueFromNewSelectedDate({ + prevValue: value, + selectedDate, + referenceDate, + }); + + handleValueChange(response.value, { + changeImportance: response.changeImportance, + validationError: getValidationErrorForNewValue(response.value), + section: options.section, + }); + }, + ); + + const currentDate = getCurrentDateFromValue(value) ?? referenceDate; + + const selectedDates = React.useMemo( + () => getSelectedDatesFromValue(value), + [getSelectedDatesFromValue, value], + ); + const context: BaseCalendarRootContext = React.useMemo( () => ({ timezone, @@ -170,12 +173,15 @@ export function useBaseCalendarRoot( autoFocus, isDateInvalid, visibleDate, + currentDate, + selectedDates, setVisibleDate: handleVisibleDateChange, monthPageSize, yearPageSize, applyDayGridKeyboardNavigation, registerDaysGridCells, registerSection, + selectDate, dateValidationProps, }), [ @@ -185,6 +191,8 @@ export function useBaseCalendarRoot( autoFocus, isDateInvalid, visibleDate, + currentDate, + selectedDates, handleVisibleDateChange, monthPageSize, yearPageSize, @@ -192,13 +200,12 @@ export function useBaseCalendarRoot( registerDaysGridCells, registerSection, dateValidationProps, + selectDate, ], ); return { value, - setValue, - referenceValue, setVisibleDate, isDateCellVisible, context, @@ -206,21 +213,10 @@ export function useBaseCalendarRoot( } export namespace useBaseCalendarRoot { - export interface Parameters + export interface PublicParameters extends TimezoneProps, FormProps, - OnErrorProps, - ExportedValidateDateProps { - /** - * The manager of the calendar (uses `useDateManager` for Calendar and `useDateRangeManager` for RangeCalendar). - */ - manager: PickerManager; - /** - * TODO: Write description - * @param {InferNonNullablePickerValue} referenceValue The reference value to get the initial visible date from. - * @returns {PickerValidDate | null} The initial visible date. - */ - getInitialVisibleDate: (referenceValue: InferNonNullablePickerValue) => PickerValidDate; + OnErrorProps { /** * The controlled value that should be selected. * To render an uncontrolled Date Calendar, use the `defaultValue` prop instead. @@ -266,6 +262,51 @@ export namespace useBaseCalendarRoot { yearPageSize?: number; } + export interface Parameters< + TValue extends PickerValidValue, + TError, + TValidationProps extends Required, + > extends PublicParameters { + /** + * The manager of the calendar (uses `useDateManager` for Calendar and `useDateRangeManager` for RangeCalendar). + */ + manager: PickerManager; + /** + * The props used to validate a single date. + */ + dateValidationProps: ValidateDateProps; + /** + * The props used to validate the value. + */ + valueValidationProps: TValidationProps; + /** + * TODO: Write description. + * @param {TValue} value The value to get the reference date from. + * @returns {PickerValidDate | null} The initial visible date. + */ + getDateToUseForReferenceDate: (value: TValue) => PickerValidDate | null; + /** + * TODO: Write description. + * @param {GetNewValueFromNewSelectedDateParameters} parameters The parameters to get the new value from the new selected date. + * @returns {GetNewValueFromNewSelectedDateReturnValue} The new value and its change importance. + */ + getNewValueFromNewSelectedDate: ( + parameters: GetNewValueFromNewSelectedDateParameters, + ) => GetNewValueFromNewSelectedDateReturnValue; + /** + * TODO: Write description. + * @param {TValue} value The current value. + * @returns {PickerValidDate | null} The current date. + */ + getCurrentDateFromValue: (value: TValue) => PickerValidDate | null; + /** + * TODO: Write description. + * @param {TValue} value The current value. + * @returns {PickerValidDate[]} The selected dates. + */ + getSelectedDatesFromValue: (value: TValue) => PickerValidDate[]; + } + export interface ValueChangeHandlerContext { /** * The section handled by the UI that triggered the change. @@ -275,29 +316,51 @@ export namespace useBaseCalendarRoot { * The validation error associated to the new value. */ validationError: TError; + /** + * The importance of the change. + */ + changeImportance: PickerChangeImportance; } export interface RegisterSectionParameters { type: 'day' | 'month' | 'year'; value: PickerValidDate; } + + export interface GetNewValueFromNewSelectedDateParameters { + /** + * The value before the change. + */ + prevValue: TValue; + /** + * The date to select. + */ + selectedDate: PickerValidDate; + /** + * The reference date. + */ + referenceDate: PickerValidDate; + } + + export interface GetNewValueFromNewSelectedDateReturnValue { + /** + * The new value. + */ + value: TValue; + /** + * The importance of the change. + */ + changeImportance: PickerChangeImportance; + } } -export function useAddDefaultsToDateValidationProps( - validationDate: ExportedValidateDateProps, -): ValidateDateProps { +export function useAddDefaultsToBaseDateValidationProps( + validationDate: BaseDateValidationProps, +): Required { const utils = useUtils(); const defaultDates = useDefaultDates(); - const { - disablePast, - disableFuture, - minDate, - maxDate, - shouldDisableDate, - shouldDisableMonth, - shouldDisableYear, - } = validationDate; + const { disablePast, disableFuture, minDate, maxDate } = validationDate; return React.useMemo( () => ({ @@ -305,20 +368,7 @@ export function useAddDefaultsToDateValidationProps( disableFuture: disableFuture ?? false, minDate: applyDefaultDate(utils, minDate, defaultDates.minDate), maxDate: applyDefaultDate(utils, maxDate, defaultDates.maxDate), - shouldDisableDate, - shouldDisableMonth, - shouldDisableYear, }), - [ - disablePast, - disableFuture, - minDate, - maxDate, - shouldDisableDate, - shouldDisableMonth, - shouldDisableYear, - utils, - defaultDates, - ], + [disablePast, disableFuture, minDate, maxDate, utils, defaultDates], ); } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/useCalendarSetVisibleMonth.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-month/useBaseCalendarSetVisibleMonth.ts similarity index 66% rename from packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/useCalendarSetVisibleMonth.ts rename to packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-month/useBaseCalendarSetVisibleMonth.ts index a39598b6638f0..61bd50912ec60 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/useCalendarSetVisibleMonth.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-month/useBaseCalendarSetVisibleMonth.ts @@ -1,9 +1,11 @@ import * as React from 'react'; -import { PickerValidDate } from '../../../../models'; -import { GenericHTMLProps } from '../../base-utils/types'; -import { mergeReactProps } from '../../base-utils/mergeReactProps'; +import { PickerValidDate } from '../../../../../models'; +import { GenericHTMLProps } from '../../../base-utils/types'; +import { mergeReactProps } from '../../../base-utils/mergeReactProps'; -export function useCalendarSetVisibleMonth(parameters: useCalendarSetVisibleMonth.Parameters) { +export function useBaseCalendarSetVisibleMonth( + parameters: useBaseCalendarSetVisibleMonth.Parameters, +) { const { ctx } = parameters; const getSetVisibleMonthProps = React.useCallback( @@ -20,7 +22,7 @@ export function useCalendarSetVisibleMonth(parameters: useCalendarSetVisibleMont return React.useMemo(() => ({ getSetVisibleMonthProps }), [getSetVisibleMonthProps]); } -export namespace useCalendarSetVisibleMonth { +export namespace useBaseCalendarSetVisibleMonth { export interface Parameters { /** * The month to navigate to. diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-month/useBaseCalendarSetVisibleMonthWrapper.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-month/useBaseCalendarSetVisibleMonthWrapper.ts new file mode 100644 index 0000000000000..7f6424fda53cf --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-month/useBaseCalendarSetVisibleMonthWrapper.ts @@ -0,0 +1,83 @@ +import * as React from 'react'; +import useEventCallback from '@mui/utils/useEventCallback'; +import { useUtils } from '../../../../hooks/useUtils'; +import { getFirstEnabledMonth, getLastEnabledMonth } from '../utils/date'; +import { useBaseCalendarSetVisibleMonth } from './useBaseCalendarSetVisibleMonth'; +import { useBaseCalendarRootContext } from '../root/BaseCalendarRootContext'; + +export function useBaseCalendarSetVisibleMonthWrapper( + parameters: useBaseCalendarSetVisibleMonthWrapper.Parameters, +) { + const { target } = parameters; + const baseRootContext = useBaseCalendarRootContext(); + const utils = useUtils(); + + const targetDate = React.useMemo(() => { + if (target === 'previous') { + return utils.addMonths(baseRootContext.visibleDate, -baseRootContext.monthPageSize); + } + + if (target === 'next') { + return utils.addMonths(baseRootContext.visibleDate, baseRootContext.monthPageSize); + } + + return utils.setMonth(baseRootContext.visibleDate, utils.getMonth(target)); + }, [baseRootContext.visibleDate, baseRootContext.monthPageSize, utils, target]); + + const isDisabled = React.useMemo(() => { + if (baseRootContext.disabled) { + return true; + } + + // TODO: Check if the logic below works correctly when multiple months are rendered at once. + const isMovingBefore = utils.isBefore(targetDate, baseRootContext.visibleDate); + + // All the months before the visible ones are fully disabled, we skip the navigation. + if (isMovingBefore) { + return utils.isAfter( + getFirstEnabledMonth(utils, baseRootContext.dateValidationProps), + targetDate, + ); + } + + // All the months after the visible ones are fully disabled, we skip the navigation. + return utils.isBefore( + getLastEnabledMonth(utils, baseRootContext.dateValidationProps), + targetDate, + ); + }, [ + baseRootContext.disabled, + baseRootContext.dateValidationProps, + baseRootContext.visibleDate, + targetDate, + utils, + ]); + + const setTarget = useEventCallback(() => { + if (isDisabled) { + return; + } + baseRootContext.setVisibleDate(targetDate, false); + }); + + const ctx = React.useMemo( + () => ({ + setTarget, + isDisabled, + }), + [setTarget, isDisabled], + ); + + return { ctx }; +} + +export namespace useBaseCalendarSetVisibleMonthWrapper { + export interface Parameters extends Pick {} + + export interface ReturnValue { + /** + * The memoized context to forward to the memoized component so that it does not need to subscribe to any context. + */ + ctx: useBaseCalendarSetVisibleMonth.Context; + } +} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/useCalendarSetVisibleYear.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-year/useBaseCalendarSetVisibleYear.ts similarity index 66% rename from packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/useCalendarSetVisibleYear.ts rename to packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-year/useBaseCalendarSetVisibleYear.ts index f7f188792571f..f964025c27cbc 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/useCalendarSetVisibleYear.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-year/useBaseCalendarSetVisibleYear.ts @@ -1,9 +1,11 @@ import * as React from 'react'; -import { PickerValidDate } from '../../../../models'; -import { GenericHTMLProps } from '../../base-utils/types'; -import { mergeReactProps } from '../../base-utils/mergeReactProps'; +import { PickerValidDate } from '../../../../../models'; +import { GenericHTMLProps } from '../../../base-utils/types'; +import { mergeReactProps } from '../../../base-utils/mergeReactProps'; -export function useCalendarSetVisibleYear(parameters: useCalendarSetVisibleYear.Parameters) { +export function useBaseCalendarSetVisibleYear( + parameters: useBaseCalendarSetVisibleYear.Parameters, +) { const { ctx } = parameters; const getSetVisibleYearProps = React.useCallback( @@ -20,7 +22,7 @@ export function useCalendarSetVisibleYear(parameters: useCalendarSetVisibleYear. return React.useMemo(() => ({ getSetVisibleYearProps }), [getSetVisibleYearProps]); } -export namespace useCalendarSetVisibleYear { +export namespace useBaseCalendarSetVisibleYear { export interface Parameters { /** * The year to navigate to. diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-year/useBaseCalendarSetVisibleYearWrapper.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-year/useBaseCalendarSetVisibleYearWrapper.ts new file mode 100644 index 0000000000000..af4a6c61ee2b6 --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-year/useBaseCalendarSetVisibleYearWrapper.ts @@ -0,0 +1,82 @@ +import * as React from 'react'; +import useEventCallback from '@mui/utils/useEventCallback'; +import { useUtils } from '../../../../hooks/useUtils'; +import { useBaseCalendarSetVisibleYear } from './useBaseCalendarSetVisibleYear'; +import { useBaseCalendarRootContext } from '../root/BaseCalendarRootContext'; +import { getFirstEnabledYear, getLastEnabledYear } from '../utils/date'; + +export function useBaseCalendarSetVisibleYearWrapper( + parameters: useBaseCalendarSetVisibleYearWrapper.Parameters, +) { + const { target } = parameters; + const baseRootContext = useBaseCalendarRootContext(); + const utils = useUtils(); + + const targetDate = React.useMemo(() => { + if (target === 'previous') { + return utils.addYears(baseRootContext.visibleDate, -1); + } + + if (target === 'next') { + return utils.addYears(baseRootContext.visibleDate, 1); + } + + return utils.setYear(baseRootContext.visibleDate, utils.getYear(target)); + }, [baseRootContext.visibleDate, utils, target]); + + const isDisabled = React.useMemo(() => { + if (baseRootContext.disabled) { + return true; + } + + const isMovingBefore = utils.isBefore(targetDate, baseRootContext.visibleDate); + + // All the years before the visible ones are fully disabled, we skip the navigation. + if (isMovingBefore) { + return utils.isAfter( + getFirstEnabledYear(utils, baseRootContext.dateValidationProps), + targetDate, + ); + } + + // All the years after the visible ones are fully disabled, we skip the navigation. + return utils.isBefore( + getLastEnabledYear(utils, baseRootContext.dateValidationProps), + targetDate, + ); + }, [ + baseRootContext.disabled, + baseRootContext.dateValidationProps, + baseRootContext.visibleDate, + targetDate, + utils, + ]); + + const setTarget = useEventCallback(() => { + if (isDisabled) { + return; + } + baseRootContext.setVisibleDate(targetDate, false); + }); + + const ctx = React.useMemo( + () => ({ + setTarget, + isDisabled, + }), + [setTarget, isDisabled], + ); + + return { ctx }; +} + +export namespace useBaseCalendarSetVisibleYearWrapper { + export interface Parameters extends Pick {} + + export interface ReturnValue { + /** + * The memoized context to forward to the memoized component so that it does not need to subscribe to any context. + */ + ctx: useBaseCalendarSetVisibleYear.Context; + } +} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/date.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/date.ts similarity index 94% rename from packages/x-date-pickers/src/internals/base/Calendar/utils/date.ts rename to packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/date.ts index a2668a8514132..066bff5efa10f 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/utils/date.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/date.ts @@ -1,5 +1,5 @@ -import { MuiPickersAdapter, PickerValidDate } from '../../../../models'; -import { BaseDateValidationProps } from '../../../models/validation'; +import { MuiPickersAdapter, PickerValidDate } from '../../../../../models'; +import { BaseDateValidationProps } from '../../../../models/validation'; export function getFirstEnabledMonth( utils: MuiPickersAdapter, From e16ee9773df2a64c49268c0b3825c6d980dedb0d Mon Sep 17 00:00:00 2001 From: flavien Date: Thu, 9 Jan 2025 11:45:46 +0100 Subject: [PATCH 079/136] Basic range calendar working --- .../base-calendar/DayRangeCalendarDemo.tsx | 18 +-- .../base-calendar/calendar.module.css | 43 ++++-- .../DateRangeCalendar/DateRangeCalendar.tsx | 8 +- .../days-cell/RangeCalendarDaysCell.tsx | 127 ++++++++++++++++++ .../RangeCalendarDaysCellDataAttributes.ts | 30 +++++ .../days-cell/useRangeCalendarDaysCell.tsx | 17 +++ .../useRangeCalendarDaysCellWrapper.ts | 60 +++++++++ .../base/RangeCalendar/index.parts.ts | 2 +- .../root/RangeCalendarRootContext.ts | 2 +- .../Calendar/days-cell/CalendarDaysCell.tsx | 74 +--------- .../months-cell/CalendarMonthsCell.tsx | 4 +- .../Calendar/years-cell/CalendarYearsCell.tsx | 4 +- .../days-cell/useBaseCalendarDaysCell.ts} | 17 ++- .../useBaseCalendarDaysCellWrapper.ts | 93 +++++++++++++ .../useBaseCalendarDaysWeekRow.ts | 3 + .../useBaseCalendarDaysWeekRowWrapper.ts | 7 +- 16 files changed, 401 insertions(+), 108 deletions(-) create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/RangeCalendarDaysCell.tsx create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/RangeCalendarDaysCellDataAttributes.ts create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts rename packages/x-date-pickers/src/internals/base/{Calendar/days-cell/useCalendarDaysCell.ts => utils/base-calendar/days-cell/useBaseCalendarDaysCell.ts} (77%) create mode 100644 packages/x-date-pickers/src/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCellWrapper.ts diff --git a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx index d087c00972ddb..9d86f3c7a2bcc 100644 --- a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx +++ b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; -import { Dayjs } from 'dayjs'; +import clsx from 'clsx'; +import dayjs, { Dayjs } from 'dayjs'; import NoSsr from '@mui/material/NoSsr'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; @@ -57,21 +58,14 @@ function DayCalendar(props: Omit) { key={week.toString()} className={styles.DaysWeekRow} > - {/* {({ days }) => + {({ days }) => days.map((day) => ( )) - } */} - {({ days }) => - days.map((day) => ( - - )) } )) @@ -85,8 +79,8 @@ function DayCalendar(props: Omit) { export default function DayRangeCalendarDemo() { const [value, setValue] = React.useState<[Dayjs | null, Dayjs | null]>([ - null, - null, + dayjs('2025-01-03'), + dayjs('2025-01-07'), ]); const handleValueChange = React.useCallback( diff --git a/docs/data/date-pickers/base-calendar/calendar.module.css b/docs/data/date-pickers/base-calendar/calendar.module.css index 0dcac2ea30521..37522bee97c24 100644 --- a/docs/data/date-pickers/base-calendar/calendar.module.css +++ b/docs/data/date-pickers/base-calendar/calendar.module.css @@ -2,7 +2,7 @@ border: 1px solid #78716c; border-radius: 8px; width: 276px; - height: 300px; + height: 312px; display: flex; flex-direction: column; font-family: 'Roboto', 'Helvetica', 'Arial', sans-serif; @@ -141,19 +141,18 @@ .DaysGridBody { display: flex; flex-direction: column; - gap: 4px; + gap: 2px; } .DaysWeekRow, .DaysGridHeader { display: flex; justify-content: center; - gap: 4px; } .DaysWeekNumber { - height: 32px; - width: 32px; + height: 36px; + width: 36px; display: inline-flex; align-items: center; justify-content: center; @@ -162,8 +161,8 @@ } .DaysCell { - height: 32px; - width: 32px; + height: 36px; + width: 36px; border-radius: 4px; border: none; background-color: transparent; @@ -183,10 +182,20 @@ text-decoration: line-through; color: #64748b; } + + &.RangeDaysCell[data-selected]:not([data-selection-start]) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + + &.RangeDaysCell[data-selected]:not([data-selection-end]) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } } .DaysGridHeaderCell { - width: 32px; + width: 36px; text-align: center; font-size: 0.75rem; color: #64748b; @@ -216,6 +225,15 @@ &:not([data-outside-month])[data-selected] { background-color: #7dd3fc; + + &.RangeDaysCell { + background-color: #e0f2fe; + + &[data-selection-start], + &[data-selection-end] { + background-color: #7dd3fc; + } + } } &:focus-visible { @@ -240,7 +258,14 @@ } &:not([data-outside-month])[data-selected] { - background-color: #0369a1; + &.RangeDaysCell { + background-color: #0c4a6e; + + &[data-selection-start], + &[data-selection-end] { + background-color: #0369a1; + } + } } &[data-outside-month] { diff --git a/packages/x-date-pickers-pro/src/DateRangeCalendar/DateRangeCalendar.tsx b/packages/x-date-pickers-pro/src/DateRangeCalendar/DateRangeCalendar.tsx index 70d1b0e474c17..6234b3250310d 100644 --- a/packages/x-date-pickers-pro/src/DateRangeCalendar/DateRangeCalendar.tsx +++ b/packages/x-date-pickers-pro/src/DateRangeCalendar/DateRangeCalendar.tsx @@ -45,7 +45,7 @@ import { isEndOfRange, isRangeValid, isStartOfRange, - isWithinRange, + isDateWithinRange, } from '../internals/utils/date-utils'; import { calculateRangeChange, calculateRangePreview } from '../internals/utils/date-range-manager'; import { RangePosition } from '../models'; @@ -452,7 +452,7 @@ const DateRangeCalendar = React.forwardRef(function DateRangeCalendar( const handleDayMouseEnter = useEventCallback( (event: React.MouseEvent, newPreviewRequest: PickerValidDate) => { - if (!isWithinRange(utils, newPreviewRequest, valueDayRange)) { + if (!isDateWithinRange(utils, newPreviewRequest, valueDayRange)) { setRangePreviewDay(newPreviewRequest); } else { setRangePreviewDay(null); @@ -488,12 +488,12 @@ const DateRangeCalendar = React.forwardRef(function DateRangeCalendar( : isSelectedEndDate; return { - isPreviewing: shouldHavePreview ? isWithinRange(utils, day, previewingRange) : false, + isPreviewing: shouldHavePreview ? isDateWithinRange(utils, day, previewingRange) : false, isStartOfPreviewing: shouldHavePreview ? isStartOfRange(utils, day, previewingRange) : false, isEndOfPreviewing: shouldHavePreview ? isEndOfRange(utils, day, previewingRange) : false, - isHighlighting: isWithinRange(utils, day, isDragging ? draggingRange : valueDayRange), + isHighlighting: isDateWithinRange(utils, day, isDragging ? draggingRange : valueDayRange), isStartOfHighlighting, isEndOfHighlighting: isDragging ? isEndOfRange(utils, day, draggingRange) diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/RangeCalendarDaysCell.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/RangeCalendarDaysCell.tsx new file mode 100644 index 0000000000000..162cc4e8c41bf --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/RangeCalendarDaysCell.tsx @@ -0,0 +1,127 @@ +'use client'; +import * as React from 'react'; +// eslint-disable-next-line no-restricted-imports +import { BaseUIComponentProps } from '@mui/x-date-pickers/internals/base/base-utils/types'; +// eslint-disable-next-line no-restricted-imports +import { CustomStyleHookMapping } from '@mui/x-date-pickers/internals/base/base-utils/getStyleHookProps'; +// eslint-disable-next-line no-restricted-imports +import { useComponentRenderer } from '@mui/x-date-pickers/internals/base/base-utils/useComponentRenderer'; +import { RangeCalendarDaysCellDataAttributes } from './RangeCalendarDaysCellDataAttributes'; +import { useRangeCalendarDaysCell } from './useRangeCalendarDaysCell'; +import { useRangeCalendarDaysCellWrapper } from './useRangeCalendarDaysCellWrapper'; + +const customStyleHookMapping: CustomStyleHookMapping = { + selected(value) { + return value ? { [RangeCalendarDaysCellDataAttributes.selected]: '' } : null; + }, + selectionStart(value) { + return value ? { [RangeCalendarDaysCellDataAttributes.selectionStart]: '' } : null; + }, + selectionEnd(value) { + return value ? { [RangeCalendarDaysCellDataAttributes.selectionEnd]: '' } : null; + }, + disabled(value) { + return value ? { [RangeCalendarDaysCellDataAttributes.disabled]: '' } : null; + }, + current(value) { + return value ? { [RangeCalendarDaysCellDataAttributes.current]: '' } : null; + }, + outsideMonth(value) { + return value ? { [RangeCalendarDaysCellDataAttributes.outsideMonth]: '' } : null; + }, +}; + +const InnerRangeCalendarDaysCell = React.forwardRef(function RangeCalendarDaysGrid( + props: InnerRangeCalendarDaysCellProps, + forwardedRef: React.ForwardedRef, +) { + const { className, render, value, ctx, ...otherProps } = props; + const { getDaysCellProps, isCurrent } = useRangeCalendarDaysCell({ value, ctx }); + + const state: RangeCalendarDaysCell.State = React.useMemo( + () => ({ + selected: ctx.isSelected, + selectionStart: ctx.isSelectionStart, + selectionEnd: ctx.isSelectionEnd, + disabled: ctx.isDisabled, + invalid: ctx.isInvalid, + outsideMonth: ctx.isOutsideCurrentMonth, + current: isCurrent, + }), + [ + ctx.isSelected, + ctx.isSelectionStart, + ctx.isSelectionEnd, + ctx.isDisabled, + ctx.isInvalid, + ctx.isOutsideCurrentMonth, + isCurrent, + ], + ); + + const { renderElement } = useComponentRenderer({ + propGetter: getDaysCellProps, + render: render ?? 'button', + ref: forwardedRef, + className, + state, + extraProps: otherProps, + customStyleHookMapping, + }); + + return renderElement(); +}); + +const MemoizedInnerRangeCalendarDaysCell = React.memo(InnerRangeCalendarDaysCell); + +const RangeCalendarDaysCell = React.forwardRef(function RangeCalendarDaysCell( + props: RangeCalendarDaysCell.Props, + forwardedRef: React.ForwardedRef, +) { + const { ref, ctx } = useRangeCalendarDaysCellWrapper({ value: props.value, forwardedRef }); + + return ; +}); + +export namespace RangeCalendarDaysCell { + export interface State { + /** + * Whether the day is within the selected range. + */ + selected: boolean; + /** + * Whether the day is the first day of the selected range. + */ + selectionStart: boolean; + /** + * Whether the day is the last day of the selected range. + */ + selectionEnd: boolean; + /** + * Whether the day is disabled. + */ + disabled: boolean; + /** + * Whether the day is invalid. + */ + invalid: boolean; + /** + * Whether the day contains the current date. + */ + current: boolean; + /** + * Whether the day is outside the month rendered by the day grid wrapping it. + */ + outsideMonth: boolean; + } + + export interface Props + extends Omit, 'value'>, + Omit {} +} + +interface InnerRangeCalendarDaysCellProps + extends Omit, 'value'>, + useRangeCalendarDaysCell.Parameters {} + +export { RangeCalendarDaysCell }; diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/RangeCalendarDaysCellDataAttributes.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/RangeCalendarDaysCellDataAttributes.ts new file mode 100644 index 0000000000000..e6f4198fab77a --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/RangeCalendarDaysCellDataAttributes.ts @@ -0,0 +1,30 @@ +export enum RangeCalendarDaysCellDataAttributes { + /** + * Present when the day is within the selected range. + */ + selected = 'data-selected', + /** + * Present when the day is the first day of the selected range. + */ + selectionStart = 'data-selection-start', + /** + * Present when the day is the last day of the selected range. + */ + selectionEnd = 'data-selection-end', + /** + * Present when the day is disabled. + */ + disabled = 'data-disabled', + /** + * Present when the day is invalid. + */ + invalid = 'data-invalid', + /** + * Present when the day is the current date. + */ + current = 'data-current', + /** + * Present when the day is outside the month rendered by the day grid wrapping it. + */ + outsideMonth = 'data-outside-month', +} diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx new file mode 100644 index 0000000000000..510fb4fa9340e --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx @@ -0,0 +1,17 @@ +// eslint-disable-next-line no-restricted-imports +import { useBaseCalendarDaysCell } from '@mui/x-date-pickers/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCell'; + +export function useRangeCalendarDaysCell(parameters: useRangeCalendarDaysCell.Parameters) { + return useBaseCalendarDaysCell(parameters); +} + +export namespace useRangeCalendarDaysCell { + export interface Parameters extends Omit { + ctx: Context; + } + + export interface Context extends useBaseCalendarDaysCell.Context { + isSelectionStart: boolean; + isSelectionEnd: boolean; + } +} diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts new file mode 100644 index 0000000000000..115fab277531f --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts @@ -0,0 +1,60 @@ +import * as React from 'react'; +import { useUtils } from '@mui/x-date-pickers/internals'; +// eslint-disable-next-line no-restricted-imports +import { useBaseCalendarDaysCellWrapper } from '@mui/x-date-pickers/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCellWrapper'; +import type { useRangeCalendarDaysCell } from './useRangeCalendarDaysCell'; +import { useRangeCalendarRootContext } from '../root/RangeCalendarRootContext'; +import { + isWithinRange, + isStartOfRange, + isEndOfRange, + isRangeValid, +} from '../../../utils/date-utils'; + +export function useRangeCalendarDaysCellWrapper( + parameters: useRangeCalendarDaysCellWrapper.Parameters, +) { + const { value } = parameters; + const { ref, ctx: baseCtx } = useBaseCalendarDaysCellWrapper(parameters); + const utils = useUtils(); + const rangeRootContext = useRangeCalendarRootContext(); + + const isSelected = React.useMemo( + () => + isRangeValid(utils, rangeRootContext.value) + ? isWithinRange(utils, value, rangeRootContext.value) + : baseCtx.isSelected, + [utils, value, rangeRootContext.value, baseCtx.isSelected], + ); + + const isSelectionStart = React.useMemo( + () => + isRangeValid(utils, rangeRootContext.value) + ? isStartOfRange(utils, value, rangeRootContext.value) + : baseCtx.isSelected, + [utils, value, rangeRootContext.value, baseCtx.isSelected], + ); + + const isSelectionEnd = React.useMemo( + () => + isRangeValid(utils, rangeRootContext.value) + ? isEndOfRange(utils, value, rangeRootContext.value) + : baseCtx.isSelected, + [utils, value, rangeRootContext.value, baseCtx.isSelected], + ); + + const ctx = React.useMemo( + () => ({ ...baseCtx, isSelected, isSelectionStart, isSelectionEnd }), + [baseCtx, isSelected, isSelectionStart, isSelectionEnd], + ); + + return { ref, ctx }; +} + +export namespace useRangeCalendarDaysCellWrapper { + export interface Parameters extends useBaseCalendarDaysCellWrapper.Parameters {} + + export interface ReturnValue extends Omit { + ctx: useRangeCalendarDaysCell.Context; + } +} diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/index.parts.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/index.parts.ts index 1b32d29fb9799..921f1c9fcefe6 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/index.parts.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/index.parts.ts @@ -6,7 +6,7 @@ export { RangeCalendarDaysGridHeader as DaysGridHeader } from './days-grid-heade export { RangeCalendarDaysGridHeaderCell as DaysGridHeaderCell } from './days-grid-header-cell/RangeCalendarDaysGridHeaderCell'; export { RangeCalendarDaysGridBody as DaysGridBody } from './days-grid-body/RangeCalendarDaysGridBody'; export { RangeCalendarDaysWeekRow as DaysWeekRow } from './days-week-row/RangeCalendarDaysWeekRow'; -// export { CalendarDaysCell as DaysCell } from './days-cell/CalendarDaysCell'; +export { RangeCalendarDaysCell as DaysCell } from './days-cell/RangeCalendarDaysCell'; // // Months // export { CalendarMonthsList as MonthsList } from './months-list/CalendarMonthsList'; diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts index 6e0053a508ff0..3deb3a52997af 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts @@ -13,7 +13,7 @@ if (process.env.NODE_ENV !== 'production') { RangeCalendarRootContext.displayName = 'RangeCalendarRootContext'; } -export function useCalendarRootContext() { +export function useRangeCalendarRootContext() { const context = React.useContext(RangeCalendarRootContext); if (context === undefined) { throw new Error( diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx index 2ba096ecc48b2..3daf393185ea2 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx @@ -1,15 +1,10 @@ 'use client'; import * as React from 'react'; -import useForkRef from '@mui/utils/useForkRef'; -import { useUtils } from '../../../hooks/useUtils'; import { BaseUIComponentProps } from '../../base-utils/types'; import { useComponentRenderer } from '../../base-utils/useComponentRenderer'; -import { useBaseCalendarDaysGridContext } from '../../utils/base-calendar/days-grid/BaseCalendarDaysGridContext'; -import { useCalendarDaysCell } from './useCalendarDaysCell'; -import { useCalendarRootContext } from '../root/CalendarRootContext'; -import { useCompositeListItem } from '../../composite/list/useCompositeListItem'; +import { useBaseCalendarDaysCellWrapper } from '../../utils/base-calendar/days-cell/useBaseCalendarDaysCellWrapper'; +import { useBaseCalendarDaysCell } from '../../utils/base-calendar/days-cell/useBaseCalendarDaysCell'; import { CustomStyleHookMapping } from '../../base-utils/getStyleHookProps'; -import { useBaseCalendarRootContext } from '../../utils/base-calendar/root/BaseCalendarRootContext'; import { CalendarDaysCellDataAttributes } from './CalendarDaysCellDataAttributes'; const customStyleHookMapping: CustomStyleHookMapping = { @@ -32,7 +27,7 @@ const InnerCalendarDaysCell = React.forwardRef(function CalendarDaysGrid( forwardedRef: React.ForwardedRef, ) { const { className, render, value, ctx, ...otherProps } = props; - const { getDaysCellProps, isCurrent } = useCalendarDaysCell({ value, ctx }); + const { getDaysCellProps, isCurrent } = useBaseCalendarDaysCell({ value, ctx }); const state: CalendarDaysCell.State = React.useMemo( () => ({ @@ -64,64 +59,9 @@ const CalendarDaysCell = React.forwardRef(function CalendarDaysCell( props: CalendarDaysCell.Props, forwardedRef: React.ForwardedRef, ) { - const rootContext = useCalendarRootContext(); - const baseRootContext = useBaseCalendarRootContext(); - const baseDaysGridContext = useBaseCalendarDaysGridContext(); - const { ref: listItemRef, index: colIndex } = useCompositeListItem(); - const utils = useUtils(); - const mergedRef = useForkRef(forwardedRef, listItemRef); + const { ref, ctx } = useBaseCalendarDaysCellWrapper({ value: props.value, forwardedRef }); - const isSelected = React.useMemo( - () => (rootContext.value == null ? false : utils.isSameDay(rootContext.value, props.value)), - [rootContext.value, props.value, utils], - ); - - const isOutsideCurrentMonth = React.useMemo( - () => - baseDaysGridContext.currentMonth == null - ? false - : !utils.isSameMonth(baseDaysGridContext.currentMonth, props.value), - [baseDaysGridContext.currentMonth, props.value, utils], - ); - - const isDateInvalid = baseRootContext.isDateInvalid; - const isInvalid = React.useMemo(() => isDateInvalid(props.value), [props.value, isDateInvalid]); - - const isDisabled = React.useMemo(() => { - if (baseRootContext.disabled) { - return true; - } - - return isInvalid; - }, [baseRootContext.disabled, isInvalid]); - - const isTabbable = React.useMemo( - () => baseDaysGridContext.tabbableDays.some((day) => utils.isSameDay(day, props.value)), - [utils, baseDaysGridContext.tabbableDays, props.value], - ); - - const ctx = React.useMemo( - () => ({ - colIndex, - isSelected, - isDisabled, - isInvalid, - isTabbable, - isOutsideCurrentMonth, - selectDay: baseDaysGridContext.selectDay, - }), - [ - isSelected, - isDisabled, - isInvalid, - isTabbable, - isOutsideCurrentMonth, - baseDaysGridContext.selectDay, - colIndex, - ], - ); - - return ; + return ; }); export namespace CalendarDaysCell { @@ -150,11 +90,11 @@ export namespace CalendarDaysCell { export interface Props extends Omit, 'value'>, - Omit {} + Omit {} } interface InnerCalendarDaysCellProps extends Omit, 'value'>, - useCalendarDaysCell.Parameters {} + useBaseCalendarDaysCell.Parameters {} export { CalendarDaysCell }; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx index 0d5a17dc3d41b..ae18243a7a3ca 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx @@ -55,8 +55,8 @@ const CalendarMonthsCell = React.forwardRef(function CalendarMonthsCell( const mergedRef = useForkRef(forwardedRef, listItemRef); const isSelected = React.useMemo( - () => (rootContext.value == null ? false : utils.isSameMonth(rootContext.value, props.value)), - [rootContext.value, props.value, utils], + () => baseRootContext.selectedDates.some((date) => utils.isSameMonth(date, props.value)), + [baseRootContext.selectedDates, props.value, utils], ); const isInvalid = React.useMemo(() => { diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx index 2418baf4e26e9..ac600036c4592 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx @@ -55,8 +55,8 @@ const CalendarYearsCell = React.forwardRef(function CalendarsYearCell( const mergedRef = useForkRef(forwardedRef, listItemRef); const isSelected = React.useMemo( - () => (rootContext.value == null ? false : utils.isSameYear(rootContext.value, props.value)), - [rootContext.value, props.value, utils], + () => baseRootContext.selectedDates.some((date) => utils.isSameYear(date, props.value)), + [baseRootContext.selectedDates, props.value, utils], ); const isInvalid = React.useMemo(() => { diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-cell/useCalendarDaysCell.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCell.ts similarity index 77% rename from packages/x-date-pickers/src/internals/base/Calendar/days-cell/useCalendarDaysCell.ts rename to packages/x-date-pickers/src/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCell.ts index 3a9c02bec2205..e4fb2ab449e72 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-cell/useCalendarDaysCell.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCell.ts @@ -1,11 +1,11 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; -import { PickerValidDate } from '../../../../models'; -import { GenericHTMLProps } from '../../base-utils/types'; -import { mergeReactProps } from '../../base-utils/mergeReactProps'; -import { useUtils } from '../../../hooks/useUtils'; +import { PickerValidDate } from '../../../../../models'; +import { GenericHTMLProps } from '../../../base-utils/types'; +import { mergeReactProps } from '../../../base-utils/mergeReactProps'; +import { useUtils } from '../../../../hooks/useUtils'; -export function useCalendarDaysCell(parameters: useCalendarDaysCell.Parameters) { +export function useBaseCalendarDaysCell(parameters: useBaseCalendarDaysCell.Parameters) { const utils = useUtils(); const { value, format = utils.formats.dayOfMonth, ctx } = parameters; @@ -47,15 +47,18 @@ export function useCalendarDaysCell(parameters: useCalendarDaysCell.Parameters) return React.useMemo(() => ({ getDaysCellProps, isCurrent }), [getDaysCellProps, isCurrent]); } -export namespace useCalendarDaysCell { +export namespace useBaseCalendarDaysCell { export interface Parameters { + /** + * The date object representing the day. + */ value: PickerValidDate; /** * The format used to display the day. * @default utils.formats.dayOfMonth */ format?: string; - ctx: useCalendarDaysCell.Context; + ctx: useBaseCalendarDaysCell.Context; } export interface Context { diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCellWrapper.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCellWrapper.ts new file mode 100644 index 0000000000000..9b13c52503693 --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCellWrapper.ts @@ -0,0 +1,93 @@ +import * as React from 'react'; +import useForkRef from '@mui/utils/useForkRef'; +import { useUtils } from '../../../../hooks/useUtils'; +import { useCompositeListItem } from '../../../composite/list/useCompositeListItem'; +import { useBaseCalendarDaysGridContext } from '../days-grid/BaseCalendarDaysGridContext'; +import { useBaseCalendarRootContext } from '../root/BaseCalendarRootContext'; +import type { useBaseCalendarDaysCell } from './useBaseCalendarDaysCell'; + +export function useBaseCalendarDaysCellWrapper( + parameters: useBaseCalendarDaysCellWrapper.Parameters, +) { + const { forwardedRef, value } = parameters; + const baseRootContext = useBaseCalendarRootContext(); + const baseDaysGridContext = useBaseCalendarDaysGridContext(); + const { ref: listItemRef, index: colIndex } = useCompositeListItem(); + const utils = useUtils(); + const mergedRef = useForkRef(forwardedRef, listItemRef); + + const isSelected = React.useMemo( + () => baseRootContext.selectedDates.some((date) => utils.isSameDay(date, value)), + [baseRootContext.selectedDates, value, utils], + ); + + const isOutsideCurrentMonth = React.useMemo( + () => + baseDaysGridContext.currentMonth == null + ? false + : !utils.isSameMonth(baseDaysGridContext.currentMonth, value), + [baseDaysGridContext.currentMonth, value, utils], + ); + + const isDateInvalid = baseRootContext.isDateInvalid; + const isInvalid = React.useMemo(() => isDateInvalid(value), [value, isDateInvalid]); + + const isDisabled = React.useMemo(() => { + if (baseRootContext.disabled) { + return true; + } + + return isInvalid; + }, [baseRootContext.disabled, isInvalid]); + + const isTabbable = React.useMemo( + () => baseDaysGridContext.tabbableDays.some((day) => utils.isSameDay(day, value)), + [utils, baseDaysGridContext.tabbableDays, value], + ); + + const ctx = React.useMemo( + () => ({ + colIndex, + isSelected, + isDisabled, + isInvalid, + isTabbable, + isOutsideCurrentMonth, + selectDay: baseDaysGridContext.selectDay, + }), + [ + isSelected, + isDisabled, + isInvalid, + isTabbable, + isOutsideCurrentMonth, + baseDaysGridContext.selectDay, + colIndex, + ], + ); + + return { + ref: mergedRef, + ctx, + }; +} + +export namespace useBaseCalendarDaysCellWrapper { + export interface Parameters extends Pick { + /** + * The ref forwarded by the parent component. + */ + forwardedRef: React.ForwardedRef; + } + + export interface ReturnValue { + /** + * The ref to forward to the component. + */ + ref: React.RefObject; + /** + * The memoized context to forward to the memoized component so that it does not need to subscribe to any context. + */ + ctx: useBaseCalendarDaysCell.Context; + } +} diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRow.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRow.ts index b0079700a0a9e..a4bd257b421d0 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRow.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRow.ts @@ -34,6 +34,9 @@ export function useBaseCalendarDaysWeekRow(parameters: useBaseCalendarDaysWeekRo export namespace useBaseCalendarDaysWeekRow { export interface Parameters { + /** + * The date object representing the week. + */ value: PickerValidDate; ctx: useBaseCalendarDaysWeekRow.Context; children?: (parameters: ChildrenParameters) => React.ReactNode; diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRowWrapper.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRowWrapper.ts index 03e03c5417976..7cf0678a12415 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRowWrapper.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRowWrapper.ts @@ -1,6 +1,5 @@ import * as React from 'react'; import useForkRef from '@mui/utils/useForkRef'; -import { PickerValidDate } from '../../../../../models'; import { useCompositeListItem } from '../../../composite/list/useCompositeListItem'; import { useBaseCalendarDaysGridBodyContext } from '../days-grid-body/BaseCalendarDaysGridBodyContext'; import { useBaseCalendarDaysGridContext } from '../days-grid/BaseCalendarDaysGridContext'; @@ -34,9 +33,11 @@ export function useBaseCalendarDaysWeekRowWrapper( } export namespace useBaseCalendarDaysWeekRowWrapper { - export interface Parameters { + export interface Parameters extends Pick { + /** + * The ref forwarded by the parent component. + */ forwardedRef: React.ForwardedRef; - value: PickerValidDate; } export interface ReturnValue { From 3fcf1709e1a24b9622a091b336feef54c91ac4a9 Mon Sep 17 00:00:00 2001 From: flavien Date: Thu, 9 Jan 2025 12:47:29 +0100 Subject: [PATCH 080/136] Work on drag & drop --- .../base-calendar/calendar.module.css | 2 + .../DateRangeCalendar/DateRangeCalendar.tsx | 8 +- .../days-cell/useRangeCalendarDaysCell.tsx | 177 +++++++++++++++- .../useRangeCalendarDaysCellWrapper.ts | 34 +++- .../RangeCalendar/root/RangeCalendarRoot.tsx | 7 +- .../root/RangeCalendarRootDragContext.ts | 28 +++ .../root/useRangeCalendarRoot.tsx | 190 ++++++++++++++---- .../base/Calendar/root/useCalendarRoot.ts | 22 +- .../days-cell/useBaseCalendarDaysCell.ts | 28 ++- .../base-calendar/root/useBaseCalendarRoot.ts | 86 +++++--- 10 files changed, 475 insertions(+), 107 deletions(-) create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootDragContext.ts diff --git a/docs/data/date-pickers/base-calendar/calendar.module.css b/docs/data/date-pickers/base-calendar/calendar.module.css index 37522bee97c24..1d8c037c7a75c 100644 --- a/docs/data/date-pickers/base-calendar/calendar.module.css +++ b/docs/data/date-pickers/base-calendar/calendar.module.css @@ -258,6 +258,8 @@ } &:not([data-outside-month])[data-selected] { + background-color: #0369a1; + &.RangeDaysCell { background-color: #0c4a6e; diff --git a/packages/x-date-pickers-pro/src/DateRangeCalendar/DateRangeCalendar.tsx b/packages/x-date-pickers-pro/src/DateRangeCalendar/DateRangeCalendar.tsx index 6234b3250310d..70d1b0e474c17 100644 --- a/packages/x-date-pickers-pro/src/DateRangeCalendar/DateRangeCalendar.tsx +++ b/packages/x-date-pickers-pro/src/DateRangeCalendar/DateRangeCalendar.tsx @@ -45,7 +45,7 @@ import { isEndOfRange, isRangeValid, isStartOfRange, - isDateWithinRange, + isWithinRange, } from '../internals/utils/date-utils'; import { calculateRangeChange, calculateRangePreview } from '../internals/utils/date-range-manager'; import { RangePosition } from '../models'; @@ -452,7 +452,7 @@ const DateRangeCalendar = React.forwardRef(function DateRangeCalendar( const handleDayMouseEnter = useEventCallback( (event: React.MouseEvent, newPreviewRequest: PickerValidDate) => { - if (!isDateWithinRange(utils, newPreviewRequest, valueDayRange)) { + if (!isWithinRange(utils, newPreviewRequest, valueDayRange)) { setRangePreviewDay(newPreviewRequest); } else { setRangePreviewDay(null); @@ -488,12 +488,12 @@ const DateRangeCalendar = React.forwardRef(function DateRangeCalendar( : isSelectedEndDate; return { - isPreviewing: shouldHavePreview ? isDateWithinRange(utils, day, previewingRange) : false, + isPreviewing: shouldHavePreview ? isWithinRange(utils, day, previewingRange) : false, isStartOfPreviewing: shouldHavePreview ? isStartOfRange(utils, day, previewingRange) : false, isEndOfPreviewing: shouldHavePreview ? isEndOfRange(utils, day, previewingRange) : false, - isHighlighting: isDateWithinRange(utils, day, isDragging ? draggingRange : valueDayRange), + isHighlighting: isWithinRange(utils, day, isDragging ? draggingRange : valueDayRange), isStartOfHighlighting, isEndOfHighlighting: isDragging ? isEndOfRange(utils, day, draggingRange) diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx index 510fb4fa9340e..7a9cf4493cfdf 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx @@ -1,8 +1,178 @@ +import * as React from 'react'; +import { PickerValidDate } from '@mui/x-date-pickers/models'; +import useEventCallback from '@mui/utils/useEventCallback'; // eslint-disable-next-line no-restricted-imports import { useBaseCalendarDaysCell } from '@mui/x-date-pickers/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCell'; +// eslint-disable-next-line no-restricted-imports +import { GenericHTMLProps } from '@mui/x-date-pickers/internals/base/base-utils/types'; +// eslint-disable-next-line no-restricted-imports +import { mergeReactProps } from '@mui/x-date-pickers/internals/base/base-utils/mergeReactProps'; export function useRangeCalendarDaysCell(parameters: useRangeCalendarDaysCell.Parameters) { - return useBaseCalendarDaysCell(parameters); + const { ctx } = parameters; + + const onDragStart = useEventCallback((event: React.DragEvent) => { + event.stopPropagation(); + if (emptyDragImgRef.current) { + event.dataTransfer.setDragImage(emptyDragImgRef.current, 0, 0); + } + ctx.setDragTarget(newDate); + event.dataTransfer.effectAllowed = 'move'; + ctx.setIsDragging(true); + const buttonDataset = (event.target as HTMLButtonElement).dataset; + if (buttonDataset.timestamp) { + event.dataTransfer.setData('draggingDate', buttonDataset.timestamp); + } + if (buttonDataset.position) { + onDatePositionChange(buttonDataset.position as RangePosition); + } + }); + + const onTouchStart = useEventCallback(() => { + ctx.setDragTarget(newDate); + }); + + const onDragEnter = useEventCallback((event: React.DragEvent) => { + if (!ctx.isDraggingRef.current) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + event.dataTransfer.dropEffect = 'move'; + ctx.setDragTarget(resolveDateFromTarget(event.target, utils, timezone)); + }); + + const onDragMove = useEventCallback((event: React.TouchEvent) => { + const target = resolveElementFromTouch(event); + if (!target) { + return; + } + + const newDate = resolveDateFromTarget(target, utils, timezone); + if (newDate) { + ctx.setDragTarget(newDate); + } + + // this prevents initiating drag when user starts touchmove outside and then moves over a draggable element + const targetsAreIdentical = target === event.changedTouches[0].target; + if (!targetsAreIdentical || !isElementDraggable(newDate)) { + return; + } + + // on mobile we should only initialize dragging state after move is detected + ctx.setIsDragging(true); + + const button = event.target as HTMLButtonElement; + const buttonDataset = button.dataset; + if (buttonDataset.position) { + onDatePositionChange(buttonDataset.position as RangePosition); + } + }); + + const onDragLeave = useEventCallback((event: React.DragEvent) => { + if (!ctx.isDraggingRef.current) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + }); + + const onDragOver = useEventCallback((event: React.DragEvent) => { + if (!ctx.isDraggingRef.current) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + event.dataTransfer.dropEffect = 'move'; + }); + + const onTouchEnd = useEventCallback((event: React.TouchEvent) => { + if (!ctx.isDraggingRef.current) { + return; + } + + ctx.setDragTarget(null); + ctx.setIsDragging(false); + + const target = resolveElementFromTouch(event, true); + if (!target) { + return; + } + + // make sure the focused element is the element where touch ended + target.focus(); + const newDate = resolveDateFromTarget(target, utils, timezone); + if (newDate) { + ctx.selectDayFromDrag(newDate); + } + }); + + const onDragEnd = useEventCallback((event: React.DragEvent) => { + if (!ctx.isDraggingRef.current) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + ctx.setIsDragging(false); + ctx.setDragTarget(null); + }); + + const onDrop = useEventCallback((event: React.DragEvent) => { + if (!ctx.isDraggingRef.current) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + ctx.setIsDragging(false); + ctx.setDragTarget(null); + // make sure the focused element is the element where drop ended + event.currentTarget.focus(); + if (isSameAsDraggingDate(event)) { + return; + } + const newDate = resolveDateFromTarget(event.target, utils, timezone); + if (newDate) { + ctx.selectDayFromDrag(newDate); + } + }); + + const { baseProps, isCurrent } = useBaseCalendarDaysCell(parameters); + + const getDaysCellProps = React.useCallback( + (externalProps: GenericHTMLProps) => { + return mergeReactProps(externalProps, { + ...baseProps, + ...(ctx.isDraggable ? { draggable: true, onDragStart, onTouchStart } : {}), + onDragEnter, + onDragLeave, + onDragMove, + onDragOver, + onDragEnd, + onTouchEnd, + onDrop, + }); + }, + [ + baseProps, + ctx.isDraggable, + onDragStart, + onTouchStart, + onDragEnter, + onDragLeave, + onDragMove, + onDragOver, + onDragEnd, + onTouchEnd, + onDrop, + ], + ); + + return React.useMemo(() => ({ getDaysCellProps, isCurrent }), [getDaysCellProps, isCurrent]); } export namespace useRangeCalendarDaysCell { @@ -13,5 +183,10 @@ export namespace useRangeCalendarDaysCell { export interface Context extends useBaseCalendarDaysCell.Context { isSelectionStart: boolean; isSelectionEnd: boolean; + isDraggable: boolean; + isDraggingRef: React.RefObject; + selectDayFromDrag: (date: PickerValidDate) => void; + setIsDragging: (value: boolean) => void; + setDragTarget: (value: PickerValidDate | null) => void; } } diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts index 115fab277531f..24c6d730fd916 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts @@ -10,6 +10,7 @@ import { isEndOfRange, isRangeValid, } from '../../../utils/date-utils'; +import { useRangeCalendarRootDragContext } from '../root/RangeCalendarRootDragContext'; export function useRangeCalendarDaysCellWrapper( parameters: useRangeCalendarDaysCellWrapper.Parameters, @@ -18,6 +19,7 @@ export function useRangeCalendarDaysCellWrapper( const { ref, ctx: baseCtx } = useBaseCalendarDaysCellWrapper(parameters); const utils = useUtils(); const rangeRootContext = useRangeCalendarRootContext(); + const rangeRootDragContext = useRangeCalendarRootDragContext(); const isSelected = React.useMemo( () => @@ -43,9 +45,37 @@ export function useRangeCalendarDaysCellWrapper( [utils, value, rangeRootContext.value, baseCtx.isSelected], ); + const isDraggable = React.useMemo(() => { + return ( + !rangeRootDragContext.disableDragEditing && + (isStartOfRange(utils, value, rangeRootContext.value) || + isEndOfRange(utils, value, rangeRootContext.value)) + ); + }, [utils, value, rangeRootContext.value, rangeRootDragContext.disableDragEditing]); + const ctx = React.useMemo( - () => ({ ...baseCtx, isSelected, isSelectionStart, isSelectionEnd }), - [baseCtx, isSelected, isSelectionStart, isSelectionEnd], + () => ({ + ...baseCtx, + isSelected, + isSelectionStart, + isSelectionEnd, + isDraggable, + isDraggingRef: rangeRootDragContext.isDraggingRef, + selectDayFromDrag: rangeRootDragContext.selectDay, + setIsDragging: rangeRootDragContext.setIsDragging, + setDragTarget: rangeRootDragContext.setDragTarget, + }), + [ + baseCtx, + isSelected, + isSelectionStart, + isSelectionEnd, + isDraggable, + rangeRootDragContext.isDraggingRef, + rangeRootDragContext.selectDay, + rangeRootDragContext.setIsDragging, + rangeRootDragContext.setDragTarget, + ], ); return { ref, ctx }; diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRoot.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRoot.tsx index 3e225198b6b83..3a161e20d3ade 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRoot.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRoot.tsx @@ -11,6 +11,7 @@ import { useComponentRenderer } from '@mui/x-date-pickers/internals/base/base-ut import { DateRangeValidationError } from '../../../../models'; import { RangeCalendarRootContext } from './RangeCalendarRootContext'; import { useRangeCalendarRoot } from './useRangeCalendarRoot'; +import { RangeCalendarRootDragContext } from './RangeCalendarRootDragContext'; const RangeCalendarRoot = React.forwardRef(function RangeCalendarRoot( props: RangeCalendarRoot.Props, @@ -37,7 +38,7 @@ const RangeCalendarRoot = React.forwardRef(function RangeCalendarRoot( maxDate, ...otherProps } = props; - const { getRootProps, context, baseContext } = useRangeCalendarRoot({ + const { getRootProps, context, baseContext, dragContext } = useRangeCalendarRoot({ readOnly, disabled, autoFocus, @@ -70,7 +71,9 @@ const RangeCalendarRoot = React.forwardRef(function RangeCalendarRoot( return ( - {renderElement()} + + {renderElement()} + ); diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootDragContext.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootDragContext.ts new file mode 100644 index 0000000000000..6bf8d08ee13a6 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootDragContext.ts @@ -0,0 +1,28 @@ +import * as React from 'react'; +import { PickerValidDate } from '@mui/x-date-pickers/models'; + +export interface RangeCalendarRootDragContext { + isDraggingRef: React.RefObject; + disableDragEditing: boolean; + selectDay: (value: PickerValidDate) => void; + setIsDragging: (value: boolean) => void; + setDragTarget: (value: PickerValidDate | null) => void; +} + +export const RangeCalendarRootDragContext = React.createContext< + RangeCalendarRootDragContext | undefined +>(undefined); + +if (process.env.NODE_ENV !== 'production') { + RangeCalendarRootDragContext.displayName = 'RangeCalendarRootDragContext'; +} + +export function useRangeCalendarRootDragContext() { + const context = React.useContext(RangeCalendarRootDragContext); + if (context === undefined) { + throw new Error( + 'Base UI X: RangeCalendarRootDragContext is missing. Range Calendar parts must be placed within .', + ); + } + return context; +} diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx index 9603f9d16ac63..8d98e43e25982 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx @@ -1,4 +1,6 @@ import * as React from 'react'; +import useEnhancedEffect from '@mui/utils/useEnhancedEffect'; +import useEventCallback from '@mui/utils/useEventCallback'; import { PickerValidDate } from '@mui/x-date-pickers/models'; import { ValidateDateProps } from '@mui/x-date-pickers/validation'; import { PickerRangeValue, RangePosition, useUtils } from '@mui/x-date-pickers/internals'; @@ -8,6 +10,8 @@ import { useBaseCalendarRoot, } from '@mui/x-date-pickers/internals/base/utils/base-calendar/root/useBaseCalendarRoot'; // eslint-disable-next-line no-restricted-imports +import { BaseCalendarRootContext } from '@mui/x-date-pickers/internals/base/utils/base-calendar/root/BaseCalendarRootContext'; +// eslint-disable-next-line no-restricted-imports import { mergeReactProps } from '@mui/x-date-pickers/internals/base/base-utils/mergeReactProps'; // eslint-disable-next-line no-restricted-imports import { GenericHTMLProps } from '@mui/x-date-pickers/internals/base/base-utils/types'; @@ -21,6 +25,9 @@ import { calculateRangeChange } from '../../../utils/date-range-manager'; import { isRangeValid } from '../../../utils/date-utils'; import { useRangePosition, UseRangePositionProps } from '../../../hooks/useRangePosition'; import { RangeCalendarRootContext } from './RangeCalendarRootContext'; +import { RangeCalendarRootDragContext } from './RangeCalendarRootDragContext'; + +const DEFAULT_AVAILABLE_RANGE_POSITIONS: RangePosition[] = ['start', 'end']; export function useRangeCalendarRoot(parameters: useRangeCalendarRoot.Parameters) { const { @@ -34,14 +41,15 @@ export function useRangeCalendarRoot(parameters: useRangeCalendarRoot.Parameters rangePosition: rangePositionProp, defaultRangePosition: defaultRangePositionProp, onRangePositionChange: onRangePositionChangeProp, + availableRangePositions = DEFAULT_AVAILABLE_RANGE_POSITIONS, + // Other range-specific parameters + disableDragEditing = false, // Parameters forwarded to `useBaseCalendarRoot` ...baseParameters } = parameters; const utils = useUtils(); const manager = useDateRangeManager(); - const availableRangePositions: RangePosition[] = ['start', 'end']; - // TODO: Add support for range position from the context when implementing the Picker Base UI X component. const { rangePosition, onRangePositionChange } = useRangePosition({ rangePosition: rangePositionProp, @@ -56,14 +64,6 @@ export function useRangeCalendarRoot(parameters: useRangeCalendarRoot.Parameters disableFuture, }); - const valueValidationProps = React.useMemo( - () => ({ - ...baseDateValidationProps, - shouldDisableDate, - }), - [baseDateValidationProps, shouldDisableDate], - ); - const shouldDisableDateForSingleDateValidation = React.useMemo(() => { if (!shouldDisableDate) { return undefined; @@ -82,39 +82,62 @@ export function useRangeCalendarRoot(parameters: useRangeCalendarRoot.Parameters [baseDateValidationProps, shouldDisableDateForSingleDateValidation], ); - const getNewValueFromNewSelectedDate = ({ - prevValue, - selectedDate, - referenceDate, - allowRangeFlip, - }: useBaseCalendarRoot.GetNewValueFromNewSelectedDateParameters & { - allowRangeFlip?: boolean; - }): useBaseCalendarRoot.GetNewValueFromNewSelectedDateReturnValue => { - const { nextSelection, newRange } = calculateRangeChange({ - newDate: selectedDate, - utils, - range: prevValue, - rangePosition, - allowRangeFlip, - shouldMergeDateAndTime: true, + const valueValidationProps = React.useMemo( + () => ({ + ...baseDateValidationProps, + shouldDisableDate, + }), + [baseDateValidationProps, shouldDisableDate], + ); + + const getNewValueFromNewSelectedDate = useEventCallback( + ({ + prevValue, + selectedDate, referenceDate, - }); + allowRangeFlip, + }: useBaseCalendarRoot.GetNewValueFromNewSelectedDateParameters & { + allowRangeFlip?: boolean; + }): useBaseCalendarRoot.GetNewValueFromNewSelectedDateReturnValue => { + const { nextSelection, newRange } = calculateRangeChange({ + newDate: selectedDate, + utils, + range: prevValue, + rangePosition, + allowRangeFlip, + shouldMergeDateAndTime: true, + referenceDate, + }); - const isNextSectionAvailable = availableRangePositions.includes(nextSelection); - if (isNextSectionAvailable) { - onRangePositionChange(nextSelection); - } + const isNextSectionAvailable = availableRangePositions.includes(nextSelection); + if (isNextSectionAvailable) { + onRangePositionChange(nextSelection); + } - const isFullRangeSelected = rangePosition === 'end' && isRangeValid(utils, newRange); + const isFullRangeSelected = rangePosition === 'end' && isRangeValid(utils, newRange); + return { + value: newRange, + changeImportance: isFullRangeSelected || !isNextSectionAvailable ? 'set' : 'accept', + }; + }, + ); + + const calendarValueManager = React.useMemo< + useBaseCalendarRoot.ValueManager + >(() => { return { - value: newRange, - changeImportance: isFullRangeSelected || !isNextSectionAvailable ? 'set' : 'accept', + getDateToUseForReferenceDate: (value) => value[0] ?? value[1], + getCurrentDateFromValue: (value) => (rangePosition === 'start' ? value[0] : value[1]), + getNewValueFromNewSelectedDate, + getSelectedDatesFromValue: (value) => value.filter((date) => date != null), }; - }; + }, [rangePosition, getNewValueFromNewSelectedDate]); const { value, + referenceDate, + setValue, setVisibleDate, isDateCellVisible, context: baseContext, @@ -123,13 +146,19 @@ export function useRangeCalendarRoot(parameters: useRangeCalendarRoot.Parameters manager, valueValidationProps, dateValidationProps, - getDateToUseForReferenceDate: (initialValue) => initialValue[0] ?? initialValue[1], - getCurrentDateFromValue: (currentValue) => - rangePosition === 'start' ? currentValue[0] : currentValue[1], - getNewValueFromNewSelectedDate, - getSelectedDatesFromValue, + calendarValueManager, }); + // // Range going for the start of the start day to the end of the end day. + // // This makes sure that `isWithinRange` works with any time in the start and end day. + // const valueDayRange = React.useMemo( + // () => [ + // !utils.isValid(value[0]) ? value[0] : utils.startOfDay(value[0]), + // !utils.isValid(value[1]) ? value[1] : utils.endOfDay(value[1]), + // ], + // [value, utils], + // ); + // TODO: Apply some logic based on the range position. const [prevValue, setPrevValue] = React.useState(value); if (value !== prevValue) { @@ -145,6 +174,15 @@ export function useRangeCalendarRoot(parameters: useRangeCalendarRoot.Parameters } } + const dragContext = useRangeCalendarDragEditing({ + baseContext, + setValue, + value, + referenceDate, + disableDragEditing, + getNewValueFromNewSelectedDate, + }); + const context: RangeCalendarRootContext = React.useMemo( () => ({ value, @@ -157,8 +195,8 @@ export function useRangeCalendarRoot(parameters: useRangeCalendarRoot.Parameters }, []); return React.useMemo( - () => ({ getRootProps, context, baseContext }), - [getRootProps, context, baseContext], + () => ({ getRootProps, context, baseContext, dragContext }), + [getRootProps, context, baseContext, dragContext], ); } @@ -166,9 +204,73 @@ export namespace useRangeCalendarRoot { export interface Parameters extends useBaseCalendarRoot.PublicParameters, ExportedValidateDateRangeProps, - UseRangePositionProps {} + UseRangePositionProps { + /** + * Range positions available for selection. + * This list is checked against when checking if a next range position can be selected. + * + * Used on Date Time Range pickers with current `rangePosition` to force a `finish` selection after just one range position selection. + * @default ['start', 'end'] + */ + availableRangePositions?: RangePosition[]; + /** + * If `true`, editing dates by dragging is disabled. + * @default false + */ + disableDragEditing?: boolean; + } +} + +function useRangeCalendarDragEditing( + parameters: UseRangeCalendarDragEditingParameters, +): RangeCalendarRootDragContext { + const { + value, + setValue, + referenceDate, + baseContext, + getNewValueFromNewSelectedDate, + disableDragEditing: disableDragEditingProp, + } = parameters; + const [isDragging, setIsDragging] = React.useState(false); + const [dragTarget, setDragTarget] = React.useState(null); + + const selectDay = useEventCallback((selectedDate: PickerValidDate) => { + const response = getNewValueFromNewSelectedDate({ + prevValue: value, + selectedDate, + referenceDate, + allowRangeFlip: true, + }); + + setValue(response.value, { changeImportance: response.changeImportance, section: 'day' }); + }); + + const disableDragEditing = disableDragEditingProp || baseContext.disabled || baseContext.readOnly; + + const isDraggingRef = React.useRef(isDragging); + useEnhancedEffect(() => { + isDraggingRef.current = isDragging; + }); + + return { + disableDragEditing, + isDraggingRef, + selectDay, + setIsDragging, + setDragTarget, + }; } -function getSelectedDatesFromValue(value: PickerRangeValue): PickerValidDate[] { - return value.filter((date) => date != null); +interface UseRangeCalendarDragEditingParameters { + value: PickerRangeValue; + referenceDate: PickerValidDate; + setValue: useBaseCalendarRoot.ReturnValue['setValue']; + baseContext: BaseCalendarRootContext; + disableDragEditing: boolean; + getNewValueFromNewSelectedDate: ( + parameters: useBaseCalendarRoot.GetNewValueFromNewSelectedDateParameters & { + allowRangeFlip?: boolean; + }, + ) => useBaseCalendarRoot.GetNewValueFromNewSelectedDateReturnValue; } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts index 1a13890e008e1..bc568330ae995 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts @@ -12,6 +12,16 @@ import { useBaseCalendarRoot, } from '../../utils/base-calendar/root/useBaseCalendarRoot'; +const calendarValueManager: useBaseCalendarRoot.ValueManager = { + getDateToUseForReferenceDate: (value) => value, + getNewValueFromNewSelectedDate: ({ selectedDate }) => ({ + value: selectedDate, + changeImportance: 'accept', + }), + getCurrentDateFromValue: (value) => value, + getSelectedDatesFromValue: (value) => (value == null ? [] : [value]), +}; + export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { const { // Validation props @@ -55,13 +65,7 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { manager, dateValidationProps: validationProps, valueValidationProps: validationProps, - getDateToUseForReferenceDate: (initialValue) => initialValue, - getNewValueFromNewSelectedDate: ({ selectedDate }) => ({ - value: selectedDate, - changeImportance: 'accept', - }), - getCurrentDateFromValue: (currentValue) => currentValue, - getSelectedDatesFromValue, + calendarValueManager, }); const [prevValue, setPrevValue] = React.useState(value); @@ -94,7 +98,3 @@ export namespace useCalendarRoot { extends useBaseCalendarRoot.PublicParameters, ExportedValidateDateProps {} } - -function getSelectedDatesFromValue(value: PickerValue): PickerValidDate[] { - return value == null ? [] : [value]; -} diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCell.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCell.ts index e4fb2ab449e72..fd24229d0012c 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCell.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCell.ts @@ -1,8 +1,6 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; import { PickerValidDate } from '../../../../../models'; -import { GenericHTMLProps } from '../../../base-utils/types'; -import { mergeReactProps } from '../../../base-utils/mergeReactProps'; import { useUtils } from '../../../../hooks/useUtils'; export function useBaseCalendarDaysCell(parameters: useBaseCalendarDaysCell.Parameters) { @@ -20,19 +18,17 @@ export function useBaseCalendarDaysCell(parameters: useBaseCalendarDaysCell.Para ctx.selectDay(value); }); - const getDaysCellProps = React.useCallback( - (externalProps: GenericHTMLProps) => { - return mergeReactProps(externalProps, { - role: 'gridcell', - 'aria-selected': ctx.isSelected, - 'aria-current': isCurrent ? 'date' : undefined, - 'aria-colindex': ctx.colIndex + 1, - children: formattedValue, - disabled: ctx.isDisabled, - tabIndex: ctx.isTabbable ? 0 : -1, - onClick, - }); - }, + const baseProps = React.useMemo( + () => ({ + role: 'gridcell', + 'aria-selected': ctx.isSelected, + 'aria-current': isCurrent ? 'date' : undefined, + 'aria-colindex': ctx.colIndex + 1, + children: formattedValue, + disabled: ctx.isDisabled, + tabIndex: ctx.isTabbable ? 0 : -1, + onClick, + }), [ formattedValue, ctx.isSelected, @@ -44,7 +40,7 @@ export function useBaseCalendarDaysCell(parameters: useBaseCalendarDaysCell.Para ], ); - return React.useMemo(() => ({ getDaysCellProps, isCurrent }), [getDaysCellProps, isCurrent]); + return React.useMemo(() => ({ baseProps, isCurrent }), [baseProps, isCurrent]); } export namespace useBaseCalendarDaysCell { diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarRoot.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarRoot.ts index bab74a0eaab9e..57c7987000a69 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarRoot.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarRoot.ts @@ -37,12 +37,14 @@ export function useBaseCalendarRoot< monthPageSize = 1, yearPageSize = 1, manager, - getDateToUseForReferenceDate, - getNewValueFromNewSelectedDate, - getCurrentDateFromValue, - getSelectedDatesFromValue, dateValidationProps, valueValidationProps, + calendarValueManager: { + getDateToUseForReferenceDate, + getNewValueFromNewSelectedDate, + getCurrentDateFromValue, + getSelectedDatesFromValue, + }, } = parameters; const utils = useUtils(); @@ -142,6 +144,19 @@ export function useBaseCalendarRoot< [adapter, dateValidationProps, timezone], ); + const setValue = useEventCallback( + ( + newValue: TValue, + options: { section: 'day' | 'month' | 'year'; changeImportance: 'set' | 'accept' }, + ) => { + handleValueChange(newValue, { + section: options.section, + changeImportance: options.changeImportance, + validationError: getValidationErrorForNewValue(newValue), + }); + }, + ); + const selectDate = useEventCallback( (selectedDate: PickerValidDate, options) => { const response = getNewValueFromNewSelectedDate({ @@ -150,10 +165,9 @@ export function useBaseCalendarRoot< referenceDate, }); - handleValueChange(response.value, { - changeImportance: response.changeImportance, - validationError: getValidationErrorForNewValue(response.value), + return setValue(response.value, { section: options.section, + changeImportance: response.changeImportance, }); }, ); @@ -206,6 +220,8 @@ export function useBaseCalendarRoot< return { value, + referenceDate, + setValue, setVisibleDate, isDateCellVisible, context, @@ -279,6 +295,42 @@ export namespace useBaseCalendarRoot { * The props used to validate the value. */ valueValidationProps: TValidationProps; + calendarValueManager: ValueManager; + } + + export interface ReturnValue { + value: TValue; + referenceDate: PickerValidDate; + setValue: ( + newValue: TValue, + options: { section: 'day' | 'month' | 'year'; changeImportance: 'set' | 'accept' }, + ) => void; + setVisibleDate: (newVisibleDate: PickerValidDate, skipIfAlreadyVisible: boolean) => void; + isDateCellVisible: (date: PickerValidDate) => boolean; + context: BaseCalendarRootContext; + } + + export interface ValueChangeHandlerContext { + /** + * The section handled by the UI that triggered the change. + */ + section: 'day' | 'month' | 'year'; + /** + * The validation error associated to the new value. + */ + validationError: TError; + /** + * The importance of the change. + */ + changeImportance: PickerChangeImportance; + } + + export interface RegisterSectionParameters { + type: 'day' | 'month' | 'year'; + value: PickerValidDate; + } + + export interface ValueManager { /** * TODO: Write description. * @param {TValue} value The value to get the reference date from. @@ -307,26 +359,6 @@ export namespace useBaseCalendarRoot { getSelectedDatesFromValue: (value: TValue) => PickerValidDate[]; } - export interface ValueChangeHandlerContext { - /** - * The section handled by the UI that triggered the change. - */ - section: 'day' | 'month' | 'year'; - /** - * The validation error associated to the new value. - */ - validationError: TError; - /** - * The importance of the change. - */ - changeImportance: PickerChangeImportance; - } - - export interface RegisterSectionParameters { - type: 'day' | 'month' | 'year'; - value: PickerValidDate; - } - export interface GetNewValueFromNewSelectedDateParameters { /** * The value before the change. From f1b72a9eb1078fecfef34138c8150bbc9829395f Mon Sep 17 00:00:00 2001 From: flavien Date: Thu, 9 Jan 2025 14:00:35 +0100 Subject: [PATCH 081/136] Work --- .../days-cell/useRangeCalendarDaysCell.tsx | 78 ++++++++----------- .../useRangeCalendarDaysCellWrapper.ts | 22 ++---- .../root/RangeCalendarRootDragContext.ts | 7 +- .../root/useRangeCalendarRoot.tsx | 60 +++++++++++--- 4 files changed, 97 insertions(+), 70 deletions(-) diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx index 7a9cf4493cfdf..e0ca6a86721b6 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import { PickerValidDate } from '@mui/x-date-pickers/models'; +import { RangePosition } from '@mui/x-date-pickers/internals'; import useEventCallback from '@mui/utils/useEventCallback'; // eslint-disable-next-line no-restricted-imports import { useBaseCalendarDaysCell } from '@mui/x-date-pickers/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCell'; @@ -7,29 +8,17 @@ import { useBaseCalendarDaysCell } from '@mui/x-date-pickers/internals/base/util import { GenericHTMLProps } from '@mui/x-date-pickers/internals/base/base-utils/types'; // eslint-disable-next-line no-restricted-imports import { mergeReactProps } from '@mui/x-date-pickers/internals/base/base-utils/mergeReactProps'; +import type { RangeCalendarRootDragContext } from '../root/RangeCalendarRootDragContext'; export function useRangeCalendarDaysCell(parameters: useRangeCalendarDaysCell.Parameters) { - const { ctx } = parameters; + const { ctx, value } = parameters; - const onDragStart = useEventCallback((event: React.DragEvent) => { - event.stopPropagation(); - if (emptyDragImgRef.current) { - event.dataTransfer.setDragImage(emptyDragImgRef.current, 0, 0); - } - ctx.setDragTarget(newDate); - event.dataTransfer.effectAllowed = 'move'; - ctx.setIsDragging(true); - const buttonDataset = (event.target as HTMLButtonElement).dataset; - if (buttonDataset.timestamp) { - event.dataTransfer.setData('draggingDate', buttonDataset.timestamp); - } - if (buttonDataset.position) { - onDatePositionChange(buttonDataset.position as RangePosition); - } - }); + const startDragging = () => { + ctx.startDragging(ctx.isSelectionStart ? 'start' : 'end'); + }; const onTouchStart = useEventCallback(() => { - ctx.setDragTarget(newDate); + ctx.setDragTarget(value); }); const onDragEnter = useEventCallback((event: React.DragEvent) => { @@ -40,10 +29,10 @@ export function useRangeCalendarDaysCell(parameters: useRangeCalendarDaysCell.Pa event.preventDefault(); event.stopPropagation(); event.dataTransfer.dropEffect = 'move'; - ctx.setDragTarget(resolveDateFromTarget(event.target, utils, timezone)); + ctx.setDragTarget(parameters.value); }); - const onDragMove = useEventCallback((event: React.TouchEvent) => { + const onTouchMove = useEventCallback((event: React.TouchEvent) => { const target = resolveElementFromTouch(event); if (!target) { return; @@ -61,13 +50,7 @@ export function useRangeCalendarDaysCell(parameters: useRangeCalendarDaysCell.Pa } // on mobile we should only initialize dragging state after move is detected - ctx.setIsDragging(true); - - const button = event.target as HTMLButtonElement; - const buttonDataset = button.dataset; - if (buttonDataset.position) { - onDatePositionChange(buttonDataset.position as RangePosition); - } + startDragging(); }); const onDragLeave = useEventCallback((event: React.DragEvent) => { @@ -95,7 +78,7 @@ export function useRangeCalendarDaysCell(parameters: useRangeCalendarDaysCell.Pa } ctx.setDragTarget(null); - ctx.setIsDragging(false); + ctx.stopDragging(); const target = resolveElementFromTouch(event, true); if (!target) { @@ -117,7 +100,7 @@ export function useRangeCalendarDaysCell(parameters: useRangeCalendarDaysCell.Pa event.preventDefault(); event.stopPropagation(); - ctx.setIsDragging(false); + ctx.stopDragging(); ctx.setDragTarget(null); }); @@ -128,17 +111,14 @@ export function useRangeCalendarDaysCell(parameters: useRangeCalendarDaysCell.Pa event.preventDefault(); event.stopPropagation(); - ctx.setIsDragging(false); + ctx.stopDragging(); ctx.setDragTarget(null); // make sure the focused element is the element where drop ended event.currentTarget.focus(); if (isSameAsDraggingDate(event)) { return; } - const newDate = resolveDateFromTarget(event.target, utils, timezone); - if (newDate) { - ctx.selectDayFromDrag(newDate); - } + ctx.selectDayFromDrag(value); }); const { baseProps, isCurrent } = useBaseCalendarDaysCell(parameters); @@ -147,26 +127,29 @@ export function useRangeCalendarDaysCell(parameters: useRangeCalendarDaysCell.Pa (externalProps: GenericHTMLProps) => { return mergeReactProps(externalProps, { ...baseProps, - ...(ctx.isDraggable ? { draggable: true, onDragStart, onTouchStart } : {}), + ...(ctx.isSelectionStart || ctx.isSelectionEnd ? { draggable: true } : {}), + onDragStart, onDragEnter, onDragLeave, - onDragMove, onDragOver, onDragEnd, + onTouchStart, + onTouchMove, onTouchEnd, onDrop, }); }, [ baseProps, - ctx.isDraggable, + ctx.isSelectionStart, + ctx.isSelectionEnd, onDragStart, - onTouchStart, onDragEnter, onDragLeave, - onDragMove, onDragOver, onDragEnd, + onTouchStart, + onTouchMove, onTouchEnd, onDrop, ], @@ -180,13 +163,18 @@ export namespace useRangeCalendarDaysCell { ctx: Context; } - export interface Context extends useBaseCalendarDaysCell.Context { + export interface Context + extends useBaseCalendarDaysCell.Context, + Pick< + RangeCalendarRootDragContext, + | 'isDraggingRef' + | 'selectDayFromDrag' + | 'startDragging' + | 'stopDragging' + | 'setDragTarget' + | 'emptyDragImgRef' + > { isSelectionStart: boolean; isSelectionEnd: boolean; - isDraggable: boolean; - isDraggingRef: React.RefObject; - selectDayFromDrag: (date: PickerValidDate) => void; - setIsDragging: (value: boolean) => void; - setDragTarget: (value: PickerValidDate | null) => void; } } diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts index 24c6d730fd916..c5d337a6eef86 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts @@ -45,36 +45,30 @@ export function useRangeCalendarDaysCellWrapper( [utils, value, rangeRootContext.value, baseCtx.isSelected], ); - const isDraggable = React.useMemo(() => { - return ( - !rangeRootDragContext.disableDragEditing && - (isStartOfRange(utils, value, rangeRootContext.value) || - isEndOfRange(utils, value, rangeRootContext.value)) - ); - }, [utils, value, rangeRootContext.value, rangeRootDragContext.disableDragEditing]); - const ctx = React.useMemo( () => ({ ...baseCtx, isSelected, isSelectionStart, isSelectionEnd, - isDraggable, isDraggingRef: rangeRootDragContext.isDraggingRef, - selectDayFromDrag: rangeRootDragContext.selectDay, - setIsDragging: rangeRootDragContext.setIsDragging, + selectDayFromDrag: rangeRootDragContext.selectDayFromDrag, + startDragging: rangeRootDragContext.startDragging, + stopDragging: rangeRootDragContext.stopDragging, setDragTarget: rangeRootDragContext.setDragTarget, + emptyDragImgRef: rangeRootDragContext.emptyDragImgRef, }), [ baseCtx, isSelected, isSelectionStart, isSelectionEnd, - isDraggable, rangeRootDragContext.isDraggingRef, - rangeRootDragContext.selectDay, - rangeRootDragContext.setIsDragging, + rangeRootDragContext.selectDayFromDrag, + rangeRootDragContext.startDragging, + rangeRootDragContext.stopDragging, rangeRootDragContext.setDragTarget, + rangeRootDragContext.emptyDragImgRef, ], ); diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootDragContext.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootDragContext.ts index 6bf8d08ee13a6..786478462cb09 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootDragContext.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootDragContext.ts @@ -1,12 +1,15 @@ import * as React from 'react'; import { PickerValidDate } from '@mui/x-date-pickers/models'; +import { RangePosition } from '@mui/x-date-pickers/internals'; export interface RangeCalendarRootDragContext { isDraggingRef: React.RefObject; disableDragEditing: boolean; - selectDay: (value: PickerValidDate) => void; - setIsDragging: (value: boolean) => void; + selectDayFromDrag: (value: PickerValidDate) => void; + startDragging: (position: RangePosition) => void; + stopDragging: () => void; setDragTarget: (value: PickerValidDate | null) => void; + emptyDragImgRef: React.RefObject; } export const RangeCalendarRootDragContext = React.createContext< diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx index 8d98e43e25982..8c9850a8eea69 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx @@ -174,11 +174,12 @@ export function useRangeCalendarRoot(parameters: useRangeCalendarRoot.Parameters } } - const dragContext = useRangeCalendarDragEditing({ + const { dragContext, draggingDatePosition } = useRangeCalendarDragEditing({ baseContext, setValue, value, referenceDate, + onRangePositionChange, disableDragEditing, getNewValueFromNewSelectedDate, }); @@ -221,21 +222,21 @@ export namespace useRangeCalendarRoot { } } -function useRangeCalendarDragEditing( - parameters: UseRangeCalendarDragEditingParameters, -): RangeCalendarRootDragContext { +function useRangeCalendarDragEditing(parameters: UseRangeCalendarDragEditingParameters) { const { value, setValue, referenceDate, baseContext, + onRangePositionChange, getNewValueFromNewSelectedDate, disableDragEditing: disableDragEditingProp, } = parameters; + const utils = useUtils(); const [isDragging, setIsDragging] = React.useState(false); const [dragTarget, setDragTarget] = React.useState(null); - const selectDay = useEventCallback((selectedDate: PickerValidDate) => { + const selectDayFromDrag = useEventCallback((selectedDate: PickerValidDate) => { const response = getNewValueFromNewSelectedDate({ prevValue: value, selectedDate, @@ -253,13 +254,53 @@ function useRangeCalendarDragEditing( isDraggingRef.current = isDragging; }); - return { + const emptyDragImgRef = React.useRef(null); + React.useEffect(() => { + // Preload the image - required for Safari support: https://stackoverflow.com/a/40923520/3303436 + emptyDragImgRef.current = document.createElement('img'); + emptyDragImgRef.current.src = + 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; + }, []); + + const startDragging = useEventCallback((position: RangePosition) => { + setIsDragging(true); + onRangePositionChange(position); + }); + + const stopDragging = useEventCallback(() => setIsDragging(false)); + + const handleDragTargetChange = useEventCallback((newDragTarget: PickerValidDate | null) => { + if (utils.isEqual(newDragTarget, dragTarget)) { + return; + } + + setDragTarget(newDragTarget); + }); + + const draggingDatePosition: RangePosition | null = React.useMemo(() => { + const [start, end] = value; + if (dragTarget) { + if (start && utils.isBefore(dragTarget, start)) { + return 'start'; + } + if (end && utils.isAfter(dragTarget, end)) { + return 'end'; + } + } + return null; + }, [value, dragTarget, utils]); + + const dragContext: RangeCalendarRootDragContext = { disableDragEditing, isDraggingRef, - selectDay, - setIsDragging, - setDragTarget, + emptyDragImgRef, + selectDayFromDrag, + startDragging, + stopDragging, + setDragTarget: handleDragTargetChange, }; + + return { dragContext, draggingDatePosition }; } interface UseRangeCalendarDragEditingParameters { @@ -273,4 +314,5 @@ interface UseRangeCalendarDragEditingParameters { allowRangeFlip?: boolean; }, ) => useBaseCalendarRoot.GetNewValueFromNewSelectedDateReturnValue; + onRangePositionChange: (position: RangePosition) => void; } From bea8b97b341e47529c8247abc9355cc9ba0d82a1 Mon Sep 17 00:00:00 2001 From: flavien Date: Thu, 9 Jan 2025 15:47:31 +0100 Subject: [PATCH 082/136] Drag & drop basic version is working --- .../base-calendar/DayRangeCalendarDemo.tsx | 8 +- .../days-cell/useRangeCalendarDaysCell.tsx | 93 +++++++++------- .../useRangeCalendarDaysCellWrapper.ts | 37 +++---- .../root/RangeCalendarRootDragContext.ts | 7 +- .../root/useRangeCalendarRoot.tsx | 100 ++++++++++++------ .../Calendar/days-cell/CalendarDaysCell.tsx | 14 ++- 6 files changed, 160 insertions(+), 99 deletions(-) diff --git a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx index 9d86f3c7a2bcc..4c14744f07907 100644 --- a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx +++ b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx @@ -78,9 +78,13 @@ function DayCalendar(props: Omit) { } export default function DayRangeCalendarDemo() { + // const [value, setValue] = React.useState<[Dayjs | null, Dayjs | null]>([ + // dayjs('2025-01-03'), + // dayjs('2025-01-07'), + // ]); const [value, setValue] = React.useState<[Dayjs | null, Dayjs | null]>([ - dayjs('2025-01-03'), - dayjs('2025-01-07'), + null, + null, ]); const handleValueChange = React.useCallback( diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx index e0ca6a86721b6..22a1e421c0282 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx @@ -1,6 +1,4 @@ import * as React from 'react'; -import { PickerValidDate } from '@mui/x-date-pickers/models'; -import { RangePosition } from '@mui/x-date-pickers/internals'; import useEventCallback from '@mui/utils/useEventCallback'; // eslint-disable-next-line no-restricted-imports import { useBaseCalendarDaysCell } from '@mui/x-date-pickers/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCell'; @@ -17,8 +15,21 @@ export function useRangeCalendarDaysCell(parameters: useRangeCalendarDaysCell.Pa ctx.startDragging(ctx.isSelectionStart ? 'start' : 'end'); }; - const onTouchStart = useEventCallback(() => { + /** + * Mouse events + */ + const onDragStart = useEventCallback((event: React.DragEvent) => { + event.stopPropagation(); + if (ctx.emptyDragImgRef.current) { + event.dataTransfer.setDragImage(ctx.emptyDragImgRef.current, 0, 0); + } ctx.setDragTarget(value); + event.dataTransfer.effectAllowed = 'move'; + startDragging(); + const buttonDataset = (event.target as HTMLButtonElement).dataset; + if (buttonDataset.timestamp) { + event.dataTransfer.setData('draggingDate', buttonDataset.timestamp); + } }); const onDragEnter = useEventCallback((event: React.DragEvent) => { @@ -32,44 +43,57 @@ export function useRangeCalendarDaysCell(parameters: useRangeCalendarDaysCell.Pa ctx.setDragTarget(parameters.value); }); - const onTouchMove = useEventCallback((event: React.TouchEvent) => { - const target = resolveElementFromTouch(event); - if (!target) { + const onDragLeave = useEventCallback((event: React.DragEvent) => { + if (!ctx.isDraggingRef.current) { return; } - const newDate = resolveDateFromTarget(target, utils, timezone); - if (newDate) { - ctx.setDragTarget(newDate); - } + event.preventDefault(); + event.stopPropagation(); + }); - // this prevents initiating drag when user starts touchmove outside and then moves over a draggable element - const targetsAreIdentical = target === event.changedTouches[0].target; - if (!targetsAreIdentical || !isElementDraggable(newDate)) { + const onDragOver = useEventCallback((event: React.DragEvent) => { + if (!ctx.isDraggingRef.current) { return; } - // on mobile we should only initialize dragging state after move is detected - startDragging(); + event.preventDefault(); + event.stopPropagation(); + event.dataTransfer.dropEffect = 'move'; }); - const onDragLeave = useEventCallback((event: React.DragEvent) => { + const onDragEnd = useEventCallback((event: React.DragEvent) => { if (!ctx.isDraggingRef.current) { return; } event.preventDefault(); event.stopPropagation(); + ctx.stopDragging(); }); - const onDragOver = useEventCallback((event: React.DragEvent) => { + const onDrop = useEventCallback((event: React.DragEvent) => { if (!ctx.isDraggingRef.current) { return; } event.preventDefault(); event.stopPropagation(); - event.dataTransfer.dropEffect = 'move'; + ctx.stopDragging(); + // make sure the focused element is the element where drop ended + event.currentTarget.focus(); + if (ctx.isEqualToDragTarget(value)) { + return; + } + ctx.selectDayFromDrag(value); + }); + + /** + * Touch events + */ + + const onTouchStart = useEventCallback(() => { + ctx.setDragTarget(value); }); const onTouchEnd = useEventCallback((event: React.TouchEvent) => { @@ -77,7 +101,6 @@ export function useRangeCalendarDaysCell(parameters: useRangeCalendarDaysCell.Pa return; } - ctx.setDragTarget(null); ctx.stopDragging(); const target = resolveElementFromTouch(event, true); @@ -93,32 +116,25 @@ export function useRangeCalendarDaysCell(parameters: useRangeCalendarDaysCell.Pa } }); - const onDragEnd = useEventCallback((event: React.DragEvent) => { - if (!ctx.isDraggingRef.current) { + const onTouchMove = useEventCallback((event: React.TouchEvent) => { + const target = resolveElementFromTouch(event); + if (!target) { return; } - event.preventDefault(); - event.stopPropagation(); - ctx.stopDragging(); - ctx.setDragTarget(null); - }); - - const onDrop = useEventCallback((event: React.DragEvent) => { - if (!ctx.isDraggingRef.current) { - return; + const newDate = resolveDateFromTarget(target, utils, timezone); + if (newDate) { + ctx.setDragTarget(newDate); } - event.preventDefault(); - event.stopPropagation(); - ctx.stopDragging(); - ctx.setDragTarget(null); - // make sure the focused element is the element where drop ended - event.currentTarget.focus(); - if (isSameAsDraggingDate(event)) { + // this prevents initiating drag when user starts touchmove outside and then moves over a draggable element + const targetsAreIdentical = target === event.changedTouches[0].target; + if (!targetsAreIdentical || !isElementDraggable(newDate)) { return; } - ctx.selectDayFromDrag(value); + + // on mobile we should only initialize dragging state after move is detected + startDragging(); }); const { baseProps, isCurrent } = useBaseCalendarDaysCell(parameters); @@ -172,6 +188,7 @@ export namespace useRangeCalendarDaysCell { | 'startDragging' | 'stopDragging' | 'setDragTarget' + | 'isEqualToDragTarget' | 'emptyDragImgRef' > { isSelectionStart: boolean; diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts index c5d337a6eef86..2803afe86c2ee 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts @@ -3,7 +3,6 @@ import { useUtils } from '@mui/x-date-pickers/internals'; // eslint-disable-next-line no-restricted-imports import { useBaseCalendarDaysCellWrapper } from '@mui/x-date-pickers/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCellWrapper'; import type { useRangeCalendarDaysCell } from './useRangeCalendarDaysCell'; -import { useRangeCalendarRootContext } from '../root/RangeCalendarRootContext'; import { isWithinRange, isStartOfRange, @@ -18,43 +17,36 @@ export function useRangeCalendarDaysCellWrapper( const { value } = parameters; const { ref, ctx: baseCtx } = useBaseCalendarDaysCellWrapper(parameters); const utils = useUtils(); - const rangeRootContext = useRangeCalendarRootContext(); const rangeRootDragContext = useRangeCalendarRootDragContext(); - const isSelected = React.useMemo( - () => - isRangeValid(utils, rangeRootContext.value) - ? isWithinRange(utils, value, rangeRootContext.value) - : baseCtx.isSelected, - [utils, value, rangeRootContext.value, baseCtx.isSelected], - ); - const isSelectionStart = React.useMemo( - () => - isRangeValid(utils, rangeRootContext.value) - ? isStartOfRange(utils, value, rangeRootContext.value) - : baseCtx.isSelected, - [utils, value, rangeRootContext.value, baseCtx.isSelected], + () => isStartOfRange(utils, value, rangeRootDragContext.highlightedRange), + [utils, value, rangeRootDragContext.highlightedRange], ); const isSelectionEnd = React.useMemo( - () => - isRangeValid(utils, rangeRootContext.value) - ? isEndOfRange(utils, value, rangeRootContext.value) - : baseCtx.isSelected, - [utils, value, rangeRootContext.value, baseCtx.isSelected], + () => isEndOfRange(utils, value, rangeRootDragContext.highlightedRange), + [utils, value, rangeRootDragContext.highlightedRange], ); + const isSelected = React.useMemo(() => { + if (!isRangeValid(utils, rangeRootDragContext.highlightedRange)) { + return baseCtx.isSelected; + } + return isWithinRange(utils, value, rangeRootDragContext.highlightedRange); + }, [utils, value, rangeRootDragContext.highlightedRange, baseCtx.isSelected]); + const ctx = React.useMemo( () => ({ ...baseCtx, isSelected, - isSelectionStart, - isSelectionEnd, + isSelectionStart: isSelectionStart && !isSelectionEnd, + isSelectionEnd: isSelectionEnd && !isSelectionStart, isDraggingRef: rangeRootDragContext.isDraggingRef, selectDayFromDrag: rangeRootDragContext.selectDayFromDrag, startDragging: rangeRootDragContext.startDragging, stopDragging: rangeRootDragContext.stopDragging, + isEqualToDragTarget: rangeRootDragContext.isEqualToDragTarget, setDragTarget: rangeRootDragContext.setDragTarget, emptyDragImgRef: rangeRootDragContext.emptyDragImgRef, }), @@ -68,6 +60,7 @@ export function useRangeCalendarDaysCellWrapper( rangeRootDragContext.startDragging, rangeRootDragContext.stopDragging, rangeRootDragContext.setDragTarget, + rangeRootDragContext.isEqualToDragTarget, rangeRootDragContext.emptyDragImgRef, ], ); diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootDragContext.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootDragContext.ts index 786478462cb09..38c30b4bcf08c 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootDragContext.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootDragContext.ts @@ -1,6 +1,6 @@ import * as React from 'react'; import { PickerValidDate } from '@mui/x-date-pickers/models'; -import { RangePosition } from '@mui/x-date-pickers/internals'; +import { PickerRangeValue, RangePosition } from '@mui/x-date-pickers/internals'; export interface RangeCalendarRootDragContext { isDraggingRef: React.RefObject; @@ -8,8 +8,11 @@ export interface RangeCalendarRootDragContext { selectDayFromDrag: (value: PickerValidDate) => void; startDragging: (position: RangePosition) => void; stopDragging: () => void; - setDragTarget: (value: PickerValidDate | null) => void; + setDragTarget: (value: PickerValidDate) => void; emptyDragImgRef: React.RefObject; + isEqualToDragTarget: (value: PickerValidDate) => boolean; + highlightedRange: PickerRangeValue; + isDragging: boolean; } export const RangeCalendarRootDragContext = React.createContext< diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx index 8c9850a8eea69..955b3a23a1a7a 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx @@ -149,16 +149,6 @@ export function useRangeCalendarRoot(parameters: useRangeCalendarRoot.Parameters calendarValueManager, }); - // // Range going for the start of the start day to the end of the end day. - // // This makes sure that `isWithinRange` works with any time in the start and end day. - // const valueDayRange = React.useMemo( - // () => [ - // !utils.isValid(value[0]) ? value[0] : utils.startOfDay(value[0]), - // !utils.isValid(value[1]) ? value[1] : utils.endOfDay(value[1]), - // ], - // [value, utils], - // ); - // TODO: Apply some logic based on the range position. const [prevValue, setPrevValue] = React.useState(value); if (value !== prevValue) { @@ -174,12 +164,13 @@ export function useRangeCalendarRoot(parameters: useRangeCalendarRoot.Parameters } } - const { dragContext, draggingDatePosition } = useRangeCalendarDragEditing({ + const { dragContext } = useRangeCalendarDragEditing({ baseContext, setValue, value, referenceDate, onRangePositionChange, + rangePosition, disableDragEditing, getNewValueFromNewSelectedDate, }); @@ -228,13 +219,45 @@ function useRangeCalendarDragEditing(parameters: UseRangeCalendarDragEditingPara setValue, referenceDate, baseContext, + rangePosition, onRangePositionChange, getNewValueFromNewSelectedDate, disableDragEditing: disableDragEditingProp, } = parameters; const utils = useUtils(); - const [isDragging, setIsDragging] = React.useState(false); - const [dragTarget, setDragTarget] = React.useState(null); + + // Range going for the start of the start day to the end of the end day. + // This makes sure that `isWithinRange` works with any time in the start and end day. + const valueDayRange = React.useMemo( + () => [ + !utils.isValid(value[0]) ? value[0] : utils.startOfDay(value[0]), + !utils.isValid(value[1]) ? value[1] : utils.endOfDay(value[1]), + ], + [value, utils], + ); + + const [dragState, setDragState] = React.useState<{ + isDragging: boolean; + targetDate: PickerValidDate | null; + draggedDate: PickerValidDate | null; + }>({ isDragging: false, targetDate: null, draggedDate: null }); + + const highlightedRange = React.useMemo(() => { + if (!valueDayRange[0] || !valueDayRange[1] || !dragState.targetDate) { + return valueDayRange; + } + + const newRange = calculateRangeChange({ + utils, + range: valueDayRange, + newDate: dragState.targetDate, + rangePosition, + allowRangeFlip: true, + }).newRange; + return newRange[0] !== null && newRange[1] !== null + ? [utils.startOfDay(newRange[0]), utils.endOfDay(newRange[1])] + : newRange; + }, [rangePosition, dragState.targetDate, utils, valueDayRange]); const selectDayFromDrag = useEventCallback((selectedDate: PickerValidDate) => { const response = getNewValueFromNewSelectedDate({ @@ -249,9 +272,9 @@ function useRangeCalendarDragEditing(parameters: UseRangeCalendarDragEditingPara const disableDragEditing = disableDragEditingProp || baseContext.disabled || baseContext.readOnly; - const isDraggingRef = React.useRef(isDragging); + const isDraggingRef = React.useRef(dragState.isDragging); useEnhancedEffect(() => { - isDraggingRef.current = isDragging; + isDraggingRef.current = dragState.isDragging; }); const emptyDragImgRef = React.useRef(null); @@ -263,44 +286,54 @@ function useRangeCalendarDragEditing(parameters: UseRangeCalendarDragEditingPara }, []); const startDragging = useEventCallback((position: RangePosition) => { - setIsDragging(true); + setDragState((prev) => ({ ...prev, isDragging: true })); onRangePositionChange(position); }); - const stopDragging = useEventCallback(() => setIsDragging(false)); + const stopDragging = useEventCallback(() => { + setDragState((prev) => ({ + ...prev, + isDragging: false, + draggedDate: null, + targetDate: null, + })); + }); - const handleDragTargetChange = useEventCallback((newDragTarget: PickerValidDate | null) => { - if (utils.isEqual(newDragTarget, dragTarget)) { + const handleSetDragTarget = useEventCallback((newTargetDate: PickerValidDate) => { + if (utils.isEqual(newTargetDate, dragState.targetDate)) { return; } - setDragTarget(newDragTarget); + setDragState((prev) => ({ ...prev, targetDate: newTargetDate })); + + if (value[0] && utils.isBeforeDay(newTargetDate, value[0])) { + onRangePositionChange('start'); + } else if (value[1] && utils.isAfterDay(newTargetDate, value[1])) { + onRangePositionChange('end'); + } }); - const draggingDatePosition: RangePosition | null = React.useMemo(() => { - const [start, end] = value; - if (dragTarget) { - if (start && utils.isBefore(dragTarget, start)) { - return 'start'; - } - if (end && utils.isAfter(dragTarget, end)) { - return 'end'; - } + const isSameAsDraggedDate = useEventCallback((date: PickerValidDate) => { + if (dragState.draggedDate == null) { + return false; } - return null; - }, [value, dragTarget, utils]); + return utils.isSameDay(date, dragState.draggedDate); + }); const dragContext: RangeCalendarRootDragContext = { + highlightedRange, disableDragEditing, isDraggingRef, emptyDragImgRef, selectDayFromDrag, startDragging, stopDragging, - setDragTarget: handleDragTargetChange, + setDragTarget: handleSetDragTarget, + isEqualToDragTarget: isSameAsDraggedDate, + isDragging: dragState.isDragging, }; - return { dragContext, draggingDatePosition }; + return { dragContext }; } interface UseRangeCalendarDragEditingParameters { @@ -315,4 +348,5 @@ interface UseRangeCalendarDragEditingParameters { }, ) => useBaseCalendarRoot.GetNewValueFromNewSelectedDateReturnValue; onRangePositionChange: (position: RangePosition) => void; + rangePosition: RangePosition; } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx index 3daf393185ea2..6814be160d157 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx @@ -1,11 +1,12 @@ 'use client'; import * as React from 'react'; -import { BaseUIComponentProps } from '../../base-utils/types'; +import { BaseUIComponentProps, GenericHTMLProps } from '../../base-utils/types'; import { useComponentRenderer } from '../../base-utils/useComponentRenderer'; import { useBaseCalendarDaysCellWrapper } from '../../utils/base-calendar/days-cell/useBaseCalendarDaysCellWrapper'; import { useBaseCalendarDaysCell } from '../../utils/base-calendar/days-cell/useBaseCalendarDaysCell'; import { CustomStyleHookMapping } from '../../base-utils/getStyleHookProps'; import { CalendarDaysCellDataAttributes } from './CalendarDaysCellDataAttributes'; +import { mergeReactProps } from '../../base-utils/mergeReactProps'; const customStyleHookMapping: CustomStyleHookMapping = { selected(value) { @@ -27,7 +28,16 @@ const InnerCalendarDaysCell = React.forwardRef(function CalendarDaysGrid( forwardedRef: React.ForwardedRef, ) { const { className, render, value, ctx, ...otherProps } = props; - const { getDaysCellProps, isCurrent } = useBaseCalendarDaysCell({ value, ctx }); + const { baseProps, isCurrent } = useBaseCalendarDaysCell({ value, ctx }); + + const getDaysCellProps = React.useCallback( + (externalProps: GenericHTMLProps) => { + return mergeReactProps(externalProps, { + ...baseProps, + }); + }, + [baseProps], + ); const state: CalendarDaysCell.State = React.useMemo( () => ({ From 8995e23d1b4491fb4e56dc8f12f965362c0cf7a5 Mon Sep 17 00:00:00 2001 From: flavien Date: Thu, 9 Jan 2025 15:54:50 +0100 Subject: [PATCH 083/136] WOrk --- .../base-calendar/calendar.module.css | 2 +- .../days-cell/useRangeCalendarDaysCell.tsx | 69 ++++++++++++------- 2 files changed, 47 insertions(+), 24 deletions(-) diff --git a/docs/data/date-pickers/base-calendar/calendar.module.css b/docs/data/date-pickers/base-calendar/calendar.module.css index 1d8c037c7a75c..0544eeef27b88 100644 --- a/docs/data/date-pickers/base-calendar/calendar.module.css +++ b/docs/data/date-pickers/base-calendar/calendar.module.css @@ -168,7 +168,7 @@ background-color: transparent; cursor: pointer; - &[data-current] { + &[data-current]:not([data-selected]) { outline: 1px solid #9ca3af; } diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx index 22a1e421c0282..7f092b1f86528 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx @@ -11,6 +11,8 @@ import type { RangeCalendarRootDragContext } from '../root/RangeCalendarRootDrag export function useRangeCalendarDaysCell(parameters: useRangeCalendarDaysCell.Parameters) { const { ctx, value } = parameters; + const isDraggable = ctx.isSelectionStart || ctx.isSelectionEnd; + const startDragging = () => { ctx.startDragging(ctx.isSelectionStart ? 'start' : 'end'); }; @@ -103,33 +105,21 @@ export function useRangeCalendarDaysCell(parameters: useRangeCalendarDaysCell.Pa ctx.stopDragging(); - const target = resolveElementFromTouch(event, true); - if (!target) { - return; - } - // make sure the focused element is the element where touch ended - target.focus(); - const newDate = resolveDateFromTarget(target, utils, timezone); - if (newDate) { - ctx.selectDayFromDrag(newDate); + ctx.selectDayFromDrag(value); + + const target = resolveElementFromTouch(event, true); + if (target) { + target.focus(); } }); const onTouchMove = useEventCallback((event: React.TouchEvent) => { - const target = resolveElementFromTouch(event); - if (!target) { - return; - } - - const newDate = resolveDateFromTarget(target, utils, timezone); - if (newDate) { - ctx.setDragTarget(newDate); - } + ctx.setDragTarget(value); // this prevents initiating drag when user starts touchmove outside and then moves over a draggable element - const targetsAreIdentical = target === event.changedTouches[0].target; - if (!targetsAreIdentical || !isElementDraggable(newDate)) { + const target = resolveElementFromTouch(event); + if (target == null || target !== event.changedTouches[0].target || !isDraggable) { return; } @@ -143,7 +133,7 @@ export function useRangeCalendarDaysCell(parameters: useRangeCalendarDaysCell.Pa (externalProps: GenericHTMLProps) => { return mergeReactProps(externalProps, { ...baseProps, - ...(ctx.isSelectionStart || ctx.isSelectionEnd ? { draggable: true } : {}), + ...(isDraggable ? { draggable: true } : {}), onDragStart, onDragEnter, onDragLeave, @@ -157,8 +147,7 @@ export function useRangeCalendarDaysCell(parameters: useRangeCalendarDaysCell.Pa }, [ baseProps, - ctx.isSelectionStart, - ctx.isSelectionEnd, + isDraggable, onDragStart, onDragEnter, onDragLeave, @@ -195,3 +184,37 @@ export namespace useRangeCalendarDaysCell { isSelectionEnd: boolean; } } + +function resolveButtonElement(element: Element | null): HTMLButtonElement | null { + if (element) { + if (element instanceof HTMLButtonElement && !element.disabled) { + return element; + } + if (element.children.length) { + return resolveButtonElement(element.children[0]); + } + return null; + } + return element; +} + +function resolveElementFromTouch( + event: React.TouchEvent, + ignoreTouchTarget?: boolean, +) { + // don't parse multi-touch result + if (event.changedTouches?.length === 1 && event.touches.length <= 1) { + const element = document.elementFromPoint( + event.changedTouches[0].clientX, + event.changedTouches[0].clientY, + ); + // `elementFromPoint` could have resolved preview div or wrapping div + // might need to recursively find the nested button + const buttonElement = resolveButtonElement(element); + if (ignoreTouchTarget && buttonElement === event.changedTouches[0].target) { + return null; + } + return buttonElement; + } + return null; +} From 9468ee1cf8c8c0a01dcdae6ab37d5d42487c1ac1 Mon Sep 17 00:00:00 2001 From: flavien Date: Thu, 9 Jan 2025 15:57:29 +0100 Subject: [PATCH 084/136] Improve range demo --- .../base-calendar/DayRangeCalendarDemo.js | 111 ++++++++++-------- .../base-calendar/DayRangeCalendarDemo.tsx | 101 +++++++++------- .../base-calendar/base-calendar.md | 2 +- 3 files changed, 125 insertions(+), 89 deletions(-) diff --git a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.js b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.js index d42667a0e3d37..33a1a92baaa14 100644 --- a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.js +++ b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.js @@ -1,6 +1,8 @@ import * as React from 'react'; +import clsx from 'clsx'; import NoSsr from '@mui/material/NoSsr'; +import { Separator } from '@base-ui-components/react/separator'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports @@ -10,21 +12,24 @@ import { } from '@mui/x-date-pickers-pro/internals/base/RangeCalendar'; import styles from './calendar.module.css'; -function Header() { +function Header(props) { + const { offset } = props; const { visibleDate } = useRangeCalendarContext(); + const date = visibleDate.add(offset, 'month'); + return (
- {visibleDate.format('MMMM YYYY')} + {date.format('MMMM YYYY')} @@ -32,58 +37,70 @@ function Header() { ); } +function DaysGrid(props) { + const { offset } = props; + return ( +
+
+ + + {({ days }) => + days.map((day) => ( + + )) + } + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => + days.map((day) => ( + + )) + } + + )) + } + + +
+ ); +} + function DayCalendar(props) { return ( - -
- - - {({ days }) => - days.map((day) => ( - - )) - } - - - {({ weeks }) => - weeks.map((week) => ( - - {/* {({ days }) => - days.map((day) => ( - - )) - } */} - {({ days }) => - days.map((day) => ( - - )) - } - - )) - } - - + + + + ); } export default function DayRangeCalendarDemo() { + // const [value, setValue] = React.useState<[Dayjs | null, Dayjs | null]>([ + // dayjs('2025-01-03'), + // dayjs('2025-01-07'), + // ]); const [value, setValue] = React.useState([null, null]); const handleValueChange = React.useCallback((newValue) => { diff --git a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx index 4c14744f07907..ee05fcab8126b 100644 --- a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx +++ b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx @@ -1,7 +1,8 @@ import * as React from 'react'; import clsx from 'clsx'; -import dayjs, { Dayjs } from 'dayjs'; +import { Dayjs } from 'dayjs'; import NoSsr from '@mui/material/NoSsr'; +import { Separator } from '@base-ui-components/react/separator'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports @@ -11,21 +12,24 @@ import { } from '@mui/x-date-pickers-pro/internals/base/RangeCalendar'; import styles from './calendar.module.css'; -function Header() { +function Header(props: { offset: 0 | 1 }) { + const { offset } = props; const { visibleDate } = useRangeCalendarContext(); + const date = visibleDate.add(offset, 'month'); + return (
- {visibleDate.format('MMMM YYYY')} + {date.format('MMMM YYYY')} @@ -33,45 +37,60 @@ function Header() { ); } +function DaysGrid(props: { offset: 0 | 1 }) { + const { offset } = props; + return ( +
+
+ + + {({ days }) => + days.map((day) => ( + + )) + } + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => + days.map((day) => ( + + )) + } + + )) + } + + +
+ ); +} + function DayCalendar(props: Omit) { return ( - -
- - - {({ days }) => - days.map((day) => ( - - )) - } - - - {({ weeks }) => - weeks.map((week) => ( - - {({ days }) => - days.map((day) => ( - - )) - } - - )) - } - - + + + + ); diff --git a/docs/data/date-pickers/base-calendar/base-calendar.md b/docs/data/date-pickers/base-calendar/base-calendar.md index 5ed6fa11f7995..c524b506b6a87 100644 --- a/docs/data/date-pickers/base-calendar/base-calendar.md +++ b/docs/data/date-pickers/base-calendar/base-calendar.md @@ -68,6 +68,6 @@ TODO {{"demo": "DateCalendarDemo.js"}} -## Range calendar (TODO: move) +## Day Range Calendar {{"demo": "DayRangeCalendarDemo.js"}} From 66263308a5d49d358475499073179966e690d2e1 Mon Sep 17 00:00:00 2001 From: flavien Date: Thu, 9 Jan 2025 17:52:42 +0100 Subject: [PATCH 085/136] Fix --- .../base-calendar/DayRangeCalendarDemo.js | 13 +- .../base-calendar/DayRangeCalendarDemo.tsx | 112 +++++++---------- .../DayRangeCalendarDemo.tsx.preview | 6 +- .../DayRangeCalendarWithTwoMonthsDemo.js | 110 +++++++++++++++++ .../DayRangeCalendarWithTwoMonthsDemo.tsx | 116 ++++++++++++++++++ ...RangeCalendarWithTwoMonthsDemo.tsx.preview | 1 + .../base-calendar/base-calendar.md | 6 + .../base-calendar/calendar.module.css | 2 +- 8 files changed, 281 insertions(+), 85 deletions(-) create mode 100644 docs/data/date-pickers/base-calendar/DayRangeCalendarWithTwoMonthsDemo.js create mode 100644 docs/data/date-pickers/base-calendar/DayRangeCalendarWithTwoMonthsDemo.tsx create mode 100644 docs/data/date-pickers/base-calendar/DayRangeCalendarWithTwoMonthsDemo.tsx.preview diff --git a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.js b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.js index 33a1a92baaa14..5a150cfe0e2b0 100644 --- a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.js +++ b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.js @@ -1,7 +1,6 @@ import * as React from 'react'; import clsx from 'clsx'; -import NoSsr from '@mui/material/NoSsr'; import { Separator } from '@base-ui-components/react/separator'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; @@ -97,10 +96,6 @@ function DayCalendar(props) { } export default function DayRangeCalendarDemo() { - // const [value, setValue] = React.useState<[Dayjs | null, Dayjs | null]>([ - // dayjs('2025-01-03'), - // dayjs('2025-01-07'), - // ]); const [value, setValue] = React.useState([null, null]); const handleValueChange = React.useCallback((newValue) => { @@ -108,10 +103,8 @@ export default function DayRangeCalendarDemo() { }, []); return ( - - - - - + + + ); } diff --git a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx index ee05fcab8126b..1d6728c438e1d 100644 --- a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx +++ b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx @@ -1,8 +1,6 @@ import * as React from 'react'; import clsx from 'clsx'; import { Dayjs } from 'dayjs'; -import NoSsr from '@mui/material/NoSsr'; -import { Separator } from '@base-ui-components/react/separator'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports @@ -12,24 +10,21 @@ import { } from '@mui/x-date-pickers-pro/internals/base/RangeCalendar'; import styles from './calendar.module.css'; -function Header(props: { offset: 0 | 1 }) { - const { offset } = props; +function Header() { const { visibleDate } = useRangeCalendarContext(); - const date = visibleDate.add(offset, 'month'); - return (
- {date.format('MMMM YYYY')} + {visibleDate.format('MMMM YYYY')} @@ -37,70 +32,51 @@ function Header(props: { offset: 0 | 1 }) { ); } -function DaysGrid(props: { offset: 0 | 1 }) { - const { offset } = props; - return ( -
-
- - - {({ days }) => - days.map((day) => ( - - )) - } - - - {({ weeks }) => - weeks.map((week) => ( - - {({ days }) => - days.map((day) => ( - - )) - } - - )) - } - - -
- ); -} - function DayCalendar(props: Omit) { return ( - - - - + +
+ + + {({ days }) => + days.map((day) => ( + + )) + } + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => + days.map((day) => ( + + )) + } + + )) + } + + ); } export default function DayRangeCalendarDemo() { - // const [value, setValue] = React.useState<[Dayjs | null, Dayjs | null]>([ - // dayjs('2025-01-03'), - // dayjs('2025-01-07'), - // ]); const [value, setValue] = React.useState<[Dayjs | null, Dayjs | null]>([ null, null, @@ -114,10 +90,8 @@ export default function DayRangeCalendarDemo() { ); return ( - - - - - + + + ); } diff --git a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx.preview b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx.preview index 57d95c16e0caf..72b23c81a5a56 100644 --- a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx.preview +++ b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx.preview @@ -1,5 +1 @@ - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/docs/data/date-pickers/base-calendar/DayRangeCalendarWithTwoMonthsDemo.js b/docs/data/date-pickers/base-calendar/DayRangeCalendarWithTwoMonthsDemo.js new file mode 100644 index 0000000000000..652ea8846a45f --- /dev/null +++ b/docs/data/date-pickers/base-calendar/DayRangeCalendarWithTwoMonthsDemo.js @@ -0,0 +1,110 @@ +import * as React from 'react'; +import clsx from 'clsx'; + +import { Separator } from '@base-ui-components/react/separator'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +// eslint-disable-next-line no-restricted-imports +import { + RangeCalendar, + useRangeCalendarContext, +} from '@mui/x-date-pickers-pro/internals/base/RangeCalendar'; +import styles from './calendar.module.css'; + +function Header(props) { + const { offset } = props; + const { visibleDate } = useRangeCalendarContext(); + + const date = visibleDate.add(offset, 'month'); + + return ( +
+ + ◀ + + {date.format('MMMM YYYY')} + + ▶ + +
+ ); +} + +function DaysGrid(props) { + const { offset } = props; + return ( +
+
+ + + {({ days }) => + days.map((day) => ( + + )) + } + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => + days.map((day) => ( + + )) + } + + )) + } + + +
+ ); +} + +function DayCalendar(props) { + return ( + + + + + + + + ); +} + +export default function DayRangeCalendarWithTwoMonthsDemo() { + const [value, setValue] = React.useState([null, null]); + + const handleValueChange = React.useCallback((newValue) => { + setValue(newValue); + }, []); + + return ( + + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/DayRangeCalendarWithTwoMonthsDemo.tsx b/docs/data/date-pickers/base-calendar/DayRangeCalendarWithTwoMonthsDemo.tsx new file mode 100644 index 0000000000000..37e34b001f5fd --- /dev/null +++ b/docs/data/date-pickers/base-calendar/DayRangeCalendarWithTwoMonthsDemo.tsx @@ -0,0 +1,116 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import { Dayjs } from 'dayjs'; +import { Separator } from '@base-ui-components/react/separator'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +// eslint-disable-next-line no-restricted-imports +import { + RangeCalendar, + useRangeCalendarContext, +} from '@mui/x-date-pickers-pro/internals/base/RangeCalendar'; +import styles from './calendar.module.css'; + +function Header(props: { offset: 0 | 1 }) { + const { offset } = props; + const { visibleDate } = useRangeCalendarContext(); + + const date = visibleDate.add(offset, 'month'); + + return ( +
+ + ◀ + + {date.format('MMMM YYYY')} + + ▶ + +
+ ); +} + +function DaysGrid(props: { offset: 0 | 1 }) { + const { offset } = props; + return ( +
+
+ + + {({ days }) => + days.map((day) => ( + + )) + } + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => + days.map((day) => ( + + )) + } + + )) + } + + +
+ ); +} + +function DayCalendar(props: Omit) { + return ( + + + + + + + + ); +} + +export default function DayRangeCalendarWithTwoMonthsDemo() { + const [value, setValue] = React.useState<[Dayjs | null, Dayjs | null]>([ + null, + null, + ]); + + const handleValueChange = React.useCallback( + (newValue: [Dayjs | null, Dayjs | null]) => { + setValue(newValue); + }, + [], + ); + + return ( + + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/DayRangeCalendarWithTwoMonthsDemo.tsx.preview b/docs/data/date-pickers/base-calendar/DayRangeCalendarWithTwoMonthsDemo.tsx.preview new file mode 100644 index 0000000000000..72b23c81a5a56 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/DayRangeCalendarWithTwoMonthsDemo.tsx.preview @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/data/date-pickers/base-calendar/base-calendar.md b/docs/data/date-pickers/base-calendar/base-calendar.md index c524b506b6a87..f4138d59b2f9e 100644 --- a/docs/data/date-pickers/base-calendar/base-calendar.md +++ b/docs/data/date-pickers/base-calendar/base-calendar.md @@ -70,4 +70,10 @@ TODO ## Day Range Calendar +### Single visible month + {{"demo": "DayRangeCalendarDemo.js"}} + +### Multiple visible months + +{{"demo": "DayRangeCalendarWithTwoMonthsDemo.js"}} diff --git a/docs/data/date-pickers/base-calendar/calendar.module.css b/docs/data/date-pickers/base-calendar/calendar.module.css index 0544eeef27b88..410f310a3baea 100644 --- a/docs/data/date-pickers/base-calendar/calendar.module.css +++ b/docs/data/date-pickers/base-calendar/calendar.module.css @@ -17,7 +17,7 @@ } .RootWithTwoPanels { - width: 552px; + width: 564px; flex-direction: row; justify-content: space-between; } From 5c728a8c59a108d9d6c26ad44606e6c590ef15c5 Mon Sep 17 00:00:00 2001 From: flavien Date: Thu, 9 Jan 2025 17:53:30 +0100 Subject: [PATCH 086/136] Fix --- .../base-calendar/DayRangeCalendarDemo.js | 103 +++++++----------- .../base-calendar/DayRangeCalendarDemo.tsx | 4 +- .../DayRangeCalendarDemo.tsx.preview | 2 +- .../DayRangeCalendarWithTwoMonthsDemo.js | 4 +- .../DayRangeCalendarWithTwoMonthsDemo.tsx | 4 +- ...RangeCalendarWithTwoMonthsDemo.tsx.preview | 2 +- 6 files changed, 50 insertions(+), 69 deletions(-) diff --git a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.js b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.js index 5a150cfe0e2b0..e67c204c64a7b 100644 --- a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.js +++ b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.js @@ -1,7 +1,6 @@ import * as React from 'react'; import clsx from 'clsx'; -import { Separator } from '@base-ui-components/react/separator'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports @@ -11,24 +10,21 @@ import { } from '@mui/x-date-pickers-pro/internals/base/RangeCalendar'; import styles from './calendar.module.css'; -function Header(props) { - const { offset } = props; +function Header() { const { visibleDate } = useRangeCalendarContext(); - const date = visibleDate.add(offset, 'month'); - return (
- {date.format('MMMM YYYY')} + {visibleDate.format('MMMM YYYY')} @@ -36,60 +32,45 @@ function Header(props) { ); } -function DaysGrid(props) { - const { offset } = props; - return ( -
-
- - - {({ days }) => - days.map((day) => ( - - )) - } - - - {({ weeks }) => - weeks.map((week) => ( - - {({ days }) => - days.map((day) => ( - - )) - } - - )) - } - - -
- ); -} - -function DayCalendar(props) { +function DayRangeCalendar(props) { return ( - - - - + +
+ + + {({ days }) => + days.map((day) => ( + + )) + } + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => + days.map((day) => ( + + )) + } + + )) + } + + ); @@ -104,7 +85,7 @@ export default function DayRangeCalendarDemo() { return ( - + ); } diff --git a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx index 1d6728c438e1d..ec645716124b8 100644 --- a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx +++ b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx @@ -32,7 +32,7 @@ function Header() { ); } -function DayCalendar(props: Omit) { +function DayRangeCalendar(props: Omit) { return ( @@ -91,7 +91,7 @@ export default function DayRangeCalendarDemo() { return ( - + ); } diff --git a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx.preview b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx.preview index 72b23c81a5a56..16ef550120c15 100644 --- a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx.preview +++ b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx.preview @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/docs/data/date-pickers/base-calendar/DayRangeCalendarWithTwoMonthsDemo.js b/docs/data/date-pickers/base-calendar/DayRangeCalendarWithTwoMonthsDemo.js index 652ea8846a45f..5b3b5729ad3da 100644 --- a/docs/data/date-pickers/base-calendar/DayRangeCalendarWithTwoMonthsDemo.js +++ b/docs/data/date-pickers/base-calendar/DayRangeCalendarWithTwoMonthsDemo.js @@ -79,7 +79,7 @@ function DaysGrid(props) { ); } -function DayCalendar(props) { +function DayRangeCalendar(props) { return ( - + ); } diff --git a/docs/data/date-pickers/base-calendar/DayRangeCalendarWithTwoMonthsDemo.tsx b/docs/data/date-pickers/base-calendar/DayRangeCalendarWithTwoMonthsDemo.tsx index 37e34b001f5fd..f9db36a489510 100644 --- a/docs/data/date-pickers/base-calendar/DayRangeCalendarWithTwoMonthsDemo.tsx +++ b/docs/data/date-pickers/base-calendar/DayRangeCalendarWithTwoMonthsDemo.tsx @@ -79,7 +79,7 @@ function DaysGrid(props: { offset: 0 | 1 }) { ); } -function DayCalendar(props: Omit) { +function DayRangeCalendar(props: Omit) { return ( - + ); } diff --git a/docs/data/date-pickers/base-calendar/DayRangeCalendarWithTwoMonthsDemo.tsx.preview b/docs/data/date-pickers/base-calendar/DayRangeCalendarWithTwoMonthsDemo.tsx.preview index 72b23c81a5a56..16ef550120c15 100644 --- a/docs/data/date-pickers/base-calendar/DayRangeCalendarWithTwoMonthsDemo.tsx.preview +++ b/docs/data/date-pickers/base-calendar/DayRangeCalendarWithTwoMonthsDemo.tsx.preview @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file From 721b41828726c203c031581fd8318d7e9feabebf Mon Sep 17 00:00:00 2001 From: flavien Date: Fri, 10 Jan 2025 08:57:49 +0100 Subject: [PATCH 087/136] Work --- .../base-calendar/calendar.module.css | 2 +- .../days-cell/RangeCalendarDaysCell.tsx | 3 ++ .../days-cell/useRangeCalendarDaysCell.tsx | 14 ++++---- .../useRangeCalendarDaysCellWrapper.ts | 5 +-- .../root/RangeCalendarRootDragContext.ts | 1 - .../root/useRangeCalendarRoot.tsx | 19 +++-------- .../Calendar/days-cell/CalendarDaysCell.tsx | 17 +++------- .../months-cell/useCalendarMonthsCell.ts | 3 ++ .../base/Calendar/root/useCalendarRoot.ts | 2 +- .../years-cell/useCalendarYearsCell.ts | 3 ++ .../days-cell/useBaseCalendarDaysCell.ts | 33 +++++++++++-------- .../useBaseCalendarDaysWeekRow.ts | 5 ++- .../base-calendar/root/useBaseCalendarRoot.ts | 17 +++++++--- .../useBaseCalendarSetVisibleMonth.ts | 3 ++ .../useBaseCalendarSetVisibleYear.ts | 3 ++ 15 files changed, 73 insertions(+), 57 deletions(-) diff --git a/docs/data/date-pickers/base-calendar/calendar.module.css b/docs/data/date-pickers/base-calendar/calendar.module.css index 410f310a3baea..d218de98ba1b6 100644 --- a/docs/data/date-pickers/base-calendar/calendar.module.css +++ b/docs/data/date-pickers/base-calendar/calendar.module.css @@ -168,7 +168,7 @@ background-color: transparent; cursor: pointer; - &[data-current]:not([data-selected]) { + &[data-current]:not([data-selected]):not(:focus-visible) { outline: 1px solid #9ca3af; } diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/RangeCalendarDaysCell.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/RangeCalendarDaysCell.tsx index 162cc4e8c41bf..f9bca56d7c08b 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/RangeCalendarDaysCell.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/RangeCalendarDaysCell.tsx @@ -23,6 +23,9 @@ const customStyleHookMapping: CustomStyleHookMapping { - return mergeReactProps(externalProps, { - ...baseProps, + return mergeReactProps(externalProps, getBaseDaysCellProps(externalProps), { ...(isDraggable ? { draggable: true } : {}), onDragStart, onDragEnter, @@ -146,7 +142,7 @@ export function useRangeCalendarDaysCell(parameters: useRangeCalendarDaysCell.Pa }); }, [ - baseProps, + getBaseDaysCellProps, isDraggable, onDragStart, onDragEnter, @@ -165,6 +161,9 @@ export function useRangeCalendarDaysCell(parameters: useRangeCalendarDaysCell.Pa export namespace useRangeCalendarDaysCell { export interface Parameters extends Omit { + /** + * The memoized context forwarded by the wrapper component so that this component does not need to subscribe to any context. + */ ctx: Context; } @@ -177,7 +176,6 @@ export namespace useRangeCalendarDaysCell { | 'startDragging' | 'stopDragging' | 'setDragTarget' - | 'isEqualToDragTarget' | 'emptyDragImgRef' > { isSelectionStart: boolean; diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts index 2803afe86c2ee..716a4baa19b56 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts @@ -46,7 +46,6 @@ export function useRangeCalendarDaysCellWrapper( selectDayFromDrag: rangeRootDragContext.selectDayFromDrag, startDragging: rangeRootDragContext.startDragging, stopDragging: rangeRootDragContext.stopDragging, - isEqualToDragTarget: rangeRootDragContext.isEqualToDragTarget, setDragTarget: rangeRootDragContext.setDragTarget, emptyDragImgRef: rangeRootDragContext.emptyDragImgRef, }), @@ -60,7 +59,6 @@ export function useRangeCalendarDaysCellWrapper( rangeRootDragContext.startDragging, rangeRootDragContext.stopDragging, rangeRootDragContext.setDragTarget, - rangeRootDragContext.isEqualToDragTarget, rangeRootDragContext.emptyDragImgRef, ], ); @@ -72,6 +70,9 @@ export namespace useRangeCalendarDaysCellWrapper { export interface Parameters extends useBaseCalendarDaysCellWrapper.Parameters {} export interface ReturnValue extends Omit { + /** + * The memoized context to forward to the memoized component so that it does not need to subscribe to any context. + */ ctx: useRangeCalendarDaysCell.Context; } } diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootDragContext.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootDragContext.ts index 38c30b4bcf08c..45e70c553d562 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootDragContext.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootDragContext.ts @@ -10,7 +10,6 @@ export interface RangeCalendarRootDragContext { stopDragging: () => void; setDragTarget: (value: PickerValidDate) => void; emptyDragImgRef: React.RefObject; - isEqualToDragTarget: (value: PickerValidDate) => boolean; highlightedRange: PickerRangeValue; isDragging: boolean; } diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx index 955b3a23a1a7a..b945cd598909a 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx @@ -175,12 +175,7 @@ export function useRangeCalendarRoot(parameters: useRangeCalendarRoot.Parameters getNewValueFromNewSelectedDate, }); - const context: RangeCalendarRootContext = React.useMemo( - () => ({ - value, - }), - [value], - ); + const context: RangeCalendarRootContext = React.useMemo(() => ({ value }), [value]); const getRootProps = React.useCallback((externalProps: GenericHTMLProps) => { return mergeReactProps(externalProps, {}); @@ -260,6 +255,10 @@ function useRangeCalendarDragEditing(parameters: UseRangeCalendarDragEditingPara }, [rangePosition, dragState.targetDate, utils, valueDayRange]); const selectDayFromDrag = useEventCallback((selectedDate: PickerValidDate) => { + if (dragState.draggedDate != null && utils.isSameDay(selectedDate, dragState.draggedDate)) { + return; + } + const response = getNewValueFromNewSelectedDate({ prevValue: value, selectedDate, @@ -313,13 +312,6 @@ function useRangeCalendarDragEditing(parameters: UseRangeCalendarDragEditingPara } }); - const isSameAsDraggedDate = useEventCallback((date: PickerValidDate) => { - if (dragState.draggedDate == null) { - return false; - } - return utils.isSameDay(date, dragState.draggedDate); - }); - const dragContext: RangeCalendarRootDragContext = { highlightedRange, disableDragEditing, @@ -329,7 +321,6 @@ function useRangeCalendarDragEditing(parameters: UseRangeCalendarDragEditingPara startDragging, stopDragging, setDragTarget: handleSetDragTarget, - isEqualToDragTarget: isSameAsDraggedDate, isDragging: dragState.isDragging, }; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx index 6814be160d157..ea03b5deb3d2b 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx @@ -1,12 +1,11 @@ 'use client'; import * as React from 'react'; -import { BaseUIComponentProps, GenericHTMLProps } from '../../base-utils/types'; +import { BaseUIComponentProps } from '../../base-utils/types'; import { useComponentRenderer } from '../../base-utils/useComponentRenderer'; import { useBaseCalendarDaysCellWrapper } from '../../utils/base-calendar/days-cell/useBaseCalendarDaysCellWrapper'; import { useBaseCalendarDaysCell } from '../../utils/base-calendar/days-cell/useBaseCalendarDaysCell'; import { CustomStyleHookMapping } from '../../base-utils/getStyleHookProps'; import { CalendarDaysCellDataAttributes } from './CalendarDaysCellDataAttributes'; -import { mergeReactProps } from '../../base-utils/mergeReactProps'; const customStyleHookMapping: CustomStyleHookMapping = { selected(value) { @@ -15,6 +14,9 @@ const customStyleHookMapping: CustomStyleHookMapping = { disabled(value) { return value ? { [CalendarDaysCellDataAttributes.disabled]: '' } : null; }, + invalid(value) { + return value ? { [CalendarDaysCellDataAttributes.invalid]: '' } : null; + }, current(value) { return value ? { [CalendarDaysCellDataAttributes.current]: '' } : null; }, @@ -28,16 +30,7 @@ const InnerCalendarDaysCell = React.forwardRef(function CalendarDaysGrid( forwardedRef: React.ForwardedRef, ) { const { className, render, value, ctx, ...otherProps } = props; - const { baseProps, isCurrent } = useBaseCalendarDaysCell({ value, ctx }); - - const getDaysCellProps = React.useCallback( - (externalProps: GenericHTMLProps) => { - return mergeReactProps(externalProps, { - ...baseProps, - }); - }, - [baseProps], - ); + const { getDaysCellProps, isCurrent } = useBaseCalendarDaysCell({ value, ctx }); const state: CalendarDaysCell.State = React.useMemo( () => ({ diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/useCalendarMonthsCell.ts b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/useCalendarMonthsCell.ts index be23aacd0fcb1..f9a748d773742 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/useCalendarMonthsCell.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/useCalendarMonthsCell.ts @@ -47,6 +47,9 @@ export namespace useCalendarMonthsCell { * @default utils.formats.month */ format?: string; + /** + * The memoized context forwarded by the wrapper component so that this component does not need to subscribe to any context. + */ ctx: Context; } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts index bc568330ae995..3ece3523f79ea 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts @@ -1,5 +1,5 @@ import * as React from 'react'; -import { DateValidationError, PickerValidDate } from '../../../../models'; +import { DateValidationError } from '../../../../models'; import { useDateManager } from '../../../../managers'; import { ExportedValidateDateProps, ValidateDateProps } from '../../../../validation/validateDate'; import { useUtils } from '../../../hooks/useUtils'; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-cell/useCalendarYearsCell.ts b/packages/x-date-pickers/src/internals/base/Calendar/years-cell/useCalendarYearsCell.ts index 027f6648be686..3c6146cd78eef 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-cell/useCalendarYearsCell.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-cell/useCalendarYearsCell.ts @@ -47,6 +47,9 @@ export namespace useCalendarYearsCell { * @default utils.formats.year */ format?: string; + /** + * The memoized context forwarded by the wrapper component so that this component does not need to subscribe to any context. + */ ctx: Context; } diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCell.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCell.ts index fd24229d0012c..193d7bf7c6cf4 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCell.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCell.ts @@ -2,6 +2,8 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; import { PickerValidDate } from '../../../../../models'; import { useUtils } from '../../../../hooks/useUtils'; +import { GenericHTMLProps } from '../../../base-utils/types'; +import { mergeReactProps } from '../../../base-utils/mergeReactProps'; export function useBaseCalendarDaysCell(parameters: useBaseCalendarDaysCell.Parameters) { const utils = useUtils(); @@ -18,17 +20,19 @@ export function useBaseCalendarDaysCell(parameters: useBaseCalendarDaysCell.Para ctx.selectDay(value); }); - const baseProps = React.useMemo( - () => ({ - role: 'gridcell', - 'aria-selected': ctx.isSelected, - 'aria-current': isCurrent ? 'date' : undefined, - 'aria-colindex': ctx.colIndex + 1, - children: formattedValue, - disabled: ctx.isDisabled, - tabIndex: ctx.isTabbable ? 0 : -1, - onClick, - }), + const getDaysCellProps = React.useCallback( + (externalProps: GenericHTMLProps) => { + return mergeReactProps(externalProps, { + role: 'gridcell', + 'aria-selected': ctx.isSelected, + 'aria-current': isCurrent ? 'date' : undefined, + 'aria-colindex': ctx.colIndex + 1, + children: formattedValue, + disabled: ctx.isDisabled, + tabIndex: ctx.isTabbable ? 0 : -1, + onClick, + }); + }, [ formattedValue, ctx.isSelected, @@ -40,7 +44,7 @@ export function useBaseCalendarDaysCell(parameters: useBaseCalendarDaysCell.Para ], ); - return React.useMemo(() => ({ baseProps, isCurrent }), [baseProps, isCurrent]); + return React.useMemo(() => ({ getDaysCellProps, isCurrent }), [getDaysCellProps, isCurrent]); } export namespace useBaseCalendarDaysCell { @@ -54,7 +58,10 @@ export namespace useBaseCalendarDaysCell { * @default utils.formats.dayOfMonth */ format?: string; - ctx: useBaseCalendarDaysCell.Context; + /** + * The memoized context forwarded by the wrapper component so that this component does not need to subscribe to any context. + */ + ctx: Context; } export interface Context { diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRow.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRow.ts index a4bd257b421d0..a953e3397052b 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRow.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRow.ts @@ -38,7 +38,10 @@ export namespace useBaseCalendarDaysWeekRow { * The date object representing the week. */ value: PickerValidDate; - ctx: useBaseCalendarDaysWeekRow.Context; + /** + * The memoized context forwarded by the wrapper component so that this component does not need to subscribe to any context. + */ + ctx: Context; children?: (parameters: ChildrenParameters) => React.ReactNode; } diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarRoot.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarRoot.ts index 57c7987000a69..80f3136f39eb5 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarRoot.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarRoot.ts @@ -25,20 +25,25 @@ export function useBaseCalendarRoot< TValidationProps extends Required, >(parameters: useBaseCalendarRoot.Parameters) { const { + // Form props readOnly = false, disabled = false, + // Focus and navigation props autoFocus = false, + monthPageSize = 1, + yearPageSize = 1, + // Value props onError, defaultValue, onValueChange, value: valueProp, timezone: timezoneProp, referenceDate: referenceDateProp, - monthPageSize = 1, - yearPageSize = 1, - manager, + // Validation props dateValidationProps, valueValidationProps, + // Manager props + manager, calendarValueManager: { getDateToUseForReferenceDate, getNewValueFromNewSelectedDate, @@ -287,6 +292,11 @@ export namespace useBaseCalendarRoot { * The manager of the calendar (uses `useDateManager` for Calendar and `useDateRangeManager` for RangeCalendar). */ manager: PickerManager; + /** + * The methods needed to manage the value of the calendar. + * It helps sharing the code between the Calendar and the RangeCalendar. + */ + calendarValueManager: ValueManager; /** * The props used to validate a single date. */ @@ -295,7 +305,6 @@ export namespace useBaseCalendarRoot { * The props used to validate the value. */ valueValidationProps: TValidationProps; - calendarValueManager: ValueManager; } export interface ReturnValue { diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-month/useBaseCalendarSetVisibleMonth.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-month/useBaseCalendarSetVisibleMonth.ts index 61bd50912ec60..de5b59187ba7f 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-month/useBaseCalendarSetVisibleMonth.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-month/useBaseCalendarSetVisibleMonth.ts @@ -28,6 +28,9 @@ export namespace useBaseCalendarSetVisibleMonth { * The month to navigate to. */ target: 'previous' | 'next' | PickerValidDate; + /** + * The memoized context forwarded by the wrapper component so that this component does not need to subscribe to any context. + */ ctx: Context; } diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-year/useBaseCalendarSetVisibleYear.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-year/useBaseCalendarSetVisibleYear.ts index f964025c27cbc..e594833fbcd0c 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-year/useBaseCalendarSetVisibleYear.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-year/useBaseCalendarSetVisibleYear.ts @@ -28,6 +28,9 @@ export namespace useBaseCalendarSetVisibleYear { * The year to navigate to. */ target: 'previous' | 'next' | PickerValidDate; + /** + * The memoized context forwarded by the wrapper component so that this component does not need to subscribe to any context. + */ ctx: Context; } From 1ec28a63be55d76f2ba4fe5f3d1a2f43137ff0fa Mon Sep 17 00:00:00 2001 From: flavien Date: Fri, 10 Jan 2025 12:18:41 +0100 Subject: [PATCH 088/136] Add month and year navigation in range calendar --- .../base-calendar/DateRangeCalendarDemo.js | 117 +++++++++++++ .../base-calendar/DateRangeCalendarDemo.tsx | 161 ++++++++++++++++++ .../base-calendar/DayRangeCalendarDemo.js | 19 +-- .../base-calendar/DayRangeCalendarDemo.tsx | 25 +-- .../DayRangeCalendarDemo.tsx.preview | 1 - .../base-calendar/base-calendar.md | 4 + .../base/RangeCalendar/index.parts.ts | 14 +- .../months-cell/RangeCalendarMonthsCell.tsx | 81 +++++++++ .../RangeCalendarMonthsCellDataAttributes.ts | 18 ++ .../months-grid/RangeCalendarMonthsGrid.tsx | 54 ++++++ .../RangeCalendarMonthsGridCssVars.ts | 6 + .../RangeCalendarMonthsGridDataAttributes.ts | 1 + .../months-list/RangeCalendarMonthsList.tsx | 50 ++++++ .../RangeCalendarMonthsListDataAttributes.ts | 1 + .../years-cell/RangeCalendarYearsCell.tsx | 81 +++++++++ .../RangeCalendarYearsCellDataAttributes.ts | 18 ++ .../years-grid/RangeCalendarYearsGrid.tsx | 49 ++++++ .../RangeCalendarYearsGridCssVars.ts | 6 + .../RangeCalendarYearsGridDataAttributes.ts | 1 + .../years-list/RangeCalendarYearsList.tsx | 49 ++++++ .../RangeCalendarYearsListDataAttributes.ts | 1 + .../months-cell/CalendarMonthsCell.tsx | 123 +------------ .../months-grid/CalendarMonthsGrid.tsx | 25 ++- .../months-list/CalendarMonthsList.tsx | 13 +- .../base/Calendar/utils/useYearsCells.ts | 35 ---- .../Calendar/years-cell/CalendarYearsCell.tsx | 119 +------------ .../Calendar/years-grid/CalendarYearsGrid.tsx | 15 +- .../Calendar/years-list/CalendarYearsList.tsx | 13 +- .../useBaseCalendarDaysCellWrapper.ts | 2 +- .../months-cell/useBaseCalendarMonthsCell.ts} | 12 +- .../useBaseCalendarMonthsCellWrapper.ts | 139 +++++++++++++++ .../BaseCalendarMonthsGridOrListContext.ts | 32 ++++ .../months-grid/useBaseCalendarMonthsGrid.ts} | 34 ++-- .../months-list/useBaseCalendarMonthsList.ts} | 18 +- .../useBaseCalendarDaysGridsNavigation.ts | 2 +- .../useBaseCalendarSetVisibleMonth.ts | 4 +- .../useBaseCalendarSetVisibleMonthWrapper.ts | 14 +- .../useBaseCalendarSetVisibleYear.ts | 4 +- .../useBaseCalendarSetVisibleYearWrapper.ts | 14 +- .../utils/keyboardNavigation.ts | 0 .../base-calendar}/utils/useMonthsCells.ts | 40 ++++- .../base-calendar/utils/useYearsCells.ts | 57 +++++++ .../years-cell/useBaseCalendarYearsCell.ts} | 12 +- .../useBaseCalendarYearsCellWrapper.ts | 134 +++++++++++++++ .../BaseCalendarYearsGridOrListContext.ts | 32 ++++ .../years-grid/useBaseCalendarYearsGrid.ts} | 32 ++-- .../years-list/useBaseCalendarYearsList.ts} | 16 +- 47 files changed, 1300 insertions(+), 398 deletions(-) create mode 100644 docs/data/date-pickers/base-calendar/DateRangeCalendarDemo.js create mode 100644 docs/data/date-pickers/base-calendar/DateRangeCalendarDemo.tsx delete mode 100644 docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx.preview create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/RangeCalendarMonthsCell.tsx create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/RangeCalendarMonthsCellDataAttributes.ts create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-grid/RangeCalendarMonthsGrid.tsx create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-grid/RangeCalendarMonthsGridCssVars.ts create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-grid/RangeCalendarMonthsGridDataAttributes.ts create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-list/RangeCalendarMonthsList.tsx create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-list/RangeCalendarMonthsListDataAttributes.ts create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-cell/RangeCalendarYearsCell.tsx create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-cell/RangeCalendarYearsCellDataAttributes.ts create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-grid/RangeCalendarYearsGrid.tsx create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-grid/RangeCalendarYearsGridCssVars.ts create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-grid/RangeCalendarYearsGridDataAttributes.ts create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-list/RangeCalendarYearsList.tsx create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-list/RangeCalendarYearsListDataAttributes.ts delete mode 100644 packages/x-date-pickers/src/internals/base/Calendar/utils/useYearsCells.ts rename packages/x-date-pickers/src/internals/base/{Calendar/months-cell/useCalendarMonthsCell.ts => utils/base-calendar/months-cell/useBaseCalendarMonthsCell.ts} (81%) create mode 100644 packages/x-date-pickers/src/internals/base/utils/base-calendar/months-cell/useBaseCalendarMonthsCellWrapper.ts create mode 100644 packages/x-date-pickers/src/internals/base/utils/base-calendar/months-grid/BaseCalendarMonthsGridOrListContext.ts rename packages/x-date-pickers/src/internals/base/{Calendar/months-grid/useCalendarMonthsGrid.ts => utils/base-calendar/months-grid/useBaseCalendarMonthsGrid.ts} (71%) rename packages/x-date-pickers/src/internals/base/{Calendar/months-list/useCalendarMonthsList.ts => utils/base-calendar/months-list/useBaseCalendarMonthsList.ts} (78%) rename packages/x-date-pickers/src/internals/base/{Calendar => utils/base-calendar}/utils/keyboardNavigation.ts (100%) rename packages/x-date-pickers/src/internals/base/{Calendar => utils/base-calendar}/utils/useMonthsCells.ts (62%) create mode 100644 packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useYearsCells.ts rename packages/x-date-pickers/src/internals/base/{Calendar/years-cell/useCalendarYearsCell.ts => utils/base-calendar/years-cell/useBaseCalendarYearsCell.ts} (81%) create mode 100644 packages/x-date-pickers/src/internals/base/utils/base-calendar/years-cell/useBaseCalendarYearsCellWrapper.ts create mode 100644 packages/x-date-pickers/src/internals/base/utils/base-calendar/years-grid/BaseCalendarYearsGridOrListContext.ts rename packages/x-date-pickers/src/internals/base/{Calendar/years-grid/useCalendarYearsGrid.ts => utils/base-calendar/years-grid/useBaseCalendarYearsGrid.ts} (62%) rename packages/x-date-pickers/src/internals/base/{Calendar/years-list/useCalendarYearsList.ts => utils/base-calendar/years-list/useBaseCalendarYearsList.ts} (70%) diff --git a/docs/data/date-pickers/base-calendar/DateRangeCalendarDemo.js b/docs/data/date-pickers/base-calendar/DateRangeCalendarDemo.js new file mode 100644 index 0000000000000..f55d1201d3137 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/DateRangeCalendarDemo.js @@ -0,0 +1,117 @@ +import * as React from 'react'; +import clsx from 'clsx'; + +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +// eslint-disable-next-line no-restricted-imports +import { + RangeCalendar, + useRangeCalendarContext, +} from '@mui/x-date-pickers-pro/internals/base/RangeCalendar'; +import styles from './calendar.module.css'; + +function Header(props) { + const { activeSection, onActiveSectionChange } = props; + const { visibleDate } = useRangeCalendarContext(); + + return ( +
+ + ◀ + + {visibleDate.format('MMMM YYYY')} + + ▶ + +
+ ); +} + +export default function DateRangeCalendarDemo() { + const [activeSection, setActiveSection] = React.useState('day'); + + return ( + + +
+ {activeSection === 'year' && ( + + {({ years }) => + years.map((year) => ( + setActiveSection('day')} + > + {year.format('YYYY')} + + )) + } + + )} + {/* {activeSection === 'month' && ( + + {({ months }) => + months.map((month) => ( + setActiveSection('day')} + > + {month.format('MMMM')} + + )) + } + + )} */} + {activeSection === 'day' && ( + + + {({ days }) => + days.map((day) => ( + + )) + } + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => + days.map((day) => ( + + )) + } + + )) + } + + + )} + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/DateRangeCalendarDemo.tsx b/docs/data/date-pickers/base-calendar/DateRangeCalendarDemo.tsx new file mode 100644 index 0000000000000..abf9caa288cec --- /dev/null +++ b/docs/data/date-pickers/base-calendar/DateRangeCalendarDemo.tsx @@ -0,0 +1,161 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +// eslint-disable-next-line no-restricted-imports +import { + RangeCalendar, + useRangeCalendarContext, +} from '@mui/x-date-pickers-pro/internals/base/RangeCalendar'; +import styles from './calendar.module.css'; + +function Header(props: { + activeSection: 'day' | 'month' | 'year'; + onActiveSectionChange: (newActiveSection: 'day' | 'month' | 'year') => void; +}) { + const { activeSection, onActiveSectionChange } = props; + const { visibleDate } = useRangeCalendarContext(); + + return ( +
+
+ + ◀ + + + + ▶ + +
+
+ + ◀ + + + + ▶ + +
+
+ ); +} + +export default function DateRangeCalendarDemo() { + const [activeSection, setActiveSection] = React.useState<'day' | 'month' | 'year'>( + 'day', + ); + + return ( + + +
+ {activeSection === 'year' && ( + + {({ years }) => + years.map((year) => ( + setActiveSection('day')} + target={year} + > + {year.format('YYYY')} + + )) + } + + )} + {activeSection === 'month' && ( + + {({ months }) => + months.map((month) => ( + setActiveSection('day')} + target={month} + > + {month.format('MMMM')} + + )) + } + + )} + {activeSection === 'day' && ( + + + {({ days }) => + days.map((day) => ( + + )) + } + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => + days.map((day) => ( + + )) + } + + )) + } + + + )} + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.js b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.js index e67c204c64a7b..87c21203d3a60 100644 --- a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.js +++ b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.js @@ -1,6 +1,5 @@ import * as React from 'react'; import clsx from 'clsx'; - import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports @@ -32,10 +31,10 @@ function Header() { ); } -function DayRangeCalendar(props) { +export default function DayRangeCalendarDemo() { return ( - +
@@ -75,17 +74,3 @@ function DayRangeCalendar(props) { ); } - -export default function DayRangeCalendarDemo() { - const [value, setValue] = React.useState([null, null]); - - const handleValueChange = React.useCallback((newValue) => { - setValue(newValue); - }, []); - - return ( - - - - ); -} diff --git a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx index ec645716124b8..87c21203d3a60 100644 --- a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx +++ b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; import clsx from 'clsx'; -import { Dayjs } from 'dayjs'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports @@ -32,10 +31,10 @@ function Header() { ); } -function DayRangeCalendar(props: Omit) { +export default function DayRangeCalendarDemo() { return ( - +
@@ -75,23 +74,3 @@ function DayRangeCalendar(props: Omit) { ); } - -export default function DayRangeCalendarDemo() { - const [value, setValue] = React.useState<[Dayjs | null, Dayjs | null]>([ - null, - null, - ]); - - const handleValueChange = React.useCallback( - (newValue: [Dayjs | null, Dayjs | null]) => { - setValue(newValue); - }, - [], - ); - - return ( - - - - ); -} diff --git a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx.preview b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx.preview deleted file mode 100644 index 16ef550120c15..0000000000000 --- a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx.preview +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/data/date-pickers/base-calendar/base-calendar.md b/docs/data/date-pickers/base-calendar/base-calendar.md index f4138d59b2f9e..5f6b91da8d770 100644 --- a/docs/data/date-pickers/base-calendar/base-calendar.md +++ b/docs/data/date-pickers/base-calendar/base-calendar.md @@ -77,3 +77,7 @@ TODO ### Multiple visible months {{"demo": "DayRangeCalendarWithTwoMonthsDemo.js"}} + +## Date Range Calendar + +{{"demo": "DateRangeCalendarDemo.js"}} diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/index.parts.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/index.parts.ts index 921f1c9fcefe6..7bd3a13a9804a 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/index.parts.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/index.parts.ts @@ -9,14 +9,16 @@ export { RangeCalendarDaysWeekRow as DaysWeekRow } from './days-week-row/RangeCa export { RangeCalendarDaysCell as DaysCell } from './days-cell/RangeCalendarDaysCell'; // // Months -// export { CalendarMonthsList as MonthsList } from './months-list/CalendarMonthsList'; -// export { CalendarMonthsGrid as MonthsGrid } from './months-grid/CalendarMonthsGrid'; -// export { CalendarMonthsCell as MonthsCell } from './months-cell/CalendarMonthsCell'; +export { RangeCalendarMonthsList as MonthsList } from './months-list/RangeCalendarMonthsList'; +export { RangeCalendarMonthsGrid as MonthsGrid } from './months-grid/RangeCalendarMonthsGrid'; +// TODO: Uncomment when the component supports good range editing +// export { RangeCalendarMonthsCell as MonthsCell } from './months-cell/RangeCalendarMonthsCell'; // // Years -// export { CalendarYearsList as YearsList } from './years-list/CalendarYearsList'; -// export { CalendarYearsGrid as YearsGrid } from './years-grid/CalendarYearsGrid'; -// export { CalendarYearsCell as YearsCell } from './years-cell/CalendarYearsCell'; +export { RangeCalendarYearsList as YearsList } from './years-list/RangeCalendarYearsList'; +export { RangeCalendarYearsGrid as YearsGrid } from './years-grid/RangeCalendarYearsGrid'; +// TODO: Uncomment when the component supports good range editing +// export { RangeCalendarYearsCell as YearsCell } from './years-cell/RangeCalendarYearsCell'; // Navigation export { RangeCalendarSetVisibleMonth as SetVisibleMonth } from './set-visible-month/RangeCalendarSetVisibleMonth'; diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/RangeCalendarMonthsCell.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/RangeCalendarMonthsCell.tsx new file mode 100644 index 0000000000000..233bd82a98587 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/RangeCalendarMonthsCell.tsx @@ -0,0 +1,81 @@ +'use client'; +import * as React from 'react'; +// eslint-disable-next-line no-restricted-imports +import { BaseUIComponentProps } from '@mui/x-date-pickers/internals/base/base-utils/types'; +// eslint-disable-next-line no-restricted-imports +import { useComponentRenderer } from '@mui/x-date-pickers/internals/base/base-utils/useComponentRenderer'; +// eslint-disable-next-line no-restricted-imports +import { useBaseCalendarMonthsCell } from '@mui/x-date-pickers/internals/base/utils/base-calendar/months-cell/useBaseCalendarMonthsCell'; +// eslint-disable-next-line no-restricted-imports +import { useBaseCalendarMonthsCellWrapper } from '@mui/x-date-pickers/internals/base/utils/base-calendar/months-cell/useBaseCalendarMonthsCellWrapper'; + +const InnerRangeCalendarMonthsCell = React.forwardRef(function InnerRangeCalendarMonthsCell( + props: InnerRangeCalendarMonthsCellProps, + forwardedRef: React.ForwardedRef, +) { + const { className, render, value, format, ctx, ...otherProps } = props; + const { getMonthsCellProps, isCurrent } = useBaseCalendarMonthsCell({ value, format, ctx }); + + const state: RangeCalendarMonthsCell.State = React.useMemo( + () => ({ + selected: ctx.isSelected, + disabled: ctx.isDisabled, + invalid: ctx.isInvalid, + current: isCurrent, + }), + [ctx.isSelected, ctx.isDisabled, ctx.isInvalid, isCurrent], + ); + + const { renderElement } = useComponentRenderer({ + propGetter: getMonthsCellProps, + render: render ?? 'button', + ref: forwardedRef, + className, + state, + extraProps: otherProps, + }); + + return renderElement(); +}); + +const MemoizedInnerRangeCalendarMonthsCell = React.memo(InnerRangeCalendarMonthsCell); + +const RangeCalendarMonthsCell = React.forwardRef(function RangeCalendarMonthsCell( + props: RangeCalendarMonthsCell.Props, + forwardedRef: React.ForwardedRef, +) { + const { ref, ctx } = useBaseCalendarMonthsCellWrapper({ value: props.value, forwardedRef }); + + return ; +}); + +export namespace RangeCalendarMonthsCell { + export interface State { + /** + * Whether the month is selected. + */ + selected: boolean; + /** + * Whether the month is disabled. + */ + disabled: boolean; + /** + * Whether the month is invalid. + */ + invalid: boolean; + /** + * Whether the month contains the current date. + */ + current: boolean; + } + + export interface Props + extends Omit, + Omit, 'value'> {} +} + +interface InnerRangeCalendarMonthsCellProps + extends useBaseCalendarMonthsCell.Parameters, + Omit, 'value'> {} + +export { RangeCalendarMonthsCell }; diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/RangeCalendarMonthsCellDataAttributes.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/RangeCalendarMonthsCellDataAttributes.ts new file mode 100644 index 0000000000000..649f69ee87237 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/RangeCalendarMonthsCellDataAttributes.ts @@ -0,0 +1,18 @@ +export enum RangeCalendarMonthsCellDataAttributes { + /** + * Present when the month is selected. + */ + selected = 'data-selected', + /** + * Present when the month is disabled. + */ + disabled = 'data-disabled', + /** + * Present when the month is invalid. + */ + invalid = 'data-invalid', + /** + * Present when the month contains the current date. + */ + current = 'data-current', +} diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-grid/RangeCalendarMonthsGrid.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-grid/RangeCalendarMonthsGrid.tsx new file mode 100644 index 0000000000000..58f47af7d77be --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-grid/RangeCalendarMonthsGrid.tsx @@ -0,0 +1,54 @@ +'use client'; +import * as React from 'react'; +// eslint-disable-next-line no-restricted-imports +import { BaseUIComponentProps } from '@mui/x-date-pickers/internals/base/base-utils/types'; +// eslint-disable-next-line no-restricted-imports +import { useComponentRenderer } from '@mui/x-date-pickers/internals/base/base-utils/useComponentRenderer'; +// eslint-disable-next-line no-restricted-imports +import { useBaseCalendarMonthsGrid } from '@mui/x-date-pickers/internals/base/utils/base-calendar/months-grid/useBaseCalendarMonthsGrid'; +// eslint-disable-next-line no-restricted-imports +import { BaseCalendarMonthsGridOrListContext } from '@mui/x-date-pickers/internals/base/utils/base-calendar/months-grid/BaseCalendarMonthsGridOrListContext'; +// eslint-disable-next-line no-restricted-imports +import { CompositeList } from '@mui/x-date-pickers/internals/base/composite/list/CompositeList'; +import { RangeCalendarMonthsGridCssVars } from './RangeCalendarMonthsGridCssVars'; + +const RangeCalendarMonthsGrid = React.forwardRef(function RangeCalendarMonthsList( + props: RangeCalendarMonthsGrid.Props, + forwardedRef: React.ForwardedRef, +) { + const { className, render, children, cellsPerRow, canChangeYear, ...otherProps } = props; + const { getMonthsGridProps, monthsCellRefs, monthsListOrGridContext } = useBaseCalendarMonthsGrid( + { + children, + cellsPerRow, + canChangeYear, + cellsPerRowCssVar: RangeCalendarMonthsGridCssVars.calendarMonthsGridCellsPerRow, + }, + ); + const state = React.useMemo(() => ({}), []); + + const { renderElement } = useComponentRenderer({ + propGetter: getMonthsGridProps, + render: render ?? 'div', + ref: forwardedRef, + className, + state, + extraProps: otherProps, + }); + + return ( + + {renderElement()} + + ); +}); + +export namespace RangeCalendarMonthsGrid { + export interface State {} + + export interface Props + extends Omit, 'children'>, + useBaseCalendarMonthsGrid.PublicParameters {} +} + +export { RangeCalendarMonthsGrid }; diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-grid/RangeCalendarMonthsGridCssVars.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-grid/RangeCalendarMonthsGridCssVars.ts new file mode 100644 index 0000000000000..af581a313e242 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-grid/RangeCalendarMonthsGridCssVars.ts @@ -0,0 +1,6 @@ +export enum RangeCalendarMonthsGridCssVars { + /** + * The number of cells per row in the grid. + */ + calendarMonthsGridCellsPerRow = '--calendar-months-grid-cells-per-row', +} diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-grid/RangeCalendarMonthsGridDataAttributes.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-grid/RangeCalendarMonthsGridDataAttributes.ts new file mode 100644 index 0000000000000..40389add036d3 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-grid/RangeCalendarMonthsGridDataAttributes.ts @@ -0,0 +1 @@ +export enum RangeCalendarMonthsGridDataAttributes {} diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-list/RangeCalendarMonthsList.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-list/RangeCalendarMonthsList.tsx new file mode 100644 index 0000000000000..a581758a9f354 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-list/RangeCalendarMonthsList.tsx @@ -0,0 +1,50 @@ +'use client'; +import * as React from 'react'; +// eslint-disable-next-line no-restricted-imports +import { useBaseCalendarMonthsList } from '@mui/x-date-pickers/internals/base/utils/base-calendar/months-list/useBaseCalendarMonthsList'; +// eslint-disable-next-line no-restricted-imports +import { BaseUIComponentProps } from '@mui/x-date-pickers/internals/base/base-utils/types'; +// eslint-disable-next-line no-restricted-imports +import { useComponentRenderer } from '@mui/x-date-pickers/internals/base/base-utils/useComponentRenderer'; +// eslint-disable-next-line no-restricted-imports +import { CompositeList } from '@mui/x-date-pickers/internals/base/composite/list/CompositeList'; +// eslint-disable-next-line no-restricted-imports +import { BaseCalendarMonthsGridOrListContext } from '@mui/x-date-pickers/internals/base/utils/base-calendar/months-grid/BaseCalendarMonthsGridOrListContext'; + +const RangeCalendarMonthsList = React.forwardRef(function RangeCalendarMonthsList( + props: RangeCalendarMonthsList.Props, + forwardedRef: React.ForwardedRef, +) { + const { className, render, children, loop, canChangeYear, ...otherProps } = props; + const { getMonthListProps, monthsCellRefs, monthsListOrGridContext } = useBaseCalendarMonthsList({ + children, + loop, + canChangeYear, + }); + const state = React.useMemo(() => ({}), []); + + const { renderElement } = useComponentRenderer({ + propGetter: getMonthListProps, + render: render ?? 'div', + ref: forwardedRef, + className, + state, + extraProps: otherProps, + }); + + return ( + + {renderElement()} + + ); +}); + +export namespace RangeCalendarMonthsList { + export interface State {} + + export interface Props + extends Omit, 'children'>, + useBaseCalendarMonthsList.Parameters {} +} + +export { RangeCalendarMonthsList }; diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-list/RangeCalendarMonthsListDataAttributes.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-list/RangeCalendarMonthsListDataAttributes.ts new file mode 100644 index 0000000000000..00e6c4a69c3d5 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-list/RangeCalendarMonthsListDataAttributes.ts @@ -0,0 +1 @@ +export enum RangeCalendarMonthsListDataAttributes {} diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-cell/RangeCalendarYearsCell.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-cell/RangeCalendarYearsCell.tsx new file mode 100644 index 0000000000000..b117cafee862d --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-cell/RangeCalendarYearsCell.tsx @@ -0,0 +1,81 @@ +'use client'; +import * as React from 'react'; +// eslint-disable-next-line no-restricted-imports +import { BaseUIComponentProps } from '@mui/x-date-pickers/internals/base/base-utils/types'; +// eslint-disable-next-line no-restricted-imports +import { useComponentRenderer } from '@mui/x-date-pickers/internals/base/base-utils/useComponentRenderer'; +// eslint-disable-next-line no-restricted-imports +import { useBaseCalendarYearsCell } from '@mui/x-date-pickers/internals/base/utils/base-calendar/years-cell/useBaseCalendarYearsCell'; +// eslint-disable-next-line no-restricted-imports +import { useBaseCalendarYearsCellWrapper } from '@mui/x-date-pickers/internals/base/utils/base-calendar/years-cell/useBaseCalendarYearsCellWrapper'; + +const InnerRangeCalendarYearsCell = React.forwardRef(function InnerRangeCalendarYearsCell( + props: InnerRangeCalendarYearsCellProps, + forwardedRef: React.ForwardedRef, +) { + const { className, render, value, format, ctx, ...otherProps } = props; + const { getYearCellProps, isCurrent } = useBaseCalendarYearsCell({ value, format, ctx }); + + const state: RangeCalendarYearsCell.State = React.useMemo( + () => ({ + selected: ctx.isSelected, + disabled: ctx.isDisabled, + invalid: ctx.isInvalid, + current: isCurrent, + }), + [ctx.isSelected, ctx.isDisabled, ctx.isInvalid, isCurrent], + ); + + const { renderElement } = useComponentRenderer({ + propGetter: getYearCellProps, + render: render ?? 'button', + ref: forwardedRef, + className, + state, + extraProps: otherProps, + }); + + return renderElement(); +}); + +const MemoizedInnerRangeCalendarYearsCell = React.memo(InnerRangeCalendarYearsCell); + +const RangeCalendarYearsCell = React.forwardRef(function RangeCalendarsYearCell( + props: RangeCalendarYearsCell.Props, + forwardedRef: React.ForwardedRef, +) { + const { ref, ctx } = useBaseCalendarYearsCellWrapper({ value: props.value, forwardedRef }); + + return ; +}); + +export namespace RangeCalendarYearsCell { + export interface State { + /** + * Whether the year is selected. + */ + selected: boolean; + /** + * Whether the year is disabled. + */ + disabled: boolean; + /** + * Whether the year is invalid. + */ + invalid: boolean; + /** + * Whether the year contains the current date. + */ + current: boolean; + } + + export interface Props + extends Omit, + Omit, 'value'> {} +} + +interface InnerRangeCalendarYearsCellProps + extends useBaseCalendarYearsCell.Parameters, + Omit, 'value'> {} + +export { RangeCalendarYearsCell }; diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-cell/RangeCalendarYearsCellDataAttributes.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-cell/RangeCalendarYearsCellDataAttributes.ts new file mode 100644 index 0000000000000..3034cbe61b274 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-cell/RangeCalendarYearsCellDataAttributes.ts @@ -0,0 +1,18 @@ +export enum RangeCalendarYearsCellDataAttributes { + /** + * Present when the year is selected. + */ + selected = 'data-selected', + /** + * Present when the year is disabled. + */ + disabled = 'data-disabled', + /** + * Present when the year is invalid. + */ + invalid = 'data-invalid', + /** + * Present when the year contains the current date. + */ + current = 'data-current', +} diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-grid/RangeCalendarYearsGrid.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-grid/RangeCalendarYearsGrid.tsx new file mode 100644 index 0000000000000..9b89c9e935ae1 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-grid/RangeCalendarYearsGrid.tsx @@ -0,0 +1,49 @@ +'use client'; +import * as React from 'react'; +// eslint-disable-next-line no-restricted-imports +import { useBaseCalendarYearsGrid } from '@mui/x-date-pickers/internals/base/utils/base-calendar/years-grid/useBaseCalendarYearsGrid'; +// eslint-disable-next-line no-restricted-imports +import { BaseUIComponentProps } from '@mui/x-date-pickers/internals/base/base-utils/types'; +// eslint-disable-next-line no-restricted-imports +import { useComponentRenderer } from '@mui/x-date-pickers/internals/base/base-utils/useComponentRenderer'; +// eslint-disable-next-line no-restricted-imports +import { CompositeList } from '@mui/x-date-pickers/internals/base/composite/list/CompositeList'; +// eslint-disable-next-line no-restricted-imports +import { BaseCalendarYearsGridOrListContext } from '@mui/x-date-pickers/internals/base/utils/base-calendar/years-grid/BaseCalendarYearsGridOrListContext'; + +const RangeCalendarYearsGrid = React.forwardRef(function CalendarYearsList( + props: RangeCalendarYearsGrid.Props, + forwardedRef: React.ForwardedRef, +) { + const { className, render, children, cellsPerRow, ...otherProps } = props; + const { getYearsGridProps, yearsCellRefs, yearsListOrGridContext } = useBaseCalendarYearsGrid({ + children, + cellsPerRow, + }); + const state = React.useMemo(() => ({}), []); + + const { renderElement } = useComponentRenderer({ + propGetter: getYearsGridProps, + render: render ?? 'div', + ref: forwardedRef, + className, + state, + extraProps: otherProps, + }); + + return ( + + {renderElement()} + + ); +}); + +export namespace RangeCalendarYearsGrid { + export interface State {} + + export interface Props + extends Omit, 'children'>, + useBaseCalendarYearsGrid.Parameters {} +} + +export { RangeCalendarYearsGrid }; diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-grid/RangeCalendarYearsGridCssVars.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-grid/RangeCalendarYearsGridCssVars.ts new file mode 100644 index 0000000000000..a2d836f043698 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-grid/RangeCalendarYearsGridCssVars.ts @@ -0,0 +1,6 @@ +export enum RangeCalendarYearsGridCssVars { + /** + * The number of cells per row in the grid. + */ + rangeCalendarYearsGridCellsPerRow = '--range-calendar-years-grid-cells-per-row', +} diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-grid/RangeCalendarYearsGridDataAttributes.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-grid/RangeCalendarYearsGridDataAttributes.ts new file mode 100644 index 0000000000000..db0c41c99b549 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-grid/RangeCalendarYearsGridDataAttributes.ts @@ -0,0 +1 @@ +export enum CalendarYearsGridDataAttributes {} diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-list/RangeCalendarYearsList.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-list/RangeCalendarYearsList.tsx new file mode 100644 index 0000000000000..322d0df1385cc --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-list/RangeCalendarYearsList.tsx @@ -0,0 +1,49 @@ +'use client'; +import * as React from 'react'; +// eslint-disable-next-line no-restricted-imports +import { useBaseCalendarYearsList } from '@mui/x-date-pickers/internals/base/utils/base-calendar/years-list/useBaseCalendarYearsList'; +// eslint-disable-next-line no-restricted-imports +import { BaseUIComponentProps } from '@mui/x-date-pickers/internals/base/base-utils/types'; +// eslint-disable-next-line no-restricted-imports +import { useComponentRenderer } from '@mui/x-date-pickers/internals/base/base-utils/useComponentRenderer'; +// eslint-disable-next-line no-restricted-imports +import { CompositeList } from '@mui/x-date-pickers/internals/base/composite/list/CompositeList'; +// eslint-disable-next-line no-restricted-imports +import { BaseCalendarYearsGridOrListContext } from '@mui/x-date-pickers/internals/base/utils/base-calendar/years-grid/BaseCalendarYearsGridOrListContext'; + +const RangeCalendarYearsList = React.forwardRef(function CalendarYearsList( + props: CalendarYearsList.Props, + forwardedRef: React.ForwardedRef, +) { + const { className, render, children, loop, ...otherProps } = props; + const { getYearsListProps, yearsCellRefs, yearsListOrGridContext } = useBaseCalendarYearsList({ + children, + loop, + }); + const state = React.useMemo(() => ({}), []); + + const { renderElement } = useComponentRenderer({ + propGetter: getYearsListProps, + render: render ?? 'div', + ref: forwardedRef, + className, + state, + extraProps: otherProps, + }); + + return ( + + {renderElement()} + + ); +}); + +export namespace CalendarYearsList { + export interface State {} + + export interface Props + extends Omit, 'children'>, + useBaseCalendarYearsList.Parameters {} +} + +export { RangeCalendarYearsList as RangeCalendarYearsList }; diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-list/RangeCalendarYearsListDataAttributes.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-list/RangeCalendarYearsListDataAttributes.ts new file mode 100644 index 0000000000000..b0d5c3446a3ef --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-list/RangeCalendarYearsListDataAttributes.ts @@ -0,0 +1 @@ +export enum CalendarYearsListDataAttributes {} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx index ae18243a7a3ca..375964fa5159e 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx @@ -1,23 +1,16 @@ 'use client'; import * as React from 'react'; -import useEventCallback from '@mui/utils/useEventCallback'; -import useForkRef from '@mui/utils/useForkRef'; -import { PickerValidDate } from '../../../../models'; -import { useNow, useUtils } from '../../../hooks/useUtils'; -import { findClosestEnabledDate } from '../../../utils/date-utils'; import { useComponentRenderer } from '../../base-utils/useComponentRenderer'; import { BaseUIComponentProps } from '../../base-utils/types'; -import { useCompositeListItem } from '../../composite/list/useCompositeListItem'; -import { useBaseCalendarRootContext } from '../../utils/base-calendar/root/BaseCalendarRootContext'; -import { useCalendarRootContext } from '../root/CalendarRootContext'; -import { useCalendarMonthsCell } from './useCalendarMonthsCell'; +import { useBaseCalendarMonthsCell } from '../../utils/base-calendar/months-cell/useBaseCalendarMonthsCell'; +import { useBaseCalendarMonthsCellWrapper } from '../../utils/base-calendar/months-cell/useBaseCalendarMonthsCellWrapper'; const InnerCalendarMonthsCell = React.forwardRef(function InnerCalendarMonthsCell( props: InnerCalendarMonthsCellProps, forwardedRef: React.ForwardedRef, ) { const { className, render, value, format, ctx, ...otherProps } = props; - const { getMonthsCellProps, isCurrent } = useCalendarMonthsCell({ value, format, ctx }); + const { getMonthsCellProps, isCurrent } = useBaseCalendarMonthsCell({ value, format, ctx }); const state: CalendarMonthsCell.State = React.useMemo( () => ({ @@ -47,111 +40,9 @@ const CalendarMonthsCell = React.forwardRef(function CalendarMonthsCell( props: CalendarMonthsCell.Props, forwardedRef: React.ForwardedRef, ) { - const rootContext = useCalendarRootContext(); - const baseRootContext = useBaseCalendarRootContext(); - const { ref: listItemRef } = useCompositeListItem(); - const utils = useUtils(); - const now = useNow(baseRootContext.timezone); - const mergedRef = useForkRef(forwardedRef, listItemRef); + const { ref, ctx } = useBaseCalendarMonthsCellWrapper({ value: props.value, forwardedRef }); - const isSelected = React.useMemo( - () => baseRootContext.selectedDates.some((date) => utils.isSameMonth(date, props.value)), - [baseRootContext.selectedDates, props.value, utils], - ); - - const isInvalid = React.useMemo(() => { - const firstEnabledMonth = utils.startOfMonth( - baseRootContext.dateValidationProps.disablePast && - utils.isAfter(now, baseRootContext.dateValidationProps.minDate) - ? now - : baseRootContext.dateValidationProps.minDate, - ); - - const lastEnabledMonth = utils.startOfMonth( - baseRootContext.dateValidationProps.disableFuture && - utils.isBefore(now, baseRootContext.dateValidationProps.maxDate) - ? now - : baseRootContext.dateValidationProps.maxDate, - ); - - const monthToValidate = utils.startOfMonth(props.value); - - if (utils.isBefore(monthToValidate, firstEnabledMonth)) { - return true; - } - - if (utils.isAfter(monthToValidate, lastEnabledMonth)) { - return true; - } - - if (!baseRootContext.dateValidationProps.shouldDisableMonth) { - return false; - } - - return baseRootContext.dateValidationProps.shouldDisableMonth(monthToValidate); - }, [baseRootContext.dateValidationProps, props.value, now, utils]); - - const isDisabled = React.useMemo(() => { - if (baseRootContext.disabled) { - return true; - } - - return isInvalid; - }, [baseRootContext.disabled, isInvalid]); - - const isTabbable = React.useMemo( - () => - utils.isValid(rootContext.value) - ? isSelected - : utils.isSameMonth(baseRootContext.currentDate, props.value), - [utils, rootContext.value, baseRootContext.currentDate, isSelected, props.value], - ); - - const selectMonth = useEventCallback((newValue: PickerValidDate) => { - if (baseRootContext.readOnly) { - return; - } - - const newCleanValue = utils.setMonth(baseRootContext.currentDate, utils.getMonth(newValue)); - - const startOfMonth = utils.startOfMonth(newCleanValue); - const endOfMonth = utils.endOfMonth(newCleanValue); - - const closestEnabledDate = baseRootContext.isDateInvalid(newCleanValue) - ? findClosestEnabledDate({ - utils, - date: newCleanValue, - minDate: utils.isBefore(baseRootContext.dateValidationProps.minDate, startOfMonth) - ? startOfMonth - : baseRootContext.dateValidationProps.minDate, - maxDate: utils.isAfter(baseRootContext.dateValidationProps.maxDate, endOfMonth) - ? endOfMonth - : baseRootContext.dateValidationProps.maxDate, - disablePast: baseRootContext.dateValidationProps.disablePast, - disableFuture: baseRootContext.dateValidationProps.disableFuture, - isDateDisabled: baseRootContext.isDateInvalid, - timezone: baseRootContext.timezone, - }) - : newCleanValue; - - if (closestEnabledDate) { - baseRootContext.setVisibleDate(closestEnabledDate, true); - baseRootContext.selectDate(closestEnabledDate, { section: 'month' }); - } - }); - - const ctx = React.useMemo( - () => ({ - isSelected, - isDisabled, - isInvalid, - isTabbable, - selectMonth, - }), - [isSelected, isDisabled, isInvalid, isTabbable, selectMonth], - ); - - return ; + return ; }); export namespace CalendarMonthsCell { @@ -175,12 +66,12 @@ export namespace CalendarMonthsCell { } export interface Props - extends Omit, + extends Omit, Omit, 'value'> {} } interface InnerCalendarMonthsCellProps - extends useCalendarMonthsCell.Parameters, + extends useBaseCalendarMonthsCell.Parameters, Omit, 'value'> {} export { CalendarMonthsCell }; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-grid/CalendarMonthsGrid.tsx b/packages/x-date-pickers/src/internals/base/Calendar/months-grid/CalendarMonthsGrid.tsx index 51fc7b68de4b0..22bfe4f6b0746 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-grid/CalendarMonthsGrid.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-grid/CalendarMonthsGrid.tsx @@ -1,20 +1,25 @@ 'use client'; import * as React from 'react'; -import { useCalendarMonthsGrid } from './useCalendarMonthsGrid'; +import { useBaseCalendarMonthsGrid } from '../../utils/base-calendar/months-grid/useBaseCalendarMonthsGrid'; import { BaseUIComponentProps } from '../../base-utils/types'; import { useComponentRenderer } from '../../base-utils/useComponentRenderer'; import { CompositeList } from '../../composite/list/CompositeList'; +import { CalendarMonthsGridCssVars } from './CalendarMonthsGridCssVars'; +import { BaseCalendarMonthsGridOrListContext } from '../../utils/base-calendar/months-grid/BaseCalendarMonthsGridOrListContext'; const CalendarMonthsGrid = React.forwardRef(function CalendarMonthsList( props: CalendarMonthsGrid.Props, forwardedRef: React.ForwardedRef, ) { const { className, render, children, cellsPerRow, canChangeYear, ...otherProps } = props; - const { getMonthsGridProps, monthsCellRefs } = useCalendarMonthsGrid({ - children, - cellsPerRow, - canChangeYear, - }); + const { getMonthsGridProps, monthsCellRefs, monthsListOrGridContext } = useBaseCalendarMonthsGrid( + { + children, + cellsPerRow, + canChangeYear, + cellsPerRowCssVar: CalendarMonthsGridCssVars.calendarMonthsGridCellsPerRow, + }, + ); const state = React.useMemo(() => ({}), []); const { renderElement } = useComponentRenderer({ @@ -26,7 +31,11 @@ const CalendarMonthsGrid = React.forwardRef(function CalendarMonthsList( extraProps: otherProps, }); - return {renderElement()}; + return ( + + {renderElement()} + + ); }); export namespace CalendarMonthsGrid { @@ -34,7 +43,7 @@ export namespace CalendarMonthsGrid { export interface Props extends Omit, 'children'>, - useCalendarMonthsGrid.Parameters {} + useBaseCalendarMonthsGrid.PublicParameters {} } export { CalendarMonthsGrid }; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsList.tsx b/packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsList.tsx index 2c115b4312b0e..8f37ecd9b8622 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsList.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsList.tsx @@ -1,16 +1,17 @@ 'use client'; import * as React from 'react'; -import { useCalendarMonthsList } from './useCalendarMonthsList'; +import { useBaseCalendarMonthsList } from '../../utils/base-calendar/months-list/useBaseCalendarMonthsList'; import { BaseUIComponentProps } from '../../base-utils/types'; import { useComponentRenderer } from '../../base-utils/useComponentRenderer'; import { CompositeList } from '../../composite/list/CompositeList'; +import { BaseCalendarMonthsGridOrListContext } from '../../utils/base-calendar/months-grid/BaseCalendarMonthsGridOrListContext'; const CalendarMonthsList = React.forwardRef(function CalendarMonthsList( props: CalendarMonthsList.Props, forwardedRef: React.ForwardedRef, ) { const { className, render, children, loop, canChangeYear, ...otherProps } = props; - const { getMonthListProps, monthsCellRefs } = useCalendarMonthsList({ + const { getMonthListProps, monthsCellRefs, monthsListOrGridContext } = useBaseCalendarMonthsList({ children, loop, canChangeYear, @@ -26,7 +27,11 @@ const CalendarMonthsList = React.forwardRef(function CalendarMonthsList( extraProps: otherProps, }); - return {renderElement()}; + return ( + + {renderElement()} + + ); }); export namespace CalendarMonthsList { @@ -34,7 +39,7 @@ export namespace CalendarMonthsList { export interface Props extends Omit, 'children'>, - useCalendarMonthsList.Parameters {} + useBaseCalendarMonthsList.Parameters {} } export { CalendarMonthsList }; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/useYearsCells.ts b/packages/x-date-pickers/src/internals/base/Calendar/utils/useYearsCells.ts deleted file mode 100644 index ff57f0abb25b7..0000000000000 --- a/packages/x-date-pickers/src/internals/base/Calendar/utils/useYearsCells.ts +++ /dev/null @@ -1,35 +0,0 @@ -import * as React from 'react'; -import { PickerValidDate } from '../../../../models'; -import { useUtils } from '../../../hooks/useUtils'; -import { useBaseCalendarRootContext } from '../../utils/base-calendar/root/BaseCalendarRootContext'; - -export function useYearsCells(): useYearsCells.ReturnValue { - const baseRootContext = useBaseCalendarRootContext(); - const utils = useUtils(); - - const years = React.useMemo( - () => - utils.getYearRange([ - baseRootContext.dateValidationProps.minDate, - baseRootContext.dateValidationProps.maxDate, - ]), - [ - utils, - baseRootContext.dateValidationProps.minDate, - baseRootContext.dateValidationProps.maxDate, - ], - ); - - const registerSection = baseRootContext.registerSection; - React.useEffect(() => { - return registerSection({ type: 'month', value: baseRootContext.visibleDate }); - }, [registerSection, baseRootContext.visibleDate]); - - return { years }; -} - -export namespace useYearsCells { - export interface ReturnValue { - years: PickerValidDate[]; - } -} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx index ac600036c4592..cd7ce5a39d972 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx @@ -1,23 +1,16 @@ 'use client'; import * as React from 'react'; -import useEventCallback from '@mui/utils/useEventCallback'; -import useForkRef from '@mui/utils/useForkRef'; -import { PickerValidDate } from '../../../../models'; -import { useNow, useUtils } from '../../../hooks/useUtils'; -import { findClosestEnabledDate } from '../../../utils/date-utils'; import { useComponentRenderer } from '../../base-utils/useComponentRenderer'; import { BaseUIComponentProps } from '../../base-utils/types'; -import { useCompositeListItem } from '../../composite/list/useCompositeListItem'; -import { useBaseCalendarRootContext } from '../../utils/base-calendar/root/BaseCalendarRootContext'; -import { useCalendarRootContext } from '../root/CalendarRootContext'; -import { useCalendarYearsCell } from './useCalendarYearsCell'; +import { useBaseCalendarYearsCell } from '../../utils/base-calendar/years-cell/useBaseCalendarYearsCell'; +import { useBaseCalendarYearsCellWrapper } from '../../utils/base-calendar/years-cell/useBaseCalendarYearsCellWrapper'; const InnerCalendarYearsCell = React.forwardRef(function InnerCalendarYearsCell( props: InnerCalendarYearsCellProps, forwardedRef: React.ForwardedRef, ) { const { className, render, value, format, ctx, ...otherProps } = props; - const { getYearCellProps, isCurrent } = useCalendarYearsCell({ value, format, ctx }); + const { getYearCellProps, isCurrent } = useBaseCalendarYearsCell({ value, format, ctx }); const state: CalendarYearsCell.State = React.useMemo( () => ({ @@ -47,107 +40,9 @@ const CalendarYearsCell = React.forwardRef(function CalendarsYearCell( props: CalendarYearsCell.Props, forwardedRef: React.ForwardedRef, ) { - const rootContext = useCalendarRootContext(); - const baseRootContext = useBaseCalendarRootContext(); - const { ref: listItemRef } = useCompositeListItem(); - const utils = useUtils(); - const now = useNow(baseRootContext.timezone); - const mergedRef = useForkRef(forwardedRef, listItemRef); + const { ref, ctx } = useBaseCalendarYearsCellWrapper({ value: props.value, forwardedRef }); - const isSelected = React.useMemo( - () => baseRootContext.selectedDates.some((date) => utils.isSameYear(date, props.value)), - [baseRootContext.selectedDates, props.value, utils], - ); - - const isInvalid = React.useMemo(() => { - if (baseRootContext.dateValidationProps.disablePast && utils.isBeforeYear(props.value, now)) { - return true; - } - if (baseRootContext.dateValidationProps.disableFuture && utils.isAfterYear(props.value, now)) { - return true; - } - if ( - baseRootContext.dateValidationProps.minDate && - utils.isBeforeYear(props.value, baseRootContext.dateValidationProps.minDate) - ) { - return true; - } - if ( - baseRootContext.dateValidationProps.maxDate && - utils.isAfterYear(props.value, baseRootContext.dateValidationProps.maxDate) - ) { - return true; - } - - if (!baseRootContext.dateValidationProps.shouldDisableYear) { - return false; - } - - const yearToValidate = utils.startOfYear(props.value); - - return baseRootContext.dateValidationProps.shouldDisableYear(yearToValidate); - }, [baseRootContext.dateValidationProps, props.value, now, utils]); - - const isDisabled = React.useMemo(() => { - if (baseRootContext.disabled) { - return true; - } - - return isInvalid; - }, [baseRootContext.disabled, isInvalid]); - - const isTabbable = React.useMemo( - () => - utils.isValid(rootContext.value) - ? isSelected - : utils.isSameYear(baseRootContext.currentDate, props.value), - [utils, rootContext.value, baseRootContext.currentDate, isSelected, props.value], - ); - - const selectYear = useEventCallback((newValue: PickerValidDate) => { - if (baseRootContext.readOnly) { - return; - } - - const newCleanValue = utils.setYear(baseRootContext.currentDate, utils.getYear(newValue)); - - const startOfYear = utils.startOfYear(newCleanValue); - const endOfYear = utils.endOfYear(newCleanValue); - - const closestEnabledDate = baseRootContext.isDateInvalid(newCleanValue) - ? findClosestEnabledDate({ - utils, - date: newCleanValue, - minDate: utils.isBefore(baseRootContext.dateValidationProps.minDate, startOfYear) - ? startOfYear - : baseRootContext.dateValidationProps.minDate, - maxDate: utils.isAfter(baseRootContext.dateValidationProps.maxDate, endOfYear) - ? endOfYear - : baseRootContext.dateValidationProps.maxDate, - disablePast: baseRootContext.dateValidationProps.disablePast, - disableFuture: baseRootContext.dateValidationProps.disableFuture, - isDateDisabled: baseRootContext.isDateInvalid, - timezone: baseRootContext.timezone, - }) - : newCleanValue; - - if (closestEnabledDate) { - baseRootContext.selectDate(closestEnabledDate, { section: 'year' }); - } - }); - - const ctx = React.useMemo( - () => ({ - isSelected, - isDisabled, - isInvalid, - isTabbable, - selectYear, - }), - [isSelected, isDisabled, isInvalid, isTabbable, selectYear], - ); - - return ; + return ; }); export namespace CalendarYearsCell { @@ -171,12 +66,12 @@ export namespace CalendarYearsCell { } export interface Props - extends Omit, + extends Omit, Omit, 'value'> {} } interface InnerCalendarYearsCellProps - extends useCalendarYearsCell.Parameters, + extends useBaseCalendarYearsCell.Parameters, Omit, 'value'> {} export { CalendarYearsCell }; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-grid/CalendarYearsGrid.tsx b/packages/x-date-pickers/src/internals/base/Calendar/years-grid/CalendarYearsGrid.tsx index 2827242ce2438..1b628b387357b 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-grid/CalendarYearsGrid.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-grid/CalendarYearsGrid.tsx @@ -1,18 +1,21 @@ 'use client'; import * as React from 'react'; -import { useCalendarYearsGrid } from './useCalendarYearsGrid'; +import { useBaseCalendarYearsGrid } from '../../utils/base-calendar/years-grid/useBaseCalendarYearsGrid'; import { BaseUIComponentProps } from '../../base-utils/types'; import { useComponentRenderer } from '../../base-utils/useComponentRenderer'; import { CompositeList } from '../../composite/list/CompositeList'; +import { BaseCalendarYearsGridOrListContext } from '../../utils/base-calendar/years-grid/BaseCalendarYearsGridOrListContext'; +import { CalendarYearsGridCssVars } from './CalendarYearsGridCssVars'; const CalendarYearsGrid = React.forwardRef(function CalendarYearsList( props: CalendarYearsGrid.Props, forwardedRef: React.ForwardedRef, ) { const { className, render, children, cellsPerRow, ...otherProps } = props; - const { getYearsGridProps, yearsCellRefs } = useCalendarYearsGrid({ + const { getYearsGridProps, yearsCellRefs, yearsListOrGridContext } = useBaseCalendarYearsGrid({ children, cellsPerRow, + cellsPerRowCssVar: CalendarYearsGridCssVars.calendarYearsGridCellsPerRow, }); const state = React.useMemo(() => ({}), []); @@ -25,7 +28,11 @@ const CalendarYearsGrid = React.forwardRef(function CalendarYearsList( extraProps: otherProps, }); - return {renderElement()}; + return ( + + {renderElement()} + + ); }); export namespace CalendarYearsGrid { @@ -33,7 +40,7 @@ export namespace CalendarYearsGrid { export interface Props extends Omit, 'children'>, - useCalendarYearsGrid.Parameters {} + useBaseCalendarYearsGrid.PublicParameters {} } export { CalendarYearsGrid }; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-list/CalendarYearsList.tsx b/packages/x-date-pickers/src/internals/base/Calendar/years-list/CalendarYearsList.tsx index 439f9835de5db..fb09ac9bc8720 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-list/CalendarYearsList.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-list/CalendarYearsList.tsx @@ -1,16 +1,17 @@ 'use client'; import * as React from 'react'; -import { useCalendarYearsList } from './useCalendarYearsList'; import { BaseUIComponentProps } from '../../base-utils/types'; import { useComponentRenderer } from '../../base-utils/useComponentRenderer'; import { CompositeList } from '../../composite/list/CompositeList'; +import { useBaseCalendarYearsList } from '../../utils/base-calendar/years-list/useBaseCalendarYearsList'; +import { BaseCalendarYearsGridOrListContext } from '../../utils/base-calendar/years-grid/BaseCalendarYearsGridOrListContext'; const CalendarYearsList = React.forwardRef(function CalendarYearsList( props: CalendarYearsList.Props, forwardedRef: React.ForwardedRef, ) { const { className, render, children, loop, ...otherProps } = props; - const { getYearsListProps, yearsCellRefs } = useCalendarYearsList({ + const { getYearsListProps, yearsCellRefs, yearsListOrGridContext } = useBaseCalendarYearsList({ children, loop, }); @@ -25,7 +26,11 @@ const CalendarYearsList = React.forwardRef(function CalendarYearsList( extraProps: otherProps, }); - return {renderElement()}; + return ( + + {renderElement()} + + ); }); export namespace CalendarYearsList { @@ -33,7 +38,7 @@ export namespace CalendarYearsList { export interface Props extends Omit, 'children'>, - useCalendarYearsList.Parameters {} + useBaseCalendarYearsList.Parameters {} } export { CalendarYearsList }; diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCellWrapper.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCellWrapper.ts index 9b13c52503693..50f18ea01ed3d 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCellWrapper.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCellWrapper.ts @@ -84,7 +84,7 @@ export namespace useBaseCalendarDaysCellWrapper { /** * The ref to forward to the component. */ - ref: React.RefObject; + ref: React.RefObject; /** * The memoized context to forward to the memoized component so that it does not need to subscribe to any context. */ diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/useCalendarMonthsCell.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-cell/useBaseCalendarMonthsCell.ts similarity index 81% rename from packages/x-date-pickers/src/internals/base/Calendar/months-cell/useCalendarMonthsCell.ts rename to packages/x-date-pickers/src/internals/base/utils/base-calendar/months-cell/useBaseCalendarMonthsCell.ts index f9a748d773742..a38a9946a55b2 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/useCalendarMonthsCell.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-cell/useBaseCalendarMonthsCell.ts @@ -1,11 +1,11 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; -import { PickerValidDate } from '../../../../models'; -import { useUtils } from '../../../hooks/useUtils'; -import { GenericHTMLProps } from '../../base-utils/types'; -import { mergeReactProps } from '../../base-utils/mergeReactProps'; +import { PickerValidDate } from '../../../../../models'; +import { useUtils } from '../../../../hooks/useUtils'; +import { GenericHTMLProps } from '../../../base-utils/types'; +import { mergeReactProps } from '../../../base-utils/mergeReactProps'; -export function useCalendarMonthsCell(parameters: useCalendarMonthsCell.Parameters) { +export function useBaseCalendarMonthsCell(parameters: useBaseCalendarMonthsCell.Parameters) { const utils = useUtils(); const { value, format = utils.formats.month, ctx } = parameters; @@ -39,7 +39,7 @@ export function useCalendarMonthsCell(parameters: useCalendarMonthsCell.Paramete return React.useMemo(() => ({ getMonthsCellProps, isCurrent }), [getMonthsCellProps, isCurrent]); } -export namespace useCalendarMonthsCell { +export namespace useBaseCalendarMonthsCell { export interface Parameters { value: PickerValidDate; /** diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-cell/useBaseCalendarMonthsCellWrapper.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-cell/useBaseCalendarMonthsCellWrapper.ts new file mode 100644 index 0000000000000..3ad2761c638ad --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-cell/useBaseCalendarMonthsCellWrapper.ts @@ -0,0 +1,139 @@ +import * as React from 'react'; +import useEventCallback from '@mui/utils/useEventCallback'; +import useForkRef from '@mui/utils/useForkRef'; +import { PickerValidDate } from '../../../../../models'; +import { useNow, useUtils } from '../../../../hooks/useUtils'; +import { findClosestEnabledDate } from '../../../../utils/date-utils'; +import { useCompositeListItem } from '../../../composite/list/useCompositeListItem'; +import { useBaseCalendarRootContext } from '../root/BaseCalendarRootContext'; +import { useBaseCalendarMonthsCell } from './useBaseCalendarMonthsCell'; +import { useBaseCalendarMonthsGridOrListContext } from '../months-grid/BaseCalendarMonthsGridOrListContext'; + +export function useBaseCalendarMonthsCellWrapper( + parameters: useBaseCalendarMonthsCellWrapper.Parameters, +) { + const { forwardedRef, value } = parameters; + const baseRootContext = useBaseCalendarRootContext(); + const baseMonthListOrGridContext = useBaseCalendarMonthsGridOrListContext(); + const { ref: listItemRef } = useCompositeListItem(); + const utils = useUtils(); + const now = useNow(baseRootContext.timezone); + const mergedRef = useForkRef(forwardedRef, listItemRef); + + const isSelected = React.useMemo( + () => baseRootContext.selectedDates.some((date) => utils.isSameMonth(date, value)), + [baseRootContext.selectedDates, value, utils], + ); + + const isInvalid = React.useMemo(() => { + const firstEnabledMonth = utils.startOfMonth( + baseRootContext.dateValidationProps.disablePast && + utils.isAfter(now, baseRootContext.dateValidationProps.minDate) + ? now + : baseRootContext.dateValidationProps.minDate, + ); + + const lastEnabledMonth = utils.startOfMonth( + baseRootContext.dateValidationProps.disableFuture && + utils.isBefore(now, baseRootContext.dateValidationProps.maxDate) + ? now + : baseRootContext.dateValidationProps.maxDate, + ); + + const monthToValidate = utils.startOfMonth(value); + + if (utils.isBefore(monthToValidate, firstEnabledMonth)) { + return true; + } + + if (utils.isAfter(monthToValidate, lastEnabledMonth)) { + return true; + } + + if (!baseRootContext.dateValidationProps.shouldDisableMonth) { + return false; + } + + return baseRootContext.dateValidationProps.shouldDisableMonth(monthToValidate); + }, [baseRootContext.dateValidationProps, value, now, utils]); + + const isDisabled = React.useMemo(() => { + if (baseRootContext.disabled) { + return true; + } + + return isInvalid; + }, [baseRootContext.disabled, isInvalid]); + + const isTabbable = React.useMemo( + () => + baseMonthListOrGridContext.tabbableMonths.some((month) => utils.isSameMonth(month, value)), + [baseMonthListOrGridContext, value, utils], + ); + + const selectMonth = useEventCallback((newValue: PickerValidDate) => { + if (baseRootContext.readOnly) { + return; + } + + const newCleanValue = utils.setMonth(baseRootContext.currentDate, utils.getMonth(newValue)); + + const startOfMonth = utils.startOfMonth(newCleanValue); + const endOfMonth = utils.endOfMonth(newCleanValue); + + const closestEnabledDate = baseRootContext.isDateInvalid(newCleanValue) + ? findClosestEnabledDate({ + utils, + date: newCleanValue, + minDate: utils.isBefore(baseRootContext.dateValidationProps.minDate, startOfMonth) + ? startOfMonth + : baseRootContext.dateValidationProps.minDate, + maxDate: utils.isAfter(baseRootContext.dateValidationProps.maxDate, endOfMonth) + ? endOfMonth + : baseRootContext.dateValidationProps.maxDate, + disablePast: baseRootContext.dateValidationProps.disablePast, + disableFuture: baseRootContext.dateValidationProps.disableFuture, + isDateDisabled: baseRootContext.isDateInvalid, + timezone: baseRootContext.timezone, + }) + : newCleanValue; + + if (closestEnabledDate) { + baseRootContext.setVisibleDate(closestEnabledDate, true); + baseRootContext.selectDate(closestEnabledDate, { section: 'month' }); + } + }); + + const ctx = React.useMemo( + () => ({ + isSelected, + isDisabled, + isInvalid, + isTabbable, + selectMonth, + }), + [isSelected, isDisabled, isInvalid, isTabbable, selectMonth], + ); + + return { ref: mergedRef, ctx }; +} + +export namespace useBaseCalendarMonthsCellWrapper { + export interface Parameters extends Pick { + /** + * The ref forwarded by the parent component. + */ + forwardedRef: React.ForwardedRef; + } + + export interface ReturnValue { + /** + * The ref to forward to the component. + */ + ref: React.RefObject; + /** + * The memoized context to forward to the memoized component so that it does not need to subscribe to any context. + */ + ctx: useBaseCalendarMonthsCell.Context; + } +} diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-grid/BaseCalendarMonthsGridOrListContext.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-grid/BaseCalendarMonthsGridOrListContext.ts new file mode 100644 index 0000000000000..68bca75c06e68 --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-grid/BaseCalendarMonthsGridOrListContext.ts @@ -0,0 +1,32 @@ +import * as React from 'react'; +import { PickerValidDate } from '../../../../../models'; + +export interface BaseCalendarMonthsGridOrListContext { + tabbableMonths: PickerValidDate[]; +} + +export const BaseCalendarMonthsGridOrListContext = React.createContext< + BaseCalendarMonthsGridOrListContext | undefined +>(undefined); + +if (process.env.NODE_ENV !== 'production') { + BaseCalendarMonthsGridOrListContext.displayName = 'BaseCalendarMonthsGridOrListContext'; +} + +export function useBaseCalendarMonthsGridOrListContext() { + const context = React.useContext(BaseCalendarMonthsGridOrListContext); + if (context === undefined) { + throw new Error( + [ + 'Base UI X: BaseCalendarMonthsGridOrListContext is missing.', + ' must be placed within or .', + ' must be placed within or .', + ].join('\n'), + ); + } + return context; +} + +export function useNullableBaseCalendarMonthsGridOrListContext() { + return React.useContext(BaseCalendarMonthsGridOrListContext) ?? null; +} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-grid/useCalendarMonthsGrid.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-grid/useBaseCalendarMonthsGrid.ts similarity index 71% rename from packages/x-date-pickers/src/internals/base/Calendar/months-grid/useCalendarMonthsGrid.ts rename to packages/x-date-pickers/src/internals/base/utils/base-calendar/months-grid/useBaseCalendarMonthsGrid.ts index 71590df123473..018686ca13c32 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-grid/useCalendarMonthsGrid.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-grid/useBaseCalendarMonthsGrid.ts @@ -1,10 +1,10 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; import useTimeout from '@mui/utils/useTimeout'; -import { PickerValidDate } from '../../../../models'; -import { GenericHTMLProps } from '../../base-utils/types'; -import { mergeReactProps } from '../../base-utils/mergeReactProps'; -import { useBaseCalendarRootContext } from '../../utils/base-calendar/root/BaseCalendarRootContext'; +import { PickerValidDate } from '../../../../../models'; +import { GenericHTMLProps } from '../../../base-utils/types'; +import { mergeReactProps } from '../../../base-utils/mergeReactProps'; +import { useBaseCalendarRootContext } from '../root/BaseCalendarRootContext'; import { applyInitialFocusInGrid, navigateInGrid, @@ -12,13 +12,12 @@ import { PageGridNavigationTarget, } from '../utils/keyboardNavigation'; import { useMonthsCells } from '../utils/useMonthsCells'; -import { CalendarMonthsGridCssVars } from './CalendarMonthsGridCssVars'; -export function useCalendarMonthsGrid(parameters: useCalendarMonthsGrid.Parameters) { - const { children, cellsPerRow, canChangeYear = true } = parameters; +export function useBaseCalendarMonthsGrid(parameters: useBaseCalendarMonthsGrid.Parameters) { + const { children, cellsPerRow, canChangeYear = true, cellsPerRowCssVar } = parameters; const baseRootContext = useBaseCalendarRootContext(); const monthsCellRefs = React.useRef<(HTMLElement | null)[]>([]); - const { months, changePage } = useMonthsCells(); + const { months, monthsListOrGridContext, changePage } = useMonthsCells(); const pageNavigationTargetRef = React.useRef(null); const getCellsInCalendar = useEventCallback(() => { @@ -71,21 +70,21 @@ export function useCalendarMonthsGrid(parameters: useCalendarMonthsGrid.Paramete children: children == null ? null : children({ months }), onKeyDown, style: { - [CalendarMonthsGridCssVars.calendarMonthsGridCellsPerRow]: cellsPerRow, + [cellsPerRowCssVar]: cellsPerRow, }, }); }, - [months, children, onKeyDown, cellsPerRow], + [months, children, onKeyDown, cellsPerRow, cellsPerRowCssVar], ); return React.useMemo( - () => ({ getMonthsGridProps, monthsCellRefs }), - [getMonthsGridProps, monthsCellRefs], + () => ({ getMonthsGridProps, monthsCellRefs, monthsListOrGridContext }), + [getMonthsGridProps, monthsCellRefs, monthsListOrGridContext], ); } -export namespace useCalendarMonthsGrid { - export interface Parameters { +export namespace useBaseCalendarMonthsGrid { + export interface PublicParameters { /** * The number of cells per row. * This is used to make sure the keyboard navigation works correctly. @@ -100,6 +99,13 @@ export namespace useCalendarMonthsGrid { children?: (parameters: ChildrenParameters) => React.ReactNode; } + export interface Parameters extends PublicParameters { + /** + * The CSS variable that must contain the number of cells per row. + */ + cellsPerRowCssVar: string; + } + export interface ChildrenParameters { months: PickerValidDate[]; } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-list/useCalendarMonthsList.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-list/useBaseCalendarMonthsList.ts similarity index 78% rename from packages/x-date-pickers/src/internals/base/Calendar/months-list/useCalendarMonthsList.ts rename to packages/x-date-pickers/src/internals/base/utils/base-calendar/months-list/useBaseCalendarMonthsList.ts index 8b818d23cc3a0..7217fcdb49006 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-list/useCalendarMonthsList.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-list/useBaseCalendarMonthsList.ts @@ -1,10 +1,10 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; import useTimeout from '@mui/utils/useTimeout'; -import { PickerValidDate } from '../../../../models'; -import { GenericHTMLProps } from '../../base-utils/types'; -import { mergeReactProps } from '../../base-utils/mergeReactProps'; -import { useBaseCalendarRootContext } from '../../utils/base-calendar/root/BaseCalendarRootContext'; +import { PickerValidDate } from '../../../../../models'; +import { GenericHTMLProps } from '../../../base-utils/types'; +import { mergeReactProps } from '../../../base-utils/mergeReactProps'; +import { useBaseCalendarRootContext } from '../root/BaseCalendarRootContext'; import { applyInitialFocusInList, navigateInList, @@ -13,11 +13,11 @@ import { } from '../utils/keyboardNavigation'; import { useMonthsCells } from '../utils/useMonthsCells'; -export function useCalendarMonthsList(parameters: useCalendarMonthsList.Parameters) { +export function useBaseCalendarMonthsList(parameters: useBaseCalendarMonthsList.Parameters) { const { children, loop = true, canChangeYear = true } = parameters; const baseRootContext = useBaseCalendarRootContext(); const monthsCellRefs = React.useRef<(HTMLElement | null)[]>([]); - const { months, changePage } = useMonthsCells(); + const { months, monthsListOrGridContext, changePage } = useMonthsCells(); const pageNavigationTargetRef = React.useRef(null); const timeout = useTimeout(); @@ -57,12 +57,12 @@ export function useCalendarMonthsList(parameters: useCalendarMonthsList.Paramete ); return React.useMemo( - () => ({ getMonthListProps, monthsCellRefs }), - [getMonthListProps, monthsCellRefs], + () => ({ getMonthListProps, monthsCellRefs, monthsListOrGridContext }), + [getMonthListProps, monthsCellRefs, monthsListOrGridContext], ); } -export namespace useCalendarMonthsList { +export namespace useBaseCalendarMonthsList { export interface Parameters { /** * Whether to loop keyboard focus back to the first item diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarDaysGridsNavigation.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarDaysGridsNavigation.ts index 4674105692f9c..3d56497779e3d 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarDaysGridsNavigation.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarDaysGridsNavigation.ts @@ -10,7 +10,7 @@ import { navigateInGrid, NavigateInGridChangePage, PageGridNavigationTarget, -} from '../../../Calendar/utils/keyboardNavigation'; +} from '../utils/keyboardNavigation'; import { getFirstEnabledMonth, getLastEnabledMonth } from '../utils/date'; import { BaseCalendarRootContext } from './BaseCalendarRootContext'; diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-month/useBaseCalendarSetVisibleMonth.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-month/useBaseCalendarSetVisibleMonth.ts index de5b59187ba7f..150d3f628ef64 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-month/useBaseCalendarSetVisibleMonth.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-month/useBaseCalendarSetVisibleMonth.ts @@ -14,9 +14,10 @@ export function useBaseCalendarSetVisibleMonth( type: 'button' as const, disabled: ctx.isDisabled, onClick: ctx.setTarget, + tabIndex: ctx.isTabbable ? 0 : -1, }); }, - [ctx.isDisabled, ctx.setTarget], + [ctx.isDisabled, ctx.isTabbable, ctx.setTarget], ); return React.useMemo(() => ({ getSetVisibleMonthProps }), [getSetVisibleMonthProps]); @@ -37,5 +38,6 @@ export namespace useBaseCalendarSetVisibleMonth { export interface Context { setTarget: () => void; isDisabled: boolean; + isTabbable: boolean; } } diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-month/useBaseCalendarSetVisibleMonthWrapper.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-month/useBaseCalendarSetVisibleMonthWrapper.ts index 7f6424fda53cf..b4baae6179d06 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-month/useBaseCalendarSetVisibleMonthWrapper.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-month/useBaseCalendarSetVisibleMonthWrapper.ts @@ -4,12 +4,14 @@ import { useUtils } from '../../../../hooks/useUtils'; import { getFirstEnabledMonth, getLastEnabledMonth } from '../utils/date'; import { useBaseCalendarSetVisibleMonth } from './useBaseCalendarSetVisibleMonth'; import { useBaseCalendarRootContext } from '../root/BaseCalendarRootContext'; +import { useNullableBaseCalendarMonthsGridOrListContext } from '../months-grid/BaseCalendarMonthsGridOrListContext'; export function useBaseCalendarSetVisibleMonthWrapper( parameters: useBaseCalendarSetVisibleMonthWrapper.Parameters, ) { const { target } = parameters; const baseRootContext = useBaseCalendarRootContext(); + const baseMonthsListOrGridContext = useNullableBaseCalendarMonthsGridOrListContext(); const utils = useUtils(); const targetDate = React.useMemo(() => { @@ -53,6 +55,15 @@ export function useBaseCalendarSetVisibleMonthWrapper( utils, ]); + const tabbableMonths = baseMonthsListOrGridContext?.tabbableMonths; + const isTabbable = React.useMemo(() => { + if (tabbableMonths == null) { + return false; + } + + return tabbableMonths.some((month) => utils.isSameMonth(month, targetDate)); + }, [tabbableMonths, targetDate, utils]); + const setTarget = useEventCallback(() => { if (isDisabled) { return; @@ -64,8 +75,9 @@ export function useBaseCalendarSetVisibleMonthWrapper( () => ({ setTarget, isDisabled, + isTabbable, }), - [setTarget, isDisabled], + [setTarget, isDisabled, isTabbable], ); return { ctx }; diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-year/useBaseCalendarSetVisibleYear.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-year/useBaseCalendarSetVisibleYear.ts index e594833fbcd0c..5de6052435b78 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-year/useBaseCalendarSetVisibleYear.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-year/useBaseCalendarSetVisibleYear.ts @@ -14,9 +14,10 @@ export function useBaseCalendarSetVisibleYear( type: 'button' as const, disabled: ctx.isDisabled, onClick: ctx.setTarget, + tabIndex: ctx.isTabbable ? 0 : -1, }); }, - [ctx.isDisabled, ctx.setTarget], + [ctx.isDisabled, ctx.setTarget, ctx.isTabbable], ); return React.useMemo(() => ({ getSetVisibleYearProps }), [getSetVisibleYearProps]); @@ -37,5 +38,6 @@ export namespace useBaseCalendarSetVisibleYear { export interface Context { setTarget: () => void; isDisabled: boolean; + isTabbable: boolean; } } diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-year/useBaseCalendarSetVisibleYearWrapper.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-year/useBaseCalendarSetVisibleYearWrapper.ts index af4a6c61ee2b6..8a9e799f340d6 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-year/useBaseCalendarSetVisibleYearWrapper.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-year/useBaseCalendarSetVisibleYearWrapper.ts @@ -4,12 +4,14 @@ import { useUtils } from '../../../../hooks/useUtils'; import { useBaseCalendarSetVisibleYear } from './useBaseCalendarSetVisibleYear'; import { useBaseCalendarRootContext } from '../root/BaseCalendarRootContext'; import { getFirstEnabledYear, getLastEnabledYear } from '../utils/date'; +import { useNullableBaseCalendarYearsGridOrListContext } from '../years-grid/BaseCalendarYearsGridOrListContext'; export function useBaseCalendarSetVisibleYearWrapper( parameters: useBaseCalendarSetVisibleYearWrapper.Parameters, ) { const { target } = parameters; const baseRootContext = useBaseCalendarRootContext(); + const baseYearsListOrGridContext = useNullableBaseCalendarYearsGridOrListContext(); const utils = useUtils(); const targetDate = React.useMemo(() => { @@ -52,6 +54,15 @@ export function useBaseCalendarSetVisibleYearWrapper( utils, ]); + const tabbableYears = baseYearsListOrGridContext?.tabbableYears; + const isTabbable = React.useMemo(() => { + if (tabbableYears == null) { + return false; + } + + return tabbableYears.some((year) => utils.isSameYear(year, targetDate)); + }, [tabbableYears, targetDate, utils]); + const setTarget = useEventCallback(() => { if (isDisabled) { return; @@ -63,8 +74,9 @@ export function useBaseCalendarSetVisibleYearWrapper( () => ({ setTarget, isDisabled, + isTabbable, }), - [setTarget, isDisabled], + [setTarget, isDisabled, isTabbable], ); return { ctx }; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/keyboardNavigation.ts similarity index 100% rename from packages/x-date-pickers/src/internals/base/Calendar/utils/keyboardNavigation.ts rename to packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/keyboardNavigation.ts diff --git a/packages/x-date-pickers/src/internals/base/Calendar/utils/useMonthsCells.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useMonthsCells.ts similarity index 62% rename from packages/x-date-pickers/src/internals/base/Calendar/utils/useMonthsCells.ts rename to packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useMonthsCells.ts index eb1535d349f17..16399b487c1a0 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/utils/useMonthsCells.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useMonthsCells.ts @@ -1,9 +1,10 @@ import * as React from 'react'; -import { PickerValidDate } from '../../../../models'; -import { getMonthsInYear } from '../../../utils/date-utils'; -import { useUtils } from '../../../hooks/useUtils'; -import { getFirstEnabledYear, getLastEnabledYear } from '../../utils/base-calendar/utils/date'; -import { useBaseCalendarRootContext } from '../../utils/base-calendar/root/BaseCalendarRootContext'; +import { PickerValidDate } from '../../../../../models'; +import { getMonthsInYear } from '../../../../utils/date-utils'; +import { useUtils } from '../../../../hooks/useUtils'; +import { getFirstEnabledYear, getLastEnabledYear } from './date'; +import { useBaseCalendarRootContext } from '../root/BaseCalendarRootContext'; +import { BaseCalendarMonthsGridOrListContext } from '../months-grid/BaseCalendarMonthsGridOrListContext'; export function useMonthsCells(): useMonthsCells.ReturnValue { const baseRootContext = useBaseCalendarRootContext(); @@ -16,6 +17,32 @@ export function useMonthsCells(): useMonthsCells.ReturnValue { const months = React.useMemo(() => getMonthsInYear(utils, currentYear), [utils, currentYear]); + const tabbableMonths = React.useMemo(() => { + let tempTabbableDays: PickerValidDate[] = []; + tempTabbableDays = months.filter((day) => + baseRootContext.selectedDates.some((selectedDay) => utils.isSameMonth(day, selectedDay)), + ); + + if (tempTabbableDays.length === 0) { + tempTabbableDays = months.filter((day) => + utils.isSameMonth(day, baseRootContext.currentDate), + ); + } + + if (tempTabbableDays.length === 0) { + tempTabbableDays = [months[0]]; + } + + return tempTabbableDays; + }, [baseRootContext.currentDate, baseRootContext.selectedDates, months, utils]); + + const monthsListOrGridContext = React.useMemo( + () => ({ + tabbableMonths, + }), + [tabbableMonths], + ); + const changePage = (direction: 'next' | 'previous') => { // TODO: Jump over months with no valid date. if (direction === 'previous') { @@ -64,12 +91,13 @@ export function useMonthsCells(): useMonthsCells.ReturnValue { return registerSection({ type: 'month', value: currentYear }); }, [registerSection, currentYear]); - return { months, changePage }; + return { months, monthsListOrGridContext, changePage }; } export namespace useMonthsCells { export interface ReturnValue { months: PickerValidDate[]; + monthsListOrGridContext: BaseCalendarMonthsGridOrListContext; changePage: (direction: 'next' | 'previous') => void; } } diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useYearsCells.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useYearsCells.ts new file mode 100644 index 0000000000000..768f2380f685d --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useYearsCells.ts @@ -0,0 +1,57 @@ +import * as React from 'react'; +import { PickerValidDate } from '../../../../../models'; +import { useUtils } from '../../../../hooks/useUtils'; +import { useBaseCalendarRootContext } from '../root/BaseCalendarRootContext'; +import { BaseCalendarYearsGridOrListContext } from '../years-grid/BaseCalendarYearsGridOrListContext'; + +export function useYearsCells(): useYearsCells.ReturnValue { + const baseRootContext = useBaseCalendarRootContext(); + const utils = useUtils(); + + const years = React.useMemo( + () => + utils.getYearRange([ + baseRootContext.dateValidationProps.minDate, + baseRootContext.dateValidationProps.maxDate, + ]), + [ + utils, + baseRootContext.dateValidationProps.minDate, + baseRootContext.dateValidationProps.maxDate, + ], + ); + + const tabbableYears = React.useMemo(() => { + let tempTabbableDays: PickerValidDate[] = []; + tempTabbableDays = years.filter((day) => + baseRootContext.selectedDates.some((selectedDay) => utils.isSameYear(day, selectedDay)), + ); + + if (tempTabbableDays.length === 0) { + tempTabbableDays = years.filter((day) => utils.isSameYear(day, baseRootContext.currentDate)); + } + + return tempTabbableDays; + }, [baseRootContext.currentDate, baseRootContext.selectedDates, years, utils]); + + const yearsListOrGridContext = React.useMemo( + () => ({ + tabbableYears, + }), + [tabbableYears], + ); + + const registerSection = baseRootContext.registerSection; + React.useEffect(() => { + return registerSection({ type: 'month', value: baseRootContext.visibleDate }); + }, [registerSection, baseRootContext.visibleDate]); + + return { years, yearsListOrGridContext }; +} + +export namespace useYearsCells { + export interface ReturnValue { + years: PickerValidDate[]; + yearsListOrGridContext: BaseCalendarYearsGridOrListContext; + } +} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-cell/useCalendarYearsCell.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/years-cell/useBaseCalendarYearsCell.ts similarity index 81% rename from packages/x-date-pickers/src/internals/base/Calendar/years-cell/useCalendarYearsCell.ts rename to packages/x-date-pickers/src/internals/base/utils/base-calendar/years-cell/useBaseCalendarYearsCell.ts index 3c6146cd78eef..c7f6091708ca4 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-cell/useCalendarYearsCell.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/years-cell/useBaseCalendarYearsCell.ts @@ -1,11 +1,11 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; -import { PickerValidDate } from '../../../../models'; -import { useUtils } from '../../../hooks/useUtils'; -import { GenericHTMLProps } from '../../base-utils/types'; -import { mergeReactProps } from '../../base-utils/mergeReactProps'; +import { PickerValidDate } from '../../../../../models'; +import { useUtils } from '../../../../hooks/useUtils'; +import { GenericHTMLProps } from '../../../base-utils/types'; +import { mergeReactProps } from '../../../base-utils/mergeReactProps'; -export function useCalendarYearsCell(parameters: useCalendarYearsCell.Parameters) { +export function useBaseCalendarYearsCell(parameters: useBaseCalendarYearsCell.Parameters) { const utils = useUtils(); const { value, format = utils.formats.year, ctx } = parameters; @@ -39,7 +39,7 @@ export function useCalendarYearsCell(parameters: useCalendarYearsCell.Parameters return React.useMemo(() => ({ getYearCellProps, isCurrent }), [getYearCellProps, isCurrent]); } -export namespace useCalendarYearsCell { +export namespace useBaseCalendarYearsCell { export interface Parameters { value: PickerValidDate; /** diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/years-cell/useBaseCalendarYearsCellWrapper.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/years-cell/useBaseCalendarYearsCellWrapper.ts new file mode 100644 index 0000000000000..43dda1b95fccf --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/years-cell/useBaseCalendarYearsCellWrapper.ts @@ -0,0 +1,134 @@ +import * as React from 'react'; +import useEventCallback from '@mui/utils/useEventCallback'; +import useForkRef from '@mui/utils/useForkRef'; +import { PickerValidDate } from '../../../../../models'; +import { useNow, useUtils } from '../../../../hooks/useUtils'; +import { findClosestEnabledDate } from '../../../../utils/date-utils'; +import { useCompositeListItem } from '../../../composite/list/useCompositeListItem'; +import { useBaseCalendarRootContext } from '../root/BaseCalendarRootContext'; +import { useBaseCalendarYearsCell } from './useBaseCalendarYearsCell'; +import { useBaseCalendarYearsGridOrListContext } from '../years-grid/BaseCalendarYearsGridOrListContext'; + +export function useBaseCalendarYearsCellWrapper( + parameters: useBaseCalendarYearsCellWrapper.Parameters, +) { + const { forwardedRef, value } = parameters; + const baseRootContext = useBaseCalendarRootContext(); + const baseYearsListOrGridContext = useBaseCalendarYearsGridOrListContext(); + const { ref: listItemRef } = useCompositeListItem(); + const utils = useUtils(); + const now = useNow(baseRootContext.timezone); + const mergedRef = useForkRef(forwardedRef, listItemRef); + + const isSelected = React.useMemo( + () => baseRootContext.selectedDates.some((date) => utils.isSameYear(date, value)), + [baseRootContext.selectedDates, value, utils], + ); + + const isInvalid = React.useMemo(() => { + if (baseRootContext.dateValidationProps.disablePast && utils.isBeforeYear(value, now)) { + return true; + } + if (baseRootContext.dateValidationProps.disableFuture && utils.isAfterYear(value, now)) { + return true; + } + if ( + baseRootContext.dateValidationProps.minDate && + utils.isBeforeYear(value, baseRootContext.dateValidationProps.minDate) + ) { + return true; + } + if ( + baseRootContext.dateValidationProps.maxDate && + utils.isAfterYear(value, baseRootContext.dateValidationProps.maxDate) + ) { + return true; + } + + if (!baseRootContext.dateValidationProps.shouldDisableYear) { + return false; + } + + const yearToValidate = utils.startOfYear(value); + + return baseRootContext.dateValidationProps.shouldDisableYear(yearToValidate); + }, [baseRootContext.dateValidationProps, value, now, utils]); + + const isDisabled = React.useMemo(() => { + if (baseRootContext.disabled) { + return true; + } + + return isInvalid; + }, [baseRootContext.disabled, isInvalid]); + + const isTabbable = React.useMemo( + () => baseYearsListOrGridContext.tabbableYears.some((year) => utils.isSameYear(year, value)), + [baseYearsListOrGridContext, value, utils], + ); + + const selectYear = useEventCallback((newValue: PickerValidDate) => { + if (baseRootContext.readOnly) { + return; + } + + const newCleanValue = utils.setYear(baseRootContext.currentDate, utils.getYear(newValue)); + + const startOfYear = utils.startOfYear(newCleanValue); + const endOfYear = utils.endOfYear(newCleanValue); + + const closestEnabledDate = baseRootContext.isDateInvalid(newCleanValue) + ? findClosestEnabledDate({ + utils, + date: newCleanValue, + minDate: utils.isBefore(baseRootContext.dateValidationProps.minDate, startOfYear) + ? startOfYear + : baseRootContext.dateValidationProps.minDate, + maxDate: utils.isAfter(baseRootContext.dateValidationProps.maxDate, endOfYear) + ? endOfYear + : baseRootContext.dateValidationProps.maxDate, + disablePast: baseRootContext.dateValidationProps.disablePast, + disableFuture: baseRootContext.dateValidationProps.disableFuture, + isDateDisabled: baseRootContext.isDateInvalid, + timezone: baseRootContext.timezone, + }) + : newCleanValue; + + if (closestEnabledDate) { + baseRootContext.selectDate(closestEnabledDate, { section: 'year' }); + } + }); + + const ctx = React.useMemo( + () => ({ + isSelected, + isDisabled, + isInvalid, + isTabbable, + selectYear, + }), + [isSelected, isDisabled, isInvalid, isTabbable, selectYear], + ); + + return { ref: mergedRef, ctx }; +} + +export namespace useBaseCalendarYearsCellWrapper { + export interface Parameters extends Pick { + /** + * The ref forwarded by the parent component. + */ + forwardedRef: React.ForwardedRef; + } + + export interface ReturnValue { + /** + * The ref to forward to the component. + */ + ref: React.RefObject; + /** + * The memoized context to forward to the memoized component so that it does not need to subscribe to any context. + */ + ctx: useBaseCalendarYearsCell.Context; + } +} diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/years-grid/BaseCalendarYearsGridOrListContext.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/years-grid/BaseCalendarYearsGridOrListContext.ts new file mode 100644 index 0000000000000..3ae79b17160dc --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/years-grid/BaseCalendarYearsGridOrListContext.ts @@ -0,0 +1,32 @@ +import * as React from 'react'; +import { PickerValidDate } from '../../../../../models'; + +export interface BaseCalendarYearsGridOrListContext { + tabbableYears: PickerValidDate[]; +} + +export const BaseCalendarYearsGridOrListContext = React.createContext< + BaseCalendarYearsGridOrListContext | undefined +>(undefined); + +if (process.env.NODE_ENV !== 'production') { + BaseCalendarYearsGridOrListContext.displayName = 'BaseCalendarYearsGridOrListContext'; +} + +export function useBaseCalendarYearsGridOrListContext() { + const context = React.useContext(BaseCalendarYearsGridOrListContext); + if (context === undefined) { + throw new Error( + [ + 'Base UI X: BaseCalendarYearsGridOrListContext is missing.', + ' must be placed within or .', + ' must be placed within or .', + ].join('\n'), + ); + } + return context; +} + +export function useNullableBaseCalendarYearsGridOrListContext() { + return React.useContext(BaseCalendarYearsGridOrListContext) ?? null; +} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-grid/useCalendarYearsGrid.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/years-grid/useBaseCalendarYearsGrid.ts similarity index 62% rename from packages/x-date-pickers/src/internals/base/Calendar/years-grid/useCalendarYearsGrid.ts rename to packages/x-date-pickers/src/internals/base/utils/base-calendar/years-grid/useBaseCalendarYearsGrid.ts index 00719a4f7bfad..db613a5b04080 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-grid/useCalendarYearsGrid.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/years-grid/useBaseCalendarYearsGrid.ts @@ -1,16 +1,15 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; -import { PickerValidDate } from '../../../../models'; -import { GenericHTMLProps } from '../../base-utils/types'; -import { mergeReactProps } from '../../base-utils/mergeReactProps'; +import { PickerValidDate } from '../../../../../models'; +import { GenericHTMLProps } from '../../../base-utils/types'; +import { mergeReactProps } from '../../../base-utils/mergeReactProps'; import { navigateInGrid } from '../utils/keyboardNavigation'; import { useYearsCells } from '../utils/useYearsCells'; -import { CalendarYearsGridCssVars } from './CalendarYearsGridCssVars'; -export function useCalendarYearsGrid(parameters: useCalendarYearsGrid.Parameters) { - const { children, cellsPerRow } = parameters; +export function useBaseCalendarYearsGrid(parameters: useBaseCalendarYearsGrid.Parameters) { + const { children, cellsPerRow, cellsPerRowCssVar } = parameters; const yearsCellRefs = React.useRef<(HTMLElement | null)[]>([]); - const { years } = useYearsCells(); + const { years, yearsListOrGridContext } = useYearsCells(); const getCellsInCalendar = useEventCallback(() => { const grid: HTMLElement[][] = Array.from( @@ -45,21 +44,21 @@ export function useCalendarYearsGrid(parameters: useCalendarYearsGrid.Parameters children: children == null ? null : children({ years }), onKeyDown, style: { - [CalendarYearsGridCssVars.calendarYearsGridCellsPerRow]: cellsPerRow, + [cellsPerRowCssVar]: cellsPerRow, }, }); }, - [years, children, onKeyDown, cellsPerRow], + [years, children, onKeyDown, cellsPerRow, cellsPerRowCssVar], ); return React.useMemo( - () => ({ getYearsGridProps, yearsCellRefs }), - [getYearsGridProps, yearsCellRefs], + () => ({ getYearsGridProps, yearsCellRefs, yearsListOrGridContext }), + [getYearsGridProps, yearsCellRefs, yearsListOrGridContext], ); } -export namespace useCalendarYearsGrid { - export interface Parameters { +export namespace useBaseCalendarYearsGrid { + export interface PublicParameters { /** * Cells rendered per row. * This is used to make sure the keyboard navigation works correctly. @@ -68,6 +67,13 @@ export namespace useCalendarYearsGrid { children?: (parameters: ChildrenParameters) => React.ReactNode; } + export interface Parameters extends PublicParameters { + /** + * The CSS variable that must contain the number of cells per row. + */ + cellsPerRowCssVar: string; + } + export interface ChildrenParameters { years: PickerValidDate[]; } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-list/useCalendarYearsList.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/years-list/useBaseCalendarYearsList.ts similarity index 70% rename from packages/x-date-pickers/src/internals/base/Calendar/years-list/useCalendarYearsList.ts rename to packages/x-date-pickers/src/internals/base/utils/base-calendar/years-list/useBaseCalendarYearsList.ts index ae3bfa77bf18e..bba86aac3a4d5 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-list/useCalendarYearsList.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/years-list/useBaseCalendarYearsList.ts @@ -1,15 +1,15 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; -import { PickerValidDate } from '../../../../models'; -import { GenericHTMLProps } from '../../base-utils/types'; -import { mergeReactProps } from '../../base-utils/mergeReactProps'; +import { PickerValidDate } from '../../../../../models'; +import { GenericHTMLProps } from '../../../base-utils/types'; +import { mergeReactProps } from '../../../base-utils/mergeReactProps'; import { navigateInList } from '../utils/keyboardNavigation'; import { useYearsCells } from '../utils/useYearsCells'; -export function useCalendarYearsList(parameters: useCalendarYearsList.Parameters) { +export function useBaseCalendarYearsList(parameters: useBaseCalendarYearsList.Parameters) { const { children, loop = true } = parameters; const yearsCellRefs = React.useRef<(HTMLElement | null)[]>([]); - const { years } = useYearsCells(); + const { years, yearsListOrGridContext } = useYearsCells(); const onKeyDown = useEventCallback((event: React.KeyboardEvent) => { navigateInList({ @@ -32,12 +32,12 @@ export function useCalendarYearsList(parameters: useCalendarYearsList.Parameters ); return React.useMemo( - () => ({ getYearsListProps, yearsCellRefs }), - [getYearsListProps, yearsCellRefs], + () => ({ getYearsListProps, yearsCellRefs, yearsListOrGridContext }), + [getYearsListProps, yearsCellRefs, yearsListOrGridContext], ); } -export namespace useCalendarYearsList { +export namespace useBaseCalendarYearsList { export interface Parameters { /** * Whether to loop keyboard focus back to the first item From ae3f22f1652ee370118379ea84ffb29dc680c4b4 Mon Sep 17 00:00:00 2001 From: flavien Date: Fri, 10 Jan 2025 13:21:18 +0100 Subject: [PATCH 089/136] Add preview on range calendar --- .../base-calendar/DateRangeCalendarDemo.tsx | 7 +- .../base-calendar/calendar.module.css | 83 +++++++++++++------ .../DateRangeCalendar/DateRangeCalendar.tsx | 2 + .../days-cell/RangeCalendarDaysCell.tsx | 29 ++++++- .../RangeCalendarDaysCellDataAttributes.ts | 14 +++- .../days-cell/useRangeCalendarDaysCell.tsx | 54 ++++++++---- .../useRangeCalendarDaysCellWrapper.ts | 77 ++++++++++++----- .../RangeCalendarDaysGridBody.tsx | 7 +- .../useRangeCalendarDaysGridBody.ts | 38 +++++++++ .../RangeCalendar/root/RangeCalendarRoot.tsx | 10 +-- .../root/RangeCalendarRootContext.ts | 14 +++- .../root/RangeCalendarRootDragContext.ts | 33 -------- .../root/useRangeCalendarRoot.tsx | 80 +++++++++++++----- 13 files changed, 319 insertions(+), 129 deletions(-) create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid-body/useRangeCalendarDaysGridBody.ts delete mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootDragContext.ts diff --git a/docs/data/date-pickers/base-calendar/DateRangeCalendarDemo.tsx b/docs/data/date-pickers/base-calendar/DateRangeCalendarDemo.tsx index abf9caa288cec..cfcc3da4877e6 100644 --- a/docs/data/date-pickers/base-calendar/DateRangeCalendarDemo.tsx +++ b/docs/data/date-pickers/base-calendar/DateRangeCalendarDemo.tsx @@ -8,6 +8,7 @@ import { useRangeCalendarContext, } from '@mui/x-date-pickers-pro/internals/base/RangeCalendar'; import styles from './calendar.module.css'; +import dayjs from 'dayjs'; function Header(props: { activeSection: 'day' | 'month' | 'year'; @@ -80,7 +81,11 @@ export default function DateRangeCalendarDemo() { return ( - +
, newPreviewRequest: PickerValidDate) => { if (!isWithinRange(utils, newPreviewRequest, valueDayRange)) { diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/RangeCalendarDaysCell.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/RangeCalendarDaysCell.tsx index f9bca56d7c08b..ea4bea604a724 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/RangeCalendarDaysCell.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/RangeCalendarDaysCell.tsx @@ -20,6 +20,15 @@ const customStyleHookMapping: CustomStyleHookMapping { + if (!ctx.isSelected && !ctx.isSelectionStart && !ctx.isSelectionEnd) { + ctx.setHoveredDate(value); + } else { + ctx.setHoveredDate(null); + } + }); + const { getDaysCellProps: getBaseDaysCellProps, isCurrent } = useBaseCalendarDaysCell(parameters); const getDaysCellProps = React.useCallback( (externalProps: GenericHTMLProps) => { - return mergeReactProps(externalProps, getBaseDaysCellProps(externalProps), { - ...(isDraggable ? { draggable: true } : {}), - onDragStart, - onDragEnter, - onDragLeave, - onDragOver, - onDragEnd, - onTouchStart, - onTouchMove, - onTouchEnd, - onDrop, - }); + return mergeReactProps( + externalProps, + { + 'aria-selected': ctx.isSelected || ctx.isSelectionStart || ctx.isSelectionEnd, + ...(isDraggable + ? { draggable: true, onDragStart, onDrop, onTouchStart, onTouchMove } + : {}), + onDragEnter, + onDragLeave, + onDragOver, + onDragEnd, + onTouchEnd, + onMouseEnter, + }, + getBaseDaysCellProps(externalProps), + ); }, [ + ctx.isSelected, + ctx.isSelectionStart, + ctx.isSelectionEnd, getBaseDaysCellProps, isDraggable, onDragStart, @@ -149,10 +167,11 @@ export function useRangeCalendarDaysCell(parameters: useRangeCalendarDaysCell.Pa onDragLeave, onDragOver, onDragEnd, + onDrop, onTouchStart, onTouchMove, onTouchEnd, - onDrop, + onMouseEnter, ], ); @@ -170,16 +189,20 @@ export namespace useRangeCalendarDaysCell { export interface Context extends useBaseCalendarDaysCell.Context, Pick< - RangeCalendarRootDragContext, + RangeCalendarRootContext, | 'isDraggingRef' | 'selectDayFromDrag' | 'startDragging' | 'stopDragging' | 'setDragTarget' + | 'setHoveredDate' | 'emptyDragImgRef' > { isSelectionStart: boolean; isSelectionEnd: boolean; + isPreviewed: boolean; + isPreviewStart: boolean; + isPreviewEnd: boolean; } } @@ -196,6 +219,7 @@ function resolveButtonElement(element: Element | null): HTMLButtonElement | null return element; } +// TODO: Check if this logic is still needed. function resolveElementFromTouch( event: React.TouchEvent, ignoreTouchTarget?: boolean, diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts index 716a4baa19b56..2bc1e993b9a7a 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts @@ -9,7 +9,7 @@ import { isEndOfRange, isRangeValid, } from '../../../utils/date-utils'; -import { useRangeCalendarRootDragContext } from '../root/RangeCalendarRootDragContext'; +import { useRangeCalendarRootContext } from '../root/RangeCalendarRootContext'; export function useRangeCalendarDaysCellWrapper( parameters: useRangeCalendarDaysCellWrapper.Parameters, @@ -17,24 +17,51 @@ export function useRangeCalendarDaysCellWrapper( const { value } = parameters; const { ref, ctx: baseCtx } = useBaseCalendarDaysCellWrapper(parameters); const utils = useUtils(); - const rangeRootDragContext = useRangeCalendarRootDragContext(); + const rootContext = useRangeCalendarRootContext(); const isSelectionStart = React.useMemo( - () => isStartOfRange(utils, value, rangeRootDragContext.highlightedRange), - [utils, value, rangeRootDragContext.highlightedRange], + () => isStartOfRange(utils, value, rootContext.highlightedRange), + [utils, value, rootContext.highlightedRange], ); const isSelectionEnd = React.useMemo( - () => isEndOfRange(utils, value, rangeRootDragContext.highlightedRange), - [utils, value, rangeRootDragContext.highlightedRange], + () => isEndOfRange(utils, value, rootContext.highlightedRange), + [utils, value, rootContext.highlightedRange], ); const isSelected = React.useMemo(() => { - if (!isRangeValid(utils, rangeRootDragContext.highlightedRange)) { + if (!isRangeValid(utils, rootContext.highlightedRange)) { return baseCtx.isSelected; } - return isWithinRange(utils, value, rangeRootDragContext.highlightedRange); - }, [utils, value, rangeRootDragContext.highlightedRange, baseCtx.isSelected]); + return ( + !isSelectionStart && + !isSelectionEnd && + isWithinRange(utils, value, rootContext.highlightedRange) + ); + }, [ + utils, + value, + rootContext.highlightedRange, + baseCtx.isSelected, + isSelectionStart, + isSelectionEnd, + ]); + + const isPreviewStart = React.useMemo( + () => isStartOfRange(utils, value, rootContext.previewRange), + [utils, value, rootContext.previewRange], + ); + + const isPreviewEnd = React.useMemo( + () => isEndOfRange(utils, value, rootContext.previewRange), + [utils, value, rootContext.previewRange], + ); + + const isPreviewed = React.useMemo(() => { + return ( + !isPreviewStart && !isPreviewEnd && isWithinRange(utils, value, rootContext.previewRange) + ); + }, [utils, value, rootContext.previewRange, isPreviewStart, isPreviewEnd]); const ctx = React.useMemo( () => ({ @@ -42,24 +69,32 @@ export function useRangeCalendarDaysCellWrapper( isSelected, isSelectionStart: isSelectionStart && !isSelectionEnd, isSelectionEnd: isSelectionEnd && !isSelectionStart, - isDraggingRef: rangeRootDragContext.isDraggingRef, - selectDayFromDrag: rangeRootDragContext.selectDayFromDrag, - startDragging: rangeRootDragContext.startDragging, - stopDragging: rangeRootDragContext.stopDragging, - setDragTarget: rangeRootDragContext.setDragTarget, - emptyDragImgRef: rangeRootDragContext.emptyDragImgRef, + isPreviewed, + isPreviewStart, + isPreviewEnd, + isDraggingRef: rootContext.isDraggingRef, + selectDayFromDrag: rootContext.selectDayFromDrag, + startDragging: rootContext.startDragging, + stopDragging: rootContext.stopDragging, + setDragTarget: rootContext.setDragTarget, + setHoveredDate: rootContext.setHoveredDate, + emptyDragImgRef: rootContext.emptyDragImgRef, }), [ baseCtx, isSelected, isSelectionStart, isSelectionEnd, - rangeRootDragContext.isDraggingRef, - rangeRootDragContext.selectDayFromDrag, - rangeRootDragContext.startDragging, - rangeRootDragContext.stopDragging, - rangeRootDragContext.setDragTarget, - rangeRootDragContext.emptyDragImgRef, + isPreviewed, + isPreviewStart, + isPreviewEnd, + rootContext.isDraggingRef, + rootContext.selectDayFromDrag, + rootContext.startDragging, + rootContext.stopDragging, + rootContext.setDragTarget, + rootContext.setHoveredDate, + rootContext.emptyDragImgRef, ], ); diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid-body/RangeCalendarDaysGridBody.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid-body/RangeCalendarDaysGridBody.tsx index 83c01c532aba5..038937f7d51da 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid-body/RangeCalendarDaysGridBody.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid-body/RangeCalendarDaysGridBody.tsx @@ -7,16 +7,15 @@ import { useComponentRenderer } from '@mui/x-date-pickers/internals/base/base-ut // eslint-disable-next-line no-restricted-imports import { CompositeList } from '@mui/x-date-pickers/internals/base/composite/list/CompositeList'; // eslint-disable-next-line no-restricted-imports -import { useBaseCalendarDaysGridBody } from '@mui/x-date-pickers/internals/base/utils/base-calendar/days-grid-body/useBaseCalendarDaysGridBody'; -// eslint-disable-next-line no-restricted-imports import { BaseCalendarDaysGridBodyContext } from '@mui/x-date-pickers/internals/base/utils/base-calendar/days-grid-body/BaseCalendarDaysGridBodyContext'; +import { useRangeCalendarDaysGridBody } from './useRangeCalendarDaysGridBody'; const RangeCalendarDaysGridBody = React.forwardRef(function CalendarDaysGrid( props: RangeCalendarDaysGridBody.Props, forwardedRef: React.ForwardedRef, ) { const { className, render, children, ...otherProps } = props; - const { getDaysGridBodyProps, context, calendarWeekRowRefs } = useBaseCalendarDaysGridBody({ + const { getDaysGridBodyProps, context, calendarWeekRowRefs } = useRangeCalendarDaysGridBody({ children, }); const state = React.useMemo(() => ({}), []); @@ -42,7 +41,7 @@ export namespace RangeCalendarDaysGridBody { export interface Props extends Omit, 'children'>, - useBaseCalendarDaysGridBody.Parameters {} + useRangeCalendarDaysGridBody.Parameters {} } export { RangeCalendarDaysGridBody }; diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid-body/useRangeCalendarDaysGridBody.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid-body/useRangeCalendarDaysGridBody.ts new file mode 100644 index 0000000000000..c80df2d9efa53 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid-body/useRangeCalendarDaysGridBody.ts @@ -0,0 +1,38 @@ +import * as React from 'react'; +import useEventCallback from '@mui/utils/useEventCallback'; +// eslint-disable-next-line no-restricted-imports +import { mergeReactProps } from '@mui/x-date-pickers/internals/base/base-utils/mergeReactProps'; +// eslint-disable-next-line no-restricted-imports +import { useBaseCalendarDaysGridBody } from '@mui/x-date-pickers/internals/base/utils/base-calendar/days-grid-body/useBaseCalendarDaysGridBody'; +// eslint-disable-next-line no-restricted-imports +import { GenericHTMLProps } from '@mui/x-date-pickers/internals/base/base-utils/types'; +import { useRangeCalendarRootContext } from '../root/RangeCalendarRootContext'; + +export function useRangeCalendarDaysGridBody(parameters: useRangeCalendarDaysGridBody.Parameters) { + const { + getDaysGridBodyProps: getBaseDaysGridBodyProps, + context, + calendarWeekRowRefs, + } = useBaseCalendarDaysGridBody(parameters); + + const rootContext = useRangeCalendarRootContext(); + + const onMouseLeave = useEventCallback(() => { + rootContext.setHoveredDate(null); + }); + + const getDaysGridBodyProps = React.useCallback( + (externalProps: GenericHTMLProps) => { + return mergeReactProps(externalProps, getBaseDaysGridBodyProps(externalProps), { + onMouseLeave, + }); + }, + [getBaseDaysGridBodyProps, onMouseLeave], + ); + + return { context, calendarWeekRowRefs, getDaysGridBodyProps }; +} + +export namespace useRangeCalendarDaysGridBody { + export interface Parameters extends useBaseCalendarDaysGridBody.Parameters {} +} diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRoot.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRoot.tsx index 3a161e20d3ade..02d36b01c55fe 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRoot.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRoot.tsx @@ -11,7 +11,6 @@ import { useComponentRenderer } from '@mui/x-date-pickers/internals/base/base-ut import { DateRangeValidationError } from '../../../../models'; import { RangeCalendarRootContext } from './RangeCalendarRootContext'; import { useRangeCalendarRoot } from './useRangeCalendarRoot'; -import { RangeCalendarRootDragContext } from './RangeCalendarRootDragContext'; const RangeCalendarRoot = React.forwardRef(function RangeCalendarRoot( props: RangeCalendarRoot.Props, @@ -36,9 +35,12 @@ const RangeCalendarRoot = React.forwardRef(function RangeCalendarRoot( disableFuture, minDate, maxDate, + availableRangePositions, + disableHoverPreview, + disableDragEditing, ...otherProps } = props; - const { getRootProps, context, baseContext, dragContext } = useRangeCalendarRoot({ + const { getRootProps, context, baseContext } = useRangeCalendarRoot({ readOnly, disabled, autoFocus, @@ -71,9 +73,7 @@ const RangeCalendarRoot = React.forwardRef(function RangeCalendarRoot( return ( - - {renderElement()} - + {renderElement()} ); diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts index 3deb3a52997af..5dd3a7fd79af3 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts @@ -1,8 +1,20 @@ import * as React from 'react'; -import { PickerRangeValue } from '@mui/x-date-pickers/internals'; +import { PickerRangeValue, RangePosition } from '@mui/x-date-pickers/internals'; +import { PickerValidDate } from '@mui/x-date-pickers/models'; export interface RangeCalendarRootContext { value: PickerRangeValue; + isDraggingRef: React.RefObject; + disableDragEditing: boolean; + selectDayFromDrag: (value: PickerValidDate) => void; + startDragging: (position: RangePosition) => void; + stopDragging: () => void; + setDragTarget: (value: PickerValidDate) => void; + emptyDragImgRef: React.RefObject; + highlightedRange: PickerRangeValue; + isDragging: boolean; + setHoveredDate: (value: PickerValidDate | null) => void; + previewRange: PickerRangeValue; } export const RangeCalendarRootContext = React.createContext( diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootDragContext.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootDragContext.ts deleted file mode 100644 index 45e70c553d562..0000000000000 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootDragContext.ts +++ /dev/null @@ -1,33 +0,0 @@ -import * as React from 'react'; -import { PickerValidDate } from '@mui/x-date-pickers/models'; -import { PickerRangeValue, RangePosition } from '@mui/x-date-pickers/internals'; - -export interface RangeCalendarRootDragContext { - isDraggingRef: React.RefObject; - disableDragEditing: boolean; - selectDayFromDrag: (value: PickerValidDate) => void; - startDragging: (position: RangePosition) => void; - stopDragging: () => void; - setDragTarget: (value: PickerValidDate) => void; - emptyDragImgRef: React.RefObject; - highlightedRange: PickerRangeValue; - isDragging: boolean; -} - -export const RangeCalendarRootDragContext = React.createContext< - RangeCalendarRootDragContext | undefined ->(undefined); - -if (process.env.NODE_ENV !== 'production') { - RangeCalendarRootDragContext.displayName = 'RangeCalendarRootDragContext'; -} - -export function useRangeCalendarRootDragContext() { - const context = React.useContext(RangeCalendarRootDragContext); - if (context === undefined) { - throw new Error( - 'Base UI X: RangeCalendarRootDragContext is missing. Range Calendar parts must be placed within .', - ); - } - return context; -} diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx index b945cd598909a..329e921b5445e 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx @@ -21,11 +21,10 @@ import { ValidateDateRangeProps, ExportedValidateDateRangeProps, } from '../../../../validation/validateDateRange'; -import { calculateRangeChange } from '../../../utils/date-range-manager'; +import { calculateRangeChange, calculateRangePreview } from '../../../utils/date-range-manager'; import { isRangeValid } from '../../../utils/date-utils'; import { useRangePosition, UseRangePositionProps } from '../../../hooks/useRangePosition'; import { RangeCalendarRootContext } from './RangeCalendarRootContext'; -import { RangeCalendarRootDragContext } from './RangeCalendarRootDragContext'; const DEFAULT_AVAILABLE_RANGE_POSITIONS: RangePosition[] = ['start', 'end']; @@ -44,6 +43,7 @@ export function useRangeCalendarRoot(parameters: useRangeCalendarRoot.Parameters availableRangePositions = DEFAULT_AVAILABLE_RANGE_POSITIONS, // Other range-specific parameters disableDragEditing = false, + disableHoverPreview = false, // Parameters forwarded to `useBaseCalendarRoot` ...baseParameters } = parameters; @@ -164,7 +164,7 @@ export function useRangeCalendarRoot(parameters: useRangeCalendarRoot.Parameters } } - const { dragContext } = useRangeCalendarDragEditing({ + const { context } = useBuildRangeCalendarRootContext({ baseContext, setValue, value, @@ -172,18 +172,17 @@ export function useRangeCalendarRoot(parameters: useRangeCalendarRoot.Parameters onRangePositionChange, rangePosition, disableDragEditing, + disableHoverPreview, getNewValueFromNewSelectedDate, }); - const context: RangeCalendarRootContext = React.useMemo(() => ({ value }), [value]); - const getRootProps = React.useCallback((externalProps: GenericHTMLProps) => { return mergeReactProps(externalProps, {}); }, []); return React.useMemo( - () => ({ getRootProps, context, baseContext, dragContext }), - [getRootProps, context, baseContext, dragContext], + () => ({ getRootProps, context, baseContext }), + [getRootProps, context, baseContext], ); } @@ -205,10 +204,17 @@ export namespace useRangeCalendarRoot { * @default false */ disableDragEditing?: boolean; + // TODO: Apply smart behavior based on the media que + /** + * If `true`, the hover preview is disabled. + * The cells that would be selected if clicking on the hovered cell won't receive a data-preview attribute. + * @default false + */ + disableHoverPreview?: boolean; } } -function useRangeCalendarDragEditing(parameters: UseRangeCalendarDragEditingParameters) { +function useBuildRangeCalendarRootContext(parameters: UseRangeCalendarDragEditingParameters) { const { value, setValue, @@ -218,6 +224,7 @@ function useRangeCalendarDragEditing(parameters: UseRangeCalendarDragEditingPara onRangePositionChange, getNewValueFromNewSelectedDate, disableDragEditing: disableDragEditingProp, + disableHoverPreview: disableHoverPreviewProp, } = parameters; const utils = useUtils(); @@ -231,31 +238,32 @@ function useRangeCalendarDragEditing(parameters: UseRangeCalendarDragEditingPara [value, utils], ); - const [dragState, setDragState] = React.useState<{ + const [state, setState] = React.useState<{ isDragging: boolean; targetDate: PickerValidDate | null; draggedDate: PickerValidDate | null; - }>({ isDragging: false, targetDate: null, draggedDate: null }); + hoveredDate: PickerValidDate | null; + }>({ isDragging: false, targetDate: null, draggedDate: null, hoveredDate: null }); const highlightedRange = React.useMemo(() => { - if (!valueDayRange[0] || !valueDayRange[1] || !dragState.targetDate) { + if (!valueDayRange[0] || !valueDayRange[1] || !state.targetDate) { return valueDayRange; } const newRange = calculateRangeChange({ utils, range: valueDayRange, - newDate: dragState.targetDate, + newDate: state.targetDate, rangePosition, allowRangeFlip: true, }).newRange; return newRange[0] !== null && newRange[1] !== null ? [utils.startOfDay(newRange[0]), utils.endOfDay(newRange[1])] : newRange; - }, [rangePosition, dragState.targetDate, utils, valueDayRange]); + }, [rangePosition, state.targetDate, utils, valueDayRange]); const selectDayFromDrag = useEventCallback((selectedDate: PickerValidDate) => { - if (dragState.draggedDate != null && utils.isSameDay(selectedDate, dragState.draggedDate)) { + if (state.draggedDate != null && utils.isSameDay(selectedDate, state.draggedDate)) { return; } @@ -270,10 +278,12 @@ function useRangeCalendarDragEditing(parameters: UseRangeCalendarDragEditingPara }); const disableDragEditing = disableDragEditingProp || baseContext.disabled || baseContext.readOnly; + const disableHoverPreview = + disableHoverPreviewProp || baseContext.disabled || baseContext.readOnly; - const isDraggingRef = React.useRef(dragState.isDragging); + const isDraggingRef = React.useRef(state.isDragging); useEnhancedEffect(() => { - isDraggingRef.current = dragState.isDragging; + isDraggingRef.current = state.isDragging; }); const emptyDragImgRef = React.useRef(null); @@ -285,12 +295,12 @@ function useRangeCalendarDragEditing(parameters: UseRangeCalendarDragEditingPara }, []); const startDragging = useEventCallback((position: RangePosition) => { - setDragState((prev) => ({ ...prev, isDragging: true })); + setState((prev) => ({ ...prev, isDragging: true })); onRangePositionChange(position); }); const stopDragging = useEventCallback(() => { - setDragState((prev) => ({ + setState((prev) => ({ ...prev, isDragging: false, draggedDate: null, @@ -299,11 +309,11 @@ function useRangeCalendarDragEditing(parameters: UseRangeCalendarDragEditingPara }); const handleSetDragTarget = useEventCallback((newTargetDate: PickerValidDate) => { - if (utils.isEqual(newTargetDate, dragState.targetDate)) { + if (utils.isEqual(newTargetDate, state.targetDate)) { return; } - setDragState((prev) => ({ ...prev, targetDate: newTargetDate })); + setState((prev) => ({ ...prev, targetDate: newTargetDate })); if (value[0] && utils.isBeforeDay(newTargetDate, value[0])) { onRangePositionChange('start'); @@ -312,8 +322,30 @@ function useRangeCalendarDragEditing(parameters: UseRangeCalendarDragEditingPara } }); - const dragContext: RangeCalendarRootDragContext = { + const setHoveredDate = useEventCallback((hoveredDate: PickerValidDate | null) => { + if (disableHoverPreview) { + return; + } + setState((prev) => ({ ...prev, hoveredDate })); + }); + + const previewRange = React.useMemo(() => { + if (disableHoverPreview) { + return [null, null]; + } + + return calculateRangePreview({ + utils, + range: valueDayRange, + newDate: state.hoveredDate, + rangePosition, + }); + }, [utils, rangePosition, state.hoveredDate, valueDayRange, disableHoverPreview]); + + const context: RangeCalendarRootContext = { + value, highlightedRange, + previewRange, disableDragEditing, isDraggingRef, emptyDragImgRef, @@ -321,10 +353,11 @@ function useRangeCalendarDragEditing(parameters: UseRangeCalendarDragEditingPara startDragging, stopDragging, setDragTarget: handleSetDragTarget, - isDragging: dragState.isDragging, + isDragging: state.isDragging, + setHoveredDate, }; - return { dragContext }; + return { context }; } interface UseRangeCalendarDragEditingParameters { @@ -340,4 +373,5 @@ interface UseRangeCalendarDragEditingParameters { ) => useBaseCalendarRoot.GetNewValueFromNewSelectedDateReturnValue; onRangePositionChange: (position: RangePosition) => void; rangePosition: RangePosition; + disableHoverPreview: boolean; } From 33d7ecf6091b3e08292f048a698e0dfaedad6fa8 Mon Sep 17 00:00:00 2001 From: flavien Date: Fri, 10 Jan 2025 14:05:22 +0100 Subject: [PATCH 090/136] Work on preview --- .../base-calendar/calendar.module.css | 36 +++--------------- .../days-cell/RangeCalendarDaysCell.tsx | 18 ++++++++- .../RangeCalendarDaysCellDataAttributes.ts | 12 +++++- .../days-cell/useRangeCalendarDaysCell.tsx | 6 +-- .../useRangeCalendarDaysCellWrapper.ts | 37 ++++++------------- .../RangeCalendar/root/RangeCalendarRoot.tsx | 31 +++++++++++----- .../RangeCalendarSetVisibleMonth.tsx | 7 +++- .../base/Calendar/root/CalendarRoot.tsx | 24 +++++++----- .../CalendarSetVisibleMonth.tsx | 7 +++- .../useBaseCalendarDaysCellWrapper.ts | 4 +- .../useBaseCalendarDaysWeekRowWrapper.ts | 4 +- .../useBaseCalendarMonthsCellWrapper.ts | 4 +- .../useBaseCalendarSetVisibleMonthWrapper.ts | 21 +++++++++-- .../useBaseCalendarSetVisibleYearWrapper.ts | 21 +++++++++-- .../useBaseCalendarYearsCellWrapper.ts | 4 +- 15 files changed, 134 insertions(+), 102 deletions(-) diff --git a/docs/data/date-pickers/base-calendar/calendar.module.css b/docs/data/date-pickers/base-calendar/calendar.module.css index cfacc6b5aa882..f80af74cc45a5 100644 --- a/docs/data/date-pickers/base-calendar/calendar.module.css +++ b/docs/data/date-pickers/base-calendar/calendar.module.css @@ -170,9 +170,7 @@ background-color: transparent; cursor: pointer; - &[data-current]:not([data-selected]):not([data-selection-start]):not([data-selection-end]):not( - :focus-visible - ) { + &[data-current]:not([data-selected]):not(:focus-visible) { outline: 1px solid #9ca3af; } @@ -188,38 +186,16 @@ } &.RangeDaysCell { - &[data-selected], - &[data-selection-end], - &[data-previewed], - &[data-preview-end] { - border-top-left-radius: 0; - border-bottom-left-radius: 0; + &[data-previewed]:not([data-selected]) { + border-top: 1px dashed var(--day-preview-border-color); + border-bottom: 1px dashed var(--day-preview-border-color); } - &[data-selected], - &[data-selection-start], - &[data-previewed], - &[data-preview-start] { - border-top-right-radius: 0; - border-bottom-right-radius: 0; - } - - &[data-preview-start], - &[data-preview-end], - &[data-previewed] { - &:not([data-selected]):not([data-selection-start]):not([data-selection-end]) { - border-top: 1px dashed var(--day-preview-border-color); - border-bottom: 1px dashed var(--day-preview-border-color); - } - } - - &[data-preview-start]:not([data-selected]):not([data-selection-start]):not( - [data-selection-end] - ) { + &[data-preview-start]:not([data-selected]) { border-left: 1px dashed var(--day-preview-border-color); } - &[data-preview-end]:not([data-selected]):not([data-selection-start]):not([data-selection-end]) { + &[data-preview-end]:not([data-selected]) { border-right: 1px dashed var(--day-preview-border-color); } } diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/RangeCalendarDaysCell.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/RangeCalendarDaysCell.tsx index ea4bea604a724..b13b642575549 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/RangeCalendarDaysCell.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/RangeCalendarDaysCell.tsx @@ -20,6 +20,9 @@ const customStyleHookMapping: CustomStyleHookMapping { - if (!ctx.isSelected && !ctx.isSelectionStart && !ctx.isSelectionEnd) { + if (!ctx.isSelected) { ctx.setHoveredDate(value); } else { ctx.setHoveredDate(null); @@ -142,7 +142,6 @@ export function useRangeCalendarDaysCell(parameters: useRangeCalendarDaysCell.Pa return mergeReactProps( externalProps, { - 'aria-selected': ctx.isSelected || ctx.isSelectionStart || ctx.isSelectionEnd, ...(isDraggable ? { draggable: true, onDragStart, onDrop, onTouchStart, onTouchMove } : {}), @@ -157,9 +156,6 @@ export function useRangeCalendarDaysCell(parameters: useRangeCalendarDaysCell.Pa ); }, [ - ctx.isSelected, - ctx.isSelectionStart, - ctx.isSelectionEnd, getBaseDaysCellProps, isDraggable, onDragStart, diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts index 2bc1e993b9a7a..2da97b05c9d9f 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts @@ -19,6 +19,13 @@ export function useRangeCalendarDaysCellWrapper( const utils = useUtils(); const rootContext = useRangeCalendarRootContext(); + const isSelected = React.useMemo(() => { + if (!isRangeValid(utils, rootContext.highlightedRange)) { + return baseCtx.isSelected; + } + return isWithinRange(utils, value, rootContext.highlightedRange); + }, [utils, value, rootContext.highlightedRange, baseCtx.isSelected]); + const isSelectionStart = React.useMemo( () => isStartOfRange(utils, value, rootContext.highlightedRange), [utils, value, rootContext.highlightedRange], @@ -29,23 +36,9 @@ export function useRangeCalendarDaysCellWrapper( [utils, value, rootContext.highlightedRange], ); - const isSelected = React.useMemo(() => { - if (!isRangeValid(utils, rootContext.highlightedRange)) { - return baseCtx.isSelected; - } - return ( - !isSelectionStart && - !isSelectionEnd && - isWithinRange(utils, value, rootContext.highlightedRange) - ); - }, [ - utils, - value, - rootContext.highlightedRange, - baseCtx.isSelected, - isSelectionStart, - isSelectionEnd, - ]); + const isPreviewed = React.useMemo(() => { + return isWithinRange(utils, value, rootContext.previewRange); + }, [utils, value, rootContext.previewRange]); const isPreviewStart = React.useMemo( () => isStartOfRange(utils, value, rootContext.previewRange), @@ -57,18 +50,12 @@ export function useRangeCalendarDaysCellWrapper( [utils, value, rootContext.previewRange], ); - const isPreviewed = React.useMemo(() => { - return ( - !isPreviewStart && !isPreviewEnd && isWithinRange(utils, value, rootContext.previewRange) - ); - }, [utils, value, rootContext.previewRange, isPreviewStart, isPreviewEnd]); - const ctx = React.useMemo( () => ({ ...baseCtx, isSelected, - isSelectionStart: isSelectionStart && !isSelectionEnd, - isSelectionEnd: isSelectionEnd && !isSelectionStart, + isSelectionStart, + isSelectionEnd, isPreviewed, isPreviewStart, isPreviewEnd, diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRoot.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRoot.tsx index 02d36b01c55fe..14f9e77aa12b7 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRoot.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRoot.tsx @@ -17,27 +17,38 @@ const RangeCalendarRoot = React.forwardRef(function RangeCalendarRoot( forwardedRef: React.ForwardedRef, ) { const { + // Rendering props className, render, + // Validation props + minDate, + maxDate, + disablePast, + disableFuture, + shouldDisableDate, + // Range position props + rangePosition: rangePositionProp, + defaultRangePosition: defaultRangePositionProp, + onRangePositionChange: onRangePositionChangeProp, + availableRangePositions, + // Other range-specific parameters + disableDragEditing, + disableHoverPreview, + // Form props readOnly, disabled, + // Focus and navigation props autoFocus, + monthPageSize, + yearPageSize, + // Value props onError, defaultValue, onValueChange, value, timezone, referenceDate, - monthPageSize, - yearPageSize, - shouldDisableDate, - disablePast, - disableFuture, - minDate, - maxDate, - availableRangePositions, - disableHoverPreview, - disableDragEditing, + // Props forwarded to the DOM element ...otherProps } = props; const { getRootProps, context, baseContext } = useRangeCalendarRoot({ diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/set-visible-month/RangeCalendarSetVisibleMonth.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/set-visible-month/RangeCalendarSetVisibleMonth.tsx index 0c16156bd495a..241279c6a5d21 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/set-visible-month/RangeCalendarSetVisibleMonth.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/set-visible-month/RangeCalendarSetVisibleMonth.tsx @@ -38,8 +38,11 @@ const RangeCalendarSetVisibleMonth = React.forwardRef(function RangeCalendarSetV props: RangeCalendarSetVisibleMonth.Props, forwardedRef: React.ForwardedRef, ) { - const { ctx } = useBaseCalendarSetVisibleMonthWrapper({ target: props.target }); - return ; + const { ref, ctx } = useBaseCalendarSetVisibleMonthWrapper({ + forwardedRef, + target: props.target, + }); + return ; }); export namespace RangeCalendarSetVisibleMonth { diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRoot.tsx b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRoot.tsx index 9260875ae18dc..34278a6901641 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRoot.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRoot.tsx @@ -13,26 +13,32 @@ const CalendarRoot = React.forwardRef(function CalendarRoot( forwardedRef: React.ForwardedRef, ) { const { + // Rendering props className, render, + // Validation props + minDate, + maxDate, + disablePast, + disableFuture, + shouldDisableDate, + shouldDisableMonth, + shouldDisableYear, + // Form props readOnly, disabled, + // Focus and navigation props autoFocus, + monthPageSize, + yearPageSize, + // Value props onError, defaultValue, onValueChange, value, timezone, referenceDate, - monthPageSize, - yearPageSize, - shouldDisableDate, - shouldDisableMonth, - shouldDisableYear, - disablePast, - disableFuture, - minDate, - maxDate, + // Props forwarded to the DOM element ...otherProps } = props; const { getRootProps, context, baseContext } = useCalendarRoot({ diff --git a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx index 0a6b621049ee0..b0193d0c67873 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx @@ -33,8 +33,11 @@ const CalendarSetVisibleMonth = React.forwardRef(function CalendarSetVisibleMont props: CalendarSetVisibleMonth.Props, forwardedRef: React.ForwardedRef, ) { - const { ctx } = useBaseCalendarSetVisibleMonthWrapper({ target: props.target }); - return ; + const { ref, ctx } = useBaseCalendarSetVisibleMonthWrapper({ + forwardedRef, + target: props.target, + }); + return ; }); export namespace CalendarSetVisibleMonth { diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCellWrapper.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCellWrapper.ts index 50f18ea01ed3d..aac201f45b615 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCellWrapper.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCellWrapper.ts @@ -8,7 +8,7 @@ import type { useBaseCalendarDaysCell } from './useBaseCalendarDaysCell'; export function useBaseCalendarDaysCellWrapper( parameters: useBaseCalendarDaysCellWrapper.Parameters, -) { +): useBaseCalendarDaysCellWrapper.ReturnValue { const { forwardedRef, value } = parameters; const baseRootContext = useBaseCalendarRootContext(); const baseDaysGridContext = useBaseCalendarDaysGridContext(); @@ -84,7 +84,7 @@ export namespace useBaseCalendarDaysCellWrapper { /** * The ref to forward to the component. */ - ref: React.RefObject; + ref: React.RefCallback | null; /** * The memoized context to forward to the memoized component so that it does not need to subscribe to any context. */ diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRowWrapper.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRowWrapper.ts index 7cf0678a12415..44c6a74d11228 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRowWrapper.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRowWrapper.ts @@ -7,7 +7,7 @@ import type { useBaseCalendarDaysWeekRow } from './useBaseCalendarDaysWeekRow'; export function useBaseCalendarDaysWeekRowWrapper( parameters: useBaseCalendarDaysWeekRowWrapper.Parameters, -) { +): useBaseCalendarDaysWeekRowWrapper.ReturnValue { const { forwardedRef, value } = parameters; const baseDaysGridContext = useBaseCalendarDaysGridContext(); const baseDaysGridBodyContext = useBaseCalendarDaysGridBodyContext(); @@ -44,7 +44,7 @@ export namespace useBaseCalendarDaysWeekRowWrapper { /** * The ref to forward to the component. */ - ref: React.RefObject; + ref: React.RefCallback | null; /** * The memoized context to forward to the memoized component so that it does not need to subscribe to any context. */ diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-cell/useBaseCalendarMonthsCellWrapper.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-cell/useBaseCalendarMonthsCellWrapper.ts index 3ad2761c638ad..8d4490888b9f6 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-cell/useBaseCalendarMonthsCellWrapper.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-cell/useBaseCalendarMonthsCellWrapper.ts @@ -11,7 +11,7 @@ import { useBaseCalendarMonthsGridOrListContext } from '../months-grid/BaseCalen export function useBaseCalendarMonthsCellWrapper( parameters: useBaseCalendarMonthsCellWrapper.Parameters, -) { +): useBaseCalendarMonthsCellWrapper.ReturnValue { const { forwardedRef, value } = parameters; const baseRootContext = useBaseCalendarRootContext(); const baseMonthListOrGridContext = useBaseCalendarMonthsGridOrListContext(); @@ -130,7 +130,7 @@ export namespace useBaseCalendarMonthsCellWrapper { /** * The ref to forward to the component. */ - ref: React.RefObject; + ref: React.RefCallback | null; /** * The memoized context to forward to the memoized component so that it does not need to subscribe to any context. */ diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-month/useBaseCalendarSetVisibleMonthWrapper.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-month/useBaseCalendarSetVisibleMonthWrapper.ts index b4baae6179d06..b8db2903fe4b1 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-month/useBaseCalendarSetVisibleMonthWrapper.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-month/useBaseCalendarSetVisibleMonthWrapper.ts @@ -1,18 +1,22 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; +import useForkRef from '@mui/utils/useForkRef'; import { useUtils } from '../../../../hooks/useUtils'; import { getFirstEnabledMonth, getLastEnabledMonth } from '../utils/date'; import { useBaseCalendarSetVisibleMonth } from './useBaseCalendarSetVisibleMonth'; import { useBaseCalendarRootContext } from '../root/BaseCalendarRootContext'; import { useNullableBaseCalendarMonthsGridOrListContext } from '../months-grid/BaseCalendarMonthsGridOrListContext'; +import { useCompositeListItem } from '../../../composite/list/useCompositeListItem'; export function useBaseCalendarSetVisibleMonthWrapper( parameters: useBaseCalendarSetVisibleMonthWrapper.Parameters, -) { - const { target } = parameters; +): useBaseCalendarSetVisibleMonthWrapper.ReturnValue { + const { forwardedRef, target } = parameters; const baseRootContext = useBaseCalendarRootContext(); const baseMonthsListOrGridContext = useNullableBaseCalendarMonthsGridOrListContext(); const utils = useUtils(); + const { ref: listItemRef } = useCompositeListItem(); + const mergedRef = useForkRef(forwardedRef, listItemRef); const targetDate = React.useMemo(() => { if (target === 'previous') { @@ -80,13 +84,22 @@ export function useBaseCalendarSetVisibleMonthWrapper( [setTarget, isDisabled, isTabbable], ); - return { ctx }; + return { ref: mergedRef, ctx }; } export namespace useBaseCalendarSetVisibleMonthWrapper { - export interface Parameters extends Pick {} + export interface Parameters extends Pick { + /** + * The ref forwarded by the parent component. + */ + forwardedRef: React.ForwardedRef; + } export interface ReturnValue { + /** + * The ref to forward to the component. + */ + ref: React.RefCallback | null; /** * The memoized context to forward to the memoized component so that it does not need to subscribe to any context. */ diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-year/useBaseCalendarSetVisibleYearWrapper.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-year/useBaseCalendarSetVisibleYearWrapper.ts index 8a9e799f340d6..f155e00a60ec0 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-year/useBaseCalendarSetVisibleYearWrapper.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-year/useBaseCalendarSetVisibleYearWrapper.ts @@ -1,18 +1,22 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; +import useForkRef from '@mui/utils/useForkRef'; import { useUtils } from '../../../../hooks/useUtils'; import { useBaseCalendarSetVisibleYear } from './useBaseCalendarSetVisibleYear'; import { useBaseCalendarRootContext } from '../root/BaseCalendarRootContext'; import { getFirstEnabledYear, getLastEnabledYear } from '../utils/date'; import { useNullableBaseCalendarYearsGridOrListContext } from '../years-grid/BaseCalendarYearsGridOrListContext'; +import { useCompositeListItem } from '../../../composite/list/useCompositeListItem'; export function useBaseCalendarSetVisibleYearWrapper( parameters: useBaseCalendarSetVisibleYearWrapper.Parameters, -) { - const { target } = parameters; +): useBaseCalendarSetVisibleYearWrapper.ReturnValue { + const { forwardedRef, target } = parameters; const baseRootContext = useBaseCalendarRootContext(); const baseYearsListOrGridContext = useNullableBaseCalendarYearsGridOrListContext(); const utils = useUtils(); + const { ref: listItemRef } = useCompositeListItem(); + const mergedRef = useForkRef(forwardedRef, listItemRef); const targetDate = React.useMemo(() => { if (target === 'previous') { @@ -79,13 +83,22 @@ export function useBaseCalendarSetVisibleYearWrapper( [setTarget, isDisabled, isTabbable], ); - return { ctx }; + return { ref: mergedRef, ctx }; } export namespace useBaseCalendarSetVisibleYearWrapper { - export interface Parameters extends Pick {} + export interface Parameters extends Pick { + /** + * The ref forwarded by the parent component. + */ + forwardedRef: React.ForwardedRef; + } export interface ReturnValue { + /** + * The ref to forward to the component. + */ + ref: React.RefCallback | null; /** * The memoized context to forward to the memoized component so that it does not need to subscribe to any context. */ diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/years-cell/useBaseCalendarYearsCellWrapper.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/years-cell/useBaseCalendarYearsCellWrapper.ts index 43dda1b95fccf..8e91c7c8ed212 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/years-cell/useBaseCalendarYearsCellWrapper.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/years-cell/useBaseCalendarYearsCellWrapper.ts @@ -11,7 +11,7 @@ import { useBaseCalendarYearsGridOrListContext } from '../years-grid/BaseCalenda export function useBaseCalendarYearsCellWrapper( parameters: useBaseCalendarYearsCellWrapper.Parameters, -) { +): useBaseCalendarYearsCellWrapper.ReturnValue { const { forwardedRef, value } = parameters; const baseRootContext = useBaseCalendarRootContext(); const baseYearsListOrGridContext = useBaseCalendarYearsGridOrListContext(); @@ -125,7 +125,7 @@ export namespace useBaseCalendarYearsCellWrapper { /** * The ref to forward to the component. */ - ref: React.RefObject; + ref: React.RefCallback | null; /** * The memoized context to forward to the memoized component so that it does not need to subscribe to any context. */ From 9bb9a4dd8fc8f0aa86fc88f7afe948223493c144 Mon Sep 17 00:00:00 2001 From: flavien Date: Fri, 10 Jan 2025 14:09:30 +0100 Subject: [PATCH 091/136] Work --- .../base/RangeCalendar/utils/useRangeGrid.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/useRangeGrid.ts diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/useRangeGrid.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/useRangeGrid.ts new file mode 100644 index 0000000000000..255f56d8d11b1 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/useRangeGrid.ts @@ -0,0 +1,20 @@ +export function useRangeGrid(parameters: useRangeGrid.Parameters): useRangeGrid.ReturnValue {} + +export namespace useRangeGrid { + export interface Parameters { + /** + * If `true`, editing dates by dragging is disabled. + * @default false + */ + disableDragEditing?: boolean; + // TODO: Apply smart behavior based on the media que + /** + * If `true`, the hover preview is disabled. + * The cells that would be selected if clicking on the hovered cell won't receive a data-preview attribute. + * @default false + */ + disableHoverPreview?: boolean; + } + + export interface ReturnValue {} +} From 314ef81357a89f42ce45abc7bc9c8511693a8736 Mon Sep 17 00:00:00 2001 From: flavien Date: Fri, 10 Jan 2025 14:22:16 +0100 Subject: [PATCH 092/136] Work --- .../base/RangeCalendar/utils/useRangeGrid.ts | 20 ------------------- 1 file changed, 20 deletions(-) delete mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/useRangeGrid.ts diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/useRangeGrid.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/useRangeGrid.ts deleted file mode 100644 index 255f56d8d11b1..0000000000000 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/useRangeGrid.ts +++ /dev/null @@ -1,20 +0,0 @@ -export function useRangeGrid(parameters: useRangeGrid.Parameters): useRangeGrid.ReturnValue {} - -export namespace useRangeGrid { - export interface Parameters { - /** - * If `true`, editing dates by dragging is disabled. - * @default false - */ - disableDragEditing?: boolean; - // TODO: Apply smart behavior based on the media que - /** - * If `true`, the hover preview is disabled. - * The cells that would be selected if clicking on the hovered cell won't receive a data-preview attribute. - * @default false - */ - disableHoverPreview?: boolean; - } - - export interface ReturnValue {} -} From c256168abd532f73f348e54d80a799695206d63a Mon Sep 17 00:00:00 2001 From: flavien Date: Fri, 10 Jan 2025 14:28:27 +0100 Subject: [PATCH 093/136] Fix --- .../date-pickers/base-calendar/calendar.module.css | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/data/date-pickers/base-calendar/calendar.module.css b/docs/data/date-pickers/base-calendar/calendar.module.css index f80af74cc45a5..4a5384e97c039 100644 --- a/docs/data/date-pickers/base-calendar/calendar.module.css +++ b/docs/data/date-pickers/base-calendar/calendar.module.css @@ -186,6 +186,18 @@ } &.RangeDaysCell { + &[data-selected]:not([data-selection-start]), + &[data-previewed]:not([data-preview-start]) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + + &[data-selected]:not([data-selection-end]), + &[data-previewed]:not([data-preview-end]) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + &[data-previewed]:not([data-selected]) { border-top: 1px dashed var(--day-preview-border-color); border-bottom: 1px dashed var(--day-preview-border-color); From 906d945f7017f85385ab9db2985c217c4ab34625 Mon Sep 17 00:00:00 2001 From: flavien Date: Fri, 10 Jan 2025 15:25:11 +0100 Subject: [PATCH 094/136] Add automatic scroll on year list and grid --- .../base-calendar/DateRangeCalendarDemo.tsx | 7 +--- .../RangeCalendarDaysWeekRow.tsx | 4 +-- .../months-grid/RangeCalendarMonthsGrid.tsx | 16 ++++----- .../months-list/RangeCalendarMonthsList.tsx | 4 +-- .../years-grid/RangeCalendarYearsGrid.tsx | 15 +++++--- .../years-list/RangeCalendarYearsList.tsx | 15 ++++---- .../days-week-row/CalendarDaysWeekRow.tsx | 4 +-- .../months-grid/CalendarMonthsGrid.tsx | 16 ++++----- .../months-list/CalendarMonthsList.tsx | 4 +-- .../Calendar/years-grid/CalendarYearsGrid.tsx | 15 ++++---- .../Calendar/years-list/CalendarYearsList.tsx | 15 ++++---- .../useBaseCalendarDaysWeekRow.ts | 9 ++--- .../useBaseCalendarDaysWeekRowWrapper.ts | 2 +- .../months-grid/useBaseCalendarMonthsGrid.ts | 10 +++--- .../months-list/useBaseCalendarMonthsList.ts | 10 +++--- .../base-calendar/utils/useYearsCells.ts | 36 ++++++++++++++++++- .../years-grid/useBaseCalendarYearsGrid.ts | 6 ++-- .../years-list/useBaseCalendarYearsList.ts | 10 +++--- 18 files changed, 117 insertions(+), 81 deletions(-) diff --git a/docs/data/date-pickers/base-calendar/DateRangeCalendarDemo.tsx b/docs/data/date-pickers/base-calendar/DateRangeCalendarDemo.tsx index cfcc3da4877e6..abf9caa288cec 100644 --- a/docs/data/date-pickers/base-calendar/DateRangeCalendarDemo.tsx +++ b/docs/data/date-pickers/base-calendar/DateRangeCalendarDemo.tsx @@ -8,7 +8,6 @@ import { useRangeCalendarContext, } from '@mui/x-date-pickers-pro/internals/base/RangeCalendar'; import styles from './calendar.module.css'; -import dayjs from 'dayjs'; function Header(props: { activeSection: 'day' | 'month' | 'year'; @@ -81,11 +80,7 @@ export default function DateRangeCalendarDemo() { return ( - +
, ) { const { className, render, value, ctx, children, ...otherProps } = props; - const { getDaysWeekRowProps, dayCellRefs } = useBaseCalendarDaysWeekRow({ + const { getDaysWeekRowProps, cellRefs } = useBaseCalendarDaysWeekRow({ value, ctx, children, @@ -32,7 +32,7 @@ const InnerRangeCalendarDaysWeekRow = React.forwardRef(function InnerRangeCalend extraProps: otherProps, }); - return {renderElement()}; + return {renderElement()}; }); const MemoizedInnerRangeCalendarDaysWeekRow = React.memo(InnerRangeCalendarDaysWeekRow); diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-grid/RangeCalendarMonthsGrid.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-grid/RangeCalendarMonthsGrid.tsx index 58f47af7d77be..6d9bf3ccba989 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-grid/RangeCalendarMonthsGrid.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-grid/RangeCalendarMonthsGrid.tsx @@ -17,14 +17,12 @@ const RangeCalendarMonthsGrid = React.forwardRef(function RangeCalendarMonthsLis forwardedRef: React.ForwardedRef, ) { const { className, render, children, cellsPerRow, canChangeYear, ...otherProps } = props; - const { getMonthsGridProps, monthsCellRefs, monthsListOrGridContext } = useBaseCalendarMonthsGrid( - { - children, - cellsPerRow, - canChangeYear, - cellsPerRowCssVar: RangeCalendarMonthsGridCssVars.calendarMonthsGridCellsPerRow, - }, - ); + const { getMonthsGridProps, cellRefs, monthsListOrGridContext } = useBaseCalendarMonthsGrid({ + children, + cellsPerRow, + canChangeYear, + cellsPerRowCssVar: RangeCalendarMonthsGridCssVars.calendarMonthsGridCellsPerRow, + }); const state = React.useMemo(() => ({}), []); const { renderElement } = useComponentRenderer({ @@ -38,7 +36,7 @@ const RangeCalendarMonthsGrid = React.forwardRef(function RangeCalendarMonthsLis return ( - {renderElement()} + {renderElement()} ); }); diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-list/RangeCalendarMonthsList.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-list/RangeCalendarMonthsList.tsx index a581758a9f354..1a3bbc09078f1 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-list/RangeCalendarMonthsList.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-list/RangeCalendarMonthsList.tsx @@ -16,7 +16,7 @@ const RangeCalendarMonthsList = React.forwardRef(function RangeCalendarMonthsLis forwardedRef: React.ForwardedRef, ) { const { className, render, children, loop, canChangeYear, ...otherProps } = props; - const { getMonthListProps, monthsCellRefs, monthsListOrGridContext } = useBaseCalendarMonthsList({ + const { getMonthListProps, cellRefs, monthsListOrGridContext } = useBaseCalendarMonthsList({ children, loop, canChangeYear, @@ -34,7 +34,7 @@ const RangeCalendarMonthsList = React.forwardRef(function RangeCalendarMonthsLis return ( - {renderElement()} + {renderElement()} ); }); diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-grid/RangeCalendarYearsGrid.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-grid/RangeCalendarYearsGrid.tsx index 9b89c9e935ae1..c052e9ba95667 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-grid/RangeCalendarYearsGrid.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-grid/RangeCalendarYearsGrid.tsx @@ -1,5 +1,6 @@ 'use client'; import * as React from 'react'; +import useForkRef from '@mui/utils/useForkRef'; // eslint-disable-next-line no-restricted-imports import { useBaseCalendarYearsGrid } from '@mui/x-date-pickers/internals/base/utils/base-calendar/years-grid/useBaseCalendarYearsGrid'; // eslint-disable-next-line no-restricted-imports @@ -10,22 +11,26 @@ import { useComponentRenderer } from '@mui/x-date-pickers/internals/base/base-ut import { CompositeList } from '@mui/x-date-pickers/internals/base/composite/list/CompositeList'; // eslint-disable-next-line no-restricted-imports import { BaseCalendarYearsGridOrListContext } from '@mui/x-date-pickers/internals/base/utils/base-calendar/years-grid/BaseCalendarYearsGridOrListContext'; +import { RangeCalendarYearsGridCssVars } from './RangeCalendarYearsGridCssVars'; const RangeCalendarYearsGrid = React.forwardRef(function CalendarYearsList( props: RangeCalendarYearsGrid.Props, forwardedRef: React.ForwardedRef, ) { const { className, render, children, cellsPerRow, ...otherProps } = props; - const { getYearsGridProps, yearsCellRefs, yearsListOrGridContext } = useBaseCalendarYearsGrid({ - children, - cellsPerRow, - }); + const { getYearsGridProps, yearsCellRefs, yearsListOrGridContext, scrollerRef } = + useBaseCalendarYearsGrid({ + children, + cellsPerRow, + cellsPerRowCssVar: RangeCalendarYearsGridCssVars.rangeCalendarYearsGridCellsPerRow, + }); const state = React.useMemo(() => ({}), []); + const ref = useForkRef(forwardedRef, scrollerRef); const { renderElement } = useComponentRenderer({ propGetter: getYearsGridProps, render: render ?? 'div', - ref: forwardedRef, + ref, className, state, extraProps: otherProps, diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-list/RangeCalendarYearsList.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-list/RangeCalendarYearsList.tsx index 322d0df1385cc..410a5d343c237 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-list/RangeCalendarYearsList.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-list/RangeCalendarYearsList.tsx @@ -1,5 +1,6 @@ 'use client'; import * as React from 'react'; +import useForkRef from '@mui/utils/useForkRef'; // eslint-disable-next-line no-restricted-imports import { useBaseCalendarYearsList } from '@mui/x-date-pickers/internals/base/utils/base-calendar/years-list/useBaseCalendarYearsList'; // eslint-disable-next-line no-restricted-imports @@ -16,16 +17,18 @@ const RangeCalendarYearsList = React.forwardRef(function CalendarYearsList( forwardedRef: React.ForwardedRef, ) { const { className, render, children, loop, ...otherProps } = props; - const { getYearsListProps, yearsCellRefs, yearsListOrGridContext } = useBaseCalendarYearsList({ - children, - loop, - }); + const { getYearsListProps, cellRefs, yearsListOrGridContext, scrollerRef } = + useBaseCalendarYearsList({ + children, + loop, + }); const state = React.useMemo(() => ({}), []); + const ref = useForkRef(forwardedRef, scrollerRef); const { renderElement } = useComponentRenderer({ propGetter: getYearsListProps, render: render ?? 'div', - ref: forwardedRef, + ref, className, state, extraProps: otherProps, @@ -33,7 +36,7 @@ const RangeCalendarYearsList = React.forwardRef(function CalendarYearsList( return ( - {renderElement()} + {renderElement()} ); }); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/CalendarDaysWeekRow.tsx b/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/CalendarDaysWeekRow.tsx index bfd221665bdaf..a415173e5bb84 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/CalendarDaysWeekRow.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-week-row/CalendarDaysWeekRow.tsx @@ -11,7 +11,7 @@ const InnerCalendarDaysWeekRow = React.forwardRef(function InnerCalendarDaysWeek forwardedRef: React.ForwardedRef, ) { const { className, render, value, ctx, children, ...otherProps } = props; - const { getDaysWeekRowProps, dayCellRefs } = useBaseCalendarDaysWeekRow({ + const { getDaysWeekRowProps, cellRefs } = useBaseCalendarDaysWeekRow({ value, ctx, children, @@ -27,7 +27,7 @@ const InnerCalendarDaysWeekRow = React.forwardRef(function InnerCalendarDaysWeek extraProps: otherProps, }); - return {renderElement()}; + return {renderElement()}; }); const MemoizedInnerCalendarDaysWeekRow = React.memo(InnerCalendarDaysWeekRow); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-grid/CalendarMonthsGrid.tsx b/packages/x-date-pickers/src/internals/base/Calendar/months-grid/CalendarMonthsGrid.tsx index 22bfe4f6b0746..467332d2d5c48 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-grid/CalendarMonthsGrid.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-grid/CalendarMonthsGrid.tsx @@ -12,14 +12,12 @@ const CalendarMonthsGrid = React.forwardRef(function CalendarMonthsList( forwardedRef: React.ForwardedRef, ) { const { className, render, children, cellsPerRow, canChangeYear, ...otherProps } = props; - const { getMonthsGridProps, monthsCellRefs, monthsListOrGridContext } = useBaseCalendarMonthsGrid( - { - children, - cellsPerRow, - canChangeYear, - cellsPerRowCssVar: CalendarMonthsGridCssVars.calendarMonthsGridCellsPerRow, - }, - ); + const { getMonthsGridProps, cellRefs, monthsListOrGridContext } = useBaseCalendarMonthsGrid({ + children, + cellsPerRow, + canChangeYear, + cellsPerRowCssVar: CalendarMonthsGridCssVars.calendarMonthsGridCellsPerRow, + }); const state = React.useMemo(() => ({}), []); const { renderElement } = useComponentRenderer({ @@ -33,7 +31,7 @@ const CalendarMonthsGrid = React.forwardRef(function CalendarMonthsList( return ( - {renderElement()} + {renderElement()} ); }); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsList.tsx b/packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsList.tsx index 8f37ecd9b8622..ad4ea217db9f6 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsList.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsList.tsx @@ -11,7 +11,7 @@ const CalendarMonthsList = React.forwardRef(function CalendarMonthsList( forwardedRef: React.ForwardedRef, ) { const { className, render, children, loop, canChangeYear, ...otherProps } = props; - const { getMonthListProps, monthsCellRefs, monthsListOrGridContext } = useBaseCalendarMonthsList({ + const { getMonthListProps, cellRefs, monthsListOrGridContext } = useBaseCalendarMonthsList({ children, loop, canChangeYear, @@ -29,7 +29,7 @@ const CalendarMonthsList = React.forwardRef(function CalendarMonthsList( return ( - {renderElement()} + {renderElement()} ); }); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-grid/CalendarYearsGrid.tsx b/packages/x-date-pickers/src/internals/base/Calendar/years-grid/CalendarYearsGrid.tsx index 1b628b387357b..0e97356261c5e 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-grid/CalendarYearsGrid.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-grid/CalendarYearsGrid.tsx @@ -1,5 +1,6 @@ 'use client'; import * as React from 'react'; +import useForkRef from '@mui/utils/useForkRef'; import { useBaseCalendarYearsGrid } from '../../utils/base-calendar/years-grid/useBaseCalendarYearsGrid'; import { BaseUIComponentProps } from '../../base-utils/types'; import { useComponentRenderer } from '../../base-utils/useComponentRenderer'; @@ -12,17 +13,19 @@ const CalendarYearsGrid = React.forwardRef(function CalendarYearsList( forwardedRef: React.ForwardedRef, ) { const { className, render, children, cellsPerRow, ...otherProps } = props; - const { getYearsGridProps, yearsCellRefs, yearsListOrGridContext } = useBaseCalendarYearsGrid({ - children, - cellsPerRow, - cellsPerRowCssVar: CalendarYearsGridCssVars.calendarYearsGridCellsPerRow, - }); + const { getYearsGridProps, yearsCellRefs, yearsListOrGridContext, scrollerRef } = + useBaseCalendarYearsGrid({ + children, + cellsPerRow, + cellsPerRowCssVar: CalendarYearsGridCssVars.calendarYearsGridCellsPerRow, + }); const state = React.useMemo(() => ({}), []); + const ref = useForkRef(forwardedRef, scrollerRef); const { renderElement } = useComponentRenderer({ propGetter: getYearsGridProps, render: render ?? 'div', - ref: forwardedRef, + ref, className, state, extraProps: otherProps, diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-list/CalendarYearsList.tsx b/packages/x-date-pickers/src/internals/base/Calendar/years-list/CalendarYearsList.tsx index fb09ac9bc8720..87d32277bc927 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-list/CalendarYearsList.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-list/CalendarYearsList.tsx @@ -1,5 +1,6 @@ 'use client'; import * as React from 'react'; +import useForkRef from '@mui/utils/useForkRef'; import { BaseUIComponentProps } from '../../base-utils/types'; import { useComponentRenderer } from '../../base-utils/useComponentRenderer'; import { CompositeList } from '../../composite/list/CompositeList'; @@ -11,16 +12,18 @@ const CalendarYearsList = React.forwardRef(function CalendarYearsList( forwardedRef: React.ForwardedRef, ) { const { className, render, children, loop, ...otherProps } = props; - const { getYearsListProps, yearsCellRefs, yearsListOrGridContext } = useBaseCalendarYearsList({ - children, - loop, - }); + const { getYearsListProps, cellRefs, yearsListOrGridContext, scrollerRef } = + useBaseCalendarYearsList({ + children, + loop, + }); const state = React.useMemo(() => ({}), []); + const ref = useForkRef(forwardedRef, scrollerRef); const { renderElement } = useComponentRenderer({ propGetter: getYearsListProps, render: render ?? 'div', - ref: forwardedRef, + ref, className, state, extraProps: otherProps, @@ -28,7 +31,7 @@ const CalendarYearsList = React.forwardRef(function CalendarYearsList( return ( - {renderElement()} + {renderElement()} ); }); diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRow.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRow.ts index a953e3397052b..0af854055dd62 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRow.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRow.ts @@ -7,7 +7,7 @@ import { BaseCalendarDaysGridBodyContext } from '../days-grid-body/BaseCalendarD export function useBaseCalendarDaysWeekRow(parameters: useBaseCalendarDaysWeekRow.Parameters) { const { children, ctx } = parameters; const ref = React.useRef(null); - const dayCellRefs = React.useRef<(HTMLElement | null)[]>([]); + const cellRefs = React.useRef<(HTMLElement | null)[]>([]); const getDaysWeekRowProps = React.useCallback( (externalProps: GenericHTMLProps) => { @@ -23,13 +23,10 @@ export function useBaseCalendarDaysWeekRow(parameters: useBaseCalendarDaysWeekRo const registerWeekRowCells = ctx.registerWeekRowCells; React.useEffect(() => { - return registerWeekRowCells(ref, dayCellRefs); + return registerWeekRowCells(ref, cellRefs); }, [registerWeekRowCells]); - return React.useMemo( - () => ({ getDaysWeekRowProps, dayCellRefs }), - [getDaysWeekRowProps, dayCellRefs], - ); + return React.useMemo(() => ({ getDaysWeekRowProps, cellRefs }), [getDaysWeekRowProps, cellRefs]); } export namespace useBaseCalendarDaysWeekRow { diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRowWrapper.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRowWrapper.ts index 44c6a74d11228..49a2749c48ea5 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRowWrapper.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRowWrapper.ts @@ -44,7 +44,7 @@ export namespace useBaseCalendarDaysWeekRowWrapper { /** * The ref to forward to the component. */ - ref: React.RefCallback | null; + ref: React.RefCallback | null; /** * The memoized context to forward to the memoized component so that it does not need to subscribe to any context. */ diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-grid/useBaseCalendarMonthsGrid.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-grid/useBaseCalendarMonthsGrid.ts index 018686ca13c32..38581ad21dd28 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-grid/useBaseCalendarMonthsGrid.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-grid/useBaseCalendarMonthsGrid.ts @@ -16,18 +16,18 @@ import { useMonthsCells } from '../utils/useMonthsCells'; export function useBaseCalendarMonthsGrid(parameters: useBaseCalendarMonthsGrid.Parameters) { const { children, cellsPerRow, canChangeYear = true, cellsPerRowCssVar } = parameters; const baseRootContext = useBaseCalendarRootContext(); - const monthsCellRefs = React.useRef<(HTMLElement | null)[]>([]); + const cellRefs = React.useRef<(HTMLElement | null)[]>([]); const { months, monthsListOrGridContext, changePage } = useMonthsCells(); const pageNavigationTargetRef = React.useRef(null); const getCellsInCalendar = useEventCallback(() => { const grid: HTMLElement[][] = Array.from( { - length: Math.ceil(monthsCellRefs.current.length / cellsPerRow), + length: Math.ceil(cellRefs.current.length / cellsPerRow), }, () => [], ); - monthsCellRefs.current.forEach((cell, index) => { + cellRefs.current.forEach((cell, index) => { const rowIndex = Math.floor(index / cellsPerRow); if (cell != null) { grid[rowIndex].push(cell); @@ -78,8 +78,8 @@ export function useBaseCalendarMonthsGrid(parameters: useBaseCalendarMonthsGrid. ); return React.useMemo( - () => ({ getMonthsGridProps, monthsCellRefs, monthsListOrGridContext }), - [getMonthsGridProps, monthsCellRefs, monthsListOrGridContext], + () => ({ getMonthsGridProps, cellRefs, monthsListOrGridContext }), + [getMonthsGridProps, cellRefs, monthsListOrGridContext], ); } diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-list/useBaseCalendarMonthsList.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-list/useBaseCalendarMonthsList.ts index 7217fcdb49006..9c03550af415f 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-list/useBaseCalendarMonthsList.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-list/useBaseCalendarMonthsList.ts @@ -16,7 +16,7 @@ import { useMonthsCells } from '../utils/useMonthsCells'; export function useBaseCalendarMonthsList(parameters: useBaseCalendarMonthsList.Parameters) { const { children, loop = true, canChangeYear = true } = parameters; const baseRootContext = useBaseCalendarRootContext(); - const monthsCellRefs = React.useRef<(HTMLElement | null)[]>([]); + const cellRefs = React.useRef<(HTMLElement | null)[]>([]); const { months, monthsListOrGridContext, changePage } = useMonthsCells(); const pageNavigationTargetRef = React.useRef(null); @@ -25,7 +25,7 @@ export function useBaseCalendarMonthsList(parameters: useBaseCalendarMonthsList. if (pageNavigationTargetRef.current) { const target = pageNavigationTargetRef.current; timeout.start(0, () => { - applyInitialFocusInList({ cells: monthsCellRefs.current, target }); + applyInitialFocusInList({ cells: cellRefs.current, target }); }); } }, [baseRootContext.visibleDate, timeout]); @@ -38,7 +38,7 @@ export function useBaseCalendarMonthsList(parameters: useBaseCalendarMonthsList. }; navigateInList({ - cells: monthsCellRefs.current, + cells: cellRefs.current, event, loop, changePage: canChangeYear ? changeListPage : undefined, @@ -57,8 +57,8 @@ export function useBaseCalendarMonthsList(parameters: useBaseCalendarMonthsList. ); return React.useMemo( - () => ({ getMonthListProps, monthsCellRefs, monthsListOrGridContext }), - [getMonthListProps, monthsCellRefs, monthsListOrGridContext], + () => ({ getMonthListProps, cellRefs, monthsListOrGridContext }), + [getMonthListProps, cellRefs, monthsListOrGridContext], ); } diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useYearsCells.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useYearsCells.ts index 768f2380f685d..a94fa6e51d0af 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useYearsCells.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useYearsCells.ts @@ -46,12 +46,46 @@ export function useYearsCells(): useYearsCells.ReturnValue { return registerSection({ type: 'month', value: baseRootContext.visibleDate }); }, [registerSection, baseRootContext.visibleDate]); - return { years, yearsListOrGridContext }; + const scrollerRef = React.useRef(null); + React.useEffect( + () => { + // TODO: Make sure this behavior remain consistent once auto focus is implemented. + if (/* autoFocus || */ scrollerRef.current === null) { + return; + } + const tabbableButton = scrollerRef.current.querySelector('[tabindex="0"]'); + if (!tabbableButton) { + return; + } + + // Taken from useScroll in x-data-grid, but vertically centered + const offsetHeight = tabbableButton.offsetHeight; + const offsetTop = tabbableButton.offsetTop; + + const clientHeight = scrollerRef.current.clientHeight; + const scrollTop = scrollerRef.current.scrollTop; + + const elementBottom = offsetTop + offsetHeight; + + if (offsetHeight > clientHeight || offsetTop < scrollTop) { + // Button already visible + return; + } + + scrollerRef.current.scrollTop = elementBottom - clientHeight / 2 - offsetHeight / 2; + }, + [ + /* autoFocus */ + ], + ); + + return { years, yearsListOrGridContext, scrollerRef }; } export namespace useYearsCells { export interface ReturnValue { years: PickerValidDate[]; yearsListOrGridContext: BaseCalendarYearsGridOrListContext; + scrollerRef: React.RefObject; } } diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/years-grid/useBaseCalendarYearsGrid.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/years-grid/useBaseCalendarYearsGrid.ts index db613a5b04080..2f155fc8697a5 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/years-grid/useBaseCalendarYearsGrid.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/years-grid/useBaseCalendarYearsGrid.ts @@ -9,7 +9,7 @@ import { useYearsCells } from '../utils/useYearsCells'; export function useBaseCalendarYearsGrid(parameters: useBaseCalendarYearsGrid.Parameters) { const { children, cellsPerRow, cellsPerRowCssVar } = parameters; const yearsCellRefs = React.useRef<(HTMLElement | null)[]>([]); - const { years, yearsListOrGridContext } = useYearsCells(); + const { years, yearsListOrGridContext, scrollerRef } = useYearsCells(); const getCellsInCalendar = useEventCallback(() => { const grid: HTMLElement[][] = Array.from( @@ -52,8 +52,8 @@ export function useBaseCalendarYearsGrid(parameters: useBaseCalendarYearsGrid.Pa ); return React.useMemo( - () => ({ getYearsGridProps, yearsCellRefs, yearsListOrGridContext }), - [getYearsGridProps, yearsCellRefs, yearsListOrGridContext], + () => ({ getYearsGridProps, yearsCellRefs, yearsListOrGridContext, scrollerRef }), + [getYearsGridProps, yearsCellRefs, yearsListOrGridContext, scrollerRef], ); } diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/years-list/useBaseCalendarYearsList.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/years-list/useBaseCalendarYearsList.ts index bba86aac3a4d5..0680e6a3f916c 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/years-list/useBaseCalendarYearsList.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/years-list/useBaseCalendarYearsList.ts @@ -8,12 +8,12 @@ import { useYearsCells } from '../utils/useYearsCells'; export function useBaseCalendarYearsList(parameters: useBaseCalendarYearsList.Parameters) { const { children, loop = true } = parameters; - const yearsCellRefs = React.useRef<(HTMLElement | null)[]>([]); - const { years, yearsListOrGridContext } = useYearsCells(); + const cellRefs = React.useRef<(HTMLElement | null)[]>([]); + const { years, yearsListOrGridContext, scrollerRef } = useYearsCells(); const onKeyDown = useEventCallback((event: React.KeyboardEvent) => { navigateInList({ - cells: yearsCellRefs.current, + cells: cellRefs.current, event, loop, changePage: undefined, @@ -32,8 +32,8 @@ export function useBaseCalendarYearsList(parameters: useBaseCalendarYearsList.Pa ); return React.useMemo( - () => ({ getYearsListProps, yearsCellRefs, yearsListOrGridContext }), - [getYearsListProps, yearsCellRefs, yearsListOrGridContext], + () => ({ getYearsListProps, cellRefs, yearsListOrGridContext, scrollerRef }), + [getYearsListProps, cellRefs, yearsListOrGridContext, scrollerRef], ); } From ad3e4b62030aa20f04cc34a65b3516f00b4498e9 Mon Sep 17 00:00:00 2001 From: flavien Date: Fri, 10 Jan 2025 15:33:46 +0100 Subject: [PATCH 095/136] Fix CI --- .../base-calendar/DateRangeCalendarDemo.js | 99 +++++++++++++------ .../DateRangeCalendar/DateRangeCalendar.tsx | 2 - .../RangeCalendarSetVisibleYear.tsx | 2 +- .../years-list/RangeCalendarYearsList.tsx | 2 +- .../root/useBaseCalendarValidation.ts | 27 ----- 5 files changed, 71 insertions(+), 61 deletions(-) delete mode 100644 packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarValidation.ts diff --git a/docs/data/date-pickers/base-calendar/DateRangeCalendarDemo.js b/docs/data/date-pickers/base-calendar/DateRangeCalendarDemo.js index f55d1201d3137..f30784c8b0a60 100644 --- a/docs/data/date-pickers/base-calendar/DateRangeCalendarDemo.js +++ b/docs/data/date-pickers/base-calendar/DateRangeCalendarDemo.js @@ -1,6 +1,5 @@ import * as React from 'react'; import clsx from 'clsx'; - import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports @@ -16,19 +15,57 @@ function Header(props) { return (
- - ◀ - - {visibleDate.format('MMMM YYYY')} - - ▶ - +
+ + ◀ + + + + ▶ + +
+
+ + ◀ + + + + ▶ + +
); } @@ -52,6 +89,7 @@ export default function DateRangeCalendarDemo() { className={styles.YearsCell} key={year.toString()} onClick={() => setActiveSection('day')} + target={year} > {year.format('YYYY')} @@ -59,22 +97,23 @@ export default function DateRangeCalendarDemo() { } )} - {/* {activeSection === 'month' && ( - - {({ months }) => - months.map((month) => ( - setActiveSection('day')} - > - {month.format('MMMM')} - - )) - } - - )} */} + {activeSection === 'month' && ( + + {({ months }) => + months.map((month) => ( + setActiveSection('day')} + target={month} + > + {month.format('MMMM')} + + )) + } + + )} {activeSection === 'day' && ( diff --git a/packages/x-date-pickers-pro/src/DateRangeCalendar/DateRangeCalendar.tsx b/packages/x-date-pickers-pro/src/DateRangeCalendar/DateRangeCalendar.tsx index 50a61b2d05772..70d1b0e474c17 100644 --- a/packages/x-date-pickers-pro/src/DateRangeCalendar/DateRangeCalendar.tsx +++ b/packages/x-date-pickers-pro/src/DateRangeCalendar/DateRangeCalendar.tsx @@ -450,8 +450,6 @@ const DateRangeCalendar = React.forwardRef(function DateRangeCalendar( rangePosition, }); - console.log(previewingRange); - const handleDayMouseEnter = useEventCallback( (event: React.MouseEvent, newPreviewRequest: PickerValidDate) => { if (!isWithinRange(utils, newPreviewRequest, valueDayRange)) { diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/set-visible-year/RangeCalendarSetVisibleYear.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/set-visible-year/RangeCalendarSetVisibleYear.tsx index baa3d8660d8b4..0304e7fe32f9f 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/set-visible-year/RangeCalendarSetVisibleYear.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/set-visible-year/RangeCalendarSetVisibleYear.tsx @@ -36,7 +36,7 @@ const RangeCalendarSetVisibleYear = React.forwardRef(function RangeCalendarSetVi props: RangeCalendarSetVisibleYear.Props, forwardedRef: React.ForwardedRef, ) { - const { ctx } = useBaseCalendarSetVisibleYearWrapper({ target: props.target }); + const { ctx } = useBaseCalendarSetVisibleYearWrapper({ target: props.target, forwardedRef }); return ; }); diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-list/RangeCalendarYearsList.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-list/RangeCalendarYearsList.tsx index 410a5d343c237..202942c611bf3 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-list/RangeCalendarYearsList.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-list/RangeCalendarYearsList.tsx @@ -49,4 +49,4 @@ export namespace CalendarYearsList { useBaseCalendarYearsList.Parameters {} } -export { RangeCalendarYearsList as RangeCalendarYearsList }; +export { RangeCalendarYearsList }; diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarValidation.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarValidation.ts deleted file mode 100644 index 29e60d67a3a45..0000000000000 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarValidation.ts +++ /dev/null @@ -1,27 +0,0 @@ -import * as React from 'react'; -import { PickersTimezone, PickerValidDate } from '../../../../../models'; -import { useLocalizationContext } from '../../../../hooks/useUtils'; -import { useValidation, validateDate, ValidateDateProps } from '../../../../../validation'; - -export function useBaseCalendarValidation(parameters: useBaseCalendarValidation.Parameters) { - const { timezone, validationProps } = parameters; - const adapter = useLocalizationContext(); - - return React.useCallback( - (day: PickerValidDate | null) => - validateDate({ - adapter, - value: day, - timezone, - props: validationProps, - }) !== null, - [adapter, validationProps, timezone], - ); -} - -export namespace useBaseCalendarValidation { - export interface Parameters { - timezone: PickersTimezone; - validationProps: any; - } -} From 14b787614b8ff4dc825f90ba139ebc5e9a6b10f2 Mon Sep 17 00:00:00 2001 From: flavien Date: Fri, 10 Jan 2025 16:15:56 +0100 Subject: [PATCH 096/136] Add auto scroll to month list and grid --- .../MonthCalendarWithListLayoutDemo.tsx | 9 ++- .../months-grid/RangeCalendarMonthsGrid.tsx | 17 +++--- .../months-list/RangeCalendarMonthsList.tsx | 15 +++-- .../months-grid/CalendarMonthsGrid.tsx | 17 +++--- .../months-list/CalendarMonthsList.tsx | 15 +++-- .../months-grid/useBaseCalendarMonthsGrid.ts | 6 +- .../months-list/useBaseCalendarMonthsList.ts | 6 +- .../utils/base-calendar/utils/useCellList.ts | 59 +++++++++++++++++++ .../base-calendar/utils/useMonthsCells.ts | 11 ++-- .../base-calendar/utils/useYearsCells.ts | 43 +------------- 10 files changed, 117 insertions(+), 81 deletions(-) create mode 100644 packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useCellList.ts diff --git a/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.tsx b/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.tsx index b64fea1a3de45..81f0d3e573877 100644 --- a/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.tsx +++ b/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Dayjs } from 'dayjs'; +import dayjs, { Dayjs } from 'dayjs'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports @@ -30,7 +30,12 @@ export default function MonthCalendarWithListLayoutDemo() { return ( - +
{({ months }) => diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-grid/RangeCalendarMonthsGrid.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-grid/RangeCalendarMonthsGrid.tsx index 6d9bf3ccba989..ef5b83670d609 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-grid/RangeCalendarMonthsGrid.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-grid/RangeCalendarMonthsGrid.tsx @@ -1,5 +1,6 @@ 'use client'; import * as React from 'react'; +import useForkRef from '@mui/utils/useForkRef'; // eslint-disable-next-line no-restricted-imports import { BaseUIComponentProps } from '@mui/x-date-pickers/internals/base/base-utils/types'; // eslint-disable-next-line no-restricted-imports @@ -17,18 +18,20 @@ const RangeCalendarMonthsGrid = React.forwardRef(function RangeCalendarMonthsLis forwardedRef: React.ForwardedRef, ) { const { className, render, children, cellsPerRow, canChangeYear, ...otherProps } = props; - const { getMonthsGridProps, cellRefs, monthsListOrGridContext } = useBaseCalendarMonthsGrid({ - children, - cellsPerRow, - canChangeYear, - cellsPerRowCssVar: RangeCalendarMonthsGridCssVars.calendarMonthsGridCellsPerRow, - }); + const { getMonthsGridProps, cellRefs, monthsListOrGridContext, scrollerRef } = + useBaseCalendarMonthsGrid({ + children, + cellsPerRow, + canChangeYear, + cellsPerRowCssVar: RangeCalendarMonthsGridCssVars.calendarMonthsGridCellsPerRow, + }); const state = React.useMemo(() => ({}), []); + const ref = useForkRef(forwardedRef, scrollerRef); const { renderElement } = useComponentRenderer({ propGetter: getMonthsGridProps, render: render ?? 'div', - ref: forwardedRef, + ref, className, state, extraProps: otherProps, diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-list/RangeCalendarMonthsList.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-list/RangeCalendarMonthsList.tsx index 1a3bbc09078f1..3d3c39c2cd98b 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-list/RangeCalendarMonthsList.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-list/RangeCalendarMonthsList.tsx @@ -1,5 +1,6 @@ 'use client'; import * as React from 'react'; +import useForkRef from '@mui/utils/useForkRef'; // eslint-disable-next-line no-restricted-imports import { useBaseCalendarMonthsList } from '@mui/x-date-pickers/internals/base/utils/base-calendar/months-list/useBaseCalendarMonthsList'; // eslint-disable-next-line no-restricted-imports @@ -16,17 +17,19 @@ const RangeCalendarMonthsList = React.forwardRef(function RangeCalendarMonthsLis forwardedRef: React.ForwardedRef, ) { const { className, render, children, loop, canChangeYear, ...otherProps } = props; - const { getMonthListProps, cellRefs, monthsListOrGridContext } = useBaseCalendarMonthsList({ - children, - loop, - canChangeYear, - }); + const { getMonthListProps, cellRefs, monthsListOrGridContext, scrollerRef } = + useBaseCalendarMonthsList({ + children, + loop, + canChangeYear, + }); const state = React.useMemo(() => ({}), []); + const ref = useForkRef(forwardedRef, scrollerRef); const { renderElement } = useComponentRenderer({ propGetter: getMonthListProps, render: render ?? 'div', - ref: forwardedRef, + ref, className, state, extraProps: otherProps, diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-grid/CalendarMonthsGrid.tsx b/packages/x-date-pickers/src/internals/base/Calendar/months-grid/CalendarMonthsGrid.tsx index 467332d2d5c48..a553b29647bfa 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-grid/CalendarMonthsGrid.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-grid/CalendarMonthsGrid.tsx @@ -1,5 +1,6 @@ 'use client'; import * as React from 'react'; +import useForkRef from '@mui/utils/useForkRef'; import { useBaseCalendarMonthsGrid } from '../../utils/base-calendar/months-grid/useBaseCalendarMonthsGrid'; import { BaseUIComponentProps } from '../../base-utils/types'; import { useComponentRenderer } from '../../base-utils/useComponentRenderer'; @@ -12,18 +13,20 @@ const CalendarMonthsGrid = React.forwardRef(function CalendarMonthsList( forwardedRef: React.ForwardedRef, ) { const { className, render, children, cellsPerRow, canChangeYear, ...otherProps } = props; - const { getMonthsGridProps, cellRefs, monthsListOrGridContext } = useBaseCalendarMonthsGrid({ - children, - cellsPerRow, - canChangeYear, - cellsPerRowCssVar: CalendarMonthsGridCssVars.calendarMonthsGridCellsPerRow, - }); + const { getMonthsGridProps, cellRefs, monthsListOrGridContext, scrollerRef } = + useBaseCalendarMonthsGrid({ + children, + cellsPerRow, + canChangeYear, + cellsPerRowCssVar: CalendarMonthsGridCssVars.calendarMonthsGridCellsPerRow, + }); const state = React.useMemo(() => ({}), []); + const ref = useForkRef(forwardedRef, scrollerRef); const { renderElement } = useComponentRenderer({ propGetter: getMonthsGridProps, render: render ?? 'div', - ref: forwardedRef, + ref, className, state, extraProps: otherProps, diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsList.tsx b/packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsList.tsx index ad4ea217db9f6..829179dcc0119 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsList.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsList.tsx @@ -1,5 +1,6 @@ 'use client'; import * as React from 'react'; +import useForkRef from '@mui/utils/useForkRef'; import { useBaseCalendarMonthsList } from '../../utils/base-calendar/months-list/useBaseCalendarMonthsList'; import { BaseUIComponentProps } from '../../base-utils/types'; import { useComponentRenderer } from '../../base-utils/useComponentRenderer'; @@ -11,17 +12,19 @@ const CalendarMonthsList = React.forwardRef(function CalendarMonthsList( forwardedRef: React.ForwardedRef, ) { const { className, render, children, loop, canChangeYear, ...otherProps } = props; - const { getMonthListProps, cellRefs, monthsListOrGridContext } = useBaseCalendarMonthsList({ - children, - loop, - canChangeYear, - }); + const { getMonthListProps, cellRefs, monthsListOrGridContext, scrollerRef } = + useBaseCalendarMonthsList({ + children, + loop, + canChangeYear, + }); const state = React.useMemo(() => ({}), []); + const ref = useForkRef(forwardedRef, scrollerRef); const { renderElement } = useComponentRenderer({ propGetter: getMonthListProps, render: render ?? 'div', - ref: forwardedRef, + ref, className, state, extraProps: otherProps, diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-grid/useBaseCalendarMonthsGrid.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-grid/useBaseCalendarMonthsGrid.ts index 38581ad21dd28..264cf4078e25a 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-grid/useBaseCalendarMonthsGrid.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-grid/useBaseCalendarMonthsGrid.ts @@ -17,7 +17,7 @@ export function useBaseCalendarMonthsGrid(parameters: useBaseCalendarMonthsGrid. const { children, cellsPerRow, canChangeYear = true, cellsPerRowCssVar } = parameters; const baseRootContext = useBaseCalendarRootContext(); const cellRefs = React.useRef<(HTMLElement | null)[]>([]); - const { months, monthsListOrGridContext, changePage } = useMonthsCells(); + const { months, monthsListOrGridContext, changePage, scrollerRef } = useMonthsCells(); const pageNavigationTargetRef = React.useRef(null); const getCellsInCalendar = useEventCallback(() => { @@ -78,8 +78,8 @@ export function useBaseCalendarMonthsGrid(parameters: useBaseCalendarMonthsGrid. ); return React.useMemo( - () => ({ getMonthsGridProps, cellRefs, monthsListOrGridContext }), - [getMonthsGridProps, cellRefs, monthsListOrGridContext], + () => ({ getMonthsGridProps, cellRefs, monthsListOrGridContext, scrollerRef }), + [getMonthsGridProps, cellRefs, monthsListOrGridContext, scrollerRef], ); } diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-list/useBaseCalendarMonthsList.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-list/useBaseCalendarMonthsList.ts index 9c03550af415f..0efb8c7adb633 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-list/useBaseCalendarMonthsList.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-list/useBaseCalendarMonthsList.ts @@ -17,7 +17,7 @@ export function useBaseCalendarMonthsList(parameters: useBaseCalendarMonthsList. const { children, loop = true, canChangeYear = true } = parameters; const baseRootContext = useBaseCalendarRootContext(); const cellRefs = React.useRef<(HTMLElement | null)[]>([]); - const { months, monthsListOrGridContext, changePage } = useMonthsCells(); + const { months, monthsListOrGridContext, changePage, scrollerRef } = useMonthsCells(); const pageNavigationTargetRef = React.useRef(null); const timeout = useTimeout(); @@ -57,8 +57,8 @@ export function useBaseCalendarMonthsList(parameters: useBaseCalendarMonthsList. ); return React.useMemo( - () => ({ getMonthListProps, cellRefs, monthsListOrGridContext }), - [getMonthListProps, cellRefs, monthsListOrGridContext], + () => ({ getMonthListProps, cellRefs, monthsListOrGridContext, scrollerRef }), + [getMonthListProps, cellRefs, monthsListOrGridContext, scrollerRef], ); } diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useCellList.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useCellList.ts new file mode 100644 index 0000000000000..7515e62f9aae3 --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useCellList.ts @@ -0,0 +1,59 @@ +import * as React from 'react'; +import { PickerValidDate } from '../../../../../models'; +import { useBaseCalendarRootContext } from '../root/BaseCalendarRootContext'; + +export function useCellList(parameters: useCellList.Parameters): useCellList.ReturnValue { + const { section, value } = parameters; + const baseRootContext = useBaseCalendarRootContext(); + + const registerSection = baseRootContext.registerSection; + React.useEffect(() => { + return registerSection({ type: section, value }); + }, [registerSection, value, section]); + + const scrollerRef = React.useRef(null); + React.useEffect( + () => { + // TODO: Make sure this behavior remain consistent once auto focus is implemented. + if (/* autoFocus || */ scrollerRef.current === null) { + return; + } + const tabbableButton = scrollerRef.current.querySelector('[tabindex="0"]'); + if (!tabbableButton) { + return; + } + + // Taken from useScroll in x-data-grid, but vertically centered + const offsetHeight = tabbableButton.offsetHeight; + const offsetTop = tabbableButton.offsetTop; + + const clientHeight = scrollerRef.current.clientHeight; + const scrollTop = scrollerRef.current.scrollTop; + + const elementBottom = offsetTop + offsetHeight; + + if (offsetHeight > clientHeight || offsetTop < scrollTop) { + // Button already visible + return; + } + + scrollerRef.current.scrollTop = elementBottom - clientHeight / 2 - offsetHeight / 2; + }, + [ + /* autoFocus */ + ], + ); + + return { scrollerRef }; +} + +export namespace useCellList { + export interface Parameters { + section: 'month' | 'year'; + value: PickerValidDate; + } + + export interface ReturnValue { + scrollerRef: React.RefObject; + } +} diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useMonthsCells.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useMonthsCells.ts index 16399b487c1a0..4335927dbea35 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useMonthsCells.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useMonthsCells.ts @@ -5,6 +5,7 @@ import { useUtils } from '../../../../hooks/useUtils'; import { getFirstEnabledYear, getLastEnabledYear } from './date'; import { useBaseCalendarRootContext } from '../root/BaseCalendarRootContext'; import { BaseCalendarMonthsGridOrListContext } from '../months-grid/BaseCalendarMonthsGridOrListContext'; +import { useCellList } from './useCellList'; export function useMonthsCells(): useMonthsCells.ReturnValue { const baseRootContext = useBaseCalendarRootContext(); @@ -15,6 +16,7 @@ export function useMonthsCells(): useMonthsCells.ReturnValue { [utils, baseRootContext.visibleDate], ); + const { scrollerRef } = useCellList({ section: 'month', value: currentYear }); const months = React.useMemo(() => getMonthsInYear(utils, currentYear), [utils, currentYear]); const tabbableMonths = React.useMemo(() => { @@ -86,16 +88,11 @@ export function useMonthsCells(): useMonthsCells.ReturnValue { } }; - const registerSection = baseRootContext.registerSection; - React.useEffect(() => { - return registerSection({ type: 'month', value: currentYear }); - }, [registerSection, currentYear]); - - return { months, monthsListOrGridContext, changePage }; + return { months, monthsListOrGridContext, changePage, scrollerRef }; } export namespace useMonthsCells { - export interface ReturnValue { + export interface ReturnValue extends useCellList.ReturnValue { months: PickerValidDate[]; monthsListOrGridContext: BaseCalendarMonthsGridOrListContext; changePage: (direction: 'next' | 'previous') => void; diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useYearsCells.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useYearsCells.ts index a94fa6e51d0af..eace69840a9a2 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useYearsCells.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useYearsCells.ts @@ -3,10 +3,12 @@ import { PickerValidDate } from '../../../../../models'; import { useUtils } from '../../../../hooks/useUtils'; import { useBaseCalendarRootContext } from '../root/BaseCalendarRootContext'; import { BaseCalendarYearsGridOrListContext } from '../years-grid/BaseCalendarYearsGridOrListContext'; +import { useCellList } from './useCellList'; export function useYearsCells(): useYearsCells.ReturnValue { const baseRootContext = useBaseCalendarRootContext(); const utils = useUtils(); + const { scrollerRef } = useCellList({ section: 'year', value: baseRootContext.visibleDate }); const years = React.useMemo( () => @@ -41,51 +43,12 @@ export function useYearsCells(): useYearsCells.ReturnValue { [tabbableYears], ); - const registerSection = baseRootContext.registerSection; - React.useEffect(() => { - return registerSection({ type: 'month', value: baseRootContext.visibleDate }); - }, [registerSection, baseRootContext.visibleDate]); - - const scrollerRef = React.useRef(null); - React.useEffect( - () => { - // TODO: Make sure this behavior remain consistent once auto focus is implemented. - if (/* autoFocus || */ scrollerRef.current === null) { - return; - } - const tabbableButton = scrollerRef.current.querySelector('[tabindex="0"]'); - if (!tabbableButton) { - return; - } - - // Taken from useScroll in x-data-grid, but vertically centered - const offsetHeight = tabbableButton.offsetHeight; - const offsetTop = tabbableButton.offsetTop; - - const clientHeight = scrollerRef.current.clientHeight; - const scrollTop = scrollerRef.current.scrollTop; - - const elementBottom = offsetTop + offsetHeight; - - if (offsetHeight > clientHeight || offsetTop < scrollTop) { - // Button already visible - return; - } - - scrollerRef.current.scrollTop = elementBottom - clientHeight / 2 - offsetHeight / 2; - }, - [ - /* autoFocus */ - ], - ); - return { years, yearsListOrGridContext, scrollerRef }; } export namespace useYearsCells { - export interface ReturnValue { + export interface ReturnValue extends useCellList.ReturnValue { years: PickerValidDate[]; yearsListOrGridContext: BaseCalendarYearsGridOrListContext; - scrollerRef: React.RefObject; } } From 7703b30bd6065c3713e7005203f16e468c0dfd3b Mon Sep 17 00:00:00 2001 From: flavien Date: Fri, 10 Jan 2025 18:05:53 +0100 Subject: [PATCH 097/136] Clean --- .../MonthCalendarWithListLayoutDemo.tsx | 9 +- .../base-calendar/calendar.module.css | 163 +++++++----------- .../useRangeCalendarDaysCellWrapper.ts | 58 ++----- .../root/RangeCalendarRootContext.ts | 2 +- .../root/useRangeCalendarRoot.tsx | 4 +- .../src/internals/utils/date-utils.ts | 14 ++ .../useBaseCalendarSetVisibleMonthWrapper.ts | 3 +- .../useBaseCalendarSetVisibleYearWrapper.ts | 3 +- 8 files changed, 96 insertions(+), 160 deletions(-) diff --git a/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.tsx b/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.tsx index 81f0d3e573877..b64fea1a3de45 100644 --- a/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.tsx +++ b/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import dayjs, { Dayjs } from 'dayjs'; +import { Dayjs } from 'dayjs'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports @@ -30,12 +30,7 @@ export default function MonthCalendarWithListLayoutDemo() { return ( - +
{({ months }) => diff --git a/docs/data/date-pickers/base-calendar/calendar.module.css b/docs/data/date-pickers/base-calendar/calendar.module.css index 4a5384e97c039..28f7d73f97936 100644 --- a/docs/data/date-pickers/base-calendar/calendar.module.css +++ b/docs/data/date-pickers/base-calendar/calendar.module.css @@ -1,5 +1,18 @@ .Root { - border: 1px solid #78716c; + --root-border-color: #78716c; + --button-hover-bg-color: #e0f2fe; + --button-focus-border-color: #0ea5e9; + --days-grid-separator-bg-color: #9ca3af; + --days-grid-header-color: #64748b; + + --cell-selected-bg-color: #7dd3fc; + --cell-inside-selection-bg-color: #e0f2fe; + --cell-preview-border-color: #a1a1aa; + --cell-outside-month-color: #cbd5e1; + --cell-disabled-color: #64748b; + --cell-current-border-color: #9ca3af; + + border: 1px solid var(--root-border-color); border-radius: 8px; width: 276px; height: 312px; @@ -55,11 +68,11 @@ cursor: pointer; &:hover { - background-color: #e0f2fe; + background-color: var(--button-hover-bg-color); } &:focus-visible { - outline: 2px solid #0ea5e9; + outline: 2px solid var(--button-focus-border-color); } &:disabled { @@ -157,12 +170,31 @@ align-items: center; justify-content: center; font-size: 0.75rem; - color: #64748b; + color: var(--days-grid-header-color); } -.DaysCell { - --day-preview-border-color: #a1a1aa; +.DaysCell, +.MonthsCell, +.YearsCell { + &:not([data-selected]):hover { + background-color: var(--button-hover-bg-color); + } + &[data-selected]:not([data-outside-month]):not([data-inside-selection]) { + background-color: var(--cell-selected-bg-color); + } + + &:focus-visible { + z-index: 1; + outline: 2px solid var(--button-focus-border-color); + } + + &:disabled { + pointer-events: none; + } +} + +.DaysCell { height: 36px; width: 36px; border-radius: 4px; @@ -171,18 +203,21 @@ cursor: pointer; &[data-current]:not([data-selected]):not(:focus-visible) { - outline: 1px solid #9ca3af; + outline: 1px solid var(--cell-current-border-color); + } + + &:not([data-outside-month]):disabled { + text-decoration: line-through; + color: var(--cell-disabled-color); } &[data-outside-month] { - color: #cbd5e1; + color: var(--cell-outside-month-color); pointer-events: none; } - &:not([data-outside-month]):disabled { - pointer-events: none; - text-decoration: line-through; - color: #64748b; + &[data-inside-selection] { + background-color: var(--cell-inside-selection-bg-color); } &.RangeDaysCell { @@ -199,16 +234,16 @@ } &[data-previewed]:not([data-selected]) { - border-top: 1px dashed var(--day-preview-border-color); - border-bottom: 1px dashed var(--day-preview-border-color); + border-top: 1px dashed var(--cell-preview-border-color); + border-bottom: 1px dashed var(--cell-preview-border-color); } &[data-preview-start]:not([data-selected]) { - border-left: 1px dashed var(--day-preview-border-color); + border-left: 1px dashed var(--cell-preview-border-color); } &[data-preview-end]:not([data-selected]) { - border-right: 1px dashed var(--day-preview-border-color); + border-right: 1px dashed var(--cell-preview-border-color); } } } @@ -220,50 +255,8 @@ color: #64748b; } -.MonthsCell, -.YearsCell { - &[data-selected] { - background-color: #7dd3fc; - } - - &:focus-visible { - border-width: 2px; - } - - &:disabled { - pointer-events: none; - } -} - -.DaysCell, -.MonthsCell, -.YearsCell { - &:not([data-selected]):hover { - background-color: #e0f2fe; - } - - &:not([data-outside-month]) { - &[data-selected] { - background-color: #7dd3fc; - - &.RangeDaysCell { - background-color: #e0f2fe; - } - } - - &[data-selection-start], - &[data-selection-end] { - background-color: #7dd3fc; - } - } - - &:focus-visible { - outline: 2px solid #0ea5e9; - } -} - .DaysGridSeparator { - background-color: #9ca3af; + background-color: var(--days-grid-separator-bg-color); margin: 24px 0; width: 1px; } @@ -273,49 +266,11 @@ pointer-events: none; } -:global(.mode-dark) :where(.DaysCell, .MonthsCell, .YearsCell) { - &:not([data-selected]):hover { - background-color: #075985; - } - - &:not([data-outside-month]) { - &[data-selected] { - background-color: #0369a1; - - &.RangeDaysCell { - background-color: #0c4a6e; - } - } - - &[data-selection-start], - &[data-selection-end] { - background-color: #0369a1; - } - } - - &[data-outside-month] { - color: #334155; - pointer-events: none; - } - - &:not([data-outside-month]):disabled { - color: #64748b; - } -} - -:global(.mode-dark) - :where( - .SetVisibleMonth, - .SetVisibleYear, - .SetActiveSectionMonth, - .SetActiveSectionYear, - .SetActiveSectionYearMD2 - ) { - &:hover { - background-color: #075985; - } -} - -:global(.mode-dark):where(.DaysGridSeparator) { - background-color: #4b5563; +:global(.mode-dark) .Root { + --button-hover-bg-color: #075985; + --days-grid-separator-bg-color: #4b5563; + --cell-selected-bg-color: #0369a1; + --cell-inside-selection-bg-color: #0c4a6e; + --cell-outside-month-color: #334155; + --cell-disabled-color: #64748b; } diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts index 2da97b05c9d9f..8a32cbea8791b 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts @@ -3,12 +3,7 @@ import { useUtils } from '@mui/x-date-pickers/internals'; // eslint-disable-next-line no-restricted-imports import { useBaseCalendarDaysCellWrapper } from '@mui/x-date-pickers/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCellWrapper'; import type { useRangeCalendarDaysCell } from './useRangeCalendarDaysCell'; -import { - isWithinRange, - isStartOfRange, - isEndOfRange, - isRangeValid, -} from '../../../utils/date-utils'; +import { getDatePositionInRange } from '../../../utils/date-utils'; import { useRangeCalendarRootContext } from '../root/RangeCalendarRootContext'; export function useRangeCalendarDaysCellWrapper( @@ -19,46 +14,25 @@ export function useRangeCalendarDaysCellWrapper( const utils = useUtils(); const rootContext = useRangeCalendarRootContext(); - const isSelected = React.useMemo(() => { - if (!isRangeValid(utils, rootContext.highlightedRange)) { - return baseCtx.isSelected; - } - return isWithinRange(utils, value, rootContext.highlightedRange); - }, [utils, value, rootContext.highlightedRange, baseCtx.isSelected]); - - const isSelectionStart = React.useMemo( - () => isStartOfRange(utils, value, rootContext.highlightedRange), - [utils, value, rootContext.highlightedRange], - ); - - const isSelectionEnd = React.useMemo( - () => isEndOfRange(utils, value, rootContext.highlightedRange), - [utils, value, rootContext.highlightedRange], - ); - - const isPreviewed = React.useMemo(() => { - return isWithinRange(utils, value, rootContext.previewRange); - }, [utils, value, rootContext.previewRange]); - - const isPreviewStart = React.useMemo( - () => isStartOfRange(utils, value, rootContext.previewRange), - [utils, value, rootContext.previewRange], + const positionInSelectedRange = React.useMemo( + () => getDatePositionInRange(utils, value, rootContext.selectedRange), + [utils, value, rootContext.selectedRange], ); - const isPreviewEnd = React.useMemo( - () => isEndOfRange(utils, value, rootContext.previewRange), + const positionInPreviewRange = React.useMemo( + () => getDatePositionInRange(utils, value, rootContext.previewRange), [utils, value, rootContext.previewRange], ); const ctx = React.useMemo( () => ({ ...baseCtx, - isSelected, - isSelectionStart, - isSelectionEnd, - isPreviewed, - isPreviewStart, - isPreviewEnd, + isSelected: positionInSelectedRange.isSelected, + isSelectionStart: positionInSelectedRange.isSelectionStart, + isSelectionEnd: positionInSelectedRange.isSelectionEnd, + isPreviewed: positionInSelectedRange.isSelected, + isPreviewStart: positionInPreviewRange.isSelectionStart, + isPreviewEnd: positionInPreviewRange.isSelectionEnd, isDraggingRef: rootContext.isDraggingRef, selectDayFromDrag: rootContext.selectDayFromDrag, startDragging: rootContext.startDragging, @@ -69,12 +43,8 @@ export function useRangeCalendarDaysCellWrapper( }), [ baseCtx, - isSelected, - isSelectionStart, - isSelectionEnd, - isPreviewed, - isPreviewStart, - isPreviewEnd, + positionInSelectedRange, + positionInPreviewRange, rootContext.isDraggingRef, rootContext.selectDayFromDrag, rootContext.startDragging, diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts index 5dd3a7fd79af3..aac9913711398 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts @@ -11,7 +11,7 @@ export interface RangeCalendarRootContext { stopDragging: () => void; setDragTarget: (value: PickerValidDate) => void; emptyDragImgRef: React.RefObject; - highlightedRange: PickerRangeValue; + selectedRange: PickerRangeValue; isDragging: boolean; setHoveredDate: (value: PickerValidDate | null) => void; previewRange: PickerRangeValue; diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx index 329e921b5445e..42380b63df10a 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx @@ -245,7 +245,7 @@ function useBuildRangeCalendarRootContext(parameters: UseRangeCalendarDragEditin hoveredDate: PickerValidDate | null; }>({ isDragging: false, targetDate: null, draggedDate: null, hoveredDate: null }); - const highlightedRange = React.useMemo(() => { + const selectedRange = React.useMemo(() => { if (!valueDayRange[0] || !valueDayRange[1] || !state.targetDate) { return valueDayRange; } @@ -344,7 +344,7 @@ function useBuildRangeCalendarRootContext(parameters: UseRangeCalendarDragEditin const context: RangeCalendarRootContext = { value, - highlightedRange, + selectedRange, previewRange, disableDragEditing, isDraggingRef, diff --git a/packages/x-date-pickers-pro/src/internals/utils/date-utils.ts b/packages/x-date-pickers-pro/src/internals/utils/date-utils.ts index 8ca8620be892f..1076889c8904a 100644 --- a/packages/x-date-pickers-pro/src/internals/utils/date-utils.ts +++ b/packages/x-date-pickers-pro/src/internals/utils/date-utils.ts @@ -31,3 +31,17 @@ export const isEndOfRange = ( ) => { return isRangeValid(utils, range) && utils.isSameDay(day, range[1]!); }; + +export function getDatePositionInRange( + utils: MuiPickersAdapter, + date: PickerValidDate, + range: PickerRangeValue, +) { + const isSelectionStart = range[0] != null && utils.isSameDay(date, range[0]); + const isSelectionEnd = range[1] != null && utils.isSameDay(date, range[1]); + const isSelected = isRangeValid(utils, range) + ? utils.isWithinRange(date, range) + : isSelectionStart || isSelectionEnd; + + return { isSelectionStart, isSelectionEnd, isSelected }; +} diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-month/useBaseCalendarSetVisibleMonthWrapper.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-month/useBaseCalendarSetVisibleMonthWrapper.ts index b8db2903fe4b1..45e98c38e918d 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-month/useBaseCalendarSetVisibleMonthWrapper.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-month/useBaseCalendarSetVisibleMonthWrapper.ts @@ -61,8 +61,9 @@ export function useBaseCalendarSetVisibleMonthWrapper( const tabbableMonths = baseMonthsListOrGridContext?.tabbableMonths; const isTabbable = React.useMemo(() => { + // If the button is not inside a month list or grid, then it is always tabbable. if (tabbableMonths == null) { - return false; + return true; } return tabbableMonths.some((month) => utils.isSameMonth(month, targetDate)); diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-year/useBaseCalendarSetVisibleYearWrapper.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-year/useBaseCalendarSetVisibleYearWrapper.ts index f155e00a60ec0..2517f811ae2f5 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-year/useBaseCalendarSetVisibleYearWrapper.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/set-visible-year/useBaseCalendarSetVisibleYearWrapper.ts @@ -60,8 +60,9 @@ export function useBaseCalendarSetVisibleYearWrapper( const tabbableYears = baseYearsListOrGridContext?.tabbableYears; const isTabbable = React.useMemo(() => { + // If the button is not inside a year list or grid, then it is always tabbable. if (tabbableYears == null) { - return false; + return true; } return tabbableYears.some((year) => utils.isSameYear(year, targetDate)); From 304ac5d43b944188229a50703dc6fc4fddb57afc Mon Sep 17 00:00:00 2001 From: flavien Date: Fri, 10 Jan 2025 18:18:54 +0100 Subject: [PATCH 098/136] Clean --- .../useRangeCalendarDaysCellWrapper.ts | 2 +- .../src/internals/utils/date-utils.ts | 41 ++++++++++++++++--- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts index 8a32cbea8791b..26ac5ffc28c47 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts @@ -30,7 +30,7 @@ export function useRangeCalendarDaysCellWrapper( isSelected: positionInSelectedRange.isSelected, isSelectionStart: positionInSelectedRange.isSelectionStart, isSelectionEnd: positionInSelectedRange.isSelectionEnd, - isPreviewed: positionInSelectedRange.isSelected, + isPreviewed: positionInPreviewRange.isSelected, isPreviewStart: positionInPreviewRange.isSelectionStart, isPreviewEnd: positionInPreviewRange.isSelectionEnd, isDraggingRef: rootContext.isDraggingRef, diff --git a/packages/x-date-pickers-pro/src/internals/utils/date-utils.ts b/packages/x-date-pickers-pro/src/internals/utils/date-utils.ts index 1076889c8904a..7cb230ee3dfb2 100644 --- a/packages/x-date-pickers-pro/src/internals/utils/date-utils.ts +++ b/packages/x-date-pickers-pro/src/internals/utils/date-utils.ts @@ -37,11 +37,40 @@ export function getDatePositionInRange( date: PickerValidDate, range: PickerRangeValue, ) { - const isSelectionStart = range[0] != null && utils.isSameDay(date, range[0]); - const isSelectionEnd = range[1] != null && utils.isSameDay(date, range[1]); - const isSelected = isRangeValid(utils, range) - ? utils.isWithinRange(date, range) - : isSelectionStart || isSelectionEnd; + const [start, end] = range; + if (start == null && end == null) { + return { isSelected: false, isSelectionStart: false, isSelectionEnd: false }; + } - return { isSelectionStart, isSelectionEnd, isSelected }; + if (start == null) { + const isSelected = utils.isSameDay(date, end!); + return { + isSelected, + isSelectionStart: isSelected, + isSelectionEnd: isSelected, + }; + } + + if (end == null) { + const isSelected = utils.isSameDay(date, start!); + return { + isSelected, + isSelectionStart: isSelected, + isSelectionEnd: isSelected, + }; + } + + if (utils.isBefore(end, start)) { + return { + isSelected: false, + isSelectionStart: false, + isSelectionEnd: false, + }; + } + + return { + isSelected: utils.isWithinRange(date, [start, end]), + isSelectionStart: utils.isSameDay(date, start), + isSelectionEnd: utils.isSameDay(date, end), + }; } From 7a37c89471b4d4858d040363cd658eee667cd745 Mon Sep 17 00:00:00 2001 From: flavien Date: Fri, 10 Jan 2025 18:25:36 +0100 Subject: [PATCH 099/136] Work --- docs/data/date-pickers/base-calendar/calendar.module.css | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/data/date-pickers/base-calendar/calendar.module.css b/docs/data/date-pickers/base-calendar/calendar.module.css index 28f7d73f97936..73190e18ca85e 100644 --- a/docs/data/date-pickers/base-calendar/calendar.module.css +++ b/docs/data/date-pickers/base-calendar/calendar.module.css @@ -60,6 +60,7 @@ .SetActiveSectionYear, .SetActiveSectionYearMD2 { border: none; + user-select: none; background-color: transparent; border-radius: 4px; height: 24px; @@ -176,6 +177,8 @@ .DaysCell, .MonthsCell, .YearsCell { + user-select: none; + &:not([data-selected]):hover { background-color: var(--button-hover-bg-color); } From 9804618d115ad160a8feea2aa9ffd03e8bff3771 Mon Sep 17 00:00:00 2001 From: flavien Date: Fri, 10 Jan 2025 18:28:35 +0100 Subject: [PATCH 100/136] Fix --- .../base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx index 493efaca96bd9..faeeafae68c1f 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx @@ -32,7 +32,7 @@ const CalendarSetVisibleYear = React.forwardRef(function CalendarSetVisibleYear( props: CalendarSetVisibleYear.Props, forwardedRef: React.ForwardedRef, ) { - const { ctx } = useBaseCalendarSetVisibleYearWrapper({ target: props.target }); + const { ctx } = useBaseCalendarSetVisibleYearWrapper({ forwardedRef, target: props.target }); return ; }); From 3d49a9e65d1efee285e5731de43c4bdb057b2ed8 Mon Sep 17 00:00:00 2001 From: flavien Date: Fri, 10 Jan 2025 18:32:26 +0100 Subject: [PATCH 101/136] Fix --- .../RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts index 26ac5ffc28c47..0aebdf95ba426 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts @@ -8,7 +8,7 @@ import { useRangeCalendarRootContext } from '../root/RangeCalendarRootContext'; export function useRangeCalendarDaysCellWrapper( parameters: useRangeCalendarDaysCellWrapper.Parameters, -) { +): useRangeCalendarDaysCellWrapper.ReturnValue { const { value } = parameters; const { ref, ctx: baseCtx } = useBaseCalendarDaysCellWrapper(parameters); const utils = useUtils(); From f11615555c0a3f4da553482feee953321a9d3aff Mon Sep 17 00:00:00 2001 From: flavien Date: Fri, 10 Jan 2025 18:54:53 +0100 Subject: [PATCH 102/136] Add notes on the doc --- .../base-calendar/DayCalendarDemo.js | 19 +---- .../base-calendar/DayCalendarDemo.tsx | 19 +---- .../base-calendar/DayCalendarDemo.tsx.preview | 1 - .../DayCalendarWithFixedWeekNumberDemo.js | 70 +++++++++++++++++++ .../DayCalendarWithFixedWeekNumberDemo.tsx | 70 +++++++++++++++++++ .../base-calendar/base-calendar.md | 67 ++++++++++++++++++ 6 files changed, 211 insertions(+), 35 deletions(-) delete mode 100644 docs/data/date-pickers/base-calendar/DayCalendarDemo.tsx.preview create mode 100644 docs/data/date-pickers/base-calendar/DayCalendarWithFixedWeekNumberDemo.js create mode 100644 docs/data/date-pickers/base-calendar/DayCalendarWithFixedWeekNumberDemo.tsx diff --git a/docs/data/date-pickers/base-calendar/DayCalendarDemo.js b/docs/data/date-pickers/base-calendar/DayCalendarDemo.js index 5775f0e2605f6..f79edb7eff8d4 100644 --- a/docs/data/date-pickers/base-calendar/DayCalendarDemo.js +++ b/docs/data/date-pickers/base-calendar/DayCalendarDemo.js @@ -1,5 +1,4 @@ import * as React from 'react'; - import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports @@ -25,10 +24,10 @@ function Header() { ); } -function DayCalendar(props) { +export default function DayCalendarDemo() { return ( - +
@@ -68,17 +67,3 @@ function DayCalendar(props) { ); } - -export default function DayCalendarDemo() { - const [value, setValue] = React.useState(null); - - const handleValueChange = React.useCallback((newValue) => { - setValue(newValue); - }, []); - - return ( - - - - ); -} diff --git a/docs/data/date-pickers/base-calendar/DayCalendarDemo.tsx b/docs/data/date-pickers/base-calendar/DayCalendarDemo.tsx index 8f83700f9e1bf..f79edb7eff8d4 100644 --- a/docs/data/date-pickers/base-calendar/DayCalendarDemo.tsx +++ b/docs/data/date-pickers/base-calendar/DayCalendarDemo.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import { Dayjs } from 'dayjs'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports @@ -25,10 +24,10 @@ function Header() { ); } -function DayCalendar(props: Omit) { +export default function DayCalendarDemo() { return ( - +
@@ -68,17 +67,3 @@ function DayCalendar(props: Omit) { ); } - -export default function DayCalendarDemo() { - const [value, setValue] = React.useState(null); - - const handleValueChange = React.useCallback((newValue: Dayjs | null) => { - setValue(newValue); - }, []); - - return ( - - - - ); -} diff --git a/docs/data/date-pickers/base-calendar/DayCalendarDemo.tsx.preview b/docs/data/date-pickers/base-calendar/DayCalendarDemo.tsx.preview deleted file mode 100644 index 72b23c81a5a56..0000000000000 --- a/docs/data/date-pickers/base-calendar/DayCalendarDemo.tsx.preview +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/data/date-pickers/base-calendar/DayCalendarWithFixedWeekNumberDemo.js b/docs/data/date-pickers/base-calendar/DayCalendarWithFixedWeekNumberDemo.js new file mode 100644 index 0000000000000..9d0e6d9f079e3 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/DayCalendarWithFixedWeekNumberDemo.js @@ -0,0 +1,70 @@ +import * as React from 'react'; + +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +// eslint-disable-next-line no-restricted-imports +import { + Calendar, + useCalendarContext, +} from '@mui/x-date-pickers/internals/base/Calendar'; +import styles from './calendar.module.css'; + +function Header() { + const { visibleDate } = useCalendarContext(); + + return ( +
+ + ◀ + + {visibleDate.format('MMMM YYYY')} + + ▶ + +
+ ); +} + +export default function DayCalendarWithFixedWeekNumberDemo() { + return ( + + +
+ + + {({ days }) => + days.map((day) => ( + + )) + } + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => + days.map((day) => ( + + )) + } + + )) + } + + + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/DayCalendarWithFixedWeekNumberDemo.tsx b/docs/data/date-pickers/base-calendar/DayCalendarWithFixedWeekNumberDemo.tsx new file mode 100644 index 0000000000000..0eded125e896d --- /dev/null +++ b/docs/data/date-pickers/base-calendar/DayCalendarWithFixedWeekNumberDemo.tsx @@ -0,0 +1,70 @@ +import * as React from 'react'; +import { Dayjs } from 'dayjs'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +// eslint-disable-next-line no-restricted-imports +import { + Calendar, + useCalendarContext, +} from '@mui/x-date-pickers/internals/base/Calendar'; +import styles from './calendar.module.css'; + +function Header() { + const { visibleDate } = useCalendarContext(); + + return ( +
+ + ◀ + + {visibleDate.format('MMMM YYYY')} + + ▶ + +
+ ); +} + +export default function DayCalendarWithFixedWeekNumberDemo() { + return ( + + +
+ + + {({ days }) => + days.map((day) => ( + + )) + } + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => + days.map((day) => ( + + )) + } + + )) + } + + + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/base-calendar.md b/docs/data/date-pickers/base-calendar/base-calendar.md index 5f6b91da8d770..afea18c37ce04 100644 --- a/docs/data/date-pickers/base-calendar/base-calendar.md +++ b/docs/data/date-pickers/base-calendar/base-calendar.md @@ -16,6 +16,27 @@ packageName: '@mui/x-date-pickers' ### Multiple visible months +1. Add the `offset` prop to each of the `` + + ```tsx + + + {/** Children for the 1st month **/} + + + {/** Children for the 2nd month **/} + + + ``` + +2. Add the `monthPageSize` prop to the `` + + ```tsx + {children} + ``` + + It will make sure that keyboard navigation and pressing `` switching month two by two. + {{"demo": "DayCalendarTwoMonthsDemo.js"}} ### With validation @@ -24,8 +45,46 @@ packageName: '@mui/x-date-pickers' ### With week number +1. Add a custom cell in `` + + ```tsx + + {({ days }) => ( + + + # + + {/** Day header cells **/} + + )} + + ``` + +2. Add a custom cell in `` + + ```tsx + + {({ days }) => ( + + + {days[0].week()} + + {/** Day cells */} + + )} + + ``` + {{"demo": "DayCalendarWithWeekNumberDemo.js"}} +### With fixed week number + +```tsx +{children} +``` + +{{"demo": "DayCalendarWithFixedWeekNumberDemo.js"}} + ## Month Calendar ### Grid layout @@ -38,6 +97,14 @@ packageName: '@mui/x-date-pickers' ### Custom cell format +```tsx + +``` + +:::success +This also works for the `` and `` components, but it's the `` that benefits from it the most. +::: + {{"demo": "MonthCalendarWithCustomCellFormatDemo.js"}} ### Multiple visible years From 19b4db95dca89a641a83a198ad9a747c93b448f3 Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 13 Jan 2025 09:09:53 +0100 Subject: [PATCH 103/136] Work on mobile d&d --- .../days-cell/useRangeCalendarDaysCell.tsx | 77 +++++++++++-------- .../useRangeCalendarDaysCellWrapper.ts | 14 +++- .../root/RangeCalendarRootContext.ts | 5 +- .../root/useRangeCalendarRoot.tsx | 64 +++++++++------ 4 files changed, 101 insertions(+), 59 deletions(-) diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx index c8021d20fb7ae..015f57f9e39df 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx @@ -18,7 +18,7 @@ export function useRangeCalendarDaysCell(parameters: useRangeCalendarDaysCell.Pa }; /** - * Mouse events + * Drag events */ const onDragStart = useEventCallback((event: React.DragEvent) => { event.stopPropagation(); @@ -84,44 +84,47 @@ export function useRangeCalendarDaysCell(parameters: useRangeCalendarDaysCell.Pa ctx.stopDragging(); // make sure the focused element is the element where drop ended event.currentTarget.focus(); - ctx.selectDayFromDrag(value); + ctx.selectDateFromDrag(value); }); /** * Touch events */ - const onTouchStart = useEventCallback(() => { ctx.setDragTarget(value); }); - const onTouchEnd = useEventCallback((event: React.TouchEvent) => { - if (!ctx.isDraggingRef.current) { + const onTouchMove = useEventCallback((event: React.TouchEvent) => { + const target = resolveElementFromTouch(event); + if (!target) { return; } - ctx.stopDragging(); + ctx.setDragTarget(target); - // make sure the focused element is the element where touch ended - ctx.selectDayFromDrag(value); - - const target = resolveElementFromTouch(event, true); - if (target) { - target.focus(); + // this prevents initiating drag when user starts touchmove outside and then moves over a draggable element + if (target !== event.changedTouches[0].target || !isDraggable) { + return; } + + // on mobile we should only initialize dragging state after move is detected + startDragging(); }); - const onTouchMove = useEventCallback((event: React.TouchEvent) => { - ctx.setDragTarget(value); + const onTouchEnd = useEventCallback((event: React.TouchEvent) => { + if (!ctx.isDraggingRef.current) { + return; + } - // this prevents initiating drag when user starts touchmove outside and then moves over a draggable element - const target = resolveElementFromTouch(event); - if (target == null || target !== event.changedTouches[0].target || !isDraggable) { + ctx.stopDragging(); + const target = resolveElementFromTouch(event, true); + if (!target) { return; } - // on mobile we should only initialize dragging state after move is detected - startDragging(); + // make sure the focused element is the element where touch ended + target.focus(); + ctx.selectDateFromDrag(target); }); /** @@ -142,13 +145,12 @@ export function useRangeCalendarDaysCell(parameters: useRangeCalendarDaysCell.Pa return mergeReactProps( externalProps, { - ...(isDraggable - ? { draggable: true, onDragStart, onDrop, onTouchStart, onTouchMove } - : {}), + ...(isDraggable ? { draggable: true, onDragStart, onDrop, onTouchStart } : {}), onDragEnter, onDragLeave, onDragOver, onDragEnd, + onTouchMove, onTouchEnd, onMouseEnter, }, @@ -187,7 +189,7 @@ export namespace useRangeCalendarDaysCell { Pick< RangeCalendarRootContext, | 'isDraggingRef' - | 'selectDayFromDrag' + | 'selectDateFromDrag' | 'startDragging' | 'stopDragging' | 'setDragTarget' @@ -203,25 +205,36 @@ export namespace useRangeCalendarDaysCell { } function resolveButtonElement(element: Element | null): HTMLButtonElement | null { - if (element) { - if (element instanceof HTMLButtonElement && !element.disabled) { - return element; - } - if (element.children.length) { - return resolveButtonElement(element.children[0]); - } + if (!element) { return null; } - return element; + + if (element instanceof HTMLButtonElement && !element.disabled) { + return element; + } + + if (element.children.length) { + const allButtons = element.querySelectorAll('button:not(:disabled)'); + if (allButtons.length > 1) { + return null; + } + + return allButtons[0] ?? null; + } + + return null; } -// TODO: Check if this logic is still needed. function resolveElementFromTouch( event: React.TouchEvent, ignoreTouchTarget?: boolean, ) { // don't parse multi-touch result if (event.changedTouches?.length === 1 && event.touches.length <= 1) { + // const element = document.elementFromPoint( + // event.changedTouches[0].clientX, + // event.changedTouches[0].clientY, + // ); const element = document.elementFromPoint( event.changedTouches[0].clientX, event.changedTouches[0].clientY, diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts index 0aebdf95ba426..d08de6d489812 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts @@ -1,4 +1,5 @@ import * as React from 'react'; +import useForkRef from '@mui/utils/useForkRef'; import { useUtils } from '@mui/x-date-pickers/internals'; // eslint-disable-next-line no-restricted-imports import { useBaseCalendarDaysCellWrapper } from '@mui/x-date-pickers/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCellWrapper'; @@ -10,9 +11,16 @@ export function useRangeCalendarDaysCellWrapper( parameters: useRangeCalendarDaysCellWrapper.Parameters, ): useRangeCalendarDaysCellWrapper.ReturnValue { const { value } = parameters; - const { ref, ctx: baseCtx } = useBaseCalendarDaysCellWrapper(parameters); + const { ref: baseRef, ctx: baseCtx } = useBaseCalendarDaysCellWrapper(parameters); const utils = useUtils(); const rootContext = useRangeCalendarRootContext(); + const cellRef = React.useRef(null); + const ref = useForkRef(baseRef, cellRef); + + const registerCell = rootContext.registerCell; + React.useEffect(() => { + return registerCell(cellRef.current!, value); + }, [registerCell, value]); const positionInSelectedRange = React.useMemo( () => getDatePositionInRange(utils, value, rootContext.selectedRange), @@ -34,7 +42,7 @@ export function useRangeCalendarDaysCellWrapper( isPreviewStart: positionInPreviewRange.isSelectionStart, isPreviewEnd: positionInPreviewRange.isSelectionEnd, isDraggingRef: rootContext.isDraggingRef, - selectDayFromDrag: rootContext.selectDayFromDrag, + selectDateFromDrag: rootContext.selectDateFromDrag, startDragging: rootContext.startDragging, stopDragging: rootContext.stopDragging, setDragTarget: rootContext.setDragTarget, @@ -46,7 +54,7 @@ export function useRangeCalendarDaysCellWrapper( positionInSelectedRange, positionInPreviewRange, rootContext.isDraggingRef, - rootContext.selectDayFromDrag, + rootContext.selectDateFromDrag, rootContext.startDragging, rootContext.stopDragging, rootContext.setDragTarget, diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts index aac9913711398..8f003c06a7ddf 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts @@ -6,15 +6,16 @@ export interface RangeCalendarRootContext { value: PickerRangeValue; isDraggingRef: React.RefObject; disableDragEditing: boolean; - selectDayFromDrag: (value: PickerValidDate) => void; + selectDateFromDrag: (valueOrElement: PickerValidDate | HTMLElement) => void; startDragging: (position: RangePosition) => void; stopDragging: () => void; - setDragTarget: (value: PickerValidDate) => void; + setDragTarget: (valueOrElement: PickerValidDate | HTMLElement) => void; emptyDragImgRef: React.RefObject; selectedRange: PickerRangeValue; isDragging: boolean; setHoveredDate: (value: PickerValidDate | null) => void; previewRange: PickerRangeValue; + registerCell: (element: HTMLElement, value: PickerValidDate) => () => void; } export const RangeCalendarRootContext = React.createContext( diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx index 42380b63df10a..dfd2310ab8a5b 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx @@ -241,12 +241,21 @@ function useBuildRangeCalendarRootContext(parameters: UseRangeCalendarDragEditin const [state, setState] = React.useState<{ isDragging: boolean; targetDate: PickerValidDate | null; - draggedDate: PickerValidDate | null; hoveredDate: PickerValidDate | null; - }>({ isDragging: false, targetDate: null, draggedDate: null, hoveredDate: null }); + }>({ isDragging: false, targetDate: null, hoveredDate: null }); + + const cellToDateMapRef = React.useRef(new Map()); + const registerCell = useEventCallback( + (element: HTMLElement, valueToRegister: PickerValidDate) => { + cellToDateMapRef.current.set(element, valueToRegister); + return () => { + cellToDateMapRef.current.delete(element); + }; + }, + ); const selectedRange = React.useMemo(() => { - if (!valueDayRange[0] || !valueDayRange[1] || !state.targetDate) { + if (!valueDayRange[0] || !valueDayRange[1] || !state.targetDate || !state.isDragging) { return valueDayRange; } @@ -260,22 +269,7 @@ function useBuildRangeCalendarRootContext(parameters: UseRangeCalendarDragEditin return newRange[0] !== null && newRange[1] !== null ? [utils.startOfDay(newRange[0]), utils.endOfDay(newRange[1])] : newRange; - }, [rangePosition, state.targetDate, utils, valueDayRange]); - - const selectDayFromDrag = useEventCallback((selectedDate: PickerValidDate) => { - if (state.draggedDate != null && utils.isSameDay(selectedDate, state.draggedDate)) { - return; - } - - const response = getNewValueFromNewSelectedDate({ - prevValue: value, - selectedDate, - referenceDate, - allowRangeFlip: true, - }); - - setValue(response.value, { changeImportance: response.changeImportance, section: 'day' }); - }); + }, [rangePosition, state.targetDate, state.isDragging, utils, valueDayRange]); const disableDragEditing = disableDragEditingProp || baseContext.disabled || baseContext.readOnly; const disableHoverPreview = @@ -308,13 +302,19 @@ function useBuildRangeCalendarRootContext(parameters: UseRangeCalendarDragEditin })); }); - const handleSetDragTarget = useEventCallback((newTargetDate: PickerValidDate) => { - if (utils.isEqual(newTargetDate, state.targetDate)) { + const handleSetDragTarget = useEventCallback((valueOrElement: PickerValidDate | HTMLElement) => { + const newTargetDate = + valueOrElement instanceof HTMLElement + ? cellToDateMapRef.current.get(valueOrElement) + : valueOrElement; + + if (newTargetDate == null || utils.isEqual(newTargetDate, state.targetDate)) { return; } setState((prev) => ({ ...prev, targetDate: newTargetDate })); + // TODO: Buggy if (value[0] && utils.isBeforeDay(newTargetDate, value[0])) { onRangePositionChange('start'); } else if (value[1] && utils.isAfterDay(newTargetDate, value[1])) { @@ -322,6 +322,25 @@ function useBuildRangeCalendarRootContext(parameters: UseRangeCalendarDragEditin } }); + const selectDateFromDrag = useEventCallback((valueOrElement: PickerValidDate | HTMLElement) => { + const selectedDate = + valueOrElement instanceof HTMLElement + ? cellToDateMapRef.current.get(valueOrElement) + : valueOrElement; + if (selectedDate == null) { + return; + } + + const response = getNewValueFromNewSelectedDate({ + prevValue: value, + selectedDate, + referenceDate, + allowRangeFlip: true, + }); + + setValue(response.value, { changeImportance: response.changeImportance, section: 'day' }); + }); + const setHoveredDate = useEventCallback((hoveredDate: PickerValidDate | null) => { if (disableHoverPreview) { return; @@ -349,12 +368,13 @@ function useBuildRangeCalendarRootContext(parameters: UseRangeCalendarDragEditin disableDragEditing, isDraggingRef, emptyDragImgRef, - selectDayFromDrag, + selectDateFromDrag, startDragging, stopDragging, setDragTarget: handleSetDragTarget, isDragging: state.isDragging, setHoveredDate, + registerCell, }; return { context }; From 01c32b5a817a3e5e2cbcabc9e10f7ddb9f4c8208 Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 13 Jan 2025 10:44:01 +0100 Subject: [PATCH 104/136] Add focus on mount prop --- .../base-calendar/DateCalendarDemo.js | 38 +++++++--- .../base-calendar/DateCalendarDemo.tsx | 27 +++++-- .../base-calendar/DateCalendarMD2Demo.js | 32 ++++++-- .../base-calendar/DateCalendarMD2Demo.tsx | 21 ++++- .../base-calendar/DateRangeCalendarDemo.js | 40 +++++++--- .../base-calendar/DateRangeCalendarDemo.tsx | 40 +++++++--- .../DayCalendarTwoMonthsDemo.tsx.preview | 1 - .../DayCalendarWithFixedWeekNumberDemo.js | 1 - .../DayCalendarWithFixedWeekNumberDemo.tsx | 1 - .../DayCalendarWithValidationDemo.tsx.preview | 8 -- .../DayCalendarWithWeekNumberDemo.tsx.preview | 1 - ...RangeCalendarWithTwoMonthsDemo.tsx.preview | 1 - .../MonthCalendarDemo.tsx.preview | 14 ---- ...onthCalendarWithListLayoutDemo.tsx.preview | 14 ---- .../YearCalendarDemo.tsx.preview | 13 ---- .../YearCalendarWithDecadeNavigationDemo.js | 45 ++++++----- .../YearCalendarWithDecadeNavigationDemo.tsx | 45 ++++++----- ...lendarWithDecadeNavigationDemo.tsx.preview | 4 - ...YearCalendarWithListLayoutDemo.tsx.preview | 13 ---- .../base-calendar/base-calendar.md | 64 ++++++++++------ .../days-cell/useRangeCalendarDaysCell.tsx | 9 +-- .../useRangeCalendarDaysCellWrapper.ts | 4 + .../days-grid/RangeCalendarDaysGrid.tsx | 9 ++- .../months-grid/RangeCalendarMonthsGrid.tsx | 13 +++- .../months-list/RangeCalendarMonthsList.tsx | 13 +++- .../root/RangeCalendarRootContext.ts | 10 ++- .../root/useRangeCalendarRoot.tsx | 1 - .../RangeCalendarSetVisibleMonth.tsx | 2 +- .../RangeCalendarSetVisibleYear.tsx | 4 +- .../years-grid/RangeCalendarYearsGrid.tsx | 4 +- .../years-list/RangeCalendarYearsList.tsx | 4 +- .../Calendar/days-grid/CalendarDaysGrid.tsx | 9 ++- .../months-grid/CalendarMonthsGrid.tsx | 13 +++- .../months-list/CalendarMonthsList.tsx | 13 +++- .../base/Calendar/root/CalendarRootContext.ts | 3 + .../CalendarSetVisibleMonth.tsx | 2 +- .../CalendarSetVisibleYear.tsx | 4 +- .../Calendar/years-grid/CalendarYearsGrid.tsx | 4 +- .../Calendar/years-list/CalendarYearsList.tsx | 4 +- .../days-grid/useBaseCalendarDaysGrid.ts | 26 +++++-- .../months-grid/useBaseCalendarMonthsGrid.ts | 20 +++-- .../months-list/useBaseCalendarMonthsList.ts | 13 ++-- .../base-calendar/utils/keyboardNavigation.ts | 2 +- .../utils/base-calendar/utils/useCellList.ts | 76 ++++++++++--------- .../base-calendar/utils/useMonthsCells.ts | 43 ++++++++--- .../base-calendar/utils/useYearsCells.ts | 65 +++++++++++----- .../years-grid/useBaseCalendarYearsGrid.ts | 10 +-- .../years-list/useBaseCalendarYearsList.ts | 10 +-- 48 files changed, 501 insertions(+), 312 deletions(-) delete mode 100644 docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.tsx.preview delete mode 100644 docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.tsx.preview delete mode 100644 docs/data/date-pickers/base-calendar/DayCalendarWithWeekNumberDemo.tsx.preview delete mode 100644 docs/data/date-pickers/base-calendar/DayRangeCalendarWithTwoMonthsDemo.tsx.preview delete mode 100644 docs/data/date-pickers/base-calendar/MonthCalendarDemo.tsx.preview delete mode 100644 docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.tsx.preview delete mode 100644 docs/data/date-pickers/base-calendar/YearCalendarDemo.tsx.preview delete mode 100644 docs/data/date-pickers/base-calendar/YearCalendarWithDecadeNavigationDemo.tsx.preview delete mode 100644 docs/data/date-pickers/base-calendar/YearCalendarWithListLayoutDemo.tsx.preview diff --git a/docs/data/date-pickers/base-calendar/DateCalendarDemo.js b/docs/data/date-pickers/base-calendar/DateCalendarDemo.js index f8c9f3d4f763c..97f7bf9c224be 100644 --- a/docs/data/date-pickers/base-calendar/DateCalendarDemo.js +++ b/docs/data/date-pickers/base-calendar/DateCalendarDemo.js @@ -73,14 +73,26 @@ function Header(props) { export default function DateCalendarDemo() { const [value, setValue] = React.useState(null); const [activeSection, setActiveSection] = React.useState('day'); + const [hasNavigated, setHasNavigated] = React.useState(false); - const handleValueChange = React.useCallback((newValue, context) => { - if (context.section === 'month' || context.section === 'year') { - setActiveSection('day'); - } + const handleActiveSectionChange = React.useCallback( + (newActiveSection) => { + setActiveSection(newActiveSection); + setHasNavigated(true); + }, + [setActiveSection, setHasNavigated], + ); + + const handleValueChange = React.useCallback( + (newValue, context) => { + if (context.section === 'month' || context.section === 'year') { + handleActiveSectionChange('day'); + } - setValue(newValue); - }, []); + setValue(newValue); + }, + [handleActiveSectionChange], + ); return ( @@ -91,10 +103,13 @@ export default function DateCalendarDemo() { >
{activeSection === 'year' && ( - + {({ years }) => years.map((year) => ( )} {activeSection === 'month' && ( - + {({ months }) => months.map((month) => ( )} {activeSection === 'day' && ( - + {({ days }) => days.map((day) => ( diff --git a/docs/data/date-pickers/base-calendar/DateCalendarDemo.tsx b/docs/data/date-pickers/base-calendar/DateCalendarDemo.tsx index e3513f66953be..fc9099e634c1f 100644 --- a/docs/data/date-pickers/base-calendar/DateCalendarDemo.tsx +++ b/docs/data/date-pickers/base-calendar/DateCalendarDemo.tsx @@ -78,16 +78,25 @@ export default function DateCalendarDemo() { const [activeSection, setActiveSection] = React.useState<'day' | 'month' | 'year'>( 'day', ); + const [hasNavigated, setHasNavigated] = React.useState(false); + + const handleActiveSectionChange = React.useCallback( + (newActiveSection: 'day' | 'month' | 'year') => { + setActiveSection(newActiveSection); + setHasNavigated(true); + }, + [setActiveSection, setHasNavigated], + ); const handleValueChange = React.useCallback( (newValue: Dayjs | null, context: Calendar.Root.ValueChangeHandlerContext) => { if (context.section === 'month' || context.section === 'year') { - setActiveSection('day'); + handleActiveSectionChange('day'); } setValue(newValue); }, - [], + [handleActiveSectionChange], ); return ( @@ -99,10 +108,13 @@ export default function DateCalendarDemo() { >
{activeSection === 'year' && ( - + {({ years }) => years.map((year) => ( )} {activeSection === 'month' && ( - + {({ months }) => months.map((month) => ( )} {activeSection === 'day' && ( - + {({ days }) => days.map((day) => ( diff --git a/docs/data/date-pickers/base-calendar/DateCalendarMD2Demo.js b/docs/data/date-pickers/base-calendar/DateCalendarMD2Demo.js index ae7bf7defb08e..d71edd973652f 100644 --- a/docs/data/date-pickers/base-calendar/DateCalendarMD2Demo.js +++ b/docs/data/date-pickers/base-calendar/DateCalendarMD2Demo.js @@ -49,14 +49,26 @@ function Header(props) { export default function DateCalendarMD2Demo() { const [value, setValue] = React.useState(null); const [activeSection, setActiveSection] = React.useState('day'); + const [hasNavigated, setHasNavigated] = React.useState(false); - const handleValueChange = React.useCallback((newValue, context) => { - if (context.section === 'year') { - setActiveSection('day'); - } + const handleActiveSectionChange = React.useCallback( + (newActiveSection) => { + setActiveSection(newActiveSection); + setHasNavigated(true); + }, + [setActiveSection, setHasNavigated], + ); + + const handleValueChange = React.useCallback( + (newValue, context) => { + if (context.section === 'year') { + handleActiveSectionChange('day'); + } - setValue(newValue); - }, []); + setValue(newValue); + }, + [setValue, handleActiveSectionChange], + ); return ( @@ -70,7 +82,11 @@ export default function DateCalendarMD2Demo() { onActiveSectionChange={setActiveSection} /> {activeSection === 'year' && ( - + {({ years }) => years.map((year) => ( )} {activeSection === 'day' && ( - + {({ days }) => days.map((day) => ( diff --git a/docs/data/date-pickers/base-calendar/DateCalendarMD2Demo.tsx b/docs/data/date-pickers/base-calendar/DateCalendarMD2Demo.tsx index e056b77865932..87f19b66bd590 100644 --- a/docs/data/date-pickers/base-calendar/DateCalendarMD2Demo.tsx +++ b/docs/data/date-pickers/base-calendar/DateCalendarMD2Demo.tsx @@ -52,16 +52,25 @@ function Header(props: { export default function DateCalendarMD2Demo() { const [value, setValue] = React.useState(null); const [activeSection, setActiveSection] = React.useState<'day' | 'year'>('day'); + const [hasNavigated, setHasNavigated] = React.useState(false); + + const handleActiveSectionChange = React.useCallback( + (newActiveSection: 'day' | 'year') => { + setActiveSection(newActiveSection); + setHasNavigated(true); + }, + [setActiveSection, setHasNavigated], + ); const handleValueChange = React.useCallback( (newValue: Dayjs | null, context: Calendar.Root.ValueChangeHandlerContext) => { if (context.section === 'year') { - setActiveSection('day'); + handleActiveSectionChange('day'); } setValue(newValue); }, - [], + [setValue, handleActiveSectionChange], ); return ( @@ -76,7 +85,11 @@ export default function DateCalendarMD2Demo() { onActiveSectionChange={setActiveSection} /> {activeSection === 'year' && ( - + {({ years }) => years.map((year) => ( )} {activeSection === 'day' && ( - + {({ days }) => days.map((day) => ( diff --git a/docs/data/date-pickers/base-calendar/DateRangeCalendarDemo.js b/docs/data/date-pickers/base-calendar/DateRangeCalendarDemo.js index f30784c8b0a60..65b0bd0c6c762 100644 --- a/docs/data/date-pickers/base-calendar/DateRangeCalendarDemo.js +++ b/docs/data/date-pickers/base-calendar/DateRangeCalendarDemo.js @@ -72,24 +72,35 @@ function Header(props) { export default function DateRangeCalendarDemo() { const [activeSection, setActiveSection] = React.useState('day'); + const [hasNavigated, setHasNavigated] = React.useState(false); + + const handleActiveSectionChange = React.useCallback( + (newActiveSection) => { + setActiveSection(newActiveSection); + setHasNavigated(true); + }, + [setActiveSection, setHasNavigated], + ); return (
{activeSection === 'year' && ( - + {({ years }) => years.map((year) => ( setActiveSection('day')} target={year} + key={year.toString()} + onClick={() => handleActiveSectionChange('day')} + className={styles.YearsCell} > {year.format('YYYY')} @@ -98,15 +109,17 @@ export default function DateRangeCalendarDemo() { )} {activeSection === 'month' && ( - + {({ months }) => months.map((month) => ( setActiveSection('day')} target={month} + key={month.toString()} + onClick={() => handleActiveSectionChange('day')} + className={styles.MonthsCell} > {month.format('MMMM')} @@ -115,7 +128,10 @@ export default function DateRangeCalendarDemo() { )} {activeSection === 'day' && ( - + {({ days }) => days.map((day) => ( diff --git a/docs/data/date-pickers/base-calendar/DateRangeCalendarDemo.tsx b/docs/data/date-pickers/base-calendar/DateRangeCalendarDemo.tsx index abf9caa288cec..2a8260db2bfc0 100644 --- a/docs/data/date-pickers/base-calendar/DateRangeCalendarDemo.tsx +++ b/docs/data/date-pickers/base-calendar/DateRangeCalendarDemo.tsx @@ -77,24 +77,35 @@ export default function DateRangeCalendarDemo() { const [activeSection, setActiveSection] = React.useState<'day' | 'month' | 'year'>( 'day', ); + const [hasNavigated, setHasNavigated] = React.useState(false); + + const handleActiveSectionChange = React.useCallback( + (newActiveSection: 'day' | 'month' | 'year') => { + setActiveSection(newActiveSection); + setHasNavigated(true); + }, + [setActiveSection, setHasNavigated], + ); return (
{activeSection === 'year' && ( - + {({ years }) => years.map((year) => ( setActiveSection('day')} target={year} + key={year.toString()} + onClick={() => handleActiveSectionChange('day')} + className={styles.YearsCell} > {year.format('YYYY')} @@ -103,15 +114,17 @@ export default function DateRangeCalendarDemo() { )} {activeSection === 'month' && ( - + {({ months }) => months.map((month) => ( setActiveSection('day')} target={month} + key={month.toString()} + onClick={() => handleActiveSectionChange('day')} + className={styles.MonthsCell} > {month.format('MMMM')} @@ -120,7 +133,10 @@ export default function DateRangeCalendarDemo() { )} {activeSection === 'day' && ( - + {({ days }) => days.map((day) => ( diff --git a/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.tsx.preview b/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.tsx.preview deleted file mode 100644 index 72b23c81a5a56..0000000000000 --- a/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.tsx.preview +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/data/date-pickers/base-calendar/DayCalendarWithFixedWeekNumberDemo.js b/docs/data/date-pickers/base-calendar/DayCalendarWithFixedWeekNumberDemo.js index 9d0e6d9f079e3..750d0fb6404f5 100644 --- a/docs/data/date-pickers/base-calendar/DayCalendarWithFixedWeekNumberDemo.js +++ b/docs/data/date-pickers/base-calendar/DayCalendarWithFixedWeekNumberDemo.js @@ -1,5 +1,4 @@ import * as React from 'react'; - import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports diff --git a/docs/data/date-pickers/base-calendar/DayCalendarWithFixedWeekNumberDemo.tsx b/docs/data/date-pickers/base-calendar/DayCalendarWithFixedWeekNumberDemo.tsx index 0eded125e896d..750d0fb6404f5 100644 --- a/docs/data/date-pickers/base-calendar/DayCalendarWithFixedWeekNumberDemo.tsx +++ b/docs/data/date-pickers/base-calendar/DayCalendarWithFixedWeekNumberDemo.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import { Dayjs } from 'dayjs'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports diff --git a/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.tsx.preview b/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.tsx.preview deleted file mode 100644 index 7ed1d697455e7..0000000000000 --- a/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.tsx.preview +++ /dev/null @@ -1,8 +0,0 @@ - - ALREADY_BOOKED_NIGHTS_SET.has(date.format('YYYY-MM-DD')) - } -/> \ No newline at end of file diff --git a/docs/data/date-pickers/base-calendar/DayCalendarWithWeekNumberDemo.tsx.preview b/docs/data/date-pickers/base-calendar/DayCalendarWithWeekNumberDemo.tsx.preview deleted file mode 100644 index 72b23c81a5a56..0000000000000 --- a/docs/data/date-pickers/base-calendar/DayCalendarWithWeekNumberDemo.tsx.preview +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/data/date-pickers/base-calendar/DayRangeCalendarWithTwoMonthsDemo.tsx.preview b/docs/data/date-pickers/base-calendar/DayRangeCalendarWithTwoMonthsDemo.tsx.preview deleted file mode 100644 index 16ef550120c15..0000000000000 --- a/docs/data/date-pickers/base-calendar/DayRangeCalendarWithTwoMonthsDemo.tsx.preview +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/data/date-pickers/base-calendar/MonthCalendarDemo.tsx.preview b/docs/data/date-pickers/base-calendar/MonthCalendarDemo.tsx.preview deleted file mode 100644 index 2dcd2a095dd1b..0000000000000 --- a/docs/data/date-pickers/base-calendar/MonthCalendarDemo.tsx.preview +++ /dev/null @@ -1,14 +0,0 @@ - -
- - {({ months }) => - months.map((month) => ( - - )) - } - - \ No newline at end of file diff --git a/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.tsx.preview b/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.tsx.preview deleted file mode 100644 index 28bf2a9fcbb53..0000000000000 --- a/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.tsx.preview +++ /dev/null @@ -1,14 +0,0 @@ - -
- - {({ months }) => - months.map((month) => ( - - )) - } - - \ No newline at end of file diff --git a/docs/data/date-pickers/base-calendar/YearCalendarDemo.tsx.preview b/docs/data/date-pickers/base-calendar/YearCalendarDemo.tsx.preview deleted file mode 100644 index b1d3480d864c2..0000000000000 --- a/docs/data/date-pickers/base-calendar/YearCalendarDemo.tsx.preview +++ /dev/null @@ -1,13 +0,0 @@ - - - {({ years }) => - years.map((year) => ( - - )) - } - - \ No newline at end of file diff --git a/docs/data/date-pickers/base-calendar/YearCalendarWithDecadeNavigationDemo.js b/docs/data/date-pickers/base-calendar/YearCalendarWithDecadeNavigationDemo.js index 73420672dc424..6a3d3c8768382 100644 --- a/docs/data/date-pickers/base-calendar/YearCalendarWithDecadeNavigationDemo.js +++ b/docs/data/date-pickers/base-calendar/YearCalendarWithDecadeNavigationDemo.js @@ -36,35 +36,34 @@ function Header() { ); } -function YearsGrid() { - const { visibleDate } = useCalendarContext(); - const decade = Math.floor(visibleDate.year() / 10) * 10; - - return ( - - {({ years }) => - years - .filter((year) => Math.floor(year.year() / 10) * 10 === decade) - .map((year) => ( - - )) - } - +const getYearsInDecade = ({ visibleDate }) => { + const reference = visibleDate.startOf('year'); + const decade = Math.floor(reference.year() / 10) * 10; + return Array.from({ length: 10 }, (_, index) => + reference.set('year', decade + index), ); -} +}; export default function YearCalendarWithDecadeNavigationDemo() { - const [value, setValue] = React.useState(null); - return ( - +
- + + {({ years }) => + years.map((year) => ( + + )) + } + ); diff --git a/docs/data/date-pickers/base-calendar/YearCalendarWithDecadeNavigationDemo.tsx b/docs/data/date-pickers/base-calendar/YearCalendarWithDecadeNavigationDemo.tsx index a236b2cdae110..d464a2097ac55 100644 --- a/docs/data/date-pickers/base-calendar/YearCalendarWithDecadeNavigationDemo.tsx +++ b/docs/data/date-pickers/base-calendar/YearCalendarWithDecadeNavigationDemo.tsx @@ -36,35 +36,34 @@ function Header() { ); } -function YearsGrid() { - const { visibleDate } = useCalendarContext(); - const decade = Math.floor(visibleDate.year() / 10) * 10; - - return ( - - {({ years }) => - years - .filter((year: Dayjs) => Math.floor(year.year() / 10) * 10 === decade) - .map((year) => ( - - )) - } - +const getYearsInDecade = ({ visibleDate }: { visibleDate: Dayjs }) => { + const reference = visibleDate.startOf('year'); + const decade = Math.floor(reference.year() / 10) * 10; + return Array.from({ length: 10 }, (_, index) => + reference.set('year', decade + index), ); -} +}; export default function YearCalendarWithDecadeNavigationDemo() { - const [value, setValue] = React.useState(null); - return ( - +
- + + {({ years }) => + years.map((year) => ( + + )) + } + ); diff --git a/docs/data/date-pickers/base-calendar/YearCalendarWithDecadeNavigationDemo.tsx.preview b/docs/data/date-pickers/base-calendar/YearCalendarWithDecadeNavigationDemo.tsx.preview deleted file mode 100644 index a205ab02e7254..0000000000000 --- a/docs/data/date-pickers/base-calendar/YearCalendarWithDecadeNavigationDemo.tsx.preview +++ /dev/null @@ -1,4 +0,0 @@ - -
- - \ No newline at end of file diff --git a/docs/data/date-pickers/base-calendar/YearCalendarWithListLayoutDemo.tsx.preview b/docs/data/date-pickers/base-calendar/YearCalendarWithListLayoutDemo.tsx.preview deleted file mode 100644 index bce391c7c37b9..0000000000000 --- a/docs/data/date-pickers/base-calendar/YearCalendarWithListLayoutDemo.tsx.preview +++ /dev/null @@ -1,13 +0,0 @@ - - - {({ years }) => - years.map((year) => ( - - )) - } - - \ No newline at end of file diff --git a/docs/data/date-pickers/base-calendar/base-calendar.md b/docs/data/date-pickers/base-calendar/base-calendar.md index afea18c37ce04..26a4ffd2ad170 100644 --- a/docs/data/date-pickers/base-calendar/base-calendar.md +++ b/docs/data/date-pickers/base-calendar/base-calendar.md @@ -12,7 +12,7 @@ packageName: '@mui/x-date-pickers' ### Single visible month -{{"demo": "DayCalendarDemo.js"}} +{{"demo": "DayCalendarDemo.js", "defaultCodeOpen": false}} ### Multiple visible months @@ -37,13 +37,21 @@ packageName: '@mui/x-date-pickers' It will make sure that keyboard navigation and pressing `` switching month two by two. -{{"demo": "DayCalendarTwoMonthsDemo.js"}} +{{"demo": "DayCalendarTwoMonthsDemo.js", "defaultCodeOpen": false}} ### With validation -{{"demo": "DayCalendarWithValidationDemo.js"}} +{{"demo": "DayCalendarWithValidationDemo.js", "defaultCodeOpen": false}} -### With week number +### With fixed week number + +```tsx +{children} +``` + +{{"demo": "DayCalendarWithFixedWeekNumberDemo.js", "defaultCodeOpen": false}} + +### Recipe: With week number 1. Add a custom cell in `` @@ -75,15 +83,7 @@ packageName: '@mui/x-date-pickers' ``` -{{"demo": "DayCalendarWithWeekNumberDemo.js"}} - -### With fixed week number - -```tsx -{children} -``` - -{{"demo": "DayCalendarWithFixedWeekNumberDemo.js"}} +{{"demo": "DayCalendarWithWeekNumberDemo.js", "defaultCodeOpen": false}} ## Month Calendar @@ -93,7 +93,7 @@ packageName: '@mui/x-date-pickers' ### List layout -{{"demo": "MonthCalendarWithListLayoutDemo.js"}} +{{"demo": "MonthCalendarWithListLayoutDemo.js", "defaultCodeOpen": false}} ### Custom cell format @@ -105,7 +105,7 @@ packageName: '@mui/x-date-pickers' This also works for the `` and `` components, but it's the `` that benefits from it the most. ::: -{{"demo": "MonthCalendarWithCustomCellFormatDemo.js"}} +{{"demo": "MonthCalendarWithCustomCellFormatDemo.js", "defaultCodeOpen": false}} ### Multiple visible years @@ -119,32 +119,50 @@ TODO ### List layout -{{"demo": "YearCalendarWithListLayoutDemo.js"}} +{{"demo": "YearCalendarWithListLayoutDemo.js", "defaultCodeOpen": false}} + +### Recipe: Grouped by decade -### Grouped by decade +```tsx +const getYearsInDecade = ({ visibleDate }: { visibleDate: Dayjs }) => { + const reference = visibleDate.startOf('year'); + const decade = Math.floor(reference.year() / 10) * 10; + return Array.from({ length: 10 }, (_, index) => + reference.set('year', decade + index), + ); +}; + + + {/** Year cells */} +; +``` + +:::success +Using the `getItems` prop instead of manually providing a list of `` as children allows the `` to always have at least one cell with `tabIndex={0}`. +::: -{{"demo": "YearCalendarWithDecadeNavigationDemo.js"}} +{{"demo": "YearCalendarWithDecadeNavigationDemo.js", "defaultCodeOpen": false}} ## Full Date Calendar ### MD2-ish layout -{{"demo": "DateCalendarMD2Demo.js"}} +{{"demo": "DateCalendarMD2Demo.js", "defaultCodeOpen": false}} ### MD3-ish layout -{{"demo": "DateCalendarDemo.js"}} +{{"demo": "DateCalendarDemo.js", "defaultCodeOpen": false}} ## Day Range Calendar ### Single visible month -{{"demo": "DayRangeCalendarDemo.js"}} +{{"demo": "DayRangeCalendarDemo.js", "defaultCodeOpen": false}} ### Multiple visible months -{{"demo": "DayRangeCalendarWithTwoMonthsDemo.js"}} +{{"demo": "DayRangeCalendarWithTwoMonthsDemo.js", "defaultCodeOpen": false}} ## Date Range Calendar -{{"demo": "DateRangeCalendarDemo.js"}} +{{"demo": "DateRangeCalendarDemo.js", "defaultCodeOpen": false}} diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx index 015f57f9e39df..77d00eaf899dc 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx @@ -11,8 +11,6 @@ import type { RangeCalendarRootContext } from '../root/RangeCalendarRootContext' export function useRangeCalendarDaysCell(parameters: useRangeCalendarDaysCell.Parameters) { const { ctx, value } = parameters; - const isDraggable = ctx.isSelectionStart || ctx.isSelectionEnd; - const startDragging = () => { ctx.startDragging(ctx.isSelectionStart ? 'start' : 'end'); }; @@ -103,7 +101,7 @@ export function useRangeCalendarDaysCell(parameters: useRangeCalendarDaysCell.Pa ctx.setDragTarget(target); // this prevents initiating drag when user starts touchmove outside and then moves over a draggable element - if (target !== event.changedTouches[0].target || !isDraggable) { + if (target !== event.changedTouches[0].target || !ctx.isDraggable) { return; } @@ -145,7 +143,7 @@ export function useRangeCalendarDaysCell(parameters: useRangeCalendarDaysCell.Pa return mergeReactProps( externalProps, { - ...(isDraggable ? { draggable: true, onDragStart, onDrop, onTouchStart } : {}), + ...(ctx.isDraggable ? { draggable: true, onDragStart, onDrop, onTouchStart } : {}), onDragEnter, onDragLeave, onDragOver, @@ -159,7 +157,7 @@ export function useRangeCalendarDaysCell(parameters: useRangeCalendarDaysCell.Pa }, [ getBaseDaysCellProps, - isDraggable, + ctx.isDraggable, onDragStart, onDragEnter, onDragLeave, @@ -196,6 +194,7 @@ export namespace useRangeCalendarDaysCell { | 'setHoveredDate' | 'emptyDragImgRef' > { + isDraggable: boolean; isSelectionStart: boolean; isSelectionEnd: boolean; isPreviewed: boolean; diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts index d08de6d489812..996f661f42d34 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts @@ -35,6 +35,9 @@ export function useRangeCalendarDaysCellWrapper( const ctx = React.useMemo( () => ({ ...baseCtx, + isDraggable: + (!rootContext.disableDragEditing && positionInSelectedRange.isSelectionStart) || + positionInSelectedRange.isSelectionEnd, isSelected: positionInSelectedRange.isSelected, isSelectionStart: positionInSelectedRange.isSelectionStart, isSelectionEnd: positionInSelectedRange.isSelectionEnd, @@ -53,6 +56,7 @@ export function useRangeCalendarDaysCellWrapper( baseCtx, positionInSelectedRange, positionInPreviewRange, + rootContext.disableDragEditing, rootContext.isDraggingRef, rootContext.selectDateFromDrag, rootContext.startDragging, diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid/RangeCalendarDaysGrid.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid/RangeCalendarDaysGrid.tsx index cbf5ba5ca95b0..72f00f56041ba 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid/RangeCalendarDaysGrid.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid/RangeCalendarDaysGrid.tsx @@ -1,5 +1,6 @@ 'use client'; import * as React from 'react'; +import useForkRef from '@mui/utils/useForkRef'; // eslint-disable-next-line no-restricted-imports import { BaseCalendarDaysGridContext } from '@mui/x-date-pickers/internals/base/utils/base-calendar/days-grid/BaseCalendarDaysGridContext'; // eslint-disable-next-line no-restricted-imports @@ -13,17 +14,19 @@ const RangeCalendarDaysGrid = React.forwardRef(function RangeCalendarDaysGrid( props: RangeCalendarDaysGrid.Props, forwardedRef: React.ForwardedRef, ) { - const { className, render, fixedWeekNumber, offset, ...otherProps } = props; - const { getDaysGridProps, context } = useBaseCalendarDaysGrid({ + const { className, render, fixedWeekNumber, offset, focusOnMount, ...otherProps } = props; + const { getDaysGridProps, context, scrollerRef } = useBaseCalendarDaysGrid({ fixedWeekNumber, + focusOnMount, offset, }); const state = React.useMemo(() => ({}), []); + const ref = useForkRef(forwardedRef, scrollerRef); const { renderElement } = useComponentRenderer({ propGetter: getDaysGridProps, render: render ?? 'div', - ref: forwardedRef, + ref, className, state, extraProps: otherProps, diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-grid/RangeCalendarMonthsGrid.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-grid/RangeCalendarMonthsGrid.tsx index ef5b83670d609..9fdbc07f8c4ea 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-grid/RangeCalendarMonthsGrid.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-grid/RangeCalendarMonthsGrid.tsx @@ -17,10 +17,21 @@ const RangeCalendarMonthsGrid = React.forwardRef(function RangeCalendarMonthsLis props: RangeCalendarMonthsGrid.Props, forwardedRef: React.ForwardedRef, ) { - const { className, render, children, cellsPerRow, canChangeYear, ...otherProps } = props; + const { + className, + render, + children, + getItems, + focusOnMount, + cellsPerRow, + canChangeYear, + ...otherProps + } = props; const { getMonthsGridProps, cellRefs, monthsListOrGridContext, scrollerRef } = useBaseCalendarMonthsGrid({ children, + getItems, + focusOnMount, cellsPerRow, canChangeYear, cellsPerRowCssVar: RangeCalendarMonthsGridCssVars.calendarMonthsGridCellsPerRow, diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-list/RangeCalendarMonthsList.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-list/RangeCalendarMonthsList.tsx index 3d3c39c2cd98b..5545940c6c563 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-list/RangeCalendarMonthsList.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-list/RangeCalendarMonthsList.tsx @@ -16,10 +16,21 @@ const RangeCalendarMonthsList = React.forwardRef(function RangeCalendarMonthsLis props: RangeCalendarMonthsList.Props, forwardedRef: React.ForwardedRef, ) { - const { className, render, children, loop, canChangeYear, ...otherProps } = props; + const { + className, + render, + children, + getItems, + focusOnMount, + loop, + canChangeYear, + ...otherProps + } = props; const { getMonthListProps, cellRefs, monthsListOrGridContext, scrollerRef } = useBaseCalendarMonthsList({ children, + getItems, + focusOnMount, loop, canChangeYear, }); diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts index 8f003c06a7ddf..4a7b845dfb1a1 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts @@ -3,7 +3,14 @@ import { PickerRangeValue, RangePosition } from '@mui/x-date-pickers/internals'; import { PickerValidDate } from '@mui/x-date-pickers/models'; export interface RangeCalendarRootContext { + /** + * The current value of the Range Calendar. + */ value: PickerRangeValue; + /** + * A ref containing `true` if the user is currently dragging. + * This is used to check if the user is dragging in event handlers without causing re-renders. + */ isDraggingRef: React.RefObject; disableDragEditing: boolean; selectDateFromDrag: (valueOrElement: PickerValidDate | HTMLElement) => void; @@ -11,11 +18,10 @@ export interface RangeCalendarRootContext { stopDragging: () => void; setDragTarget: (valueOrElement: PickerValidDate | HTMLElement) => void; emptyDragImgRef: React.RefObject; + registerCell: (element: HTMLElement, value: PickerValidDate) => () => void; selectedRange: PickerRangeValue; - isDragging: boolean; setHoveredDate: (value: PickerValidDate | null) => void; previewRange: PickerRangeValue; - registerCell: (element: HTMLElement, value: PickerValidDate) => () => void; } export const RangeCalendarRootContext = React.createContext( diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx index dfd2310ab8a5b..70c294b89a4b1 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx @@ -372,7 +372,6 @@ function useBuildRangeCalendarRootContext(parameters: UseRangeCalendarDragEditin startDragging, stopDragging, setDragTarget: handleSetDragTarget, - isDragging: state.isDragging, setHoveredDate, registerCell, }; diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/set-visible-month/RangeCalendarSetVisibleMonth.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/set-visible-month/RangeCalendarSetVisibleMonth.tsx index 241279c6a5d21..b827e9a5a8dd3 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/set-visible-month/RangeCalendarSetVisibleMonth.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/set-visible-month/RangeCalendarSetVisibleMonth.tsx @@ -42,7 +42,7 @@ const RangeCalendarSetVisibleMonth = React.forwardRef(function RangeCalendarSetV forwardedRef, target: props.target, }); - return ; + return ; }); export namespace RangeCalendarSetVisibleMonth { diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/set-visible-year/RangeCalendarSetVisibleYear.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/set-visible-year/RangeCalendarSetVisibleYear.tsx index 0304e7fe32f9f..78ed305c420a4 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/set-visible-year/RangeCalendarSetVisibleYear.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/set-visible-year/RangeCalendarSetVisibleYear.tsx @@ -36,9 +36,9 @@ const RangeCalendarSetVisibleYear = React.forwardRef(function RangeCalendarSetVi props: RangeCalendarSetVisibleYear.Props, forwardedRef: React.ForwardedRef, ) { - const { ctx } = useBaseCalendarSetVisibleYearWrapper({ target: props.target, forwardedRef }); + const { ctx, ref } = useBaseCalendarSetVisibleYearWrapper({ target: props.target, forwardedRef }); - return ; + return ; }); export namespace RangeCalendarSetVisibleYear { diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-grid/RangeCalendarYearsGrid.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-grid/RangeCalendarYearsGrid.tsx index c052e9ba95667..4aae3f48d8f98 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-grid/RangeCalendarYearsGrid.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-grid/RangeCalendarYearsGrid.tsx @@ -17,10 +17,12 @@ const RangeCalendarYearsGrid = React.forwardRef(function CalendarYearsList( props: RangeCalendarYearsGrid.Props, forwardedRef: React.ForwardedRef, ) { - const { className, render, children, cellsPerRow, ...otherProps } = props; + const { className, render, children, cellsPerRow, getItems, focusOnMount, ...otherProps } = props; const { getYearsGridProps, yearsCellRefs, yearsListOrGridContext, scrollerRef } = useBaseCalendarYearsGrid({ children, + getItems, + focusOnMount, cellsPerRow, cellsPerRowCssVar: RangeCalendarYearsGridCssVars.rangeCalendarYearsGridCellsPerRow, }); diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-list/RangeCalendarYearsList.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-list/RangeCalendarYearsList.tsx index 202942c611bf3..528078669037e 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-list/RangeCalendarYearsList.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-list/RangeCalendarYearsList.tsx @@ -16,10 +16,12 @@ const RangeCalendarYearsList = React.forwardRef(function CalendarYearsList( props: CalendarYearsList.Props, forwardedRef: React.ForwardedRef, ) { - const { className, render, children, loop, ...otherProps } = props; + const { className, render, children, getItems, focusOnMount, loop, ...otherProps } = props; const { getYearsListProps, cellRefs, yearsListOrGridContext, scrollerRef } = useBaseCalendarYearsList({ children, + getItems, + focusOnMount, loop, }); const state = React.useMemo(() => ({}), []); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-grid/CalendarDaysGrid.tsx b/packages/x-date-pickers/src/internals/base/Calendar/days-grid/CalendarDaysGrid.tsx index c9b942aecfcf1..d8449c05605a2 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-grid/CalendarDaysGrid.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-grid/CalendarDaysGrid.tsx @@ -1,5 +1,6 @@ 'use client'; import * as React from 'react'; +import useForkRef from '@mui/utils/useForkRef'; import { useBaseCalendarDaysGrid } from '../../utils/base-calendar/days-grid/useBaseCalendarDaysGrid'; import { BaseUIComponentProps } from '../../base-utils/types'; import { useComponentRenderer } from '../../base-utils/useComponentRenderer'; @@ -9,17 +10,19 @@ const CalendarDaysGrid = React.forwardRef(function CalendarDaysGrid( props: CalendarDaysGrid.Props, forwardedRef: React.ForwardedRef, ) { - const { className, render, fixedWeekNumber, offset, ...otherProps } = props; - const { getDaysGridProps, context } = useBaseCalendarDaysGrid({ + const { className, render, fixedWeekNumber, focusOnMount, offset, ...otherProps } = props; + const { getDaysGridProps, context, scrollerRef } = useBaseCalendarDaysGrid({ fixedWeekNumber, + focusOnMount, offset, }); const state = React.useMemo(() => ({}), []); + const ref = useForkRef(forwardedRef, scrollerRef); const { renderElement } = useComponentRenderer({ propGetter: getDaysGridProps, render: render ?? 'div', - ref: forwardedRef, + ref, className, state, extraProps: otherProps, diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-grid/CalendarMonthsGrid.tsx b/packages/x-date-pickers/src/internals/base/Calendar/months-grid/CalendarMonthsGrid.tsx index a553b29647bfa..7d0b26270a2b4 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-grid/CalendarMonthsGrid.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-grid/CalendarMonthsGrid.tsx @@ -12,10 +12,21 @@ const CalendarMonthsGrid = React.forwardRef(function CalendarMonthsList( props: CalendarMonthsGrid.Props, forwardedRef: React.ForwardedRef, ) { - const { className, render, children, cellsPerRow, canChangeYear, ...otherProps } = props; + const { + className, + render, + children, + getItems, + focusOnMount, + cellsPerRow, + canChangeYear, + ...otherProps + } = props; const { getMonthsGridProps, cellRefs, monthsListOrGridContext, scrollerRef } = useBaseCalendarMonthsGrid({ children, + getItems, + focusOnMount, cellsPerRow, canChangeYear, cellsPerRowCssVar: CalendarMonthsGridCssVars.calendarMonthsGridCellsPerRow, diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsList.tsx b/packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsList.tsx index 829179dcc0119..27dbf27d4c3d1 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsList.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-list/CalendarMonthsList.tsx @@ -11,10 +11,21 @@ const CalendarMonthsList = React.forwardRef(function CalendarMonthsList( props: CalendarMonthsList.Props, forwardedRef: React.ForwardedRef, ) { - const { className, render, children, loop, canChangeYear, ...otherProps } = props; + const { + className, + render, + children, + getItems, + focusOnMount, + loop, + canChangeYear, + ...otherProps + } = props; const { getMonthListProps, cellRefs, monthsListOrGridContext, scrollerRef } = useBaseCalendarMonthsList({ children, + getItems, + focusOnMount, loop, canChangeYear, }); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts index 5a45e98589dde..f0cf016bbdd78 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts @@ -2,6 +2,9 @@ import * as React from 'react'; import { PickerValue } from '../../../models'; export interface CalendarRootContext { + /** + * The current value of the Calendar. + */ value: PickerValue; } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx index b0193d0c67873..580085c4ae3b0 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-month/CalendarSetVisibleMonth.tsx @@ -37,7 +37,7 @@ const CalendarSetVisibleMonth = React.forwardRef(function CalendarSetVisibleMont forwardedRef, target: props.target, }); - return ; + return ; }); export namespace CalendarSetVisibleMonth { diff --git a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx index faeeafae68c1f..94359db04aee4 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/set-visible-year/CalendarSetVisibleYear.tsx @@ -32,9 +32,9 @@ const CalendarSetVisibleYear = React.forwardRef(function CalendarSetVisibleYear( props: CalendarSetVisibleYear.Props, forwardedRef: React.ForwardedRef, ) { - const { ctx } = useBaseCalendarSetVisibleYearWrapper({ forwardedRef, target: props.target }); + const { ctx, ref } = useBaseCalendarSetVisibleYearWrapper({ forwardedRef, target: props.target }); - return ; + return ; }); export namespace CalendarSetVisibleYear { diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-grid/CalendarYearsGrid.tsx b/packages/x-date-pickers/src/internals/base/Calendar/years-grid/CalendarYearsGrid.tsx index 0e97356261c5e..60adfe96d8524 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-grid/CalendarYearsGrid.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-grid/CalendarYearsGrid.tsx @@ -12,10 +12,12 @@ const CalendarYearsGrid = React.forwardRef(function CalendarYearsList( props: CalendarYearsGrid.Props, forwardedRef: React.ForwardedRef, ) { - const { className, render, children, cellsPerRow, ...otherProps } = props; + const { className, render, children, cellsPerRow, getItems, focusOnMount, ...otherProps } = props; const { getYearsGridProps, yearsCellRefs, yearsListOrGridContext, scrollerRef } = useBaseCalendarYearsGrid({ children, + getItems, + focusOnMount, cellsPerRow, cellsPerRowCssVar: CalendarYearsGridCssVars.calendarYearsGridCellsPerRow, }); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-list/CalendarYearsList.tsx b/packages/x-date-pickers/src/internals/base/Calendar/years-list/CalendarYearsList.tsx index 87d32277bc927..54971790fd243 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-list/CalendarYearsList.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-list/CalendarYearsList.tsx @@ -11,10 +11,12 @@ const CalendarYearsList = React.forwardRef(function CalendarYearsList( props: CalendarYearsList.Props, forwardedRef: React.ForwardedRef, ) { - const { className, render, children, loop, ...otherProps } = props; + const { className, render, children, getItems, focusOnMount, loop, ...otherProps } = props; const { getYearsListProps, cellRefs, yearsListOrGridContext, scrollerRef } = useBaseCalendarYearsList({ children, + getItems, + focusOnMount, loop, }); const state = React.useMemo(() => ({}), []); diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-grid/useBaseCalendarDaysGrid.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-grid/useBaseCalendarDaysGrid.ts index 4f57b38c55b79..1d3f979c7ccea 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-grid/useBaseCalendarDaysGrid.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-grid/useBaseCalendarDaysGrid.ts @@ -7,9 +7,10 @@ import { GenericHTMLProps } from '../../../base-utils/types'; import { mergeReactProps } from '../../../base-utils/mergeReactProps'; import { useBaseCalendarRootContext } from '../root/BaseCalendarRootContext'; import { BaseCalendarDaysGridContext } from './BaseCalendarDaysGridContext'; +import { useCellList } from '../utils/useCellList'; export function useBaseCalendarDaysGrid(parameters: useBaseCalendarDaysGrid.Parameters) { - const { fixedWeekNumber, offset = 0 } = parameters; + const { fixedWeekNumber, focusOnMount, offset = 0 } = parameters; const utils = useUtils(); const baseRootContext = useBaseCalendarRootContext(); @@ -18,6 +19,12 @@ export function useBaseCalendarDaysGrid(parameters: useBaseCalendarDaysGrid.Para return offset === 0 ? cleanVisibleDate : utils.addMonths(cleanVisibleDate, offset); }, [utils, baseRootContext.visibleDate, offset]); + const { scrollerRef } = useCellList({ + focusOnMount, + section: 'day', + value: currentMonth, + }); + const daysGrid = React.useMemo(() => { const toDisplay = utils.getWeekArray(currentMonth); let nextMonth = utils.addMonths(currentMonth, 1); @@ -80,21 +87,19 @@ export function useBaseCalendarDaysGrid(parameters: useBaseCalendarDaysGrid.Para return tempTabbableDays; }, [baseRootContext.currentDate, baseRootContext.selectedDates, daysGrid, utils, currentMonth]); - const registerSection = baseRootContext.registerSection; - React.useEffect(() => { - return registerSection({ type: 'day', value: currentMonth }); - }, [registerSection, currentMonth]); - const context: BaseCalendarDaysGridContext = React.useMemo( () => ({ selectDay, daysGrid, currentMonth, tabbableDays }), [selectDay, daysGrid, currentMonth, tabbableDays], ); - return React.useMemo(() => ({ getDaysGridProps, context }), [getDaysGridProps, context]); + return React.useMemo( + () => ({ getDaysGridProps, context, scrollerRef }), + [getDaysGridProps, context, scrollerRef], + ); } export namespace useBaseCalendarDaysGrid { - export interface Parameters { + export interface Parameters extends useCellList.PublicParameters { /** * The day view will show as many weeks as needed after the end of the current month to match this value. * Put it to 6 to have a fixed number of weeks in Gregorian calendars @@ -106,5 +111,10 @@ export namespace useBaseCalendarDaysGrid { * @default 0 */ offset?: number; + /** + * If `true`, the first tabbable children inside this component will be focused on mount. + * @default false + */ + focusOnMount?: boolean; } } diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-grid/useBaseCalendarMonthsGrid.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-grid/useBaseCalendarMonthsGrid.ts index 264cf4078e25a..5db3c8502a15b 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-grid/useBaseCalendarMonthsGrid.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-grid/useBaseCalendarMonthsGrid.ts @@ -14,10 +14,20 @@ import { import { useMonthsCells } from '../utils/useMonthsCells'; export function useBaseCalendarMonthsGrid(parameters: useBaseCalendarMonthsGrid.Parameters) { - const { children, cellsPerRow, canChangeYear = true, cellsPerRowCssVar } = parameters; + const { + children, + cellsPerRow, + getItems, + focusOnMount, + canChangeYear = true, + cellsPerRowCssVar, + } = parameters; const baseRootContext = useBaseCalendarRootContext(); const cellRefs = React.useRef<(HTMLElement | null)[]>([]); - const { months, monthsListOrGridContext, changePage, scrollerRef } = useMonthsCells(); + const { items, monthsListOrGridContext, changePage, scrollerRef } = useMonthsCells({ + getItems, + focusOnMount, + }); const pageNavigationTargetRef = React.useRef(null); const getCellsInCalendar = useEventCallback(() => { @@ -67,14 +77,14 @@ export function useBaseCalendarMonthsGrid(parameters: useBaseCalendarMonthsGrid. (externalProps: GenericHTMLProps) => { return mergeReactProps(externalProps, { role: 'radiogroup', - children: children == null ? null : children({ months }), + children: children == null ? null : children({ months: items }), onKeyDown, style: { [cellsPerRowCssVar]: cellsPerRow, }, }); }, - [months, children, onKeyDown, cellsPerRow, cellsPerRowCssVar], + [items, children, onKeyDown, cellsPerRow, cellsPerRowCssVar], ); return React.useMemo( @@ -84,7 +94,7 @@ export function useBaseCalendarMonthsGrid(parameters: useBaseCalendarMonthsGrid. } export namespace useBaseCalendarMonthsGrid { - export interface PublicParameters { + export interface PublicParameters extends useMonthsCells.Parameters { /** * The number of cells per row. * This is used to make sure the keyboard navigation works correctly. diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-list/useBaseCalendarMonthsList.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-list/useBaseCalendarMonthsList.ts index 0efb8c7adb633..3338b981f89af 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-list/useBaseCalendarMonthsList.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-list/useBaseCalendarMonthsList.ts @@ -14,10 +14,13 @@ import { import { useMonthsCells } from '../utils/useMonthsCells'; export function useBaseCalendarMonthsList(parameters: useBaseCalendarMonthsList.Parameters) { - const { children, loop = true, canChangeYear = true } = parameters; + const { children, getItems, focusOnMount, loop = true, canChangeYear = true } = parameters; const baseRootContext = useBaseCalendarRootContext(); const cellRefs = React.useRef<(HTMLElement | null)[]>([]); - const { months, monthsListOrGridContext, changePage, scrollerRef } = useMonthsCells(); + const { items, monthsListOrGridContext, changePage, scrollerRef } = useMonthsCells({ + getItems, + focusOnMount, + }); const pageNavigationTargetRef = React.useRef(null); const timeout = useTimeout(); @@ -49,11 +52,11 @@ export function useBaseCalendarMonthsList(parameters: useBaseCalendarMonthsList. (externalProps: GenericHTMLProps) => { return mergeReactProps(externalProps, { role: 'radiogroup', - children: children == null ? null : children({ months }), + children: children == null ? null : children({ months: items }), onKeyDown, }); }, - [months, children, onKeyDown], + [items, children, onKeyDown], ); return React.useMemo( @@ -63,7 +66,7 @@ export function useBaseCalendarMonthsList(parameters: useBaseCalendarMonthsList. } export namespace useBaseCalendarMonthsList { - export interface Parameters { + export interface Parameters extends useMonthsCells.Parameters { /** * Whether to loop keyboard focus back to the first item * when the end of the list is reached while using the arrow keys. diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/keyboardNavigation.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/keyboardNavigation.ts index 7bc17ac9c9b3c..a25a6b9148fd8 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/keyboardNavigation.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/keyboardNavigation.ts @@ -62,7 +62,7 @@ export function navigateInList({ break; } - if (nextIndex > -1) { + if (nextIndex > -1 && nextIndex < navigableCells.length) { navigableCells[nextIndex].focus(); } } diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useCellList.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useCellList.ts index 7515e62f9aae3..8c7e07073b9f7 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useCellList.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useCellList.ts @@ -3,7 +3,7 @@ import { PickerValidDate } from '../../../../../models'; import { useBaseCalendarRootContext } from '../root/BaseCalendarRootContext'; export function useCellList(parameters: useCellList.Parameters): useCellList.ReturnValue { - const { section, value } = parameters; + const { section, value, focusOnMount = false } = parameters; const baseRootContext = useBaseCalendarRootContext(); const registerSection = baseRootContext.registerSection; @@ -11,45 +11,53 @@ export function useCellList(parameters: useCellList.Parameters): useCellList.Ret return registerSection({ type: section, value }); }, [registerSection, value, section]); + const initialFocusOnMount = React.useRef(focusOnMount); const scrollerRef = React.useRef(null); - React.useEffect( - () => { - // TODO: Make sure this behavior remain consistent once auto focus is implemented. - if (/* autoFocus || */ scrollerRef.current === null) { - return; - } - const tabbableButton = scrollerRef.current.querySelector('[tabindex="0"]'); - if (!tabbableButton) { - return; - } - - // Taken from useScroll in x-data-grid, but vertically centered - const offsetHeight = tabbableButton.offsetHeight; - const offsetTop = tabbableButton.offsetTop; - - const clientHeight = scrollerRef.current.clientHeight; - const scrollTop = scrollerRef.current.scrollTop; - - const elementBottom = offsetTop + offsetHeight; - - if (offsetHeight > clientHeight || offsetTop < scrollTop) { - // Button already visible - return; - } - - scrollerRef.current.scrollTop = elementBottom - clientHeight / 2 - offsetHeight / 2; - }, - [ - /* autoFocus */ - ], - ); + React.useEffect(() => { + if (scrollerRef.current === null) { + return; + } + + const tabbableButton = scrollerRef.current.querySelector('[tabindex="0"]'); + if (!tabbableButton) { + return; + } + + if (initialFocusOnMount.current) { + tabbableButton.focus(); + } + + // Taken from useScroll in x-data-grid, but vertically centered + const offsetHeight = tabbableButton.offsetHeight; + const offsetTop = tabbableButton.offsetTop; + + const clientHeight = scrollerRef.current.clientHeight; + const scrollTop = scrollerRef.current.scrollTop; + + const elementBottom = offsetTop + offsetHeight; + + if (offsetHeight > clientHeight || offsetTop < scrollTop) { + // Button already visible + return; + } + + scrollerRef.current.scrollTop = elementBottom - clientHeight / 2 - offsetHeight / 2; + }, []); return { scrollerRef }; } export namespace useCellList { - export interface Parameters { - section: 'month' | 'year'; + export interface PublicParameters { + /** + * If `true`, the first tabbable children inside this component will be focused on mount. + * @default false + */ + focusOnMount?: boolean; + } + + export interface Parameters extends PublicParameters { + section: 'day' | 'month' | 'year'; value: PickerValidDate; } diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useMonthsCells.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useMonthsCells.ts index 4335927dbea35..2fd3c2a40f698 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useMonthsCells.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useMonthsCells.ts @@ -7,7 +7,8 @@ import { useBaseCalendarRootContext } from '../root/BaseCalendarRootContext'; import { BaseCalendarMonthsGridOrListContext } from '../months-grid/BaseCalendarMonthsGridOrListContext'; import { useCellList } from './useCellList'; -export function useMonthsCells(): useMonthsCells.ReturnValue { +export function useMonthsCells(parameters: useMonthsCells.Parameters): useMonthsCells.ReturnValue { + const { getItems, focusOnMount } = parameters; const baseRootContext = useBaseCalendarRootContext(); const utils = useUtils(); @@ -16,27 +17,34 @@ export function useMonthsCells(): useMonthsCells.ReturnValue { [utils, baseRootContext.visibleDate], ); - const { scrollerRef } = useCellList({ section: 'month', value: currentYear }); - const months = React.useMemo(() => getMonthsInYear(utils, currentYear), [utils, currentYear]); + const items = React.useMemo(() => { + if (getItems) { + return getItems({ + year: currentYear, + }); + } + + return getMonthsInYear(utils, currentYear); + }, [utils, getItems, currentYear]); + + const { scrollerRef } = useCellList({ focusOnMount, section: 'month', value: currentYear }); const tabbableMonths = React.useMemo(() => { let tempTabbableDays: PickerValidDate[] = []; - tempTabbableDays = months.filter((day) => + tempTabbableDays = items.filter((day) => baseRootContext.selectedDates.some((selectedDay) => utils.isSameMonth(day, selectedDay)), ); if (tempTabbableDays.length === 0) { - tempTabbableDays = months.filter((day) => - utils.isSameMonth(day, baseRootContext.currentDate), - ); + tempTabbableDays = items.filter((day) => utils.isSameMonth(day, baseRootContext.currentDate)); } if (tempTabbableDays.length === 0) { - tempTabbableDays = [months[0]]; + tempTabbableDays = [items[0]]; } return tempTabbableDays; - }, [baseRootContext.currentDate, baseRootContext.selectedDates, months, utils]); + }, [baseRootContext.currentDate, baseRootContext.selectedDates, items, utils]); const monthsListOrGridContext = React.useMemo( () => ({ @@ -88,12 +96,25 @@ export function useMonthsCells(): useMonthsCells.ReturnValue { } }; - return { months, monthsListOrGridContext, changePage, scrollerRef }; + return { items, monthsListOrGridContext, changePage, scrollerRef }; } export namespace useMonthsCells { + export interface Parameters extends useCellList.PublicParameters { + /** + * Generate the list of items to render the given visible date. + * @param {GetCellsParameters} parameters The current parameters of the list. + * @returns {PickerValidDate[]} The list of items. + */ + getItems?: (parameters: GetCellsParameters) => PickerValidDate[]; + } + + export interface GetCellsParameters { + year: PickerValidDate; + } + export interface ReturnValue extends useCellList.ReturnValue { - months: PickerValidDate[]; + items: PickerValidDate[]; monthsListOrGridContext: BaseCalendarMonthsGridOrListContext; changePage: (direction: 'next' | 'previous') => void; } diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useYearsCells.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useYearsCells.ts index eace69840a9a2..f6da4034305a8 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useYearsCells.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useYearsCells.ts @@ -5,36 +5,50 @@ import { useBaseCalendarRootContext } from '../root/BaseCalendarRootContext'; import { BaseCalendarYearsGridOrListContext } from '../years-grid/BaseCalendarYearsGridOrListContext'; import { useCellList } from './useCellList'; -export function useYearsCells(): useYearsCells.ReturnValue { +export function useYearsCells(parameters: useYearsCells.Parameters): useYearsCells.ReturnValue { + const { getItems, focusOnMount } = parameters; const baseRootContext = useBaseCalendarRootContext(); const utils = useUtils(); - const { scrollerRef } = useCellList({ section: 'year', value: baseRootContext.visibleDate }); - - const years = React.useMemo( - () => - utils.getYearRange([ - baseRootContext.dateValidationProps.minDate, - baseRootContext.dateValidationProps.maxDate, - ]), - [ - utils, + + const items = React.useMemo(() => { + if (getItems) { + return getItems({ + visibleDate: baseRootContext.visibleDate, + minDate: baseRootContext.dateValidationProps.minDate, + maxDate: baseRootContext.dateValidationProps.maxDate, + }); + } + + return utils.getYearRange([ baseRootContext.dateValidationProps.minDate, baseRootContext.dateValidationProps.maxDate, - ], - ); + ]); + }, [ + utils, + getItems, + baseRootContext.visibleDate, + baseRootContext.dateValidationProps.minDate, + baseRootContext.dateValidationProps.maxDate, + ]); + + const { scrollerRef } = useCellList({ + focusOnMount, + section: 'year', + value: baseRootContext.visibleDate, + }); const tabbableYears = React.useMemo(() => { let tempTabbableDays: PickerValidDate[] = []; - tempTabbableDays = years.filter((day) => + tempTabbableDays = items.filter((day) => baseRootContext.selectedDates.some((selectedDay) => utils.isSameYear(day, selectedDay)), ); if (tempTabbableDays.length === 0) { - tempTabbableDays = years.filter((day) => utils.isSameYear(day, baseRootContext.currentDate)); + tempTabbableDays = items.filter((day) => utils.isSameYear(day, baseRootContext.currentDate)); } return tempTabbableDays; - }, [baseRootContext.currentDate, baseRootContext.selectedDates, years, utils]); + }, [baseRootContext.currentDate, baseRootContext.selectedDates, items, utils]); const yearsListOrGridContext = React.useMemo( () => ({ @@ -43,12 +57,27 @@ export function useYearsCells(): useYearsCells.ReturnValue { [tabbableYears], ); - return { years, yearsListOrGridContext, scrollerRef }; + return { items, scrollerRef, yearsListOrGridContext }; } export namespace useYearsCells { + export interface Parameters extends useCellList.PublicParameters { + /** + * Generate the list of items to render the given visible date. + * @param {GetCellsParameters} parameters The current parameters of the list. + * @returns {PickerValidDate[]} The list of items. + */ + getItems?: (parameters: GetCellsParameters) => PickerValidDate[]; + } + + export interface GetCellsParameters { + visibleDate: PickerValidDate; + minDate: PickerValidDate; + maxDate: PickerValidDate; + } + export interface ReturnValue extends useCellList.ReturnValue { - years: PickerValidDate[]; yearsListOrGridContext: BaseCalendarYearsGridOrListContext; + items: PickerValidDate[]; } } diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/years-grid/useBaseCalendarYearsGrid.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/years-grid/useBaseCalendarYearsGrid.ts index 2f155fc8697a5..470c1082328b3 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/years-grid/useBaseCalendarYearsGrid.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/years-grid/useBaseCalendarYearsGrid.ts @@ -7,9 +7,9 @@ import { navigateInGrid } from '../utils/keyboardNavigation'; import { useYearsCells } from '../utils/useYearsCells'; export function useBaseCalendarYearsGrid(parameters: useBaseCalendarYearsGrid.Parameters) { - const { children, cellsPerRow, cellsPerRowCssVar } = parameters; + const { children, cellsPerRow, cellsPerRowCssVar, getItems, focusOnMount } = parameters; const yearsCellRefs = React.useRef<(HTMLElement | null)[]>([]); - const { years, yearsListOrGridContext, scrollerRef } = useYearsCells(); + const { items, scrollerRef, yearsListOrGridContext } = useYearsCells({ getItems, focusOnMount }); const getCellsInCalendar = useEventCallback(() => { const grid: HTMLElement[][] = Array.from( @@ -41,14 +41,14 @@ export function useBaseCalendarYearsGrid(parameters: useBaseCalendarYearsGrid.Pa (externalProps: GenericHTMLProps) => { return mergeReactProps(externalProps, { role: 'radiogroup', - children: children == null ? null : children({ years }), + children: children == null ? null : children({ years: items }), onKeyDown, style: { [cellsPerRowCssVar]: cellsPerRow, }, }); }, - [years, children, onKeyDown, cellsPerRow, cellsPerRowCssVar], + [items, children, onKeyDown, cellsPerRow, cellsPerRowCssVar], ); return React.useMemo( @@ -58,7 +58,7 @@ export function useBaseCalendarYearsGrid(parameters: useBaseCalendarYearsGrid.Pa } export namespace useBaseCalendarYearsGrid { - export interface PublicParameters { + export interface PublicParameters extends useYearsCells.Parameters { /** * Cells rendered per row. * This is used to make sure the keyboard navigation works correctly. diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/years-list/useBaseCalendarYearsList.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/years-list/useBaseCalendarYearsList.ts index 0680e6a3f916c..05d4271a2421b 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/years-list/useBaseCalendarYearsList.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/years-list/useBaseCalendarYearsList.ts @@ -7,9 +7,9 @@ import { navigateInList } from '../utils/keyboardNavigation'; import { useYearsCells } from '../utils/useYearsCells'; export function useBaseCalendarYearsList(parameters: useBaseCalendarYearsList.Parameters) { - const { children, loop = true } = parameters; + const { children, getItems, focusOnMount, loop = true } = parameters; const cellRefs = React.useRef<(HTMLElement | null)[]>([]); - const { years, yearsListOrGridContext, scrollerRef } = useYearsCells(); + const { items, yearsListOrGridContext, scrollerRef } = useYearsCells({ getItems, focusOnMount }); const onKeyDown = useEventCallback((event: React.KeyboardEvent) => { navigateInList({ @@ -24,11 +24,11 @@ export function useBaseCalendarYearsList(parameters: useBaseCalendarYearsList.Pa (externalProps: GenericHTMLProps) => { return mergeReactProps(externalProps, { role: 'radiogroup', - children: children == null ? null : children({ years }), + children: children == null ? null : children({ years: items }), onKeyDown, }); }, - [years, children, onKeyDown], + [items, children, onKeyDown], ); return React.useMemo( @@ -38,7 +38,7 @@ export function useBaseCalendarYearsList(parameters: useBaseCalendarYearsList.Pa } export namespace useBaseCalendarYearsList { - export interface Parameters { + export interface Parameters extends useYearsCells.Parameters { /** * Whether to loop keyboard focus back to the first item * when the end of the list is reached while using the arrow keys. From 759404744413f8c35a45639c14bc024d9f7f1656 Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 13 Jan 2025 10:46:25 +0100 Subject: [PATCH 105/136] Fix --- docs/data/date-pickers/base-calendar/calendar.module.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/data/date-pickers/base-calendar/calendar.module.css b/docs/data/date-pickers/base-calendar/calendar.module.css index 73190e18ca85e..e30d3cf190d57 100644 --- a/docs/data/date-pickers/base-calendar/calendar.module.css +++ b/docs/data/date-pickers/base-calendar/calendar.module.css @@ -255,7 +255,7 @@ width: 36px; text-align: center; font-size: 0.75rem; - color: #64748b; + color: var(--days-grid-header-color); } .DaysGridSeparator { From 02de511a1add32b9d98b2c394642f3ffddae2f1c Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 13 Jan 2025 10:56:02 +0100 Subject: [PATCH 106/136] Fix --- .../utils/base-calendar/utils/useCellList.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useCellList.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useCellList.ts index 8c7e07073b9f7..0434d740080db 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useCellList.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useCellList.ts @@ -2,6 +2,14 @@ import * as React from 'react'; import { PickerValidDate } from '../../../../../models'; import { useBaseCalendarRootContext } from '../root/BaseCalendarRootContext'; +/** + * Internal utility hook to handle a list of cells: + * - Registers the section in the Calendar Root. + * - Focuses the first tabbable child on mount if `props.focusOnMount` is `true`. + * - Scrolls the scroller to center the focused element if it is not visible. + * @param {useCellList.Parameters} parameters The parameters of the hook. + * @returns {useCellList.ReturnValue} The return value of the hook. + */ export function useCellList(parameters: useCellList.Parameters): useCellList.ReturnValue { const { section, value, focusOnMount = false } = parameters; const baseRootContext = useBaseCalendarRootContext(); @@ -57,11 +65,20 @@ export namespace useCellList { } export interface Parameters extends PublicParameters { + /** + * The type of the section. + */ section: 'day' | 'month' | 'year'; + /** + * The value of the section. + */ value: PickerValidDate; } export interface ReturnValue { + /** + * The ref that must be attached to the scroller element. + */ scrollerRef: React.RefObject; } } From fef7e41b2be26e0819003eb5eddf2433aa4f5ee0 Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 13 Jan 2025 11:05:23 +0100 Subject: [PATCH 107/136] Add reversed order recipe --- .../DayCalendarTwoMonthsDemo.tsx.preview | 1 + .../DayCalendarWithValidationDemo.tsx.preview | 8 ++++ .../DayCalendarWithWeekNumberDemo.tsx.preview | 1 + ...RangeCalendarWithTwoMonthsDemo.tsx.preview | 1 + .../MonthCalendarDemo.tsx.preview | 14 +++++++ ...onthCalendarWithListLayoutDemo.tsx.preview | 14 +++++++ .../YearCalendarDemo.tsx.preview | 13 +++++++ .../YearCalendarWithDecadeNavigationDemo.tsx | 2 +- ...YearCalendarWithListLayoutDemo.tsx.preview | 13 +++++++ .../YearCalendarWithReversedOrderDemo.js | 37 +++++++++++++++++++ .../YearCalendarWithReversedOrderDemo.tsx | 37 +++++++++++++++++++ .../base-calendar/base-calendar.md | 22 ++++++++++- .../base-calendar/utils/useMonthsCells.ts | 6 ++- .../base-calendar/utils/useYearsCells.ts | 12 ++++-- 14 files changed, 173 insertions(+), 8 deletions(-) create mode 100644 docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.tsx.preview create mode 100644 docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.tsx.preview create mode 100644 docs/data/date-pickers/base-calendar/DayCalendarWithWeekNumberDemo.tsx.preview create mode 100644 docs/data/date-pickers/base-calendar/DayRangeCalendarWithTwoMonthsDemo.tsx.preview create mode 100644 docs/data/date-pickers/base-calendar/MonthCalendarDemo.tsx.preview create mode 100644 docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.tsx.preview create mode 100644 docs/data/date-pickers/base-calendar/YearCalendarDemo.tsx.preview create mode 100644 docs/data/date-pickers/base-calendar/YearCalendarWithListLayoutDemo.tsx.preview create mode 100644 docs/data/date-pickers/base-calendar/YearCalendarWithReversedOrderDemo.js create mode 100644 docs/data/date-pickers/base-calendar/YearCalendarWithReversedOrderDemo.tsx diff --git a/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.tsx.preview b/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.tsx.preview new file mode 100644 index 0000000000000..72b23c81a5a56 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.tsx.preview @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.tsx.preview b/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.tsx.preview new file mode 100644 index 0000000000000..7ed1d697455e7 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.tsx.preview @@ -0,0 +1,8 @@ + + ALREADY_BOOKED_NIGHTS_SET.has(date.format('YYYY-MM-DD')) + } +/> \ No newline at end of file diff --git a/docs/data/date-pickers/base-calendar/DayCalendarWithWeekNumberDemo.tsx.preview b/docs/data/date-pickers/base-calendar/DayCalendarWithWeekNumberDemo.tsx.preview new file mode 100644 index 0000000000000..72b23c81a5a56 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/DayCalendarWithWeekNumberDemo.tsx.preview @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/data/date-pickers/base-calendar/DayRangeCalendarWithTwoMonthsDemo.tsx.preview b/docs/data/date-pickers/base-calendar/DayRangeCalendarWithTwoMonthsDemo.tsx.preview new file mode 100644 index 0000000000000..16ef550120c15 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/DayRangeCalendarWithTwoMonthsDemo.tsx.preview @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/data/date-pickers/base-calendar/MonthCalendarDemo.tsx.preview b/docs/data/date-pickers/base-calendar/MonthCalendarDemo.tsx.preview new file mode 100644 index 0000000000000..2dcd2a095dd1b --- /dev/null +++ b/docs/data/date-pickers/base-calendar/MonthCalendarDemo.tsx.preview @@ -0,0 +1,14 @@ + +
+ + {({ months }) => + months.map((month) => ( + + )) + } + + \ No newline at end of file diff --git a/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.tsx.preview b/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.tsx.preview new file mode 100644 index 0000000000000..28bf2a9fcbb53 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.tsx.preview @@ -0,0 +1,14 @@ + +
+ + {({ months }) => + months.map((month) => ( + + )) + } + + \ No newline at end of file diff --git a/docs/data/date-pickers/base-calendar/YearCalendarDemo.tsx.preview b/docs/data/date-pickers/base-calendar/YearCalendarDemo.tsx.preview new file mode 100644 index 0000000000000..b1d3480d864c2 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/YearCalendarDemo.tsx.preview @@ -0,0 +1,13 @@ + + + {({ years }) => + years.map((year) => ( + + )) + } + + \ No newline at end of file diff --git a/docs/data/date-pickers/base-calendar/YearCalendarWithDecadeNavigationDemo.tsx b/docs/data/date-pickers/base-calendar/YearCalendarWithDecadeNavigationDemo.tsx index d464a2097ac55..e3ab80b298838 100644 --- a/docs/data/date-pickers/base-calendar/YearCalendarWithDecadeNavigationDemo.tsx +++ b/docs/data/date-pickers/base-calendar/YearCalendarWithDecadeNavigationDemo.tsx @@ -36,7 +36,7 @@ function Header() { ); } -const getYearsInDecade = ({ visibleDate }: { visibleDate: Dayjs }) => { +const getYearsInDecade: Calendar.YearsGrid.Props['getItems'] = ({ visibleDate }) => { const reference = visibleDate.startOf('year'); const decade = Math.floor(reference.year() / 10) * 10; return Array.from({ length: 10 }, (_, index) => diff --git a/docs/data/date-pickers/base-calendar/YearCalendarWithListLayoutDemo.tsx.preview b/docs/data/date-pickers/base-calendar/YearCalendarWithListLayoutDemo.tsx.preview new file mode 100644 index 0000000000000..bce391c7c37b9 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/YearCalendarWithListLayoutDemo.tsx.preview @@ -0,0 +1,13 @@ + + + {({ years }) => + years.map((year) => ( + + )) + } + + \ No newline at end of file diff --git a/docs/data/date-pickers/base-calendar/YearCalendarWithReversedOrderDemo.js b/docs/data/date-pickers/base-calendar/YearCalendarWithReversedOrderDemo.js new file mode 100644 index 0000000000000..b65ba56bbfb95 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/YearCalendarWithReversedOrderDemo.js @@ -0,0 +1,37 @@ +import * as React from 'react'; + +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +// eslint-disable-next-line no-restricted-imports +import { Calendar } from '@mui/x-date-pickers/internals/base/Calendar'; +import styles from './calendar.module.css'; + +const getYears = ({ getDefaultItems }) => { + return getDefaultItems().toReversed(); +}; + +export default function YearCalendarWithReversedOrderDemo() { + const [value, setValue] = React.useState(null); + + return ( + + + + {({ years }) => + years.map((year) => ( + + )) + } + + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/YearCalendarWithReversedOrderDemo.tsx b/docs/data/date-pickers/base-calendar/YearCalendarWithReversedOrderDemo.tsx new file mode 100644 index 0000000000000..f7f14ae15e301 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/YearCalendarWithReversedOrderDemo.tsx @@ -0,0 +1,37 @@ +import * as React from 'react'; +import { Dayjs } from 'dayjs'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +// eslint-disable-next-line no-restricted-imports +import { Calendar } from '@mui/x-date-pickers/internals/base/Calendar'; +import styles from './calendar.module.css'; + +const getYears: Calendar.YearsGrid.Props['getItems'] = ({ getDefaultItems }) => { + return getDefaultItems().toReversed(); +}; + +export default function YearCalendarWithReversedOrderDemo() { + const [value, setValue] = React.useState(null); + + return ( + + + + {({ years }) => + years.map((year) => ( + + )) + } + + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/base-calendar.md b/docs/data/date-pickers/base-calendar/base-calendar.md index 26a4ffd2ad170..d1795eb016236 100644 --- a/docs/data/date-pickers/base-calendar/base-calendar.md +++ b/docs/data/date-pickers/base-calendar/base-calendar.md @@ -121,10 +121,28 @@ TODO {{"demo": "YearCalendarWithListLayoutDemo.js", "defaultCodeOpen": false}} +### Recipe: Reversed order + +```tsx +const getYears = ({ getDefaultItems }) => { + return getDefaultItems().toReversed(); +}; + + + {/** Year cells */} +; +``` + +:::success +Using the `getItems` prop instead of manually providing a list of `` as children allows the `` to always have at least one cell with `tabIndex={0}` and to correctly scroll to the first item with `tabIndex={0}` +::: + +{{"demo": "YearCalendarWithReversedOrderDemo.js", "defaultCodeOpen": false}} + ### Recipe: Grouped by decade ```tsx -const getYearsInDecade = ({ visibleDate }: { visibleDate: Dayjs }) => { +const getYearsInDecade = ({ visibleDate }) => { const reference = visibleDate.startOf('year'); const decade = Math.floor(reference.year() / 10) * 10; return Array.from({ length: 10 }, (_, index) => @@ -138,7 +156,7 @@ const getYearsInDecade = ({ visibleDate }: { visibleDate: Dayjs }) => { ``` :::success -Using the `getItems` prop instead of manually providing a list of `` as children allows the `` to always have at least one cell with `tabIndex={0}`. +Using the `getItems` prop instead of manually providing a list of `` as children allows the `` to always have at least one cell with `tabIndex={0}` and to correctly scroll to the first item with `tabIndex={0}` ::: {{"demo": "YearCalendarWithDecadeNavigationDemo.js", "defaultCodeOpen": false}} diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useMonthsCells.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useMonthsCells.ts index 2fd3c2a40f698..327e3fc37c671 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useMonthsCells.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useMonthsCells.ts @@ -18,13 +18,16 @@ export function useMonthsCells(parameters: useMonthsCells.Parameters): useMonths ); const items = React.useMemo(() => { + const getDefaultItems = () => getMonthsInYear(utils, currentYear); + if (getItems) { return getItems({ year: currentYear, + getDefaultItems, }); } - return getMonthsInYear(utils, currentYear); + return getDefaultItems(); }, [utils, getItems, currentYear]); const { scrollerRef } = useCellList({ focusOnMount, section: 'month', value: currentYear }); @@ -111,6 +114,7 @@ export namespace useMonthsCells { export interface GetCellsParameters { year: PickerValidDate; + getDefaultItems: () => PickerValidDate[]; } export interface ReturnValue extends useCellList.ReturnValue { diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useYearsCells.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useYearsCells.ts index f6da4034305a8..ef741f2921391 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useYearsCells.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useYearsCells.ts @@ -11,18 +11,21 @@ export function useYearsCells(parameters: useYearsCells.Parameters): useYearsCel const utils = useUtils(); const items = React.useMemo(() => { + const getDefaultItems = () => + utils.getYearRange([ + baseRootContext.dateValidationProps.minDate, + baseRootContext.dateValidationProps.maxDate, + ]); if (getItems) { return getItems({ visibleDate: baseRootContext.visibleDate, minDate: baseRootContext.dateValidationProps.minDate, maxDate: baseRootContext.dateValidationProps.maxDate, + getDefaultItems, }); } - return utils.getYearRange([ - baseRootContext.dateValidationProps.minDate, - baseRootContext.dateValidationProps.maxDate, - ]); + return getDefaultItems(); }, [ utils, getItems, @@ -74,6 +77,7 @@ export namespace useYearsCells { visibleDate: PickerValidDate; minDate: PickerValidDate; maxDate: PickerValidDate; + getDefaultItems: () => PickerValidDate[]; } export interface ReturnValue extends useCellList.ReturnValue { From a70b7a462d88ee8f53453960470f8319be2692bb Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 13 Jan 2025 11:57:23 +0100 Subject: [PATCH 108/136] Fix --- .../DayCalendarWithValidationDemo.tsx | 81 ++++++++----------- .../base-calendar/base-calendar.md | 6 ++ 2 files changed, 39 insertions(+), 48 deletions(-) diff --git a/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.tsx b/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.tsx index 9f1fc9b1228b4..998f58b97f6dc 100644 --- a/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.tsx +++ b/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import dayjs, { Dayjs } from 'dayjs'; +import dayjs from 'dayjs'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports @@ -25,10 +25,40 @@ function Header() { ); } -function DayCalendar(props: Omit) { +const ALREADY_BOOKED_NIGHTS = [ + dayjs().add(3, 'day'), + dayjs().add(8, 'day'), + dayjs().add(9, 'day'), + dayjs().add(10, 'day'), + dayjs().add(13, 'day'), + dayjs().add(14, 'day'), + dayjs().add(15, 'day'), + dayjs().add(16, 'day'), + dayjs().add(17, 'day'), + dayjs().add(27, 'day'), + dayjs().add(28, 'day'), + dayjs().add(29, 'day'), + dayjs().add(30, 'day'), + dayjs().add(45, 'day'), + dayjs().add(46, 'day'), + dayjs().add(48, 'day'), + dayjs().add(49, 'day'), +]; + +const ALREADY_BOOKED_NIGHTS_SET = new Set( + ALREADY_BOOKED_NIGHTS.map((date) => date.format('YYYY-MM-DD')), +); + +export default function DayCalendarWithValidationDemo() { return ( - + + ALREADY_BOOKED_NIGHTS_SET.has(date.format('YYYY-MM-DD')) + } + className={styles.Root} + >
@@ -68,48 +98,3 @@ function DayCalendar(props: Omit) { ); } - -const ALREADY_BOOKED_NIGHTS = [ - dayjs().add(3, 'day'), - dayjs().add(8, 'day'), - dayjs().add(9, 'day'), - dayjs().add(10, 'day'), - dayjs().add(13, 'day'), - dayjs().add(14, 'day'), - dayjs().add(15, 'day'), - dayjs().add(16, 'day'), - dayjs().add(17, 'day'), - dayjs().add(27, 'day'), - dayjs().add(28, 'day'), - dayjs().add(29, 'day'), - dayjs().add(30, 'day'), - dayjs().add(45, 'day'), - dayjs().add(46, 'day'), - dayjs().add(48, 'day'), - dayjs().add(49, 'day'), -]; - -const ALREADY_BOOKED_NIGHTS_SET = new Set( - ALREADY_BOOKED_NIGHTS.map((date) => date.format('YYYY-MM-DD')), -); - -export default function DayCalendarWithValidationDemo() { - const [value, setValue] = React.useState(null); - - const handleValueChange = React.useCallback((newValue: Dayjs | null) => { - setValue(newValue); - }, []); - - return ( - - - ALREADY_BOOKED_NIGHTS_SET.has(date.format('YYYY-MM-DD')) - } - /> - - ); -} diff --git a/docs/data/date-pickers/base-calendar/base-calendar.md b/docs/data/date-pickers/base-calendar/base-calendar.md index d1795eb016236..7f5bfbdccc85e 100644 --- a/docs/data/date-pickers/base-calendar/base-calendar.md +++ b/docs/data/date-pickers/base-calendar/base-calendar.md @@ -41,6 +41,12 @@ packageName: '@mui/x-date-pickers' ### With validation +```tsx + + {children} + +``` + {{"demo": "DayCalendarWithValidationDemo.js", "defaultCodeOpen": false}} ### With fixed week number From 561b85e3ea63f19ecbb9f6f5c269429b30e8acf2 Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 13 Jan 2025 14:04:59 +0100 Subject: [PATCH 109/136] Add booking demo --- .../DayCalendarWithValidationDemo.js | 34 +--- .../DayCalendarWithValidationDemo.tsx | 32 +-- .../DayRangeCalendarAirbnbRecipe.js | 186 ++++++++++++++++++ .../DayRangeCalendarAirbnbRecipe.tsx | 186 ++++++++++++++++++ .../DayRangeCalendarAirbnbRecipe.tsx.preview | 3 + .../base-calendar/base-calendar.md | 10 +- .../base-calendar/calendar.module.css | 5 +- docs/package.json | 1 + .../RangeCalendar/root/RangeCalendarRoot.tsx | 42 ++-- .../base/Calendar/root/CalendarRoot.tsx | 37 ++-- .../root/BaseCalendarRootContext.ts | 5 - .../base-calendar/root/useBaseCalendarRoot.ts | 43 ++-- pnpm-lock.yaml | 13 ++ 13 files changed, 479 insertions(+), 118 deletions(-) create mode 100644 docs/data/date-pickers/base-calendar/DayRangeCalendarAirbnbRecipe.js create mode 100644 docs/data/date-pickers/base-calendar/DayRangeCalendarAirbnbRecipe.tsx create mode 100644 docs/data/date-pickers/base-calendar/DayRangeCalendarAirbnbRecipe.tsx.preview diff --git a/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.js b/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.js index 998f58b97f6dc..d50a9cd346143 100644 --- a/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.js +++ b/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.js @@ -1,5 +1,5 @@ import * as React from 'react'; -import dayjs from 'dayjs'; + import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports @@ -25,40 +25,10 @@ function Header() { ); } -const ALREADY_BOOKED_NIGHTS = [ - dayjs().add(3, 'day'), - dayjs().add(8, 'day'), - dayjs().add(9, 'day'), - dayjs().add(10, 'day'), - dayjs().add(13, 'day'), - dayjs().add(14, 'day'), - dayjs().add(15, 'day'), - dayjs().add(16, 'day'), - dayjs().add(17, 'day'), - dayjs().add(27, 'day'), - dayjs().add(28, 'day'), - dayjs().add(29, 'day'), - dayjs().add(30, 'day'), - dayjs().add(45, 'day'), - dayjs().add(46, 'day'), - dayjs().add(48, 'day'), - dayjs().add(49, 'day'), -]; - -const ALREADY_BOOKED_NIGHTS_SET = new Set( - ALREADY_BOOKED_NIGHTS.map((date) => date.format('YYYY-MM-DD')), -); - export default function DayCalendarWithValidationDemo() { return ( - - ALREADY_BOOKED_NIGHTS_SET.has(date.format('YYYY-MM-DD')) - } - className={styles.Root} - > +
diff --git a/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.tsx b/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.tsx index 998f58b97f6dc..f06afa8db50c4 100644 --- a/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.tsx +++ b/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.tsx @@ -25,40 +25,10 @@ function Header() { ); } -const ALREADY_BOOKED_NIGHTS = [ - dayjs().add(3, 'day'), - dayjs().add(8, 'day'), - dayjs().add(9, 'day'), - dayjs().add(10, 'day'), - dayjs().add(13, 'day'), - dayjs().add(14, 'day'), - dayjs().add(15, 'day'), - dayjs().add(16, 'day'), - dayjs().add(17, 'day'), - dayjs().add(27, 'day'), - dayjs().add(28, 'day'), - dayjs().add(29, 'day'), - dayjs().add(30, 'day'), - dayjs().add(45, 'day'), - dayjs().add(46, 'day'), - dayjs().add(48, 'day'), - dayjs().add(49, 'day'), -]; - -const ALREADY_BOOKED_NIGHTS_SET = new Set( - ALREADY_BOOKED_NIGHTS.map((date) => date.format('YYYY-MM-DD')), -); - export default function DayCalendarWithValidationDemo() { return ( - - ALREADY_BOOKED_NIGHTS_SET.has(date.format('YYYY-MM-DD')) - } - className={styles.Root} - > +
diff --git a/docs/data/date-pickers/base-calendar/DayRangeCalendarAirbnbRecipe.js b/docs/data/date-pickers/base-calendar/DayRangeCalendarAirbnbRecipe.js new file mode 100644 index 0000000000000..9b009692c3f7b --- /dev/null +++ b/docs/data/date-pickers/base-calendar/DayRangeCalendarAirbnbRecipe.js @@ -0,0 +1,186 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import dayjs from 'dayjs'; +import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query'; +import { Separator } from '@base-ui-components/react/separator'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; + +// eslint-disable-next-line no-restricted-imports +import { + RangeCalendar, + useRangeCalendarContext, +} from '@mui/x-date-pickers-pro/internals/base/RangeCalendar'; +import styles from './calendar.module.css'; + +/** + * Fake server request to fetch the booked dates for the visible range. + * @param {PickerValidDate} month The month to fetch the booked dates for. + * @returns {Promise} The booked dates for the visible range. + */ +async function fetchBookedDates(month) { + const BOOKED_NIGHTS = [ + dayjs().add(3, 'day'), + dayjs().add(8, 'day'), + dayjs().add(9, 'day'), + dayjs().add(10, 'day'), + dayjs().add(13, 'day'), + dayjs().add(14, 'day'), + dayjs().add(15, 'day'), + dayjs().add(16, 'day'), + dayjs().add(17, 'day'), + dayjs().add(27, 'day'), + dayjs().add(28, 'day'), + dayjs().add(29, 'day'), + dayjs().add(30, 'day'), + dayjs().add(45, 'day'), + dayjs().add(46, 'day'), + dayjs().add(48, 'day'), + dayjs().add(49, 'day'), + dayjs().add(80, 'day'), + dayjs().add(81, 'day'), + dayjs().add(82, 'day'), + dayjs().add(83, 'day'), + dayjs().add(84, 'day'), + dayjs().add(85, 'day'), + dayjs().add(86, 'day'), + dayjs().add(92, 'day'), + dayjs().add(93, 'day'), + dayjs().add(100, 'day'), + ]; + + return new Promise((resolve) => { + setTimeout( + () => { + const startOfVisibleRange = month.startOf('month').startOf('week'); + const endOfVisibleRange = month.add(1, 'month').endOf('month').endOf('week'); + const bookedDates = BOOKED_NIGHTS.filter( + (date) => + date.isAfter(startOfVisibleRange) && date.isBefore(endOfVisibleRange), + ).map((date) => date.format('YYYY-MM-DD')); + + resolve(new Set(bookedDates)); + }, + // Fake latency between 300ms and 600ms + Math.floor(300 + Math.random() * 300), + ); + }); +} + +function Header() { + const { visibleDate } = useRangeCalendarContext(); + + return ( +
+
+ + ◀ + + {visibleDate.format('MMMM YYYY')} + +
+
+ + {visibleDate.add(1, 'month').format('MMMM YYYY')} + + ▶ + +
+
+ ); +} + +function DaysGrid(props) { + const { offset } = props; + return ( + + + {({ days }) => + days.map((day) => ( + + )) + } + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => + days.map((day) => ( + + )) + } + + )) + } + + + ); +} + +function BookingCalendar() { + const [visibleDate, setVisibleDate] = React.useState(() => dayjs()); + const bookedDates = useQuery({ + queryKey: ['bookedDates', visibleDate.format('MM YYYY')], + queryFn: () => fetchBookedDates(visibleDate), + }); + + const shouldDisableDate = React.useCallback( + (date) => { + return bookedDates.data?.has(date.format('YYYY-MM-DD')) ?? false; + }, + [bookedDates.data], + ); + + const maxDate = React.useMemo(() => dayjs().add(1, 'year').endOf('year'), []); + + return ( + +
+
+ + + +
+ + ); +} + +const queryClient = new QueryClient(); + +export default function DayRangeCalendarAirbnbRecipe() { + return ( + + + + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/DayRangeCalendarAirbnbRecipe.tsx b/docs/data/date-pickers/base-calendar/DayRangeCalendarAirbnbRecipe.tsx new file mode 100644 index 0000000000000..7018acd48108a --- /dev/null +++ b/docs/data/date-pickers/base-calendar/DayRangeCalendarAirbnbRecipe.tsx @@ -0,0 +1,186 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import dayjs from 'dayjs'; +import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query'; +import { Separator } from '@base-ui-components/react/separator'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { PickerValidDate } from '@mui/x-date-pickers/models'; +// eslint-disable-next-line no-restricted-imports +import { + RangeCalendar, + useRangeCalendarContext, +} from '@mui/x-date-pickers-pro/internals/base/RangeCalendar'; +import styles from './calendar.module.css'; + +/** + * Fake server request to fetch the booked dates for the visible range. + * @param {PickerValidDate} month The month to fetch the booked dates for. + * @returns {Promise} The booked dates for the visible range. + */ +async function fetchBookedDates(month: PickerValidDate) { + const BOOKED_NIGHTS = [ + dayjs().add(3, 'day'), + dayjs().add(8, 'day'), + dayjs().add(9, 'day'), + dayjs().add(10, 'day'), + dayjs().add(13, 'day'), + dayjs().add(14, 'day'), + dayjs().add(15, 'day'), + dayjs().add(16, 'day'), + dayjs().add(17, 'day'), + dayjs().add(27, 'day'), + dayjs().add(28, 'day'), + dayjs().add(29, 'day'), + dayjs().add(30, 'day'), + dayjs().add(45, 'day'), + dayjs().add(46, 'day'), + dayjs().add(48, 'day'), + dayjs().add(49, 'day'), + dayjs().add(80, 'day'), + dayjs().add(81, 'day'), + dayjs().add(82, 'day'), + dayjs().add(83, 'day'), + dayjs().add(84, 'day'), + dayjs().add(85, 'day'), + dayjs().add(86, 'day'), + dayjs().add(92, 'day'), + dayjs().add(93, 'day'), + dayjs().add(100, 'day'), + ]; + + return new Promise>((resolve) => { + setTimeout( + () => { + const startOfVisibleRange = month.startOf('month').startOf('week'); + const endOfVisibleRange = month.add(1, 'month').endOf('month').endOf('week'); + const bookedDates = BOOKED_NIGHTS.filter( + (date) => + date.isAfter(startOfVisibleRange) && date.isBefore(endOfVisibleRange), + ).map((date) => date.format('YYYY-MM-DD')); + + resolve(new Set(bookedDates)); + }, + // Fake latency between 300ms and 600ms + Math.floor(300 + Math.random() * 300), + ); + }); +} + +function Header() { + const { visibleDate } = useRangeCalendarContext(); + + return ( +
+
+ + ◀ + + {visibleDate.format('MMMM YYYY')} + +
+
+ + {visibleDate.add(1, 'month').format('MMMM YYYY')} + + ▶ + +
+
+ ); +} + +function DaysGrid(props: { offset: 0 | 1 }) { + const { offset } = props; + return ( + + + {({ days }) => + days.map((day) => ( + + )) + } + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => + days.map((day) => ( + + )) + } + + )) + } + + + ); +} + +function BookingCalendar() { + const [visibleDate, setVisibleDate] = React.useState(() => dayjs()); + const bookedDates = useQuery({ + queryKey: ['bookedDates', visibleDate.format('MM YYYY')], + queryFn: () => fetchBookedDates(visibleDate), + }); + + const shouldDisableDate = React.useCallback( + (date: PickerValidDate) => { + return bookedDates.data?.has(date.format('YYYY-MM-DD')) ?? false; + }, + [bookedDates.data], + ); + + const maxDate = React.useMemo(() => dayjs().add(1, 'year').endOf('year'), []); + + return ( + +
+
+ + + +
+ + ); +} + +const queryClient = new QueryClient(); + +export default function DayRangeCalendarAirbnbRecipe() { + return ( + + + + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/DayRangeCalendarAirbnbRecipe.tsx.preview b/docs/data/date-pickers/base-calendar/DayRangeCalendarAirbnbRecipe.tsx.preview new file mode 100644 index 0000000000000..e9338fcd06bc3 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/DayRangeCalendarAirbnbRecipe.tsx.preview @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/docs/data/date-pickers/base-calendar/base-calendar.md b/docs/data/date-pickers/base-calendar/base-calendar.md index 85fe8110321e7..82ebc2d65b3f8 100644 --- a/docs/data/date-pickers/base-calendar/base-calendar.md +++ b/docs/data/date-pickers/base-calendar/base-calendar.md @@ -42,9 +42,7 @@ packageName: '@mui/x-date-pickers' ### With validation ```tsx - - {children} - +{children} ``` {{"demo": "DayCalendarWithValidationDemo.js", "defaultCodeOpen": false}} @@ -207,6 +205,12 @@ It's due to the DOM structure and the user can easily reproduce the old behavior {{"demo": "DayRangeCalendarWithTwoMonthsDemo.js", "defaultCodeOpen": false}} +### Recipe: Booking UI + +The following demo shows a more advanced use case with lazy-loaded validation data: + +{{"demo": "DayRangeCalendarAirbnbRecipe.js", "defaultCodeOpen": false}} + ## Date Range Calendar {{"demo": "DateRangeCalendarDemo.js", "defaultCodeOpen": false}} diff --git a/docs/data/date-pickers/base-calendar/calendar.module.css b/docs/data/date-pickers/base-calendar/calendar.module.css index b18deb140262f..de77af6d2f9b7 100644 --- a/docs/data/date-pickers/base-calendar/calendar.module.css +++ b/docs/data/date-pickers/base-calendar/calendar.module.css @@ -217,10 +217,13 @@ } &:not([data-outside-month]):disabled { - text-decoration: line-through; color: var(--cell-disabled-color); } + &:not([data-outside-month])[data-invalid] { + text-decoration: line-through; + } + &[data-outside-month] { color: var(--cell-outside-month-color); pointer-events: none; diff --git a/docs/package.json b/docs/package.json index 1ab5fe7ae38f1..da73a774993c8 100644 --- a/docs/package.json +++ b/docs/package.json @@ -48,6 +48,7 @@ "@mui/x-tree-view": "workspace:*", "@react-spring/web": "^9.7.5", "@tanstack/query-core": "^5.64.0", + "@tanstack/react-query": "^5.64.0", "ast-types": "^0.14.2", "autoprefixer": "^10.4.20", "babel-plugin-module-resolver": "^5.0.2", diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRoot.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRoot.tsx index 14f9e77aa12b7..9a72d18cbde37 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRoot.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRoot.tsx @@ -20,7 +20,24 @@ const RangeCalendarRoot = React.forwardRef(function RangeCalendarRoot( // Rendering props className, render, + // Form props + readOnly, + disabled, + // Focus and navigation props + monthPageSize, + yearPageSize, + // Value props + onValueChange, + defaultValue, + value, + timezone, + referenceDate, + // Visible date props + onVisibleDateChange, + visibleDate, + defaultVisibleDate, // Validation props + onError, minDate, maxDate, disablePast, @@ -34,35 +51,24 @@ const RangeCalendarRoot = React.forwardRef(function RangeCalendarRoot( // Other range-specific parameters disableDragEditing, disableHoverPreview, - // Form props - readOnly, - disabled, - // Focus and navigation props - autoFocus, - monthPageSize, - yearPageSize, - // Value props - onError, - defaultValue, - onValueChange, - value, - timezone, - referenceDate, + // Props forwarded to the DOM element ...otherProps } = props; const { getRootProps, context, baseContext } = useRangeCalendarRoot({ readOnly, disabled, - autoFocus, - onError, + monthPageSize, + yearPageSize, defaultValue, onValueChange, value, timezone, referenceDate, - monthPageSize, - yearPageSize, + onVisibleDateChange, + visibleDate, + defaultVisibleDate, + onError, shouldDisableDate, disablePast, disableFuture, diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRoot.tsx b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRoot.tsx index 34278a6901641..fbb64321121d9 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRoot.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRoot.tsx @@ -16,43 +16,48 @@ const CalendarRoot = React.forwardRef(function CalendarRoot( // Rendering props className, render, - // Validation props - minDate, - maxDate, - disablePast, - disableFuture, - shouldDisableDate, - shouldDisableMonth, - shouldDisableYear, // Form props readOnly, disabled, // Focus and navigation props - autoFocus, monthPageSize, yearPageSize, // Value props - onError, - defaultValue, onValueChange, + defaultValue, value, timezone, referenceDate, + // Visible date props + onVisibleDateChange, + visibleDate, + defaultVisibleDate, + // Validation props + onError, + minDate, + maxDate, + disablePast, + disableFuture, + shouldDisableDate, + shouldDisableMonth, + shouldDisableYear, // Props forwarded to the DOM element ...otherProps } = props; const { getRootProps, context, baseContext } = useCalendarRoot({ readOnly, disabled, - autoFocus, - onError, - defaultValue, + monthPageSize, + yearPageSize, onValueChange, + defaultValue, value, timezone, referenceDate, - monthPageSize, - yearPageSize, + onVisibleDateChange, + visibleDate, + defaultVisibleDate, + onError, shouldDisableDate, shouldDisableMonth, shouldDisableYear, diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/BaseCalendarRootContext.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/BaseCalendarRootContext.ts index 2e49a275a2b1e..de181799ff256 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/BaseCalendarRootContext.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/BaseCalendarRootContext.ts @@ -17,11 +17,6 @@ export interface BaseCalendarRootContext { * Whether the calendar is read-only. */ readOnly: boolean; - // TODO: Implement the behavior. - /** - * Whether the calendar should auto-focus on mount. - */ - autoFocus: boolean; /** * The date currently visible. * It is used to determine: diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarRoot.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarRoot.ts index 80f3136f39eb5..deb540060514c 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarRoot.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarRoot.ts @@ -1,5 +1,6 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; +import useControlled from '@mui/utils/useControlled'; import { OnErrorProps, PickerChangeImportance, @@ -29,17 +30,20 @@ export function useBaseCalendarRoot< readOnly = false, disabled = false, // Focus and navigation props - autoFocus = false, monthPageSize = 1, yearPageSize = 1, // Value props - onError, defaultValue, onValueChange, value: valueProp, timezone: timezoneProp, referenceDate: referenceDateProp, + // Visible date props + onVisibleDateChange, + visibleDate: visibleDateProp, + defaultVisibleDate, // Validation props + onError, dateValidationProps, valueValidationProps, // Manager props @@ -56,7 +60,7 @@ export function useBaseCalendarRoot< const adapter = useLocalizationContext(); const { value, handleValueChange, timezone } = useControlledValueWithTimezone({ - name: 'CalendarRoot', + name: '(Range)CalendarRoot', timezone: timezoneProp, value: valueProp, defaultValue, @@ -119,13 +123,20 @@ export function useBaseCalendarRoot< return true; }; - const [visibleDate, setVisibleDate] = React.useState(referenceDate); + const [visibleDate, setVisibleDate] = useControlled({ + name: '(Range)CalendarRoot', + state: 'visibleDate', + controlled: visibleDateProp, + default: defaultVisibleDate ?? referenceDate, + }); + const handleVisibleDateChange = useEventCallback( (newVisibleDate: PickerValidDate, skipIfAlreadyVisible: boolean) => { if (skipIfAlreadyVisible && isDateCellVisible(newVisibleDate)) { return; } + onVisibleDateChange?.(newVisibleDate); setVisibleDate(newVisibleDate); }, ); @@ -189,7 +200,6 @@ export function useBaseCalendarRoot< timezone, disabled, readOnly, - autoFocus, isDateInvalid, visibleDate, currentDate, @@ -207,7 +217,6 @@ export function useBaseCalendarRoot< timezone, disabled, readOnly, - autoFocus, isDateInvalid, visibleDate, currentDate, @@ -240,12 +249,12 @@ export namespace useBaseCalendarRoot { OnErrorProps { /** * The controlled value that should be selected. - * To render an uncontrolled Date Calendar, use the `defaultValue` prop instead. + * To render an uncontrolled Calendar, use the `defaultValue` prop instead. */ value?: TValue; /** * The uncontrolled value that should be initially selected. - * To render a controlled accordion, use the `value` prop instead. + * To render a controlled Calendar, use the `value` prop instead. */ defaultValue?: TValue; /** @@ -258,17 +267,27 @@ export namespace useBaseCalendarRoot { value: TValue, context: useBaseCalendarRoot.ValueChangeHandlerContext, ) => void; + /** + * The date used to decide which month should be displayed in the Days Grid and which year should be displayed in the Months List and Months Grid. + * To render an uncontrolled Calendar, use the `defaultVisibleDate` prop instead. + */ + visibleDate?: PickerValidDate; + /** + * The date used to decide which month should be initially displayed in the Days Grid and which year should be initially displayed in the Months List and Months Grid. + * To render a controlled Calendar, use the `visibleDate` prop instead. + */ + defaultVisibleDate?: PickerValidDate; /** * The date used to generate the new value when both `value` and `defaultValue` are empty. * @default The closest valid date using the validation props, except callbacks such as `shouldDisableDate`. */ referenceDate?: PickerValidDate; /** - * If `true`, one of the cells will be automatically focused when the component is mounted. - * If a value or a default value is provided, the focused cell will be the one corresponding to the selected date. - * @default false + * Event handler called when the visible date changes. + * Provides the new visible date as an argument. + * @param {PickerValidDate} visibleDate The new visible date. */ - autoFocus?: boolean; + onVisibleDateChange?: (visibleDate: PickerValidDate) => void; /** * The amount of months to navigate by when pressing or when using keyboard navigation in the day grid. * This is mostly useful when displaying multiple day grids. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8340ce6dcf3c6..4513b4e26898a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -488,6 +488,9 @@ importers: '@tanstack/query-core': specifier: ^5.64.0 version: 5.64.0 + '@tanstack/react-query': + specifier: ^5.64.0 + version: 5.64.0(react@19.0.0) ast-types: specifier: ^0.14.2 version: 0.14.2 @@ -4029,6 +4032,11 @@ packages: '@tanstack/query-core@5.64.0': resolution: {integrity: sha512-/MPJt/AaaMzdWJZTafgMyYhEX/lGjQrNz8+NDQSk8fNoU5PHqh05FhQaBrEQafW2PeBHsRbefEf//qKMiSAbQQ==} + '@tanstack/react-query@5.64.0': + resolution: {integrity: sha512-tBMzlROROUcTDMpDt1NC3n9ndKnJHPB3RCpa6Bf9f31TFvqhLz879x8jldtKU+6IwMSw1Pn4K1AKA+2SYyA6TA==} + peerDependencies: + react: ^18 || ^19 + '@testing-library/dom@10.4.0': resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} engines: {node: '>=18'} @@ -13201,6 +13209,11 @@ snapshots: '@tanstack/query-core@5.64.0': {} + '@tanstack/react-query@5.64.0(react@19.0.0)': + dependencies: + '@tanstack/query-core': 5.64.0 + react: 19.0.0 + '@testing-library/dom@10.4.0': dependencies: '@babel/code-frame': 7.26.2 From d9aa45fb7885d9fae923f7a6c146c08df4732fa3 Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 13 Jan 2025 15:29:39 +0100 Subject: [PATCH 110/136] Improve CSS --- .../DayCalendarTwoMonthsDemo.tsx | 110 ----------------- ...emo.js => DayCalendarWithTwoMonthsDemo.js} | 2 +- .../DayCalendarWithTwoMonthsDemo.tsx | 114 ++++++++++++++++++ ... DayCalendarWithTwoMonthsDemo.tsx.preview} | 0 .../base-calendar/base-calendar.md | 2 +- .../base-calendar/calendar.module.css | 85 +++++++++---- .../base/Calendar/root/CalendarRoot.tsx | 5 +- .../base/Calendar/root/CalendarRootContext.ts | 25 ---- .../base/Calendar/root/useCalendarRoot.ts | 12 +- .../useBaseCalendarDaysWeekRow.ts | 3 + 10 files changed, 185 insertions(+), 173 deletions(-) delete mode 100644 docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.tsx rename docs/data/date-pickers/base-calendar/{DayCalendarTwoMonthsDemo.js => DayCalendarWithTwoMonthsDemo.js} (98%) create mode 100644 docs/data/date-pickers/base-calendar/DayCalendarWithTwoMonthsDemo.tsx rename docs/data/date-pickers/base-calendar/{DayCalendarTwoMonthsDemo.tsx.preview => DayCalendarWithTwoMonthsDemo.tsx.preview} (100%) delete mode 100644 packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts diff --git a/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.tsx b/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.tsx deleted file mode 100644 index 3adb85cb1f179..0000000000000 --- a/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import * as React from 'react'; -import clsx from 'clsx'; -import { Dayjs } from 'dayjs'; -import { Separator } from '@base-ui-components/react/separator'; -import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; -import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; -// eslint-disable-next-line no-restricted-imports -import { - Calendar, - useCalendarContext, -} from '@mui/x-date-pickers/internals/base/Calendar'; -import styles from './calendar.module.css'; - -function Header(props: { offset: 0 | 1 }) { - const { offset } = props; - const { visibleDate } = useCalendarContext(); - - const date = visibleDate.add(offset, 'month'); - - return ( -
- - ◀ - - {date.format('MMMM YYYY')} - - ▶ - -
- ); -} - -function DaysGrid(props: { offset: 0 | 1 }) { - const { offset } = props; - return ( -
-
- - - {({ days }) => - days.map((day) => ( - - )) - } - - - {({ weeks }) => - weeks.map((week) => ( - - {({ days }) => - days.map((day) => ( - - )) - } - - )) - } - - -
- ); -} - -function DayCalendar(props: Omit) { - return ( - - - - - - - - ); -} - -export default function DayCalendarTwoMonthsDemo() { - const [value, setValue] = React.useState(null); - - const handleValueChange = React.useCallback((newValue: Dayjs | null) => { - setValue(newValue); - }, []); - - return ( - - - - ); -} diff --git a/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.js b/docs/data/date-pickers/base-calendar/DayCalendarWithTwoMonthsDemo.js similarity index 98% rename from docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.js rename to docs/data/date-pickers/base-calendar/DayCalendarWithTwoMonthsDemo.js index 624334c186992..bcba14cffc7f9 100644 --- a/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.js +++ b/docs/data/date-pickers/base-calendar/DayCalendarWithTwoMonthsDemo.js @@ -95,7 +95,7 @@ function DayCalendar(props) { ); } -export default function DayCalendarTwoMonthsDemo() { +export default function DayCalendarWithTwoMonthsDemo() { const [value, setValue] = React.useState(null); const handleValueChange = React.useCallback((newValue) => { diff --git a/docs/data/date-pickers/base-calendar/DayCalendarWithTwoMonthsDemo.tsx b/docs/data/date-pickers/base-calendar/DayCalendarWithTwoMonthsDemo.tsx new file mode 100644 index 0000000000000..43e855441c0b8 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/DayCalendarWithTwoMonthsDemo.tsx @@ -0,0 +1,114 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import { Dayjs } from 'dayjs'; +import { Separator } from '@base-ui-components/react/separator'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +// eslint-disable-next-line no-restricted-imports +import { + Calendar, + useCalendarContext, +} from '@mui/x-date-pickers/internals/base/Calendar'; +import styles from './calendar.module.css'; + +function Header() { + const { visibleDate } = useCalendarContext(); + + return ( +
+
+ + ◀ + + {visibleDate.format('MMMM YYYY')} + +
+
+ + {visibleDate.add(1, 'month').format('MMMM YYYY')} + + ▶ + +
+
+ ); +} + +function DaysGrid(props: { offset: 0 | 1 }) { + const { offset } = props; + return ( + + + {({ days }) => + days.map((day) => ( + + )) + } + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => + days.map((day) => ( + + )) + } + + )) + } + + + ); +} + +function DayCalendar(props: Omit) { + return ( + + +
+
+ + + +
+ + + ); +} + +export default function DayCalendarWithTwoMonthsDemo() { + const [value, setValue] = React.useState(null); + + const handleValueChange = React.useCallback((newValue: Dayjs | null) => { + setValue(newValue); + }, []); + + return ( + + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.tsx.preview b/docs/data/date-pickers/base-calendar/DayCalendarWithTwoMonthsDemo.tsx.preview similarity index 100% rename from docs/data/date-pickers/base-calendar/DayCalendarTwoMonthsDemo.tsx.preview rename to docs/data/date-pickers/base-calendar/DayCalendarWithTwoMonthsDemo.tsx.preview diff --git a/docs/data/date-pickers/base-calendar/base-calendar.md b/docs/data/date-pickers/base-calendar/base-calendar.md index 82ebc2d65b3f8..c483adbab2433 100644 --- a/docs/data/date-pickers/base-calendar/base-calendar.md +++ b/docs/data/date-pickers/base-calendar/base-calendar.md @@ -37,7 +37,7 @@ packageName: '@mui/x-date-pickers' It will make sure that keyboard navigation and pressing `` switching month two by two. -{{"demo": "DayCalendarTwoMonthsDemo.js", "defaultCodeOpen": false}} +{{"demo": "DayCalendarWithTwoMonthsDemo.js", "defaultCodeOpen": false}} ### With validation diff --git a/docs/data/date-pickers/base-calendar/calendar.module.css b/docs/data/date-pickers/base-calendar/calendar.module.css index de77af6d2f9b7..d0f086ce4e9f4 100644 --- a/docs/data/date-pickers/base-calendar/calendar.module.css +++ b/docs/data/date-pickers/base-calendar/calendar.module.css @@ -125,8 +125,9 @@ padding: 12px; display: grid; column-gap: 4px; - row-gap: 12px; + row-gap: 8px; overflow-y: auto; + z-index: 1; } .MonthsGrid { @@ -139,12 +140,8 @@ .MonthsCell, .YearsCell { - height: 24px; - min-height: 24px; - background-color: transparent; - border: none; - cursor: pointer; - border-radius: 4px; + height: 28px; + min-height: 28px; &:disabled { pointer-events: none; @@ -157,6 +154,7 @@ display: flex; flex-direction: column; gap: 4px; + z-index: 1; } .DaysGridBody { @@ -184,35 +182,60 @@ .DaysCell, .MonthsCell, .YearsCell { + position: relative; user-select: none; + border: none; + cursor: pointer; + background-color: transparent; + outline: none; + box-sizing: border-box; + border-radius: 4px; - &:not([data-selected]):hover { + &:not([data-selected]):hover::before { background-color: var(--button-hover-bg-color); } - &[data-selected]:not([data-outside-month]):not([data-inside-selection]) { + &[data-selected]:not([data-outside-month]):not([data-inside-selection])::before { background-color: var(--cell-selected-bg-color); } - &:focus-visible { - z-index: 1; - outline: 2px solid var(--button-focus-border-color); - } - &:disabled { pointer-events: none; } + + &::before { + content: ''; + position: absolute; + top: 2px; + bottom: 2px; + left: 2px; + right: 2px; + border-radius: 4px; + border: none; + z-index: -1; + background-color: transparent; + } + + &::after { + content: ''; + border-radius: 4px; + position: absolute; + top: 2px; + bottom: 2px; + left: 2px; + right: 2px; + } + + &:focus-visible::after { + outline: 2px solid var(--button-focus-border-color); + } } .DaysCell { height: 36px; width: 36px; - border-radius: 4px; - border: none; - background-color: transparent; - cursor: pointer; - &[data-current]:not([data-selected]):not(:focus-visible) { + &[data-current]:not([data-selected]):not(:focus-visible)::after { outline: 1px solid var(--cell-current-border-color); } @@ -229,18 +252,36 @@ pointer-events: none; } - &[data-inside-selection] { + &[data-inside-selection]:not([data-outside-month])::before { + left: 0; + right: 0; background-color: var(--cell-inside-selection-bg-color); } + &[data-selection-start]::before { + right: 0; + } + + &[data-selection-end]::before { + left: 0; + } + &.RangeDaysCell { - &[data-selected]:not([data-selection-start]), + &[data-selected]:not([data-selection-start])::before { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + + &[data-selected]:not([data-selection-end])::before { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + &[data-previewed]:not([data-preview-start]) { border-top-left-radius: 0; border-bottom-left-radius: 0; } - &[data-selected]:not([data-selection-end]), &[data-previewed]:not([data-preview-end]) { border-top-right-radius: 0; border-bottom-right-radius: 0; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRoot.tsx b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRoot.tsx index fbb64321121d9..a1d7823169c4e 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRoot.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRoot.tsx @@ -5,7 +5,6 @@ import { useComponentRenderer } from '../../base-utils/useComponentRenderer'; import { BaseUIComponentProps } from '../../base-utils/types'; import { BaseCalendarRootContext } from '../../utils/base-calendar/root/BaseCalendarRootContext'; import { useBaseCalendarRoot } from '../../utils/base-calendar/root/useBaseCalendarRoot'; -import { CalendarRootContext } from './CalendarRootContext'; import { useCalendarRoot } from './useCalendarRoot'; const CalendarRoot = React.forwardRef(function CalendarRoot( @@ -44,7 +43,7 @@ const CalendarRoot = React.forwardRef(function CalendarRoot( // Props forwarded to the DOM element ...otherProps } = props; - const { getRootProps, context, baseContext } = useCalendarRoot({ + const { getRootProps, baseContext } = useCalendarRoot({ readOnly, disabled, monthPageSize, @@ -80,7 +79,7 @@ const CalendarRoot = React.forwardRef(function CalendarRoot( return ( - {renderElement()} + {renderElement()} ); }); diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts deleted file mode 100644 index f0cf016bbdd78..0000000000000 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootContext.ts +++ /dev/null @@ -1,25 +0,0 @@ -import * as React from 'react'; -import { PickerValue } from '../../../models'; - -export interface CalendarRootContext { - /** - * The current value of the Calendar. - */ - value: PickerValue; -} - -export const CalendarRootContext = React.createContext(undefined); - -if (process.env.NODE_ENV !== 'production') { - CalendarRootContext.displayName = 'CalendarRootContext'; -} - -export function useCalendarRootContext() { - const context = React.useContext(CalendarRootContext); - if (context === undefined) { - throw new Error( - 'Base UI X: CalendarRootContext is missing. Calendar parts must be placed within .', - ); - } - return context; -} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts index 3ece3523f79ea..7005010bdd884 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts @@ -76,21 +76,11 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { } } - const context: CalendarRootContext = React.useMemo( - () => ({ - value, - }), - [value], - ); - const getRootProps = React.useCallback((externalProps: GenericHTMLProps) => { return mergeReactProps(externalProps, {}); }, []); - return React.useMemo( - () => ({ getRootProps, context, baseContext }), - [getRootProps, context, baseContext], - ); + return React.useMemo(() => ({ getRootProps, baseContext }), [getRootProps, baseContext]); } export namespace useCalendarRoot { diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRow.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRow.ts index 0af854055dd62..8d3a16b09afbb 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRow.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-week-row/useBaseCalendarDaysWeekRow.ts @@ -43,6 +43,9 @@ export namespace useBaseCalendarDaysWeekRow { } export interface ChildrenParameters { + /** + * The days of the week. + */ days: PickerValidDate[]; } From 5d59149f294c11a02a69fe4a351dd7955e1bd902 Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 13 Jan 2025 15:29:52 +0100 Subject: [PATCH 111/136] Fix demo --- .../DayCalendarWithTwoMonthsDemo.js | 118 +++++++++--------- 1 file changed, 61 insertions(+), 57 deletions(-) diff --git a/docs/data/date-pickers/base-calendar/DayCalendarWithTwoMonthsDemo.js b/docs/data/date-pickers/base-calendar/DayCalendarWithTwoMonthsDemo.js index bcba14cffc7f9..5fc51b94b43df 100644 --- a/docs/data/date-pickers/base-calendar/DayCalendarWithTwoMonthsDemo.js +++ b/docs/data/date-pickers/base-calendar/DayCalendarWithTwoMonthsDemo.js @@ -11,27 +11,31 @@ import { } from '@mui/x-date-pickers/internals/base/Calendar'; import styles from './calendar.module.css'; -function Header(props) { - const { offset } = props; +function Header() { const { visibleDate } = useCalendarContext(); - const date = visibleDate.add(offset, 'month'); - return (
- - ◀ - - {date.format('MMMM YYYY')} - - ▶ - +
+ + ◀ + + {visibleDate.format('MMMM YYYY')} + +
+
+ + {visibleDate.add(1, 'month').format('MMMM YYYY')} + + ▶ + +
); } @@ -39,43 +43,40 @@ function Header(props) { function DaysGrid(props) { const { offset } = props; return ( -
-
- - - {({ days }) => - days.map((day) => ( - - )) - } - - - {({ weeks }) => - weeks.map((week) => ( - - {({ days }) => - days.map((day) => ( - - )) - } - - )) - } - - -
+ + + {({ days }) => + days.map((day) => ( + + )) + } + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => + days.map((day) => ( + + )) + } + + )) + } + + ); } @@ -87,9 +88,12 @@ function DayCalendar(props) { monthPageSize={2} className={clsx(styles.Root, styles.RootWithTwoPanels)} > - - - +
+
+ + + +
); From ab16486e44a853ffee1e0092ae9bf5fec887f4db Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 13 Jan 2025 15:56:32 +0100 Subject: [PATCH 112/136] Work --- .../DayRangeCalendarAirbnbRecipe.js | 23 ++++++---- .../DayRangeCalendarAirbnbRecipe.tsx | 29 +++++++----- .../base-calendar/calendar.module.css | 44 ++++++++++--------- .../days-cell/useRangeCalendarDaysCell.tsx | 4 -- 4 files changed, 56 insertions(+), 44 deletions(-) diff --git a/docs/data/date-pickers/base-calendar/DayRangeCalendarAirbnbRecipe.js b/docs/data/date-pickers/base-calendar/DayRangeCalendarAirbnbRecipe.js index 9b009692c3f7b..e3c87a2ac26b9 100644 --- a/docs/data/date-pickers/base-calendar/DayRangeCalendarAirbnbRecipe.js +++ b/docs/data/date-pickers/base-calendar/DayRangeCalendarAirbnbRecipe.js @@ -5,7 +5,6 @@ import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-quer import { Separator } from '@base-ui-components/react/separator'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; - // eslint-disable-next-line no-restricted-imports import { RangeCalendar, @@ -15,8 +14,8 @@ import styles from './calendar.module.css'; /** * Fake server request to fetch the booked dates for the visible range. - * @param {PickerValidDate} month The month to fetch the booked dates for. - * @returns {Promise} The booked dates for the visible range. + * @param {Dayjs} month The month to fetch the booked dates for. + * @returns {Promise} The booked dates for the visible range. */ async function fetchBookedDates(month) { const BOOKED_NIGHTS = [ @@ -67,6 +66,13 @@ async function fetchBookedDates(month) { }); } +function useBookedDates(visibleDate) { + return useQuery({ + queryKey: ['bookedDates', visibleDate.format('MM YYYY')], + queryFn: () => fetchBookedDates(visibleDate), + }); +} + function Header() { const { visibleDate } = useRangeCalendarContext(); @@ -98,6 +104,9 @@ function Header() { function DaysGrid(props) { const { offset } = props; + const { visibleDate } = useRangeCalendarContext(); + const bookedDates = useBookedDates(visibleDate); + return ( @@ -125,6 +134,8 @@ function DaysGrid(props) { value={day} key={day.toString()} className={clsx(styles.DaysCell, styles.RangeDaysCell)} + // TODO: Passing `disabled: undefined` should keep the built-in behavior + {...(bookedDates.isLoading ? { disabled: true } : undefined)} /> )) } @@ -138,10 +149,7 @@ function DaysGrid(props) { function BookingCalendar() { const [visibleDate, setVisibleDate] = React.useState(() => dayjs()); - const bookedDates = useQuery({ - queryKey: ['bookedDates', visibleDate.format('MM YYYY')], - queryFn: () => fetchBookedDates(visibleDate), - }); + const bookedDates = useBookedDates(visibleDate); const shouldDisableDate = React.useCallback( (date) => { @@ -157,7 +165,6 @@ function BookingCalendar() { monthPageSize={2} visibleDate={visibleDate} onVisibleDateChange={setVisibleDate} - disabled={bookedDates.isLoading} disablePast maxDate={maxDate} shouldDisableDate={shouldDisableDate} diff --git a/docs/data/date-pickers/base-calendar/DayRangeCalendarAirbnbRecipe.tsx b/docs/data/date-pickers/base-calendar/DayRangeCalendarAirbnbRecipe.tsx index 7018acd48108a..7dc2f3fcee8dd 100644 --- a/docs/data/date-pickers/base-calendar/DayRangeCalendarAirbnbRecipe.tsx +++ b/docs/data/date-pickers/base-calendar/DayRangeCalendarAirbnbRecipe.tsx @@ -1,11 +1,10 @@ import * as React from 'react'; import clsx from 'clsx'; -import dayjs from 'dayjs'; +import dayjs, { Dayjs } from 'dayjs'; import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query'; import { Separator } from '@base-ui-components/react/separator'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; -import { PickerValidDate } from '@mui/x-date-pickers/models'; // eslint-disable-next-line no-restricted-imports import { RangeCalendar, @@ -15,10 +14,10 @@ import styles from './calendar.module.css'; /** * Fake server request to fetch the booked dates for the visible range. - * @param {PickerValidDate} month The month to fetch the booked dates for. - * @returns {Promise} The booked dates for the visible range. + * @param {Dayjs} month The month to fetch the booked dates for. + * @returns {Promise} The booked dates for the visible range. */ -async function fetchBookedDates(month: PickerValidDate) { +async function fetchBookedDates(month: Dayjs) { const BOOKED_NIGHTS = [ dayjs().add(3, 'day'), dayjs().add(8, 'day'), @@ -67,6 +66,13 @@ async function fetchBookedDates(month: PickerValidDate) { }); } +function useBookedDates(visibleDate: Dayjs) { + return useQuery({ + queryKey: ['bookedDates', visibleDate.format('MM YYYY')], + queryFn: () => fetchBookedDates(visibleDate), + }); +} + function Header() { const { visibleDate } = useRangeCalendarContext(); @@ -98,6 +104,9 @@ function Header() { function DaysGrid(props: { offset: 0 | 1 }) { const { offset } = props; + const { visibleDate } = useRangeCalendarContext(); + const bookedDates = useBookedDates(visibleDate); + return ( @@ -125,6 +134,8 @@ function DaysGrid(props: { offset: 0 | 1 }) { value={day} key={day.toString()} className={clsx(styles.DaysCell, styles.RangeDaysCell)} + // TODO: Passing `disabled: undefined` should keep the built-in behavior + {...(bookedDates.isLoading ? { disabled: true } : undefined)} /> )) } @@ -138,13 +149,10 @@ function DaysGrid(props: { offset: 0 | 1 }) { function BookingCalendar() { const [visibleDate, setVisibleDate] = React.useState(() => dayjs()); - const bookedDates = useQuery({ - queryKey: ['bookedDates', visibleDate.format('MM YYYY')], - queryFn: () => fetchBookedDates(visibleDate), - }); + const bookedDates = useBookedDates(visibleDate); const shouldDisableDate = React.useCallback( - (date: PickerValidDate) => { + (date: Dayjs) => { return bookedDates.data?.has(date.format('YYYY-MM-DD')) ?? false; }, [bookedDates.data], @@ -157,7 +165,6 @@ function BookingCalendar() { monthPageSize={2} visibleDate={visibleDate} onVisibleDateChange={setVisibleDate} - disabled={bookedDates.isLoading} disablePast maxDate={maxDate} shouldDisableDate={shouldDisableDate} diff --git a/docs/data/date-pickers/base-calendar/calendar.module.css b/docs/data/date-pickers/base-calendar/calendar.module.css index d0f086ce4e9f4..0e175164d749a 100644 --- a/docs/data/date-pickers/base-calendar/calendar.module.css +++ b/docs/data/date-pickers/base-calendar/calendar.module.css @@ -277,27 +277,29 @@ border-bottom-right-radius: 0; } - &[data-previewed]:not([data-preview-start]) { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - } - - &[data-previewed]:not([data-preview-end]) { - border-top-right-radius: 0; - border-bottom-right-radius: 0; - } - - &[data-previewed]:not([data-selected]) { - border-top: 1px dashed var(--cell-preview-border-color); - border-bottom: 1px dashed var(--cell-preview-border-color); - } - - &[data-preview-start]:not([data-selected]) { - border-left: 1px dashed var(--cell-preview-border-color); - } - - &[data-preview-end]:not([data-selected]) { - border-right: 1px dashed var(--cell-preview-border-color); + &:not([data-outside-month])[data-previewed] { + &:not([data-preview-start]) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + + &:not([data-preview-end]) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + + &:not([data-selected]) { + border-top: 1px dashed var(--cell-preview-border-color); + border-bottom: 1px dashed var(--cell-preview-border-color); + } + + &[data-preview-start]:not([data-selected]) { + border-left: 1px dashed var(--cell-preview-border-color); + } + + &[data-preview-end]:not([data-selected]) { + border-right: 1px dashed var(--cell-preview-border-color); + } } } } diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx index 77d00eaf899dc..4afb2bc155d35 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx @@ -230,10 +230,6 @@ function resolveElementFromTouch( ) { // don't parse multi-touch result if (event.changedTouches?.length === 1 && event.touches.length <= 1) { - // const element = document.elementFromPoint( - // event.changedTouches[0].clientX, - // event.changedTouches[0].clientY, - // ); const element = document.elementFromPoint( event.changedTouches[0].clientX, event.changedTouches[0].clientY, From 3e7e6e7522331b51386259ebb91c7aa6e353533a Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 13 Jan 2025 16:02:37 +0100 Subject: [PATCH 113/136] Add data-empty to the roots --- .../base-calendar/calendar.module.css | 48 +++++++++---------- .../RangeCalendar/root/RangeCalendarRoot.tsx | 16 +++++-- .../root/RangeCalendarRootDataAttributes.ts | 7 ++- .../root/useRangeCalendarRoot.tsx | 6 ++- .../base/Calendar/root/CalendarRoot.tsx | 4 +- .../root/CalendarRootDataAttributes.ts | 7 ++- .../base/Calendar/root/useCalendarRoot.ts | 7 ++- 7 files changed, 61 insertions(+), 34 deletions(-) diff --git a/docs/data/date-pickers/base-calendar/calendar.module.css b/docs/data/date-pickers/base-calendar/calendar.module.css index 0e175164d749a..12a5c2f0fb833 100644 --- a/docs/data/date-pickers/base-calendar/calendar.module.css +++ b/docs/data/date-pickers/base-calendar/calendar.module.css @@ -276,31 +276,31 @@ border-top-right-radius: 0; border-bottom-right-radius: 0; } + } +} - &:not([data-outside-month])[data-previewed] { - &:not([data-preview-start]) { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - } - - &:not([data-preview-end]) { - border-top-right-radius: 0; - border-bottom-right-radius: 0; - } - - &:not([data-selected]) { - border-top: 1px dashed var(--cell-preview-border-color); - border-bottom: 1px dashed var(--cell-preview-border-color); - } - - &[data-preview-start]:not([data-selected]) { - border-left: 1px dashed var(--cell-preview-border-color); - } - - &[data-preview-end]:not([data-selected]) { - border-right: 1px dashed var(--cell-preview-border-color); - } - } +.Root:not([data-empty]) .RangeDaysCell:not([data-outside-month])[data-previewed] { + &:not([data-preview-start]) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + + &:not([data-preview-end]) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + + &:not([data-selected]) { + border-top: 1px dashed var(--cell-preview-border-color); + border-bottom: 1px dashed var(--cell-preview-border-color); + } + + &[data-preview-start]:not([data-selected]) { + border-left: 1px dashed var(--cell-preview-border-color); + } + + &[data-preview-end]:not([data-selected]) { + border-right: 1px dashed var(--cell-preview-border-color); } } diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRoot.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRoot.tsx index 9a72d18cbde37..90467de9dc05e 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRoot.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRoot.tsx @@ -55,7 +55,7 @@ const RangeCalendarRoot = React.forwardRef(function RangeCalendarRoot( // Props forwarded to the DOM element ...otherProps } = props; - const { getRootProps, context, baseContext } = useRangeCalendarRoot({ + const { getRootProps, context, baseContext, isEmpty } = useRangeCalendarRoot({ readOnly, disabled, monthPageSize, @@ -76,7 +76,12 @@ const RangeCalendarRoot = React.forwardRef(function RangeCalendarRoot( maxDate, }); - const state: RangeCalendarRoot.State = React.useMemo(() => ({}), []); + const state: RangeCalendarRoot.State = React.useMemo( + () => ({ + empty: isEmpty, + }), + [isEmpty], + ); const { renderElement } = useComponentRenderer({ propGetter: getRootProps, @@ -97,7 +102,12 @@ const RangeCalendarRoot = React.forwardRef(function RangeCalendarRoot( }); export namespace RangeCalendarRoot { - export interface State {} + export interface State { + /** + * Whether no start date and no end date are selected. + */ + empty: boolean; + } export interface Props extends useRangeCalendarRoot.Parameters, diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootDataAttributes.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootDataAttributes.ts index 74c45a5a14500..420c5ac148d3b 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootDataAttributes.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootDataAttributes.ts @@ -1 +1,6 @@ -export enum RangeCalendarRootDataAttributes {} +export enum RangeCalendarRootDataAttributes { + /** + * Present when no start date and no end date are selected. + */ + empty = 'data-empty', +} diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx index 70c294b89a4b1..d3a14de1b302f 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx @@ -180,9 +180,11 @@ export function useRangeCalendarRoot(parameters: useRangeCalendarRoot.Parameters return mergeReactProps(externalProps, {}); }, []); + const isEmpty = React.useMemo(() => value[0] == null && value[1] == null, [value]); + return React.useMemo( - () => ({ getRootProps, context, baseContext }), - [getRootProps, context, baseContext], + () => ({ getRootProps, context, baseContext, isEmpty }), + [getRootProps, context, baseContext, isEmpty], ); } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRoot.tsx b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRoot.tsx index a1d7823169c4e..6128b1913ff6e 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRoot.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRoot.tsx @@ -43,7 +43,7 @@ const CalendarRoot = React.forwardRef(function CalendarRoot( // Props forwarded to the DOM element ...otherProps } = props; - const { getRootProps, baseContext } = useCalendarRoot({ + const { getRootProps, baseContext, isEmpty } = useCalendarRoot({ readOnly, disabled, monthPageSize, @@ -66,7 +66,7 @@ const CalendarRoot = React.forwardRef(function CalendarRoot( maxDate, }); - const state: CalendarRoot.State = React.useMemo(() => ({}), []); + const state: CalendarRoot.State = React.useMemo(() => ({ empty: isEmpty }), [isEmpty]); const { renderElement } = useComponentRenderer({ propGetter: getRootProps, diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootDataAttributes.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootDataAttributes.ts index 83bfc64fae9c2..b8b9de6fa494c 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootDataAttributes.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRootDataAttributes.ts @@ -1 +1,6 @@ -export enum CalendarRootDataAttributes {} +export enum CalendarRootDataAttributes { + /** + * Present when no date is selected. + */ + empty = 'data-empty', +} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts index 7005010bdd884..c81534a59e55d 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts @@ -80,7 +80,12 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { return mergeReactProps(externalProps, {}); }, []); - return React.useMemo(() => ({ getRootProps, baseContext }), [getRootProps, baseContext]); + const isEmpty = value == null; + + return React.useMemo( + () => ({ getRootProps, baseContext, isEmpty }), + [getRootProps, baseContext, isEmpty], + ); } export namespace useCalendarRoot { From c423ce08fd2504c6fbd3b41bcb2eedcd8b5e5402 Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 13 Jan 2025 16:05:57 +0100 Subject: [PATCH 114/136] Fix --- .../internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx index d3a14de1b302f..df7561ef24cf8 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx @@ -180,7 +180,7 @@ export function useRangeCalendarRoot(parameters: useRangeCalendarRoot.Parameters return mergeReactProps(externalProps, {}); }, []); - const isEmpty = React.useMemo(() => value[0] == null && value[1] == null, [value]); + const isEmpty = value[0] == null && value[1] == null; return React.useMemo( () => ({ getRootProps, context, baseContext, isEmpty }), From 8e3591a8d1ee2017520213e959c8aceb8dc0b3f7 Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 13 Jan 2025 16:10:34 +0100 Subject: [PATCH 115/136] Work --- docs/data/date-pickers/base-calendar/base-calendar.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/data/date-pickers/base-calendar/base-calendar.md b/docs/data/date-pickers/base-calendar/base-calendar.md index c483adbab2433..79221f8e96843 100644 --- a/docs/data/date-pickers/base-calendar/base-calendar.md +++ b/docs/data/date-pickers/base-calendar/base-calendar.md @@ -41,6 +41,8 @@ packageName: '@mui/x-date-pickers' ### With validation +For now, the validation behaviors are exactly the same as on ``: + ```tsx {children} ``` From 42fa613fda28c6ca0834aff22d6445e17d04526b Mon Sep 17 00:00:00 2001 From: flavien Date: Mon, 13 Jan 2025 16:14:46 +0100 Subject: [PATCH 116/136] Fix --- .../src/internals/base/Calendar/root/useCalendarRoot.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts index c81534a59e55d..c5fb35123d14b 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts @@ -4,7 +4,6 @@ import { useDateManager } from '../../../../managers'; import { ExportedValidateDateProps, ValidateDateProps } from '../../../../validation/validateDate'; import { useUtils } from '../../../hooks/useUtils'; import { PickerValue } from '../../../models'; -import { CalendarRootContext } from './CalendarRootContext'; import { mergeReactProps } from '../../base-utils/mergeReactProps'; import { GenericHTMLProps } from '../../base-utils/types'; import { From ef921e92a341639c9a920c8fc5ac79a71a2bf833 Mon Sep 17 00:00:00 2001 From: flavien Date: Tue, 14 Jan 2025 09:03:44 +0100 Subject: [PATCH 117/136] Start working on month range --- .../days-cell/useRangeCalendarDaysCell.tsx | 223 +---------------- .../useRangeCalendarDaysCellWrapper.ts | 54 +--- .../months-cell/RangeCalendarMonthsCell.tsx | 13 +- .../useRangeCalendarMonthsCell.tsx | 36 +++ .../useRangeCalendarMonthsCellWrapper.ts | 36 +++ .../root/RangeCalendarRootContext.ts | 4 +- .../root/useRangeCalendarRoot.tsx | 39 +-- .../base/RangeCalendar/utils/date-range.ts | 65 +++++ .../base/RangeCalendar/utils/useRangeCell.ts | 234 ++++++++++++++++++ .../utils/useRangeCellWrapper.ts | 74 ++++++ .../src/internals/utils/date-utils.ts | 33 ++- 11 files changed, 509 insertions(+), 302 deletions(-) create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/useRangeCalendarMonthsCell.tsx create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/useRangeCalendarMonthsCellWrapper.ts create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/date-range.ts create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/useRangeCell.ts create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/useRangeCellWrapper.ts diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx index 4afb2bc155d35..102775a4fe06b 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx @@ -1,246 +1,35 @@ import * as React from 'react'; -import useEventCallback from '@mui/utils/useEventCallback'; // eslint-disable-next-line no-restricted-imports import { useBaseCalendarDaysCell } from '@mui/x-date-pickers/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCell'; // eslint-disable-next-line no-restricted-imports import { GenericHTMLProps } from '@mui/x-date-pickers/internals/base/base-utils/types'; // eslint-disable-next-line no-restricted-imports import { mergeReactProps } from '@mui/x-date-pickers/internals/base/base-utils/mergeReactProps'; -import type { RangeCalendarRootContext } from '../root/RangeCalendarRootContext'; +import { useRangeCell } from '../utils/useRangeCell'; export function useRangeCalendarDaysCell(parameters: useRangeCalendarDaysCell.Parameters) { const { ctx, value } = parameters; - - const startDragging = () => { - ctx.startDragging(ctx.isSelectionStart ? 'start' : 'end'); - }; - - /** - * Drag events - */ - const onDragStart = useEventCallback((event: React.DragEvent) => { - event.stopPropagation(); - if (ctx.emptyDragImgRef.current) { - event.dataTransfer.setDragImage(ctx.emptyDragImgRef.current, 0, 0); - } - ctx.setDragTarget(value); - event.dataTransfer.effectAllowed = 'move'; - startDragging(); - const buttonDataset = (event.target as HTMLButtonElement).dataset; - if (buttonDataset.timestamp) { - event.dataTransfer.setData('draggingDate', buttonDataset.timestamp); - } - }); - - const onDragEnter = useEventCallback((event: React.DragEvent) => { - if (!ctx.isDraggingRef.current) { - return; - } - - event.preventDefault(); - event.stopPropagation(); - event.dataTransfer.dropEffect = 'move'; - ctx.setDragTarget(parameters.value); - }); - - const onDragLeave = useEventCallback((event: React.DragEvent) => { - if (!ctx.isDraggingRef.current) { - return; - } - - event.preventDefault(); - event.stopPropagation(); - }); - - const onDragOver = useEventCallback((event: React.DragEvent) => { - if (!ctx.isDraggingRef.current) { - return; - } - - event.preventDefault(); - event.stopPropagation(); - event.dataTransfer.dropEffect = 'move'; - }); - - const onDragEnd = useEventCallback((event: React.DragEvent) => { - if (!ctx.isDraggingRef.current) { - return; - } - - event.preventDefault(); - event.stopPropagation(); - ctx.stopDragging(); - }); - - const onDrop = useEventCallback((event: React.DragEvent) => { - if (!ctx.isDraggingRef.current) { - return; - } - - event.preventDefault(); - event.stopPropagation(); - ctx.stopDragging(); - // make sure the focused element is the element where drop ended - event.currentTarget.focus(); - ctx.selectDateFromDrag(value); - }); - - /** - * Touch events - */ - const onTouchStart = useEventCallback(() => { - ctx.setDragTarget(value); - }); - - const onTouchMove = useEventCallback((event: React.TouchEvent) => { - const target = resolveElementFromTouch(event); - if (!target) { - return; - } - - ctx.setDragTarget(target); - - // this prevents initiating drag when user starts touchmove outside and then moves over a draggable element - if (target !== event.changedTouches[0].target || !ctx.isDraggable) { - return; - } - - // on mobile we should only initialize dragging state after move is detected - startDragging(); - }); - - const onTouchEnd = useEventCallback((event: React.TouchEvent) => { - if (!ctx.isDraggingRef.current) { - return; - } - - ctx.stopDragging(); - const target = resolveElementFromTouch(event, true); - if (!target) { - return; - } - - // make sure the focused element is the element where touch ended - target.focus(); - ctx.selectDateFromDrag(target); - }); - - /** - * Mouse events - */ - const onMouseEnter = useEventCallback(() => { - if (!ctx.isSelected) { - ctx.setHoveredDate(value); - } else { - ctx.setHoveredDate(null); - } - }); + const rangeCellProps = useRangeCell({ ctx, value, section: 'day' }); const { getDaysCellProps: getBaseDaysCellProps, isCurrent } = useBaseCalendarDaysCell(parameters); const getDaysCellProps = React.useCallback( (externalProps: GenericHTMLProps) => { - return mergeReactProps( - externalProps, - { - ...(ctx.isDraggable ? { draggable: true, onDragStart, onDrop, onTouchStart } : {}), - onDragEnter, - onDragLeave, - onDragOver, - onDragEnd, - onTouchMove, - onTouchEnd, - onMouseEnter, - }, - getBaseDaysCellProps(externalProps), - ); + return mergeReactProps(externalProps, rangeCellProps, getBaseDaysCellProps(externalProps)); }, - [ - getBaseDaysCellProps, - ctx.isDraggable, - onDragStart, - onDragEnter, - onDragLeave, - onDragOver, - onDragEnd, - onDrop, - onTouchStart, - onTouchMove, - onTouchEnd, - onMouseEnter, - ], + [rangeCellProps, getBaseDaysCellProps], ); return React.useMemo(() => ({ getDaysCellProps, isCurrent }), [getDaysCellProps, isCurrent]); } export namespace useRangeCalendarDaysCell { - export interface Parameters extends Omit { + export interface Parameters extends useBaseCalendarDaysCell.Parameters { /** * The memoized context forwarded by the wrapper component so that this component does not need to subscribe to any context. */ ctx: Context; } - export interface Context - extends useBaseCalendarDaysCell.Context, - Pick< - RangeCalendarRootContext, - | 'isDraggingRef' - | 'selectDateFromDrag' - | 'startDragging' - | 'stopDragging' - | 'setDragTarget' - | 'setHoveredDate' - | 'emptyDragImgRef' - > { - isDraggable: boolean; - isSelectionStart: boolean; - isSelectionEnd: boolean; - isPreviewed: boolean; - isPreviewStart: boolean; - isPreviewEnd: boolean; - } -} - -function resolveButtonElement(element: Element | null): HTMLButtonElement | null { - if (!element) { - return null; - } - - if (element instanceof HTMLButtonElement && !element.disabled) { - return element; - } - - if (element.children.length) { - const allButtons = element.querySelectorAll('button:not(:disabled)'); - if (allButtons.length > 1) { - return null; - } - - return allButtons[0] ?? null; - } - - return null; -} - -function resolveElementFromTouch( - event: React.TouchEvent, - ignoreTouchTarget?: boolean, -) { - // don't parse multi-touch result - if (event.changedTouches?.length === 1 && event.touches.length <= 1) { - const element = document.elementFromPoint( - event.changedTouches[0].clientX, - event.changedTouches[0].clientY, - ); - // `elementFromPoint` could have resolved preview div or wrapping div - // might need to recursively find the nested button - const buttonElement = resolveButtonElement(element); - if (ignoreTouchTarget && buttonElement === event.changedTouches[0].target) { - return null; - } - return buttonElement; - } - return null; + export interface Context extends useBaseCalendarDaysCell.Context, useRangeCell.Context {} } diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts index 996f661f42d34..f38bb2fc28862 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCellWrapper.ts @@ -1,70 +1,24 @@ import * as React from 'react'; import useForkRef from '@mui/utils/useForkRef'; -import { useUtils } from '@mui/x-date-pickers/internals'; // eslint-disable-next-line no-restricted-imports import { useBaseCalendarDaysCellWrapper } from '@mui/x-date-pickers/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCellWrapper'; import type { useRangeCalendarDaysCell } from './useRangeCalendarDaysCell'; -import { getDatePositionInRange } from '../../../utils/date-utils'; -import { useRangeCalendarRootContext } from '../root/RangeCalendarRootContext'; +import { useRangeCellWrapper } from '../utils/useRangeCellWrapper'; export function useRangeCalendarDaysCellWrapper( parameters: useRangeCalendarDaysCellWrapper.Parameters, ): useRangeCalendarDaysCellWrapper.ReturnValue { const { value } = parameters; const { ref: baseRef, ctx: baseCtx } = useBaseCalendarDaysCellWrapper(parameters); - const utils = useUtils(); - const rootContext = useRangeCalendarRootContext(); - const cellRef = React.useRef(null); + const { cellRef, ctx: rangeCellCtx } = useRangeCellWrapper({ value, section: 'day' }); const ref = useForkRef(baseRef, cellRef); - const registerCell = rootContext.registerCell; - React.useEffect(() => { - return registerCell(cellRef.current!, value); - }, [registerCell, value]); - - const positionInSelectedRange = React.useMemo( - () => getDatePositionInRange(utils, value, rootContext.selectedRange), - [utils, value, rootContext.selectedRange], - ); - - const positionInPreviewRange = React.useMemo( - () => getDatePositionInRange(utils, value, rootContext.previewRange), - [utils, value, rootContext.previewRange], - ); - const ctx = React.useMemo( () => ({ ...baseCtx, - isDraggable: - (!rootContext.disableDragEditing && positionInSelectedRange.isSelectionStart) || - positionInSelectedRange.isSelectionEnd, - isSelected: positionInSelectedRange.isSelected, - isSelectionStart: positionInSelectedRange.isSelectionStart, - isSelectionEnd: positionInSelectedRange.isSelectionEnd, - isPreviewed: positionInPreviewRange.isSelected, - isPreviewStart: positionInPreviewRange.isSelectionStart, - isPreviewEnd: positionInPreviewRange.isSelectionEnd, - isDraggingRef: rootContext.isDraggingRef, - selectDateFromDrag: rootContext.selectDateFromDrag, - startDragging: rootContext.startDragging, - stopDragging: rootContext.stopDragging, - setDragTarget: rootContext.setDragTarget, - setHoveredDate: rootContext.setHoveredDate, - emptyDragImgRef: rootContext.emptyDragImgRef, + ...rangeCellCtx, }), - [ - baseCtx, - positionInSelectedRange, - positionInPreviewRange, - rootContext.disableDragEditing, - rootContext.isDraggingRef, - rootContext.selectDateFromDrag, - rootContext.startDragging, - rootContext.stopDragging, - rootContext.setDragTarget, - rootContext.setHoveredDate, - rootContext.emptyDragImgRef, - ], + [baseCtx, rangeCellCtx], ); return { ref, ctx }; diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/RangeCalendarMonthsCell.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/RangeCalendarMonthsCell.tsx index 233bd82a98587..0a0dec1e01872 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/RangeCalendarMonthsCell.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/RangeCalendarMonthsCell.tsx @@ -4,17 +4,16 @@ import * as React from 'react'; import { BaseUIComponentProps } from '@mui/x-date-pickers/internals/base/base-utils/types'; // eslint-disable-next-line no-restricted-imports import { useComponentRenderer } from '@mui/x-date-pickers/internals/base/base-utils/useComponentRenderer'; +import { useRangeCalendarMonthsCell } from './useRangeCalendarMonthsCell'; +import { useRangeCalendarMonthsCellWrapper } from './useRangeCalendarMonthsCellWrapper'; // eslint-disable-next-line no-restricted-imports -import { useBaseCalendarMonthsCell } from '@mui/x-date-pickers/internals/base/utils/base-calendar/months-cell/useBaseCalendarMonthsCell'; -// eslint-disable-next-line no-restricted-imports -import { useBaseCalendarMonthsCellWrapper } from '@mui/x-date-pickers/internals/base/utils/base-calendar/months-cell/useBaseCalendarMonthsCellWrapper'; const InnerRangeCalendarMonthsCell = React.forwardRef(function InnerRangeCalendarMonthsCell( props: InnerRangeCalendarMonthsCellProps, forwardedRef: React.ForwardedRef, ) { const { className, render, value, format, ctx, ...otherProps } = props; - const { getMonthsCellProps, isCurrent } = useBaseCalendarMonthsCell({ value, format, ctx }); + const { getMonthsCellProps, isCurrent } = useRangeCalendarMonthsCell({ value, format, ctx }); const state: RangeCalendarMonthsCell.State = React.useMemo( () => ({ @@ -44,7 +43,7 @@ const RangeCalendarMonthsCell = React.forwardRef(function RangeCalendarMonthsCel props: RangeCalendarMonthsCell.Props, forwardedRef: React.ForwardedRef, ) { - const { ref, ctx } = useBaseCalendarMonthsCellWrapper({ value: props.value, forwardedRef }); + const { ref, ctx } = useRangeCalendarMonthsCellWrapper({ value: props.value, forwardedRef }); return ; }); @@ -70,12 +69,12 @@ export namespace RangeCalendarMonthsCell { } export interface Props - extends Omit, + extends Omit, Omit, 'value'> {} } interface InnerRangeCalendarMonthsCellProps - extends useBaseCalendarMonthsCell.Parameters, + extends useRangeCalendarMonthsCell.Parameters, Omit, 'value'> {} export { RangeCalendarMonthsCell }; diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/useRangeCalendarMonthsCell.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/useRangeCalendarMonthsCell.tsx new file mode 100644 index 0000000000000..c183ddf23ba6f --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/useRangeCalendarMonthsCell.tsx @@ -0,0 +1,36 @@ +import * as React from 'react'; +// eslint-disable-next-line no-restricted-imports +import { useBaseCalendarMonthsCell } from '@mui/x-date-pickers/internals/base/utils/base-calendar/months-cell/useBaseCalendarMonthsCell'; +// eslint-disable-next-line no-restricted-imports +import { GenericHTMLProps } from '@mui/x-date-pickers/internals/base/base-utils/types'; +// eslint-disable-next-line no-restricted-imports +import { mergeReactProps } from '@mui/x-date-pickers/internals/base/base-utils/mergeReactProps'; +import { useRangeCell } from '../utils/useRangeCell'; + +export function useRangeCalendarMonthsCell(parameters: useRangeCalendarMonthsCell.Parameters) { + const { ctx, value } = parameters; + const rangeCellProps = useRangeCell({ ctx, value, section: 'month' }); + + const { getMonthsCellProps: getBaseMonthsCellProps, isCurrent } = + useBaseCalendarMonthsCell(parameters); + + const getMonthsCellProps = React.useCallback( + (externalProps: GenericHTMLProps) => { + return mergeReactProps(externalProps, rangeCellProps, getBaseMonthsCellProps(externalProps)); + }, + [rangeCellProps, getBaseMonthsCellProps], + ); + + return React.useMemo(() => ({ getMonthsCellProps, isCurrent }), [getMonthsCellProps, isCurrent]); +} + +export namespace useRangeCalendarMonthsCell { + export interface Parameters extends useBaseCalendarMonthsCell.Parameters { + /** + * The memoized context forwarded by the wrapper component so that this component does not need to subscribe to any context. + */ + ctx: Context; + } + + export interface Context extends useBaseCalendarMonthsCell.Context, useRangeCell.Context {} +} diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/useRangeCalendarMonthsCellWrapper.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/useRangeCalendarMonthsCellWrapper.ts new file mode 100644 index 0000000000000..bb130efb05a2d --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/useRangeCalendarMonthsCellWrapper.ts @@ -0,0 +1,36 @@ +import * as React from 'react'; +import useForkRef from '@mui/utils/useForkRef'; +// eslint-disable-next-line no-restricted-imports +import { useBaseCalendarMonthsCellWrapper } from '@mui/x-date-pickers/internals/base/utils/base-calendar/months-cell/useBaseCalendarMonthsCellWrapper'; +import type { useRangeCalendarMonthsCell } from './useRangeCalendarMonthsCell'; +import { useRangeCellWrapper } from '../utils/useRangeCellWrapper'; + +export function useRangeCalendarMonthsCellWrapper( + parameters: useRangeCalendarMonthsCellWrapper.Parameters, +): useRangeCalendarMonthsCellWrapper.ReturnValue { + const { value } = parameters; + const { ref: baseRef, ctx: baseCtx } = useBaseCalendarMonthsCellWrapper(parameters); + const { cellRef, ctx: rangeCellCtx } = useRangeCellWrapper({ value, section: 'day' }); + const ref = useForkRef(baseRef, cellRef); + + const ctx = React.useMemo( + () => ({ + ...baseCtx, + ...rangeCellCtx, + }), + [baseCtx, rangeCellCtx], + ); + + return { ref, ctx }; +} + +export namespace useRangeCalendarMonthsCellWrapper { + export interface Parameters extends useBaseCalendarMonthsCellWrapper.Parameters {} + + export interface ReturnValue extends Omit { + /** + * The memoized context to forward to the memoized component so that it does not need to subscribe to any context. + */ + ctx: useRangeCalendarMonthsCell.Context; + } +} diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts index 4a7b845dfb1a1..e1bc6e89ae7e2 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts @@ -8,10 +8,10 @@ export interface RangeCalendarRootContext { */ value: PickerRangeValue; /** - * A ref containing `true` if the user is currently dragging. + * A ref containing the section being dragged. * This is used to check if the user is dragging in event handlers without causing re-renders. */ - isDraggingRef: React.RefObject; + draggedSectionRef: React.RefObject<'year' | 'month' | 'day' | null>; disableDragEditing: boolean; selectDateFromDrag: (valueOrElement: PickerValidDate | HTMLElement) => void; startDragging: (position: RangePosition) => void; diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx index df7561ef24cf8..6ba6202989380 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx @@ -230,21 +230,21 @@ function useBuildRangeCalendarRootContext(parameters: UseRangeCalendarDragEditin } = parameters; const utils = useUtils(); - // Range going for the start of the start day to the end of the end day. + // TODO: Add rounded range for month and year. + // Range going for the start of the first selected cell to the end of the last selected cell. // This makes sure that `isWithinRange` works with any time in the start and end day. - const valueDayRange = React.useMemo( - () => [ + const valueRoundedRange = React.useMemo(() => { + return [ !utils.isValid(value[0]) ? value[0] : utils.startOfDay(value[0]), !utils.isValid(value[1]) ? value[1] : utils.endOfDay(value[1]), - ], - [value, utils], - ); + ]; + }, [value, utils]); const [state, setState] = React.useState<{ - isDragging: boolean; + draggedSection: 'day' | 'month' | 'year' | null; targetDate: PickerValidDate | null; hoveredDate: PickerValidDate | null; - }>({ isDragging: false, targetDate: null, hoveredDate: null }); + }>({ draggedSection: null, targetDate: null, hoveredDate: null }); const cellToDateMapRef = React.useRef(new Map()); const registerCell = useEventCallback( @@ -257,13 +257,18 @@ function useBuildRangeCalendarRootContext(parameters: UseRangeCalendarDragEditin ); const selectedRange = React.useMemo(() => { - if (!valueDayRange[0] || !valueDayRange[1] || !state.targetDate || !state.isDragging) { - return valueDayRange; + if ( + !valueRoundedRange[0] || + !valueRoundedRange[1] || + !state.targetDate || + state.draggedSection == null + ) { + return valueRoundedRange; } const newRange = calculateRangeChange({ utils, - range: valueDayRange, + range: valueRoundedRange, newDate: state.targetDate, rangePosition, allowRangeFlip: true, @@ -271,15 +276,15 @@ function useBuildRangeCalendarRootContext(parameters: UseRangeCalendarDragEditin return newRange[0] !== null && newRange[1] !== null ? [utils.startOfDay(newRange[0]), utils.endOfDay(newRange[1])] : newRange; - }, [rangePosition, state.targetDate, state.isDragging, utils, valueDayRange]); + }, [rangePosition, state.targetDate, state.draggedSection, utils, valueRoundedRange]); const disableDragEditing = disableDragEditingProp || baseContext.disabled || baseContext.readOnly; const disableHoverPreview = disableHoverPreviewProp || baseContext.disabled || baseContext.readOnly; - const isDraggingRef = React.useRef(state.isDragging); + const draggedSectionRef = React.useRef(state.draggedSection); useEnhancedEffect(() => { - isDraggingRef.current = state.isDragging; + draggedSectionRef.current = state.draggedSection; }); const emptyDragImgRef = React.useRef(null); @@ -357,18 +362,18 @@ function useBuildRangeCalendarRootContext(parameters: UseRangeCalendarDragEditin return calculateRangePreview({ utils, - range: valueDayRange, + range: valueRoundedRange, newDate: state.hoveredDate, rangePosition, }); - }, [utils, rangePosition, state.hoveredDate, valueDayRange, disableHoverPreview]); + }, [utils, rangePosition, state.hoveredDate, valueRoundedRange, disableHoverPreview]); const context: RangeCalendarRootContext = { value, selectedRange, previewRange, disableDragEditing, - isDraggingRef, + draggedSectionRef, emptyDragImgRef, selectDateFromDrag, startDragging, diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/date-range.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/date-range.ts new file mode 100644 index 0000000000000..6f13071d8eb6d --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/date-range.ts @@ -0,0 +1,65 @@ +import { PickerRangeValue } from '@mui/x-date-pickers/internals'; +import { MuiPickersAdapter, PickerValidDate } from '@mui/x-date-pickers/models'; + +export function getDatePositionInRange({ + utils, + date, + range, + section, +}: { + utils: MuiPickersAdapter; + date: PickerValidDate; + range: PickerRangeValue; + section: 'year' | 'month' | 'day'; +}) { + const [start, end] = range; + if (start == null && end == null) { + return { isSelected: false, isSelectionStart: false, isSelectionEnd: false }; + } + + const sectionMethods = getCalendarSectionMethods(utils, section); + if (start == null) { + const isSelected = sectionMethods.isSame(date, end!); + return { + isSelected, + isSelectionStart: isSelected, + isSelectionEnd: isSelected, + }; + } + + if (end == null) { + const isSelected = sectionMethods.isSame(date, start!); + return { + isSelected, + isSelectionStart: isSelected, + isSelectionEnd: isSelected, + }; + } + + if (utils.isBefore(end, start)) { + return { + isSelected: false, + isSelectionStart: false, + isSelectionEnd: false, + }; + } + + return { + isSelected: utils.isWithinRange(date, [start, end]), + isSelectionStart: sectionMethods.isSame(date, start), + isSelectionEnd: sectionMethods.isSame(date, end), + }; +} + +export function getCalendarSectionMethods( + utils: MuiPickersAdapter, + section: 'day' | 'month' | 'year', +) { + if (section === 'year') { + return { isSame: utils.isSameYear, startOf: utils.startOfYear, endOf: utils.endOfYear }; + } + if (section === 'month') { + return { isSame: utils.isSameMonth, startOf: utils.startOfMonth, endOf: utils.endOfMonth }; + } + return { isSame: utils.isSameDay, startOf: utils.startOfDay, endOf: utils.endOfDay }; +} diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/useRangeCell.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/useRangeCell.ts new file mode 100644 index 0000000000000..94591ffbbd13c --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/useRangeCell.ts @@ -0,0 +1,234 @@ +import * as React from 'react'; +import useEventCallback from '@mui/utils/useEventCallback'; +import { PickerValidDate } from '@mui/x-date-pickers/models'; +import { RangeCalendarRootContext } from '../root/RangeCalendarRootContext'; + +/** + * Add support for drag&drop and preview to the cell components of the Range Calendar. + */ +export function useRangeCell(parameters: useRangeCell.Parameters) { + const { ctx, value, section } = parameters; + + const startDragging = () => { + ctx.startDragging(ctx.isSelectionStart ? 'start' : 'end'); + }; + + const isDraggingCurrentSection = () => ctx.draggedSectionRef.current === section; + + /** + * Drag events + */ + const onDragStart = useEventCallback((event: React.DragEvent) => { + event.stopPropagation(); + if (ctx.emptyDragImgRef.current) { + event.dataTransfer.setDragImage(ctx.emptyDragImgRef.current, 0, 0); + } + ctx.setDragTarget(value); + event.dataTransfer.effectAllowed = 'move'; + startDragging(); + const buttonDataset = (event.target as HTMLButtonElement).dataset; + if (buttonDataset.timestamp) { + event.dataTransfer.setData('draggingDate', buttonDataset.timestamp); + } + }); + + const onDragEnter = useEventCallback((event: React.DragEvent) => { + if (!isDraggingCurrentSection()) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + event.dataTransfer.dropEffect = 'move'; + ctx.setDragTarget(parameters.value); + }); + + const onDragLeave = useEventCallback((event: React.DragEvent) => { + if (!isDraggingCurrentSection()) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + }); + + const onDragOver = useEventCallback((event: React.DragEvent) => { + if (!isDraggingCurrentSection()) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + event.dataTransfer.dropEffect = 'move'; + }); + + const onDragEnd = useEventCallback((event: React.DragEvent) => { + if (!isDraggingCurrentSection()) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + ctx.stopDragging(); + }); + + const onDrop = useEventCallback((event: React.DragEvent) => { + if (!isDraggingCurrentSection()) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + ctx.stopDragging(); + // make sure the focused element is the element where drop ended + event.currentTarget.focus(); + ctx.selectDateFromDrag(value); + }); + + /** + * Touch events + */ + const onTouchStart = useEventCallback(() => { + ctx.setDragTarget(value); + }); + + const onTouchMove = useEventCallback((event: React.TouchEvent) => { + const target = resolveElementFromTouch(event); + if (!target) { + return; + } + + ctx.setDragTarget(target); + + // this prevents initiating drag when user starts touchmove outside and then moves over a draggable element + if (target !== event.changedTouches[0].target || !ctx.isDraggable) { + return; + } + + // on mobile we should only initialize dragging state after move is detected + startDragging(); + }); + + const onTouchEnd = useEventCallback((event: React.TouchEvent) => { + if (!isDraggingCurrentSection()) { + return; + } + + ctx.stopDragging(); + const target = resolveElementFromTouch(event, true); + if (!target) { + return; + } + + // make sure the focused element is the element where touch ended + target.focus(); + ctx.selectDateFromDrag(target); + }); + + /** + * Mouse events + */ + const onMouseEnter = useEventCallback(() => { + if (!ctx.isSelected) { + ctx.setHoveredDate(value); + } else { + ctx.setHoveredDate(null); + } + }); + + return React.useMemo( + () => ({ + ...(ctx.isDraggable ? { draggable: true, onDragStart, onDrop, onTouchStart } : {}), + onDragEnter, + onDragLeave, + onDragOver, + onDragEnd, + onTouchMove, + onTouchEnd, + onMouseEnter, + }), + [ + ctx.isDraggable, + onDragStart, + onDragEnter, + onDragLeave, + onDragOver, + onDragEnd, + onDrop, + onTouchStart, + onTouchMove, + onTouchEnd, + onMouseEnter, + ], + ); +} + +export namespace useRangeCell { + export interface Parameters { + value: PickerValidDate; + ctx: Context; + section: 'day' | 'month' | 'year'; + } + + export interface Context + extends Pick< + RangeCalendarRootContext, + | 'draggedSectionRef' + | 'selectDateFromDrag' + | 'startDragging' + | 'stopDragging' + | 'setDragTarget' + | 'setHoveredDate' + | 'emptyDragImgRef' + > { + isDraggable: boolean; + isSelectionStart: boolean; + isSelectionEnd: boolean; + isPreviewed: boolean; + isPreviewStart: boolean; + isPreviewEnd: boolean; + isSelected: boolean; + } +} + +function resolveButtonElement(element: Element | null): HTMLButtonElement | null { + if (!element) { + return null; + } + + if (element instanceof HTMLButtonElement && !element.disabled) { + return element; + } + + if (element.children.length) { + const allButtons = element.querySelectorAll('button:not(:disabled)'); + if (allButtons.length > 1) { + return null; + } + + return allButtons[0] ?? null; + } + + return null; +} + +function resolveElementFromTouch( + event: React.TouchEvent, + ignoreTouchTarget?: boolean, +) { + // don't parse multi-touch result + if (event.changedTouches?.length === 1 && event.touches.length <= 1) { + const element = document.elementFromPoint( + event.changedTouches[0].clientX, + event.changedTouches[0].clientY, + ); + // `elementFromPoint` could have resolved preview div or wrapping div + // might need to recursively find the nested button + const buttonElement = resolveButtonElement(element); + if (ignoreTouchTarget && buttonElement === event.changedTouches[0].target) { + return null; + } + return buttonElement; + } + return null; +} diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/useRangeCellWrapper.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/useRangeCellWrapper.ts new file mode 100644 index 0000000000000..f2f1404d694da --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/useRangeCellWrapper.ts @@ -0,0 +1,74 @@ +import * as React from 'react'; +import { PickerValidDate } from '@mui/x-date-pickers/models'; +import { useUtils } from '@mui/x-date-pickers/internals'; +import { getDatePositionInRange } from '../../../utils/date-utils'; +import { useRangeCalendarRootContext } from '../root/RangeCalendarRootContext'; +import { useRangeCell } from './useRangeCell'; + +/** + * Add support for drag&drop and preview to the cell components of the Range Calendar. + * Should be called by the wrapper component. + */ +export function useRangeCellWrapper(parameters: useRangeCellWrapper.Parameters) { + const { value, section } = parameters; + const utils = useUtils(); + const rootContext = useRangeCalendarRootContext(); + const cellRef = React.useRef(null); + + const registerCell = rootContext.registerCell; + React.useEffect(() => { + return registerCell(cellRef.current!, value); + }, [registerCell, value]); + + const positionInSelectedRange = React.useMemo( + () => getDatePositionInRange({ utils, section, date: value, range: rootContext.selectedRange }), + [utils, section, value, rootContext.selectedRange], + ); + + const positionInPreviewRange = React.useMemo( + () => getDatePositionInRange({ utils, section, date: value, range: rootContext.previewRange }), + [utils, section, value, rootContext.previewRange], + ); + + const ctx = React.useMemo( + () => ({ + isDraggable: + (!rootContext.disableDragEditing && positionInSelectedRange.isSelectionStart) || + positionInSelectedRange.isSelectionEnd, + isSelected: positionInSelectedRange.isSelected, + isSelectionStart: positionInSelectedRange.isSelectionStart, + isSelectionEnd: positionInSelectedRange.isSelectionEnd, + isPreviewed: positionInPreviewRange.isSelected, + isPreviewStart: positionInPreviewRange.isSelectionStart, + isPreviewEnd: positionInPreviewRange.isSelectionEnd, + draggedSectionRef: rootContext.draggedSectionRef, + selectDateFromDrag: rootContext.selectDateFromDrag, + startDragging: rootContext.startDragging, + stopDragging: rootContext.stopDragging, + setDragTarget: rootContext.setDragTarget, + setHoveredDate: rootContext.setHoveredDate, + emptyDragImgRef: rootContext.emptyDragImgRef, + }), + [ + positionInSelectedRange, + positionInPreviewRange, + rootContext.disableDragEditing, + rootContext.draggedSectionRef, + rootContext.selectDateFromDrag, + rootContext.startDragging, + rootContext.stopDragging, + rootContext.setDragTarget, + rootContext.setHoveredDate, + rootContext.emptyDragImgRef, + ], + ); + + return { cellRef, ctx }; +} + +export namespace useRangeCellWrapper { + export interface Parameters { + value: PickerValidDate; + section: 'day' | 'month' | 'year'; + } +} diff --git a/packages/x-date-pickers-pro/src/internals/utils/date-utils.ts b/packages/x-date-pickers-pro/src/internals/utils/date-utils.ts index 7cb230ee3dfb2..657c1eb23f7c4 100644 --- a/packages/x-date-pickers-pro/src/internals/utils/date-utils.ts +++ b/packages/x-date-pickers-pro/src/internals/utils/date-utils.ts @@ -32,18 +32,33 @@ export const isEndOfRange = ( return isRangeValid(utils, range) && utils.isSameDay(day, range[1]!); }; -export function getDatePositionInRange( - utils: MuiPickersAdapter, - date: PickerValidDate, - range: PickerRangeValue, -) { +export function getDatePositionInRange({ + utils, + date, + range, + section, +}: { + utils: MuiPickersAdapter; + date: PickerValidDate; + range: PickerRangeValue; + section: 'year' | 'month' | 'day'; +}) { const [start, end] = range; if (start == null && end == null) { return { isSelected: false, isSelectionStart: false, isSelectionEnd: false }; } + let comparisonFn: (a: PickerValidDate, b: PickerValidDate) => boolean; + if (section === 'year') { + comparisonFn = utils.isSameYear; + } else if (section === 'month') { + comparisonFn = utils.isSameMonth; + } else { + comparisonFn = utils.isSameDay; + } + if (start == null) { - const isSelected = utils.isSameDay(date, end!); + const isSelected = comparisonFn(date, end!); return { isSelected, isSelectionStart: isSelected, @@ -52,7 +67,7 @@ export function getDatePositionInRange( } if (end == null) { - const isSelected = utils.isSameDay(date, start!); + const isSelected = comparisonFn(date, start!); return { isSelected, isSelectionStart: isSelected, @@ -70,7 +85,7 @@ export function getDatePositionInRange( return { isSelected: utils.isWithinRange(date, [start, end]), - isSelectionStart: utils.isSameDay(date, start), - isSelectionEnd: utils.isSameDay(date, end), + isSelectionStart: comparisonFn(date, start), + isSelectionEnd: comparisonFn(date, end), }; } From c511f4f58bc45d3ffaf2fe74e267068b3371b291 Mon Sep 17 00:00:00 2001 From: flavien Date: Tue, 14 Jan 2025 09:19:38 +0100 Subject: [PATCH 118/136] Work on dnd --- .../root/RangeCalendarRootContext.ts | 4 +- .../root/useRangeCalendarRoot.tsx | 84 +++++++++---------- .../base/RangeCalendar/utils/date-range.ts | 21 +++++ .../base/RangeCalendar/utils/useRangeCell.ts | 6 +- .../utils/useRangeCellWrapper.ts | 2 +- .../src/internals/utils/date-utils.ts | 58 ------------- 6 files changed, 69 insertions(+), 106 deletions(-) diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts index e1bc6e89ae7e2..cc1a36ce97cd6 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts @@ -14,13 +14,13 @@ export interface RangeCalendarRootContext { draggedSectionRef: React.RefObject<'year' | 'month' | 'day' | null>; disableDragEditing: boolean; selectDateFromDrag: (valueOrElement: PickerValidDate | HTMLElement) => void; - startDragging: (position: RangePosition) => void; + startDragging: (position: RangePosition, section: 'year' | 'month' | 'day') => void; stopDragging: () => void; setDragTarget: (valueOrElement: PickerValidDate | HTMLElement) => void; emptyDragImgRef: React.RefObject; registerCell: (element: HTMLElement, value: PickerValidDate) => () => void; selectedRange: PickerRangeValue; - setHoveredDate: (value: PickerValidDate | null) => void; + setHoveredDate: (value: PickerValidDate | null, section: 'year' | 'month' | 'day') => void; previewRange: PickerRangeValue; } diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx index 6ba6202989380..03466753aef18 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx @@ -25,6 +25,7 @@ import { calculateRangeChange, calculateRangePreview } from '../../../utils/date import { isRangeValid } from '../../../utils/date-utils'; import { useRangePosition, UseRangePositionProps } from '../../../hooks/useRangePosition'; import { RangeCalendarRootContext } from './RangeCalendarRootContext'; +import { getRoundedRange } from '../utils/date-range'; const DEFAULT_AVAILABLE_RANGE_POSITIONS: RangePosition[] = ['start', 'end']; @@ -69,9 +70,7 @@ export function useRangeCalendarRoot(parameters: useRangeCalendarRoot.Parameters return undefined; } - return (dayToTest: PickerValidDate) => - // TODO: Add correct range position. - shouldDisableDate(dayToTest, rangePosition /* draggingDatePosition || rangePosition */); + return (dayToTest: PickerValidDate) => shouldDisableDate(dayToTest, rangePosition); }, [shouldDisableDate, rangePosition]); const dateValidationProps = React.useMemo( @@ -230,20 +229,10 @@ function useBuildRangeCalendarRootContext(parameters: UseRangeCalendarDragEditin } = parameters; const utils = useUtils(); - // TODO: Add rounded range for month and year. - // Range going for the start of the first selected cell to the end of the last selected cell. - // This makes sure that `isWithinRange` works with any time in the start and end day. - const valueRoundedRange = React.useMemo(() => { - return [ - !utils.isValid(value[0]) ? value[0] : utils.startOfDay(value[0]), - !utils.isValid(value[1]) ? value[1] : utils.endOfDay(value[1]), - ]; - }, [value, utils]); - const [state, setState] = React.useState<{ draggedSection: 'day' | 'month' | 'year' | null; targetDate: PickerValidDate | null; - hoveredDate: PickerValidDate | null; + hoveredDate: { value: PickerValidDate; section: 'day' | 'month' | 'year' } | null; }>({ draggedSection: null, targetDate: null, hoveredDate: null }); const cellToDateMapRef = React.useRef(new Map()); @@ -257,26 +246,25 @@ function useBuildRangeCalendarRootContext(parameters: UseRangeCalendarDragEditin ); const selectedRange = React.useMemo(() => { - if ( - !valueRoundedRange[0] || - !valueRoundedRange[1] || - !state.targetDate || - state.draggedSection == null - ) { - return valueRoundedRange; + if (!state.targetDate || state.draggedSection == null) { + return value; + } + + const roundedRange = getRoundedRange({ utils, range: value, section: state.draggedSection }); + if (roundedRange[0] == null || roundedRange[1] == null) { + return roundedRange; } - const newRange = calculateRangeChange({ + const rangeAfterDragAndDrop = calculateRangeChange({ utils, - range: valueRoundedRange, + range: roundedRange, newDate: state.targetDate, rangePosition, allowRangeFlip: true, }).newRange; - return newRange[0] !== null && newRange[1] !== null - ? [utils.startOfDay(newRange[0]), utils.endOfDay(newRange[1])] - : newRange; - }, [rangePosition, state.targetDate, state.draggedSection, utils, valueRoundedRange]); + + return getRoundedRange({ utils, range: rangeAfterDragAndDrop, section: state.draggedSection }); + }, [rangePosition, state.targetDate, state.draggedSection, utils, value]); const disableDragEditing = disableDragEditingProp || baseContext.disabled || baseContext.readOnly; const disableHoverPreview = @@ -295,15 +283,17 @@ function useBuildRangeCalendarRootContext(parameters: UseRangeCalendarDragEditin 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; }, []); - const startDragging = useEventCallback((position: RangePosition) => { - setState((prev) => ({ ...prev, isDragging: true })); - onRangePositionChange(position); - }); + const startDragging: RangeCalendarRootContext['startDragging'] = useEventCallback( + (position, section) => { + setState((prev) => ({ ...prev, draggedSection: section })); + onRangePositionChange(position); + }, + ); const stopDragging = useEventCallback(() => { setState((prev) => ({ ...prev, - isDragging: false, + draggedSection: null, draggedDate: null, targetDate: null, })); @@ -348,25 +338,35 @@ function useBuildRangeCalendarRootContext(parameters: UseRangeCalendarDragEditin setValue(response.value, { changeImportance: response.changeImportance, section: 'day' }); }); - const setHoveredDate = useEventCallback((hoveredDate: PickerValidDate | null) => { - if (disableHoverPreview) { - return; - } - setState((prev) => ({ ...prev, hoveredDate })); - }); + const setHoveredDate = useEventCallback( + (date: PickerValidDate | null, section: 'day' | 'month' | 'year') => { + if (disableHoverPreview) { + return; + } + setState((prev) => ({ + ...prev, + hoveredDate: date == null ? null : { value: date, section }, + })); + }, + ); const previewRange = React.useMemo(() => { - if (disableHoverPreview) { + if (disableHoverPreview || state.hoveredDate == null) { return [null, null]; } + const roundedRange = getRoundedRange({ + utils, + range: value, + section: state.hoveredDate?.section, + }); return calculateRangePreview({ utils, - range: valueRoundedRange, - newDate: state.hoveredDate, + range: roundedRange, + newDate: state.hoveredDate.value, rangePosition, }); - }, [utils, rangePosition, state.hoveredDate, valueRoundedRange, disableHoverPreview]); + }, [utils, rangePosition, state.hoveredDate, value, disableHoverPreview]); const context: RangeCalendarRootContext = { value, diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/date-range.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/date-range.ts index 6f13071d8eb6d..2e6e2048aadda 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/date-range.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/date-range.ts @@ -63,3 +63,24 @@ export function getCalendarSectionMethods( } return { isSame: utils.isSameDay, startOf: utils.startOfDay, endOf: utils.endOfDay }; } + +/** + * Create a range going for the start of the first selected cell to the end of the last selected cell. + * This makes sure that `isWithinRange` works with any time in the start and end day. + */ +export function getRoundedRange({ + utils, + range, + section, +}: { + utils: MuiPickersAdapter; + range: PickerRangeValue; + section: 'day' | 'month' | 'year'; +}): PickerRangeValue { + const sectionMethods = getCalendarSectionMethods(utils, section); + + return [ + utils.isValid(range[0]) ? sectionMethods.startOf(range[0]) : null, + utils.isValid(range[1]) ? sectionMethods.endOf(range[1]) : null, + ]; +} diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/useRangeCell.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/useRangeCell.ts index 94591ffbbd13c..ec12f5639289a 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/useRangeCell.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/useRangeCell.ts @@ -10,7 +10,7 @@ export function useRangeCell(parameters: useRangeCell.Parameters) { const { ctx, value, section } = parameters; const startDragging = () => { - ctx.startDragging(ctx.isSelectionStart ? 'start' : 'end'); + ctx.startDragging(ctx.isSelectionStart ? 'start' : 'end', section); }; const isDraggingCurrentSection = () => ctx.draggedSectionRef.current === section; @@ -130,9 +130,9 @@ export function useRangeCell(parameters: useRangeCell.Parameters) { */ const onMouseEnter = useEventCallback(() => { if (!ctx.isSelected) { - ctx.setHoveredDate(value); + ctx.setHoveredDate(value, section); } else { - ctx.setHoveredDate(null); + ctx.setHoveredDate(null, section); } }); diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/useRangeCellWrapper.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/useRangeCellWrapper.ts index f2f1404d694da..dea49bb9179a8 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/useRangeCellWrapper.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/useRangeCellWrapper.ts @@ -1,9 +1,9 @@ import * as React from 'react'; import { PickerValidDate } from '@mui/x-date-pickers/models'; import { useUtils } from '@mui/x-date-pickers/internals'; -import { getDatePositionInRange } from '../../../utils/date-utils'; import { useRangeCalendarRootContext } from '../root/RangeCalendarRootContext'; import { useRangeCell } from './useRangeCell'; +import { getDatePositionInRange } from './date-range'; /** * Add support for drag&drop and preview to the cell components of the Range Calendar. diff --git a/packages/x-date-pickers-pro/src/internals/utils/date-utils.ts b/packages/x-date-pickers-pro/src/internals/utils/date-utils.ts index 657c1eb23f7c4..8ca8620be892f 100644 --- a/packages/x-date-pickers-pro/src/internals/utils/date-utils.ts +++ b/packages/x-date-pickers-pro/src/internals/utils/date-utils.ts @@ -31,61 +31,3 @@ export const isEndOfRange = ( ) => { return isRangeValid(utils, range) && utils.isSameDay(day, range[1]!); }; - -export function getDatePositionInRange({ - utils, - date, - range, - section, -}: { - utils: MuiPickersAdapter; - date: PickerValidDate; - range: PickerRangeValue; - section: 'year' | 'month' | 'day'; -}) { - const [start, end] = range; - if (start == null && end == null) { - return { isSelected: false, isSelectionStart: false, isSelectionEnd: false }; - } - - let comparisonFn: (a: PickerValidDate, b: PickerValidDate) => boolean; - if (section === 'year') { - comparisonFn = utils.isSameYear; - } else if (section === 'month') { - comparisonFn = utils.isSameMonth; - } else { - comparisonFn = utils.isSameDay; - } - - if (start == null) { - const isSelected = comparisonFn(date, end!); - return { - isSelected, - isSelectionStart: isSelected, - isSelectionEnd: isSelected, - }; - } - - if (end == null) { - const isSelected = comparisonFn(date, start!); - return { - isSelected, - isSelectionStart: isSelected, - isSelectionEnd: isSelected, - }; - } - - if (utils.isBefore(end, start)) { - return { - isSelected: false, - isSelectionStart: false, - isSelectionEnd: false, - }; - } - - return { - isSelected: utils.isWithinRange(date, [start, end]), - isSelectionStart: comparisonFn(date, start), - isSelectionEnd: comparisonFn(date, end), - }; -} From 9506c8872a7523d411ca78e693af8e13cf8c9f62 Mon Sep 17 00:00:00 2001 From: flavien Date: Tue, 14 Jan 2025 09:25:34 +0100 Subject: [PATCH 119/136] Work --- .../root/RangeCalendarRootContext.ts | 8 ++-- .../root/useRangeCalendarRoot.tsx | 46 ++++++++++--------- .../utils/{date-range.ts => range.ts} | 29 ++++++------ .../base/RangeCalendar/utils/useRangeCell.ts | 4 +- .../utils/useRangeCellWrapper.ts | 6 ++- .../root/BaseCalendarRootContext.ts | 5 +- .../base-calendar/root/useBaseCalendarRoot.ts | 13 +++--- .../base/utils/base-calendar/utils/types.ts | 1 + .../utils/base-calendar/utils/useCellList.ts | 3 +- 9 files changed, 63 insertions(+), 52 deletions(-) rename packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/{date-range.ts => range.ts} (87%) create mode 100644 packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/types.ts diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts index cc1a36ce97cd6..1ef44fa097c37 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts @@ -1,6 +1,8 @@ import * as React from 'react'; import { PickerRangeValue, RangePosition } from '@mui/x-date-pickers/internals'; import { PickerValidDate } from '@mui/x-date-pickers/models'; +// eslint-disable-next-line no-restricted-imports +import { BaseCalendarSection } from '@mui/x-date-pickers/internals/base/utils/base-calendar/utils/types'; export interface RangeCalendarRootContext { /** @@ -11,16 +13,16 @@ export interface RangeCalendarRootContext { * A ref containing the section being dragged. * This is used to check if the user is dragging in event handlers without causing re-renders. */ - draggedSectionRef: React.RefObject<'year' | 'month' | 'day' | null>; + draggedSectionRef: React.RefObject; disableDragEditing: boolean; selectDateFromDrag: (valueOrElement: PickerValidDate | HTMLElement) => void; - startDragging: (position: RangePosition, section: 'year' | 'month' | 'day') => void; + startDragging: (position: RangePosition, section: BaseCalendarSection) => void; stopDragging: () => void; setDragTarget: (valueOrElement: PickerValidDate | HTMLElement) => void; emptyDragImgRef: React.RefObject; registerCell: (element: HTMLElement, value: PickerValidDate) => () => void; selectedRange: PickerRangeValue; - setHoveredDate: (value: PickerValidDate | null, section: 'year' | 'month' | 'day') => void; + setHoveredDate: (value: PickerValidDate | null, section: BaseCalendarSection) => void; previewRange: PickerRangeValue; } diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx index 03466753aef18..72ca245f0144c 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx @@ -5,6 +5,8 @@ import { PickerValidDate } from '@mui/x-date-pickers/models'; import { ValidateDateProps } from '@mui/x-date-pickers/validation'; import { PickerRangeValue, RangePosition, useUtils } from '@mui/x-date-pickers/internals'; // eslint-disable-next-line no-restricted-imports +import { BaseCalendarSection } from '@mui/x-date-pickers/internals/base/utils/base-calendar/utils/types'; +// eslint-disable-next-line no-restricted-imports import { useAddDefaultsToBaseDateValidationProps, useBaseCalendarRoot, @@ -25,7 +27,7 @@ import { calculateRangeChange, calculateRangePreview } from '../../../utils/date import { isRangeValid } from '../../../utils/date-utils'; import { useRangePosition, UseRangePositionProps } from '../../../hooks/useRangePosition'; import { RangeCalendarRootContext } from './RangeCalendarRootContext'; -import { getRoundedRange } from '../utils/date-range'; +import { getRoundedRange } from '../utils/range'; const DEFAULT_AVAILABLE_RANGE_POSITIONS: RangePosition[] = ['start', 'end']; @@ -230,9 +232,9 @@ function useBuildRangeCalendarRootContext(parameters: UseRangeCalendarDragEditin const utils = useUtils(); const [state, setState] = React.useState<{ - draggedSection: 'day' | 'month' | 'year' | null; + draggedSection: BaseCalendarSection | null; targetDate: PickerValidDate | null; - hoveredDate: { value: PickerValidDate; section: 'day' | 'month' | 'year' } | null; + hoveredDate: { value: PickerValidDate; section: BaseCalendarSection } | null; }>({ draggedSection: null, targetDate: null, hoveredDate: null }); const cellToDateMapRef = React.useRef(new Map()); @@ -319,27 +321,29 @@ function useBuildRangeCalendarRootContext(parameters: UseRangeCalendarDragEditin } }); - const selectDateFromDrag = useEventCallback((valueOrElement: PickerValidDate | HTMLElement) => { - const selectedDate = - valueOrElement instanceof HTMLElement - ? cellToDateMapRef.current.get(valueOrElement) - : valueOrElement; - if (selectedDate == null) { - return; - } + const selectDateFromDrag: RangeCalendarRootContext['selectDateFromDrag'] = useEventCallback( + (valueOrElement) => { + const selectedDate = + valueOrElement instanceof HTMLElement + ? cellToDateMapRef.current.get(valueOrElement) + : valueOrElement; + if (selectedDate == null) { + return; + } - const response = getNewValueFromNewSelectedDate({ - prevValue: value, - selectedDate, - referenceDate, - allowRangeFlip: true, - }); + const response = getNewValueFromNewSelectedDate({ + prevValue: value, + selectedDate, + referenceDate, + allowRangeFlip: true, + }); - setValue(response.value, { changeImportance: response.changeImportance, section: 'day' }); - }); + setValue(response.value, { changeImportance: response.changeImportance, section: 'day' }); + }, + ); - const setHoveredDate = useEventCallback( - (date: PickerValidDate | null, section: 'day' | 'month' | 'year') => { + const setHoveredDate: RangeCalendarRootContext['setHoveredDate'] = useEventCallback( + (date, section) => { if (disableHoverPreview) { return; } diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/date-range.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/range.ts similarity index 87% rename from packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/date-range.ts rename to packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/range.ts index 2e6e2048aadda..d1baa8c7bbfcf 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/date-range.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/range.ts @@ -1,5 +1,7 @@ import { PickerRangeValue } from '@mui/x-date-pickers/internals'; import { MuiPickersAdapter, PickerValidDate } from '@mui/x-date-pickers/models'; +// eslint-disable-next-line no-restricted-imports +import { BaseCalendarSection } from '@mui/x-date-pickers/internals/base/utils/base-calendar/utils/types'; export function getDatePositionInRange({ utils, @@ -10,7 +12,7 @@ export function getDatePositionInRange({ utils: MuiPickersAdapter; date: PickerValidDate; range: PickerRangeValue; - section: 'year' | 'month' | 'day'; + section: BaseCalendarSection; }) { const [start, end] = range; if (start == null && end == null) { @@ -51,19 +53,6 @@ export function getDatePositionInRange({ }; } -export function getCalendarSectionMethods( - utils: MuiPickersAdapter, - section: 'day' | 'month' | 'year', -) { - if (section === 'year') { - return { isSame: utils.isSameYear, startOf: utils.startOfYear, endOf: utils.endOfYear }; - } - if (section === 'month') { - return { isSame: utils.isSameMonth, startOf: utils.startOfMonth, endOf: utils.endOfMonth }; - } - return { isSame: utils.isSameDay, startOf: utils.startOfDay, endOf: utils.endOfDay }; -} - /** * Create a range going for the start of the first selected cell to the end of the last selected cell. * This makes sure that `isWithinRange` works with any time in the start and end day. @@ -75,7 +64,7 @@ export function getRoundedRange({ }: { utils: MuiPickersAdapter; range: PickerRangeValue; - section: 'day' | 'month' | 'year'; + section: BaseCalendarSection; }): PickerRangeValue { const sectionMethods = getCalendarSectionMethods(utils, section); @@ -84,3 +73,13 @@ export function getRoundedRange({ utils.isValid(range[1]) ? sectionMethods.endOf(range[1]) : null, ]; } + +export function getCalendarSectionMethods(utils: MuiPickersAdapter, section: BaseCalendarSection) { + if (section === 'year') { + return { isSame: utils.isSameYear, startOf: utils.startOfYear, endOf: utils.endOfYear }; + } + if (section === 'month') { + return { isSame: utils.isSameMonth, startOf: utils.startOfMonth, endOf: utils.endOfMonth }; + } + return { isSame: utils.isSameDay, startOf: utils.startOfDay, endOf: utils.endOfDay }; +} diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/useRangeCell.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/useRangeCell.ts index ec12f5639289a..10bbfa0e6525e 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/useRangeCell.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/useRangeCell.ts @@ -1,6 +1,8 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; import { PickerValidDate } from '@mui/x-date-pickers/models'; +// eslint-disable-next-line no-restricted-imports +import { BaseCalendarSection } from '@mui/x-date-pickers/internals/base/utils/base-calendar/utils/types'; import { RangeCalendarRootContext } from '../root/RangeCalendarRootContext'; /** @@ -167,7 +169,7 @@ export namespace useRangeCell { export interface Parameters { value: PickerValidDate; ctx: Context; - section: 'day' | 'month' | 'year'; + section: BaseCalendarSection; } export interface Context diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/useRangeCellWrapper.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/useRangeCellWrapper.ts index dea49bb9179a8..305a411dd70f7 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/useRangeCellWrapper.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/useRangeCellWrapper.ts @@ -1,9 +1,11 @@ import * as React from 'react'; import { PickerValidDate } from '@mui/x-date-pickers/models'; import { useUtils } from '@mui/x-date-pickers/internals'; +// eslint-disable-next-line no-restricted-imports +import { BaseCalendarSection } from '@mui/x-date-pickers/internals/base/utils/base-calendar/utils/types'; import { useRangeCalendarRootContext } from '../root/RangeCalendarRootContext'; import { useRangeCell } from './useRangeCell'; -import { getDatePositionInRange } from './date-range'; +import { getDatePositionInRange } from './range'; /** * Add support for drag&drop and preview to the cell components of the Range Calendar. @@ -69,6 +71,6 @@ export function useRangeCellWrapper(parameters: useRangeCellWrapper.Parameters) export namespace useRangeCellWrapper { export interface Parameters { value: PickerValidDate; - section: 'day' | 'month' | 'year'; + section: BaseCalendarSection; } } diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/BaseCalendarRootContext.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/BaseCalendarRootContext.ts index de181799ff256..5e5e14e13b4a2 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/BaseCalendarRootContext.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/BaseCalendarRootContext.ts @@ -3,6 +3,7 @@ import { PickersTimezone, PickerValidDate } from '../../../../../models'; import { ValidateDateProps } from '../../../../../validation'; import type { useBaseCalendarRoot } from './useBaseCalendarRoot'; import type { useBaseCalendarDaysGridBody } from '../days-grid-body/useBaseCalendarDaysGridBody'; +import { BaseCalendarSection } from '../utils/types'; export interface BaseCalendarRootContext { /** @@ -41,9 +42,9 @@ export interface BaseCalendarRootContext { * Selects a date. * @param {PickerValidDate} date The date to select. * @param {object} options The options to select the date. - * @param {'day' | 'month' | 'year'} options.section The section handled by the UI that triggered the change. + * @param {BaseCalendarSection} options.section The section handled by the UI that triggered the change. */ - selectDate: (date: PickerValidDate, options: { section: 'day' | 'month' | 'year' }) => void; + selectDate: (date: PickerValidDate, options: { section: BaseCalendarSection }) => void; /** * Determines if the given date is invalid. * @param {PickerValidDate} date The date to check. diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarRoot.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarRoot.ts index deb540060514c..dde46573bd3f9 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarRoot.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarRoot.ts @@ -19,6 +19,7 @@ import { useControlledValueWithTimezone } from '../../../../hooks/useValueWithTi import { BaseDateValidationProps } from '../../../../models/validation'; import { useBaseCalendarDaysGridNavigation } from './useBaseCalendarDaysGridsNavigation'; import { BaseCalendarRootContext } from './BaseCalendarRootContext'; +import { BaseCalendarSection } from '../utils/types'; export function useBaseCalendarRoot< TValue extends PickerValidValue, @@ -92,9 +93,7 @@ export function useBaseCalendarRoot< validator: manager.validator, }); - const sectionsRef = React.useRef< - Record<'day' | 'month' | 'year', Record> - >({ + const sectionsRef = React.useRef>>({ day: {}, month: {}, year: {}, @@ -163,7 +162,7 @@ export function useBaseCalendarRoot< const setValue = useEventCallback( ( newValue: TValue, - options: { section: 'day' | 'month' | 'year'; changeImportance: 'set' | 'accept' }, + options: { section: BaseCalendarSection; changeImportance: 'set' | 'accept' }, ) => { handleValueChange(newValue, { section: options.section, @@ -331,7 +330,7 @@ export namespace useBaseCalendarRoot { referenceDate: PickerValidDate; setValue: ( newValue: TValue, - options: { section: 'day' | 'month' | 'year'; changeImportance: 'set' | 'accept' }, + options: { section: BaseCalendarSection; changeImportance: 'set' | 'accept' }, ) => void; setVisibleDate: (newVisibleDate: PickerValidDate, skipIfAlreadyVisible: boolean) => void; isDateCellVisible: (date: PickerValidDate) => boolean; @@ -342,7 +341,7 @@ export namespace useBaseCalendarRoot { /** * The section handled by the UI that triggered the change. */ - section: 'day' | 'month' | 'year'; + section: BaseCalendarSection; /** * The validation error associated to the new value. */ @@ -354,7 +353,7 @@ export namespace useBaseCalendarRoot { } export interface RegisterSectionParameters { - type: 'day' | 'month' | 'year'; + type: BaseCalendarSection; value: PickerValidDate; } diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/types.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/types.ts new file mode 100644 index 0000000000000..f4639f45fd5d3 --- /dev/null +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/types.ts @@ -0,0 +1 @@ +export type BaseCalendarSection = 'day' | 'month' | 'year'; diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useCellList.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useCellList.ts index 0434d740080db..7bfcd4b256671 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useCellList.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/utils/useCellList.ts @@ -1,6 +1,7 @@ import * as React from 'react'; import { PickerValidDate } from '../../../../../models'; import { useBaseCalendarRootContext } from '../root/BaseCalendarRootContext'; +import { BaseCalendarSection } from './types'; /** * Internal utility hook to handle a list of cells: @@ -68,7 +69,7 @@ export namespace useCellList { /** * The type of the section. */ - section: 'day' | 'month' | 'year'; + section: BaseCalendarSection; /** * The value of the section. */ From 19b867d8c0cd39c1b2d2fd04be903039ca7f143b Mon Sep 17 00:00:00 2001 From: flavien Date: Tue, 14 Jan 2025 09:51:09 +0100 Subject: [PATCH 120/136] Basic month range picker working --- .../base-calendar/MonthRangeCalendarDemo.js | 53 +++++++++++ .../base-calendar/MonthRangeCalendarDemo.tsx | 53 +++++++++++ .../MonthRangeCalendarDemo.tsx.preview | 14 +++ .../base-calendar/base-calendar.md | 10 +++ .../base-calendar/calendar.module.css | 30 ++++--- .../base/RangeCalendar/index.parts.ts | 3 +- .../months-cell/RangeCalendarMonthsCell.tsx | 90 ++++++++++++++++++- .../RangeCalendarMonthsCellDataAttributes.ts | 32 ++++++- .../useRangeCalendarMonthsCellWrapper.ts | 2 +- .../base/RangeCalendar/utils/range.ts | 5 +- 10 files changed, 270 insertions(+), 22 deletions(-) create mode 100644 docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.js create mode 100644 docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.tsx create mode 100644 docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.tsx.preview diff --git a/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.js b/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.js new file mode 100644 index 0000000000000..edb9798dee215 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.js @@ -0,0 +1,53 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +// eslint-disable-next-line no-restricted-imports +import { + RangeCalendar, + useRangeCalendarContext, +} from '@mui/x-date-pickers-pro/internals/base/RangeCalendar'; +import styles from './calendar.module.css'; + +function Header() { + const { visibleDate } = useRangeCalendarContext(); + + return ( +
+ + ◀ + + {visibleDate.format('YYYY')} + + ▶ + +
+ ); +} + +export default function MonthRangeCalendarDemo() { + return ( + + +
+ + {({ months }) => + months.map((month) => ( + + )) + } + + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.tsx b/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.tsx new file mode 100644 index 0000000000000..a5a40d49616d5 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.tsx @@ -0,0 +1,53 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +// eslint-disable-next-line no-restricted-imports +import { + RangeCalendar, + useRangeCalendarContext, +} from '@mui/x-date-pickers-pro/internals/base/RangeCalendar'; +import styles from './calendar.module.css'; + +function Header() { + const { visibleDate } = useRangeCalendarContext(); + + return ( +
+ + ◀ + + {visibleDate.format('YYYY')} + + ▶ + +
+ ); +} + +export default function MonthRangeCalendarDemo() { + return ( + + +
+ + {({ months }) => + months.map((month) => ( + + )) + } + + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.tsx.preview b/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.tsx.preview new file mode 100644 index 0000000000000..8dd3870ace494 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.tsx.preview @@ -0,0 +1,14 @@ + +
+ + {({ months }) => + months.map((month) => ( + + )) + } + + \ No newline at end of file diff --git a/docs/data/date-pickers/base-calendar/base-calendar.md b/docs/data/date-pickers/base-calendar/base-calendar.md index 79221f8e96843..d2f533c9d9697 100644 --- a/docs/data/date-pickers/base-calendar/base-calendar.md +++ b/docs/data/date-pickers/base-calendar/base-calendar.md @@ -213,6 +213,16 @@ The following demo shows a more advanced use case with lazy-loaded validation da {{"demo": "DayRangeCalendarAirbnbRecipe.js", "defaultCodeOpen": false}} +## Month Range Calendar + +### Single visible year + +{{"demo": "MonthRangeCalendarDemo.js", "defaultCodeOpen": false}} + +### Multiple visible years + +TODO + ## Date Range Calendar {{"demo": "DateRangeCalendarDemo.js", "defaultCodeOpen": false}} diff --git a/docs/data/date-pickers/base-calendar/calendar.module.css b/docs/data/date-pickers/base-calendar/calendar.module.css index 12a5c2f0fb833..92fcdec208846 100644 --- a/docs/data/date-pickers/base-calendar/calendar.module.css +++ b/docs/data/date-pickers/base-calendar/calendar.module.css @@ -116,7 +116,7 @@ display: flex; flex-direction: column; align-items: stretch; - gap: 8px; + gap: 2px; overflow-y: auto; } @@ -124,8 +124,7 @@ .YearsGrid { padding: 12px; display: grid; - column-gap: 4px; - row-gap: 8px; + row-gap: 2px; overflow-y: auto; z-index: 1; } @@ -229,15 +228,6 @@ &:focus-visible::after { outline: 2px solid var(--button-focus-border-color); } -} - -.DaysCell { - height: 36px; - width: 36px; - - &[data-current]:not([data-selected]):not(:focus-visible)::after { - outline: 1px solid var(--cell-current-border-color); - } &:not([data-outside-month]):disabled { color: var(--cell-disabled-color); @@ -266,7 +256,9 @@ left: 0; } - &.RangeDaysCell { + &.RangeDaysCell, + &.RangeMonthsCell, + &.RangeYearsCell { &[data-selected]:not([data-selection-start])::before { border-top-left-radius: 0; border-bottom-left-radius: 0; @@ -279,7 +271,17 @@ } } -.Root:not([data-empty]) .RangeDaysCell:not([data-outside-month])[data-previewed] { +.DaysCell { + height: 36px; + width: 36px; + + &[data-current]:not([data-selected]):not(:focus-visible)::after { + outline: 1px solid var(--cell-current-border-color); + } +} + +.Root:not([data-empty]) .RangeDaysCell:not([data-outside-month])[data-previewed], +.Root:not([data-empty]) .RangeMonthsCell[data-previewed] { &:not([data-preview-start]) { border-top-left-radius: 0; border-bottom-left-radius: 0; diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/index.parts.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/index.parts.ts index 7bd3a13a9804a..fe142d96f248c 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/index.parts.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/index.parts.ts @@ -11,8 +11,7 @@ export { RangeCalendarDaysCell as DaysCell } from './days-cell/RangeCalendarDays // // Months export { RangeCalendarMonthsList as MonthsList } from './months-list/RangeCalendarMonthsList'; export { RangeCalendarMonthsGrid as MonthsGrid } from './months-grid/RangeCalendarMonthsGrid'; -// TODO: Uncomment when the component supports good range editing -// export { RangeCalendarMonthsCell as MonthsCell } from './months-cell/RangeCalendarMonthsCell'; +export { RangeCalendarMonthsCell as MonthsCell } from './months-cell/RangeCalendarMonthsCell'; // // Years export { RangeCalendarYearsList as YearsList } from './years-list/RangeCalendarYearsList'; diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/RangeCalendarMonthsCell.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/RangeCalendarMonthsCell.tsx index 0a0dec1e01872..4c984e07d5817 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/RangeCalendarMonthsCell.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/RangeCalendarMonthsCell.tsx @@ -3,11 +3,51 @@ import * as React from 'react'; // eslint-disable-next-line no-restricted-imports import { BaseUIComponentProps } from '@mui/x-date-pickers/internals/base/base-utils/types'; // eslint-disable-next-line no-restricted-imports +import { CustomStyleHookMapping } from '@mui/x-date-pickers/internals/base/base-utils/getStyleHookProps'; +// eslint-disable-next-line no-restricted-imports import { useComponentRenderer } from '@mui/x-date-pickers/internals/base/base-utils/useComponentRenderer'; import { useRangeCalendarMonthsCell } from './useRangeCalendarMonthsCell'; import { useRangeCalendarMonthsCellWrapper } from './useRangeCalendarMonthsCellWrapper'; +import { RangeCalendarMonthsCellDataAttributes } from './RangeCalendarMonthsCellDataAttributes'; // eslint-disable-next-line no-restricted-imports +// TODO: Avoid duplication between day, month, and year cells +const customStyleHookMapping: CustomStyleHookMapping = { + selected(value) { + return value ? { [RangeCalendarMonthsCellDataAttributes.selected]: '' } : null; + }, + selectionStart(value) { + return value ? { [RangeCalendarMonthsCellDataAttributes.selectionStart]: '' } : null; + }, + selectionEnd(value) { + return value ? { [RangeCalendarMonthsCellDataAttributes.selectionEnd]: '' } : null; + }, + insideSelection(value) { + return value ? { [RangeCalendarMonthsCellDataAttributes.insideSelection]: '' } : null; + }, + previewed(value) { + return value ? { [RangeCalendarMonthsCellDataAttributes.previewed]: '' } : null; + }, + previewStart(value) { + return value ? { [RangeCalendarMonthsCellDataAttributes.previewStart]: '' } : null; + }, + previewEnd(value) { + return value ? { [RangeCalendarMonthsCellDataAttributes.previewEnd]: '' } : null; + }, + insidePreview(value) { + return value ? { [RangeCalendarMonthsCellDataAttributes.insidePreview]: '' } : null; + }, + disabled(value) { + return value ? { [RangeCalendarMonthsCellDataAttributes.disabled]: '' } : null; + }, + invalid(value) { + return value ? { [RangeCalendarMonthsCellDataAttributes.invalid]: '' } : null; + }, + current(value) { + return value ? { [RangeCalendarMonthsCellDataAttributes.current]: '' } : null; + }, +}; + const InnerRangeCalendarMonthsCell = React.forwardRef(function InnerRangeCalendarMonthsCell( props: InnerRangeCalendarMonthsCellProps, forwardedRef: React.ForwardedRef, @@ -18,11 +58,28 @@ const InnerRangeCalendarMonthsCell = React.forwardRef(function InnerRangeCalenda const state: RangeCalendarMonthsCell.State = React.useMemo( () => ({ selected: ctx.isSelected, + selectionStart: ctx.isSelectionStart, + selectionEnd: ctx.isSelectionEnd, + insideSelection: ctx.isSelected && !ctx.isSelectionStart && !ctx.isSelectionEnd, + previewed: ctx.isPreviewed, + previewStart: ctx.isPreviewStart, + previewEnd: ctx.isPreviewEnd, + insidePreview: ctx.isPreviewed && !ctx.isPreviewStart && !ctx.isPreviewEnd, disabled: ctx.isDisabled, invalid: ctx.isInvalid, current: isCurrent, }), - [ctx.isSelected, ctx.isDisabled, ctx.isInvalid, isCurrent], + [ + ctx.isSelected, + ctx.isSelectionStart, + ctx.isSelectionEnd, + ctx.isPreviewed, + ctx.isPreviewStart, + ctx.isPreviewEnd, + ctx.isDisabled, + ctx.isInvalid, + isCurrent, + ], ); const { renderElement } = useComponentRenderer({ @@ -32,6 +89,7 @@ const InnerRangeCalendarMonthsCell = React.forwardRef(function InnerRangeCalenda className, state, extraProps: otherProps, + customStyleHookMapping, }); return renderElement(); @@ -51,9 +109,37 @@ const RangeCalendarMonthsCell = React.forwardRef(function RangeCalendarMonthsCel export namespace RangeCalendarMonthsCell { export interface State { /** - * Whether the month is selected. + * Whether the month is within the selected range. */ selected: boolean; + /** + * Whether the month is the first month of the selected range. + */ + selectionStart: boolean; + /** + * Whether the month is the last month of the selected range. + */ + selectionEnd: boolean; + /** + * Whether the month is within the selected range and is not its first or last month. + */ + insideSelection: boolean; + /** + * Whether the month is within the preview range and is not its first or last month. + */ + previewed: boolean; + /** + * Whether the month is the first month of the preview range. + */ + previewStart: boolean; + /** + * Whether the month is the last month of the preview range. + */ + previewEnd: boolean; + /** + * Whether the month is within the preview range and is not its first or last month. + */ + insidePreview: boolean; /** * Whether the month is disabled. */ diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/RangeCalendarMonthsCellDataAttributes.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/RangeCalendarMonthsCellDataAttributes.ts index 649f69ee87237..2d3aa37abf989 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/RangeCalendarMonthsCellDataAttributes.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/RangeCalendarMonthsCellDataAttributes.ts @@ -1,8 +1,36 @@ export enum RangeCalendarMonthsCellDataAttributes { /** - * Present when the month is selected. + * Present when the month is within the selected range. */ selected = 'data-selected', + /** + * Present when the month is the first month of the selected range. + */ + selectionStart = 'data-selection-start', + /** + * Present when the month is the last month of the selected range. + */ + selectionEnd = 'data-selection-end', + /** + * Present when the month is within the selected range and is not its first or last month. + */ + insideSelection = 'data-inside-selection', + /** + * Present when the month is within the preview range. + */ + previewed = 'data-previewed', + /** + * Present when the month is the first month of the preview range. + */ + previewStart = 'data-preview-start', + /** + * Present when the month is the last month of the preview range. + */ + previewEnd = 'data-preview-end', + /** + * Present when the month is within the preview range and is not its first or last month. + */ + insidePreview = 'data-inside-preview', /** * Present when the month is disabled. */ @@ -12,7 +40,7 @@ export enum RangeCalendarMonthsCellDataAttributes { */ invalid = 'data-invalid', /** - * Present when the month contains the current date. + * Present when the month is the current date. */ current = 'data-current', } diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/useRangeCalendarMonthsCellWrapper.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/useRangeCalendarMonthsCellWrapper.ts index bb130efb05a2d..9ee837ab45555 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/useRangeCalendarMonthsCellWrapper.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/useRangeCalendarMonthsCellWrapper.ts @@ -10,7 +10,7 @@ export function useRangeCalendarMonthsCellWrapper( ): useRangeCalendarMonthsCellWrapper.ReturnValue { const { value } = parameters; const { ref: baseRef, ctx: baseCtx } = useBaseCalendarMonthsCellWrapper(parameters); - const { cellRef, ctx: rangeCellCtx } = useRangeCellWrapper({ value, section: 'day' }); + const { cellRef, ctx: rangeCellCtx } = useRangeCellWrapper({ value, section: 'month' }); const ref = useForkRef(baseRef, cellRef); const ctx = React.useMemo( diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/range.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/range.ts index d1baa8c7bbfcf..fcfa719888513 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/range.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/range.ts @@ -47,7 +47,10 @@ export function getDatePositionInRange({ } return { - isSelected: utils.isWithinRange(date, [start, end]), + isSelected: utils.isWithinRange(date, [ + sectionMethods.startOf(start), + sectionMethods.endOf(end), + ]), isSelectionStart: sectionMethods.isSame(date, start), isSelectionEnd: sectionMethods.isSame(date, end), }; From 0ab7792e135fcf4512a22286f14746d6741eb40b Mon Sep 17 00:00:00 2001 From: flavien Date: Tue, 14 Jan 2025 09:56:01 +0100 Subject: [PATCH 121/136] Clean code --- .../root/useRangeCalendarRoot.tsx | 152 +++++++++--------- 1 file changed, 80 insertions(+), 72 deletions(-) diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx index 72ca245f0144c..8431cba7ab190 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx @@ -165,7 +165,7 @@ export function useRangeCalendarRoot(parameters: useRangeCalendarRoot.Parameters } } - const { context } = useBuildRangeCalendarRootContext({ + const context = useBuildRangeCalendarRootContext({ baseContext, setValue, value, @@ -217,7 +217,7 @@ export namespace useRangeCalendarRoot { } } -function useBuildRangeCalendarRootContext(parameters: UseRangeCalendarDragEditingParameters) { +function useBuildRangeCalendarRootContext(parameters: UseBuildRangeCalendarRootContextParameters) { const { value, setValue, @@ -229,17 +229,21 @@ function useBuildRangeCalendarRootContext(parameters: UseRangeCalendarDragEditin disableDragEditing: disableDragEditingProp, disableHoverPreview: disableHoverPreviewProp, } = parameters; - const utils = useUtils(); - const [state, setState] = React.useState<{ - draggedSection: BaseCalendarSection | null; - targetDate: PickerValidDate | null; - hoveredDate: { value: PickerValidDate; section: BaseCalendarSection } | null; - }>({ draggedSection: null, targetDate: null, hoveredDate: null }); + const disableDragEditing = disableDragEditingProp || baseContext.disabled || baseContext.readOnly; + const disableHoverPreview = + disableHoverPreviewProp || baseContext.disabled || baseContext.readOnly; + const utils = useUtils(); const cellToDateMapRef = React.useRef(new Map()); - const registerCell = useEventCallback( - (element: HTMLElement, valueToRegister: PickerValidDate) => { + const [state, setState] = React.useState({ + draggedSection: null, + targetDate: null, + hoveredDate: null, + }); + + const registerCell: RangeCalendarRootContext['registerCell'] = useEventCallback( + (element, valueToRegister) => { cellToDateMapRef.current.set(element, valueToRegister); return () => { cellToDateMapRef.current.delete(element); @@ -247,44 +251,6 @@ function useBuildRangeCalendarRootContext(parameters: UseRangeCalendarDragEditin }, ); - const selectedRange = React.useMemo(() => { - if (!state.targetDate || state.draggedSection == null) { - return value; - } - - const roundedRange = getRoundedRange({ utils, range: value, section: state.draggedSection }); - if (roundedRange[0] == null || roundedRange[1] == null) { - return roundedRange; - } - - const rangeAfterDragAndDrop = calculateRangeChange({ - utils, - range: roundedRange, - newDate: state.targetDate, - rangePosition, - allowRangeFlip: true, - }).newRange; - - return getRoundedRange({ utils, range: rangeAfterDragAndDrop, section: state.draggedSection }); - }, [rangePosition, state.targetDate, state.draggedSection, utils, value]); - - const disableDragEditing = disableDragEditingProp || baseContext.disabled || baseContext.readOnly; - const disableHoverPreview = - disableHoverPreviewProp || baseContext.disabled || baseContext.readOnly; - - const draggedSectionRef = React.useRef(state.draggedSection); - useEnhancedEffect(() => { - draggedSectionRef.current = state.draggedSection; - }); - - const emptyDragImgRef = React.useRef(null); - React.useEffect(() => { - // Preload the image - required for Safari support: https://stackoverflow.com/a/40923520/3303436 - emptyDragImgRef.current = document.createElement('img'); - emptyDragImgRef.current.src = - 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; - }, []); - const startDragging: RangeCalendarRootContext['startDragging'] = useEventCallback( (position, section) => { setState((prev) => ({ ...prev, draggedSection: section })); @@ -292,7 +258,7 @@ function useBuildRangeCalendarRootContext(parameters: UseRangeCalendarDragEditin }, ); - const stopDragging = useEventCallback(() => { + const stopDragging: RangeCalendarRootContext['stopDragging'] = useEventCallback(() => { setState((prev) => ({ ...prev, draggedSection: null, @@ -301,26 +267,6 @@ function useBuildRangeCalendarRootContext(parameters: UseRangeCalendarDragEditin })); }); - const handleSetDragTarget = useEventCallback((valueOrElement: PickerValidDate | HTMLElement) => { - const newTargetDate = - valueOrElement instanceof HTMLElement - ? cellToDateMapRef.current.get(valueOrElement) - : valueOrElement; - - if (newTargetDate == null || utils.isEqual(newTargetDate, state.targetDate)) { - return; - } - - setState((prev) => ({ ...prev, targetDate: newTargetDate })); - - // TODO: Buggy - if (value[0] && utils.isBeforeDay(newTargetDate, value[0])) { - onRangePositionChange('start'); - } else if (value[1] && utils.isAfterDay(newTargetDate, value[1])) { - onRangePositionChange('end'); - } - }); - const selectDateFromDrag: RangeCalendarRootContext['selectDateFromDrag'] = useEventCallback( (valueOrElement) => { const selectedDate = @@ -354,6 +300,49 @@ function useBuildRangeCalendarRootContext(parameters: UseRangeCalendarDragEditin }, ); + const setDragTarget: RangeCalendarRootContext['setDragTarget'] = useEventCallback( + (valueOrElement) => { + const newTargetDate = + valueOrElement instanceof HTMLElement + ? cellToDateMapRef.current.get(valueOrElement) + : valueOrElement; + + if (newTargetDate == null || utils.isEqual(newTargetDate, state.targetDate)) { + return; + } + + setState((prev) => ({ ...prev, targetDate: newTargetDate })); + + // TODO: Buggy + if (value[0] && utils.isBeforeDay(newTargetDate, value[0])) { + onRangePositionChange('start'); + } else if (value[1] && utils.isAfterDay(newTargetDate, value[1])) { + onRangePositionChange('end'); + } + }, + ); + + const selectedRange = React.useMemo(() => { + if (!state.targetDate || state.draggedSection == null) { + return value; + } + + const roundedRange = getRoundedRange({ utils, range: value, section: state.draggedSection }); + if (roundedRange[0] == null || roundedRange[1] == null) { + return roundedRange; + } + + const rangeAfterDragAndDrop = calculateRangeChange({ + utils, + range: roundedRange, + newDate: state.targetDate, + rangePosition, + allowRangeFlip: true, + }).newRange; + + return getRoundedRange({ utils, range: rangeAfterDragAndDrop, section: state.draggedSection }); + }, [rangePosition, state.targetDate, state.draggedSection, utils, value]); + const previewRange = React.useMemo(() => { if (disableHoverPreview || state.hoveredDate == null) { return [null, null]; @@ -372,6 +361,19 @@ function useBuildRangeCalendarRootContext(parameters: UseRangeCalendarDragEditin }); }, [utils, rangePosition, state.hoveredDate, value, disableHoverPreview]); + const emptyDragImgRef = React.useRef(null); + React.useEffect(() => { + // Preload the image - required for Safari support: https://stackoverflow.com/a/40923520/3303436 + emptyDragImgRef.current = document.createElement('img'); + emptyDragImgRef.current.src = + 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; + }, []); + + const draggedSectionRef = React.useRef(state.draggedSection); + useEnhancedEffect(() => { + draggedSectionRef.current = state.draggedSection; + }); + const context: RangeCalendarRootContext = { value, selectedRange, @@ -382,15 +384,15 @@ function useBuildRangeCalendarRootContext(parameters: UseRangeCalendarDragEditin selectDateFromDrag, startDragging, stopDragging, - setDragTarget: handleSetDragTarget, + setDragTarget, setHoveredDate, registerCell, }; - return { context }; + return context; } -interface UseRangeCalendarDragEditingParameters { +interface UseBuildRangeCalendarRootContextParameters { value: PickerRangeValue; referenceDate: PickerValidDate; setValue: useBaseCalendarRoot.ReturnValue['setValue']; @@ -405,3 +407,9 @@ interface UseRangeCalendarDragEditingParameters { rangePosition: RangePosition; disableHoverPreview: boolean; } + +interface UseBuildRangeCalendarRootContextState { + draggedSection: BaseCalendarSection | null; + targetDate: PickerValidDate | null; + hoveredDate: { value: PickerValidDate; section: BaseCalendarSection } | null; +} From d862993b144ab7ccd15f826cde0dd2b8510dcb77 Mon Sep 17 00:00:00 2001 From: flavien Date: Tue, 14 Jan 2025 10:01:58 +0100 Subject: [PATCH 122/136] New demo disableHoverPreview --- .../DayRangeCalendarWithoutPreviewDemo.js | 80 +++++++++++++++++++ .../DayRangeCalendarWithoutPreviewDemo.tsx | 80 +++++++++++++++++++ .../base-calendar/MonthRangeCalendarDemo.js | 2 +- .../MonthRangeCalendarDemo.tsx.preview | 2 +- .../base-calendar/base-calendar.md | 16 ++++ .../RangeCalendar/root/RangeCalendarRoot.tsx | 11 ++- 6 files changed, 186 insertions(+), 5 deletions(-) create mode 100644 docs/data/date-pickers/base-calendar/DayRangeCalendarWithoutPreviewDemo.js create mode 100644 docs/data/date-pickers/base-calendar/DayRangeCalendarWithoutPreviewDemo.tsx diff --git a/docs/data/date-pickers/base-calendar/DayRangeCalendarWithoutPreviewDemo.js b/docs/data/date-pickers/base-calendar/DayRangeCalendarWithoutPreviewDemo.js new file mode 100644 index 0000000000000..e057dff9313a3 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/DayRangeCalendarWithoutPreviewDemo.js @@ -0,0 +1,80 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +// eslint-disable-next-line no-restricted-imports +import { + RangeCalendar, + useRangeCalendarContext, +} from '@mui/x-date-pickers-pro/internals/base/RangeCalendar'; +import styles from './calendar.module.css'; + +function Header() { + const { visibleDate } = useRangeCalendarContext(); + + return ( +
+ + ◀ + + {visibleDate.format('MMMM YYYY')} + + ▶ + +
+ ); +} + +export default function DayRangeCalendarWithoutPreviewDemo() { + return ( + + +
+ + + {({ days }) => + days.map((day) => ( + + )) + } + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => + days.map((day) => ( + + )) + } + + )) + } + + + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/DayRangeCalendarWithoutPreviewDemo.tsx b/docs/data/date-pickers/base-calendar/DayRangeCalendarWithoutPreviewDemo.tsx new file mode 100644 index 0000000000000..e057dff9313a3 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/DayRangeCalendarWithoutPreviewDemo.tsx @@ -0,0 +1,80 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +// eslint-disable-next-line no-restricted-imports +import { + RangeCalendar, + useRangeCalendarContext, +} from '@mui/x-date-pickers-pro/internals/base/RangeCalendar'; +import styles from './calendar.module.css'; + +function Header() { + const { visibleDate } = useRangeCalendarContext(); + + return ( +
+ + ◀ + + {visibleDate.format('MMMM YYYY')} + + ▶ + +
+ ); +} + +export default function DayRangeCalendarWithoutPreviewDemo() { + return ( + + +
+ + + {({ days }) => + days.map((day) => ( + + )) + } + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => + days.map((day) => ( + + )) + } + + )) + } + + + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.js b/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.js index edb9798dee215..a5a40d49616d5 100644 --- a/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.js +++ b/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.js @@ -41,7 +41,7 @@ export default function MonthRangeCalendarDemo() { months.map((month) => ( )) diff --git a/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.tsx.preview b/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.tsx.preview index 8dd3870ace494..4790caa27b3e0 100644 --- a/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.tsx.preview +++ b/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.tsx.preview @@ -5,7 +5,7 @@ months.map((month) => ( )) diff --git a/docs/data/date-pickers/base-calendar/base-calendar.md b/docs/data/date-pickers/base-calendar/base-calendar.md index d2f533c9d9697..101f35de312ea 100644 --- a/docs/data/date-pickers/base-calendar/base-calendar.md +++ b/docs/data/date-pickers/base-calendar/base-calendar.md @@ -179,6 +179,10 @@ Using the `getItems` prop instead of manually providing a list of `{children} +``` + +{{"demo": "DayRangeCalendarWithoutPreviewDemo.js", "defaultCodeOpen": false}} + ### Recipe: Booking UI The following demo shows a more advanced use case with lazy-loaded validation data: @@ -215,6 +227,10 @@ The following demo shows a more advanced use case with lazy-loaded validation da ## Month Range Calendar +:::warning +Work in progress, this is probably quite buggy +::: + ### Single visible year {{"demo": "MonthRangeCalendarDemo.js", "defaultCodeOpen": false}} diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRoot.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRoot.tsx index 90467de9dc05e..7d81b1d85243d 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRoot.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRoot.tsx @@ -44,9 +44,9 @@ const RangeCalendarRoot = React.forwardRef(function RangeCalendarRoot( disableFuture, shouldDisableDate, // Range position props - rangePosition: rangePositionProp, - defaultRangePosition: defaultRangePositionProp, - onRangePositionChange: onRangePositionChangeProp, + rangePosition, + defaultRangePosition, + onRangePositionChange, availableRangePositions, // Other range-specific parameters disableDragEditing, @@ -74,6 +74,11 @@ const RangeCalendarRoot = React.forwardRef(function RangeCalendarRoot( disableFuture, minDate, maxDate, + rangePosition, + defaultRangePosition, + onRangePositionChange, + disableDragEditing, + disableHoverPreview, }); const state: RangeCalendarRoot.State = React.useMemo( From 158846ee05930c63abb26a3927a52fc198e69aa2 Mon Sep 17 00:00:00 2001 From: flavien Date: Tue, 14 Jan 2025 13:15:42 +0100 Subject: [PATCH 123/136] Add years range picker --- .../base-calendar/DateRangeCalendarDemo.js | 2 +- .../base-calendar/DateRangeCalendarDemo.tsx | 2 +- .../DayRangeCalendarAirbnbRecipe.js | 1 - .../base-calendar/DayRangeCalendarDemo.js | 2 +- .../base-calendar/DayRangeCalendarDemo.tsx | 2 +- .../DayRangeCalendarWithoutPreviewDemo.js | 6 +- .../DayRangeCalendarWithoutPreviewDemo.tsx | 6 +- .../base-calendar/MonthRangeCalendarDemo.js | 2 +- .../base-calendar/MonthRangeCalendarDemo.tsx | 2 +- .../MonthRangeCalendarDemo.tsx.preview | 2 +- .../base-calendar/YearRangeCalendarDemo.js | 27 ++++ .../base-calendar/YearRangeCalendarDemo.tsx | 27 ++++ .../YearRangeCalendarDemo.tsx.preview | 13 ++ .../base-calendar/base-calendar.md | 10 ++ .../base-calendar/calendar.module.css | 10 +- .../days-cell/RangeCalendarDaysCell.tsx | 116 ++--------------- .../days-cell/useRangeCalendarDaysCell.tsx | 4 +- .../base/RangeCalendar/index.parts.ts | 3 +- .../months-cell/RangeCalendarMonthsCell.tsx | 114 +---------------- .../useRangeCalendarMonthsCell.tsx | 5 +- .../RangeCalendarMonthsGridCssVars.ts | 2 +- .../base/RangeCalendar/utils/rangeCell.ts | 118 ++++++++++++++++++ .../years-cell/RangeCalendarYearsCell.tsx | 51 +++----- .../RangeCalendarYearsCellDataAttributes.ts | 32 ++++- .../useRangeCalendarMonthsCellWrapper.ts | 36 ++++++ .../years-cell/useRangeCalendarYearsCell.tsx | 35 ++++++ .../years-grid/RangeCalendarYearsGrid.tsx | 2 +- .../Calendar/days-cell/CalendarDaysCell.tsx | 6 +- .../months-cell/CalendarMonthsCell.tsx | 6 +- .../Calendar/years-cell/CalendarYearsCell.tsx | 8 +- .../days-cell/useBaseCalendarDaysCell.ts | 9 +- .../useBaseCalendarDaysCellWrapper.ts | 4 + .../months-cell/useBaseCalendarMonthsCell.ts | 9 +- .../useBaseCalendarMonthsCellWrapper.ts | 5 +- .../years-cell/useBaseCalendarYearsCell.ts | 11 +- .../useBaseCalendarYearsCellWrapper.ts | 5 +- 36 files changed, 385 insertions(+), 310 deletions(-) create mode 100644 docs/data/date-pickers/base-calendar/YearRangeCalendarDemo.js create mode 100644 docs/data/date-pickers/base-calendar/YearRangeCalendarDemo.tsx create mode 100644 docs/data/date-pickers/base-calendar/YearRangeCalendarDemo.tsx.preview create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/rangeCell.ts create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-cell/useRangeCalendarMonthsCellWrapper.ts create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-cell/useRangeCalendarYearsCell.tsx diff --git a/docs/data/date-pickers/base-calendar/DateRangeCalendarDemo.js b/docs/data/date-pickers/base-calendar/DateRangeCalendarDemo.js index 65b0bd0c6c762..c4b317d514bc2 100644 --- a/docs/data/date-pickers/base-calendar/DateRangeCalendarDemo.js +++ b/docs/data/date-pickers/base-calendar/DateRangeCalendarDemo.js @@ -84,7 +84,7 @@ export default function DateRangeCalendarDemo() { return ( - +
- +
- +
diff --git a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx index 87c21203d3a60..7274228d02023 100644 --- a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx +++ b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx @@ -34,7 +34,7 @@ function Header() { export default function DayRangeCalendarDemo() { return ( - +
diff --git a/docs/data/date-pickers/base-calendar/DayRangeCalendarWithoutPreviewDemo.js b/docs/data/date-pickers/base-calendar/DayRangeCalendarWithoutPreviewDemo.js index e057dff9313a3..04410cdb400ce 100644 --- a/docs/data/date-pickers/base-calendar/DayRangeCalendarWithoutPreviewDemo.js +++ b/docs/data/date-pickers/base-calendar/DayRangeCalendarWithoutPreviewDemo.js @@ -34,11 +34,7 @@ function Header() { export default function DayRangeCalendarWithoutPreviewDemo() { return ( - +
diff --git a/docs/data/date-pickers/base-calendar/DayRangeCalendarWithoutPreviewDemo.tsx b/docs/data/date-pickers/base-calendar/DayRangeCalendarWithoutPreviewDemo.tsx index e057dff9313a3..04410cdb400ce 100644 --- a/docs/data/date-pickers/base-calendar/DayRangeCalendarWithoutPreviewDemo.tsx +++ b/docs/data/date-pickers/base-calendar/DayRangeCalendarWithoutPreviewDemo.tsx @@ -34,11 +34,7 @@ function Header() { export default function DayRangeCalendarWithoutPreviewDemo() { return ( - +
diff --git a/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.js b/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.js index a5a40d49616d5..ddddc14041b16 100644 --- a/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.js +++ b/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.js @@ -34,7 +34,7 @@ function Header() { export default function MonthRangeCalendarDemo() { return ( - +
{({ months }) => diff --git a/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.tsx b/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.tsx index a5a40d49616d5..ddddc14041b16 100644 --- a/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.tsx +++ b/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.tsx @@ -34,7 +34,7 @@ function Header() { export default function MonthRangeCalendarDemo() { return ( - +
{({ months }) => diff --git a/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.tsx.preview b/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.tsx.preview index 4790caa27b3e0..0657052bc9a13 100644 --- a/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.tsx.preview +++ b/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.tsx.preview @@ -1,4 +1,4 @@ - +
{({ months }) => diff --git a/docs/data/date-pickers/base-calendar/YearRangeCalendarDemo.js b/docs/data/date-pickers/base-calendar/YearRangeCalendarDemo.js new file mode 100644 index 0000000000000..cff02a53f2721 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/YearRangeCalendarDemo.js @@ -0,0 +1,27 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +// eslint-disable-next-line no-restricted-imports +import { RangeCalendar } from '@mui/x-date-pickers-pro/internals/base/RangeCalendar'; +import styles from './calendar.module.css'; + +export default function YearRangeCalendarDemo() { + return ( + + + + {({ years }) => + years.map((year) => ( + + )) + } + + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/YearRangeCalendarDemo.tsx b/docs/data/date-pickers/base-calendar/YearRangeCalendarDemo.tsx new file mode 100644 index 0000000000000..cff02a53f2721 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/YearRangeCalendarDemo.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +// eslint-disable-next-line no-restricted-imports +import { RangeCalendar } from '@mui/x-date-pickers-pro/internals/base/RangeCalendar'; +import styles from './calendar.module.css'; + +export default function YearRangeCalendarDemo() { + return ( + + + + {({ years }) => + years.map((year) => ( + + )) + } + + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/YearRangeCalendarDemo.tsx.preview b/docs/data/date-pickers/base-calendar/YearRangeCalendarDemo.tsx.preview new file mode 100644 index 0000000000000..3c0a3d7d32c8d --- /dev/null +++ b/docs/data/date-pickers/base-calendar/YearRangeCalendarDemo.tsx.preview @@ -0,0 +1,13 @@ + + + {({ years }) => + years.map((year) => ( + + )) + } + + \ No newline at end of file diff --git a/docs/data/date-pickers/base-calendar/base-calendar.md b/docs/data/date-pickers/base-calendar/base-calendar.md index 101f35de312ea..ec435b3f3c5fa 100644 --- a/docs/data/date-pickers/base-calendar/base-calendar.md +++ b/docs/data/date-pickers/base-calendar/base-calendar.md @@ -239,6 +239,16 @@ Work in progress, this is probably quite buggy TODO +## Year Range Calendar + +:::warning +Work in progress, this is probably quite buggy +::: + +### Single visible year + +{{"demo": "YearRangeCalendarDemo.js", "defaultCodeOpen": false}} + ## Date Range Calendar {{"demo": "DateRangeCalendarDemo.js", "defaultCodeOpen": false}} diff --git a/docs/data/date-pickers/base-calendar/calendar.module.css b/docs/data/date-pickers/base-calendar/calendar.module.css index 92fcdec208846..a3c6821eff7d2 100644 --- a/docs/data/date-pickers/base-calendar/calendar.module.css +++ b/docs/data/date-pickers/base-calendar/calendar.module.css @@ -130,11 +130,17 @@ } .MonthsGrid { - grid-template-columns: repeat(var(--calendar-months-grid-cells-per-row), 1fr); + grid-template-columns: repeat( + var(--calendar-months-grid-cells-per-row, var(--range-calendar-months-grid-cells-per-row)), + 1fr + ); } .YearsGrid { - grid-template-columns: repeat(var(--calendar-years-grid-cells-per-row), 1fr); + grid-template-columns: repeat( + var(--calendar-years-grid-cells-per-row, var(--range-calendar-years-grid-cells-per-row)), + 1fr + ); } .MonthsCell, diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/RangeCalendarDaysCell.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/RangeCalendarDaysCell.tsx index b13b642575549..2f3e9bba92e3c 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/RangeCalendarDaysCell.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/RangeCalendarDaysCell.tsx @@ -9,41 +9,10 @@ import { useComponentRenderer } from '@mui/x-date-pickers/internals/base/base-ut import { RangeCalendarDaysCellDataAttributes } from './RangeCalendarDaysCellDataAttributes'; import { useRangeCalendarDaysCell } from './useRangeCalendarDaysCell'; import { useRangeCalendarDaysCellWrapper } from './useRangeCalendarDaysCellWrapper'; +import { RangeCellState, rangeCellStyleHookMapping, useRangeCellState } from '../utils/rangeCell'; const customStyleHookMapping: CustomStyleHookMapping = { - selected(value) { - return value ? { [RangeCalendarDaysCellDataAttributes.selected]: '' } : null; - }, - selectionStart(value) { - return value ? { [RangeCalendarDaysCellDataAttributes.selectionStart]: '' } : null; - }, - selectionEnd(value) { - return value ? { [RangeCalendarDaysCellDataAttributes.selectionEnd]: '' } : null; - }, - insideSelection(value) { - return value ? { [RangeCalendarDaysCellDataAttributes.insideSelection]: '' } : null; - }, - previewed(value) { - return value ? { [RangeCalendarDaysCellDataAttributes.previewed]: '' } : null; - }, - previewStart(value) { - return value ? { [RangeCalendarDaysCellDataAttributes.previewStart]: '' } : null; - }, - previewEnd(value) { - return value ? { [RangeCalendarDaysCellDataAttributes.previewEnd]: '' } : null; - }, - insidePreview(value) { - return value ? { [RangeCalendarDaysCellDataAttributes.insidePreview]: '' } : null; - }, - disabled(value) { - return value ? { [RangeCalendarDaysCellDataAttributes.disabled]: '' } : null; - }, - invalid(value) { - return value ? { [RangeCalendarDaysCellDataAttributes.invalid]: '' } : null; - }, - current(value) { - return value ? { [RangeCalendarDaysCellDataAttributes.current]: '' } : null; - }, + ...rangeCellStyleHookMapping, outsideMonth(value) { return value ? { [RangeCalendarDaysCellDataAttributes.outsideMonth]: '' } : null; }, @@ -54,35 +23,12 @@ const InnerRangeCalendarDaysCell = React.forwardRef(function RangeCalendarDaysGr forwardedRef: React.ForwardedRef, ) { const { className, render, value, ctx, ...otherProps } = props; - const { getDaysCellProps, isCurrent } = useRangeCalendarDaysCell({ value, ctx }); + const { getDaysCellProps } = useRangeCalendarDaysCell({ value, ctx }); - const state: RangeCalendarDaysCell.State = React.useMemo( - () => ({ - selected: ctx.isSelected, - selectionStart: ctx.isSelectionStart, - selectionEnd: ctx.isSelectionEnd, - insideSelection: ctx.isSelected && !ctx.isSelectionStart && !ctx.isSelectionEnd, - previewed: ctx.isPreviewed, - previewStart: ctx.isPreviewStart, - previewEnd: ctx.isPreviewEnd, - insidePreview: ctx.isPreviewed && !ctx.isPreviewStart && !ctx.isPreviewEnd, - disabled: ctx.isDisabled, - invalid: ctx.isInvalid, - outsideMonth: ctx.isOutsideCurrentMonth, - current: isCurrent, - }), - [ - ctx.isSelected, - ctx.isSelectionStart, - ctx.isSelectionEnd, - ctx.isPreviewed, - ctx.isPreviewStart, - ctx.isPreviewEnd, - ctx.isDisabled, - ctx.isInvalid, - ctx.isOutsideCurrentMonth, - isCurrent, - ], + const cellState = useRangeCellState(ctx); + const state = React.useMemo( + () => ({ ...cellState, outsideMonth: ctx.isOutsideCurrentMonth }), + [cellState, ctx.isOutsideCurrentMonth], ); const { renderElement } = useComponentRenderer({ @@ -110,53 +56,9 @@ const RangeCalendarDaysCell = React.forwardRef(function RangeCalendarDaysCell( }); export namespace RangeCalendarDaysCell { - export interface State { - /** - * Whether the day is within the selected range. - */ - selected: boolean; - /** - * Whether the day is the first day of the selected range. - */ - selectionStart: boolean; - /** - * Whether the day is the last day of the selected range. - */ - selectionEnd: boolean; - /** - * Whether the day is within the selected range and is not its first or last day. - */ - insideSelection: boolean; - /** - * Whether the day is within the preview range and is not its first or last day. - */ - previewed: boolean; - /** - * Whether the day is the first day of the preview range. - */ - previewStart: boolean; - /** - * Whether the day is the last day of the preview range. - */ - previewEnd: boolean; - /** - * Whether the day is within the preview range and is not its first or last day. - */ - insidePreview: boolean; - /** - * Whether the day is disabled. - */ - disabled: boolean; - /** - * Whether the day is invalid. - */ - invalid: boolean; - /** - * Whether the day contains the current date. - */ - current: boolean; + export interface State extends RangeCellState { /** - * Whether the day is outside the month rendered by the day grid wrapping it. + * Whether the cell is outside the month rendered by the day grid wrapping it. */ outsideMonth: boolean; } diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx index 102775a4fe06b..593bb9def66c7 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx @@ -11,7 +11,7 @@ export function useRangeCalendarDaysCell(parameters: useRangeCalendarDaysCell.Pa const { ctx, value } = parameters; const rangeCellProps = useRangeCell({ ctx, value, section: 'day' }); - const { getDaysCellProps: getBaseDaysCellProps, isCurrent } = useBaseCalendarDaysCell(parameters); + const { getDaysCellProps: getBaseDaysCellProps } = useBaseCalendarDaysCell(parameters); const getDaysCellProps = React.useCallback( (externalProps: GenericHTMLProps) => { @@ -20,7 +20,7 @@ export function useRangeCalendarDaysCell(parameters: useRangeCalendarDaysCell.Pa [rangeCellProps, getBaseDaysCellProps], ); - return React.useMemo(() => ({ getDaysCellProps, isCurrent }), [getDaysCellProps, isCurrent]); + return React.useMemo(() => ({ getDaysCellProps }), [getDaysCellProps]); } export namespace useRangeCalendarDaysCell { diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/index.parts.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/index.parts.ts index fe142d96f248c..5b64f4cdaa3b4 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/index.parts.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/index.parts.ts @@ -16,8 +16,7 @@ export { RangeCalendarMonthsCell as MonthsCell } from './months-cell/RangeCalend // // Years export { RangeCalendarYearsList as YearsList } from './years-list/RangeCalendarYearsList'; export { RangeCalendarYearsGrid as YearsGrid } from './years-grid/RangeCalendarYearsGrid'; -// TODO: Uncomment when the component supports good range editing -// export { RangeCalendarYearsCell as YearsCell } from './years-cell/RangeCalendarYearsCell'; +export { RangeCalendarYearsCell as YearsCell } from './years-cell/RangeCalendarYearsCell'; // Navigation export { RangeCalendarSetVisibleMonth as SetVisibleMonth } from './set-visible-month/RangeCalendarSetVisibleMonth'; diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/RangeCalendarMonthsCell.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/RangeCalendarMonthsCell.tsx index 4c984e07d5817..b8efea4065b00 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/RangeCalendarMonthsCell.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/RangeCalendarMonthsCell.tsx @@ -8,44 +8,10 @@ import { CustomStyleHookMapping } from '@mui/x-date-pickers/internals/base/base- import { useComponentRenderer } from '@mui/x-date-pickers/internals/base/base-utils/useComponentRenderer'; import { useRangeCalendarMonthsCell } from './useRangeCalendarMonthsCell'; import { useRangeCalendarMonthsCellWrapper } from './useRangeCalendarMonthsCellWrapper'; -import { RangeCalendarMonthsCellDataAttributes } from './RangeCalendarMonthsCellDataAttributes'; -// eslint-disable-next-line no-restricted-imports +import { RangeCellState, rangeCellStyleHookMapping, useRangeCellState } from '../utils/rangeCell'; -// TODO: Avoid duplication between day, month, and year cells const customStyleHookMapping: CustomStyleHookMapping = { - selected(value) { - return value ? { [RangeCalendarMonthsCellDataAttributes.selected]: '' } : null; - }, - selectionStart(value) { - return value ? { [RangeCalendarMonthsCellDataAttributes.selectionStart]: '' } : null; - }, - selectionEnd(value) { - return value ? { [RangeCalendarMonthsCellDataAttributes.selectionEnd]: '' } : null; - }, - insideSelection(value) { - return value ? { [RangeCalendarMonthsCellDataAttributes.insideSelection]: '' } : null; - }, - previewed(value) { - return value ? { [RangeCalendarMonthsCellDataAttributes.previewed]: '' } : null; - }, - previewStart(value) { - return value ? { [RangeCalendarMonthsCellDataAttributes.previewStart]: '' } : null; - }, - previewEnd(value) { - return value ? { [RangeCalendarMonthsCellDataAttributes.previewEnd]: '' } : null; - }, - insidePreview(value) { - return value ? { [RangeCalendarMonthsCellDataAttributes.insidePreview]: '' } : null; - }, - disabled(value) { - return value ? { [RangeCalendarMonthsCellDataAttributes.disabled]: '' } : null; - }, - invalid(value) { - return value ? { [RangeCalendarMonthsCellDataAttributes.invalid]: '' } : null; - }, - current(value) { - return value ? { [RangeCalendarMonthsCellDataAttributes.current]: '' } : null; - }, + ...rangeCellStyleHookMapping, }; const InnerRangeCalendarMonthsCell = React.forwardRef(function InnerRangeCalendarMonthsCell( @@ -53,34 +19,9 @@ const InnerRangeCalendarMonthsCell = React.forwardRef(function InnerRangeCalenda forwardedRef: React.ForwardedRef, ) { const { className, render, value, format, ctx, ...otherProps } = props; - const { getMonthsCellProps, isCurrent } = useRangeCalendarMonthsCell({ value, format, ctx }); + const { getMonthsCellProps } = useRangeCalendarMonthsCell({ value, format, ctx }); - const state: RangeCalendarMonthsCell.State = React.useMemo( - () => ({ - selected: ctx.isSelected, - selectionStart: ctx.isSelectionStart, - selectionEnd: ctx.isSelectionEnd, - insideSelection: ctx.isSelected && !ctx.isSelectionStart && !ctx.isSelectionEnd, - previewed: ctx.isPreviewed, - previewStart: ctx.isPreviewStart, - previewEnd: ctx.isPreviewEnd, - insidePreview: ctx.isPreviewed && !ctx.isPreviewStart && !ctx.isPreviewEnd, - disabled: ctx.isDisabled, - invalid: ctx.isInvalid, - current: isCurrent, - }), - [ - ctx.isSelected, - ctx.isSelectionStart, - ctx.isSelectionEnd, - ctx.isPreviewed, - ctx.isPreviewStart, - ctx.isPreviewEnd, - ctx.isDisabled, - ctx.isInvalid, - isCurrent, - ], - ); + const state: RangeCalendarMonthsCell.State = useRangeCellState(ctx); const { renderElement } = useComponentRenderer({ propGetter: getMonthsCellProps, @@ -107,52 +48,7 @@ const RangeCalendarMonthsCell = React.forwardRef(function RangeCalendarMonthsCel }); export namespace RangeCalendarMonthsCell { - export interface State { - /** - * Whether the month is within the selected range. - */ - selected: boolean; - /** - * Whether the month is the first month of the selected range. - */ - selectionStart: boolean; - /** - * Whether the month is the last month of the selected range. - */ - selectionEnd: boolean; - /** - * Whether the month is within the selected range and is not its first or last month. - */ - insideSelection: boolean; - /** - * Whether the month is within the preview range and is not its first or last month. - */ - previewed: boolean; - /** - * Whether the month is the first month of the preview range. - */ - previewStart: boolean; - /** - * Whether the month is the last month of the preview range. - */ - previewEnd: boolean; - /** - * Whether the month is within the preview range and is not its first or last month. - */ - insidePreview: boolean; - /** - * Whether the month is disabled. - */ - disabled: boolean; - /** - * Whether the month is invalid. - */ - invalid: boolean; - /** - * Whether the month contains the current date. - */ - current: boolean; - } + export interface State extends RangeCellState {} export interface Props extends Omit, diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/useRangeCalendarMonthsCell.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/useRangeCalendarMonthsCell.tsx index c183ddf23ba6f..8bfe58faa9287 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/useRangeCalendarMonthsCell.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/useRangeCalendarMonthsCell.tsx @@ -11,8 +11,7 @@ export function useRangeCalendarMonthsCell(parameters: useRangeCalendarMonthsCel const { ctx, value } = parameters; const rangeCellProps = useRangeCell({ ctx, value, section: 'month' }); - const { getMonthsCellProps: getBaseMonthsCellProps, isCurrent } = - useBaseCalendarMonthsCell(parameters); + const { getMonthsCellProps: getBaseMonthsCellProps } = useBaseCalendarMonthsCell(parameters); const getMonthsCellProps = React.useCallback( (externalProps: GenericHTMLProps) => { @@ -21,7 +20,7 @@ export function useRangeCalendarMonthsCell(parameters: useRangeCalendarMonthsCel [rangeCellProps, getBaseMonthsCellProps], ); - return React.useMemo(() => ({ getMonthsCellProps, isCurrent }), [getMonthsCellProps, isCurrent]); + return React.useMemo(() => ({ getMonthsCellProps }), [getMonthsCellProps]); } export namespace useRangeCalendarMonthsCell { diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-grid/RangeCalendarMonthsGridCssVars.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-grid/RangeCalendarMonthsGridCssVars.ts index af581a313e242..233602570c241 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-grid/RangeCalendarMonthsGridCssVars.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-grid/RangeCalendarMonthsGridCssVars.ts @@ -2,5 +2,5 @@ export enum RangeCalendarMonthsGridCssVars { /** * The number of cells per row in the grid. */ - calendarMonthsGridCellsPerRow = '--calendar-months-grid-cells-per-row', + calendarMonthsGridCellsPerRow = '--range-calendar-months-grid-cells-per-row', } diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/rangeCell.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/rangeCell.ts new file mode 100644 index 0000000000000..5d9ad97eb6e84 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/rangeCell.ts @@ -0,0 +1,118 @@ +import * as React from 'react'; +// eslint-disable-next-line no-restricted-imports +import { CustomStyleHookMapping } from '@mui/x-date-pickers/internals/base/base-utils/getStyleHookProps'; +import { useRangeCell } from './useRangeCell'; + +export const rangeCellStyleHookMapping: CustomStyleHookMapping = { + selected(value) { + return value ? { 'data-selected': '' } : null; + }, + selectionStart(value) { + return value ? { 'data-selection-start': '' } : null; + }, + selectionEnd(value) { + return value ? { 'data-selection-end': '' } : null; + }, + insideSelection(value) { + return value ? { 'data-inside-selection': '' } : null; + }, + previewed(value) { + return value ? { 'data-previewed': '' } : null; + }, + previewStart(value) { + return value ? { 'data-preview-start': '' } : null; + }, + previewEnd(value) { + return value ? { 'data-preview-end': '' } : null; + }, + insidePreview(value) { + return value ? { 'data-inside-preview': '' } : null; + }, + disabled(value) { + return value ? { 'data-disabled': '' } : null; + }, + invalid(value) { + return value ? { 'data-invalid': '' } : null; + }, + current(value) { + return value ? { 'data-current': '' } : null; + }, +}; + +export function useRangeCellState( + ctx: useRangeCell.Context & { isDisabled: boolean; isInvalid: boolean; isCurrent: boolean }, +): RangeCellState { + return React.useMemo( + () => ({ + selected: ctx.isSelected, + selectionStart: ctx.isSelectionStart, + selectionEnd: ctx.isSelectionEnd, + insideSelection: ctx.isSelected && !ctx.isSelectionStart && !ctx.isSelectionEnd, + previewed: ctx.isPreviewed, + previewStart: ctx.isPreviewStart, + previewEnd: ctx.isPreviewEnd, + insidePreview: ctx.isPreviewed && !ctx.isPreviewStart && !ctx.isPreviewEnd, + disabled: ctx.isDisabled, + invalid: ctx.isInvalid, + current: ctx.isCurrent, + }), + [ + ctx.isSelected, + ctx.isSelectionStart, + ctx.isSelectionEnd, + ctx.isPreviewed, + ctx.isPreviewStart, + ctx.isPreviewEnd, + ctx.isDisabled, + ctx.isInvalid, + ctx.isCurrent, + ], + ); +} + +export interface RangeCellState { + /** + * Whether the cell is within the selected range. + */ + selected: boolean; + /** + * Whether the cell is the first cell of the selected range. + */ + selectionStart: boolean; + /** + * Whether the cell is the last cell of the selected range. + */ + selectionEnd: boolean; + /** + * Whether the cell is within the selected range and is not its first or last cell. + */ + insideSelection: boolean; + /** + * Whether the cell is within the preview range and is not its first or last cell. + */ + previewed: boolean; + /** + * Whether the cell is the first cell of the preview range. + */ + previewStart: boolean; + /** + * Whether the cell is the last cell of the preview range. + */ + previewEnd: boolean; + /** + * Whether the cell is within the preview range and is not its first or last cell. + */ + insidePreview: boolean; + /** + * Whether the cell is disabled. + */ + disabled: boolean; + /** + * Whether the cell is invalid. + */ + invalid: boolean; + /** + * Whether the cell contains the current date. + */ + current: boolean; +} diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-cell/RangeCalendarYearsCell.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-cell/RangeCalendarYearsCell.tsx index b117cafee862d..4f8d03658c295 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-cell/RangeCalendarYearsCell.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-cell/RangeCalendarYearsCell.tsx @@ -5,34 +5,32 @@ import { BaseUIComponentProps } from '@mui/x-date-pickers/internals/base/base-ut // eslint-disable-next-line no-restricted-imports import { useComponentRenderer } from '@mui/x-date-pickers/internals/base/base-utils/useComponentRenderer'; // eslint-disable-next-line no-restricted-imports -import { useBaseCalendarYearsCell } from '@mui/x-date-pickers/internals/base/utils/base-calendar/years-cell/useBaseCalendarYearsCell'; -// eslint-disable-next-line no-restricted-imports -import { useBaseCalendarYearsCellWrapper } from '@mui/x-date-pickers/internals/base/utils/base-calendar/years-cell/useBaseCalendarYearsCellWrapper'; +import { CustomStyleHookMapping } from '@mui/x-date-pickers/internals/base/base-utils/getStyleHookProps'; +import { RangeCellState, rangeCellStyleHookMapping, useRangeCellState } from '../utils/rangeCell'; +import { useRangeCalendarYearsCell } from './useRangeCalendarYearsCell'; +import { useRangeCalendarYearsCellWrapper } from './useRangeCalendarMonthsCellWrapper'; + +const customStyleHookMapping: CustomStyleHookMapping = { + ...rangeCellStyleHookMapping, +}; const InnerRangeCalendarYearsCell = React.forwardRef(function InnerRangeCalendarYearsCell( props: InnerRangeCalendarYearsCellProps, forwardedRef: React.ForwardedRef, ) { const { className, render, value, format, ctx, ...otherProps } = props; - const { getYearCellProps, isCurrent } = useBaseCalendarYearsCell({ value, format, ctx }); + const { getYearsCellProps } = useRangeCalendarYearsCell({ value, format, ctx }); - const state: RangeCalendarYearsCell.State = React.useMemo( - () => ({ - selected: ctx.isSelected, - disabled: ctx.isDisabled, - invalid: ctx.isInvalid, - current: isCurrent, - }), - [ctx.isSelected, ctx.isDisabled, ctx.isInvalid, isCurrent], - ); + const state: RangeCalendarYearsCell.State = useRangeCellState(ctx); const { renderElement } = useComponentRenderer({ - propGetter: getYearCellProps, + propGetter: getYearsCellProps, render: render ?? 'button', ref: forwardedRef, className, state, extraProps: otherProps, + customStyleHookMapping, }); return renderElement(); @@ -44,38 +42,21 @@ const RangeCalendarYearsCell = React.forwardRef(function RangeCalendarsYearCell( props: RangeCalendarYearsCell.Props, forwardedRef: React.ForwardedRef, ) { - const { ref, ctx } = useBaseCalendarYearsCellWrapper({ value: props.value, forwardedRef }); + const { ref, ctx } = useRangeCalendarYearsCellWrapper({ value: props.value, forwardedRef }); return ; }); export namespace RangeCalendarYearsCell { - export interface State { - /** - * Whether the year is selected. - */ - selected: boolean; - /** - * Whether the year is disabled. - */ - disabled: boolean; - /** - * Whether the year is invalid. - */ - invalid: boolean; - /** - * Whether the year contains the current date. - */ - current: boolean; - } + export interface State extends RangeCellState {} export interface Props - extends Omit, + extends Omit, Omit, 'value'> {} } interface InnerRangeCalendarYearsCellProps - extends useBaseCalendarYearsCell.Parameters, + extends useRangeCalendarYearsCell.Parameters, Omit, 'value'> {} export { RangeCalendarYearsCell }; diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-cell/RangeCalendarYearsCellDataAttributes.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-cell/RangeCalendarYearsCellDataAttributes.ts index 3034cbe61b274..59157d07bb354 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-cell/RangeCalendarYearsCellDataAttributes.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-cell/RangeCalendarYearsCellDataAttributes.ts @@ -1,8 +1,36 @@ export enum RangeCalendarYearsCellDataAttributes { /** - * Present when the year is selected. + * Present when the year is within the selected range. */ selected = 'data-selected', + /** + * Present when the year is the first year of the selected range. + */ + selectionStart = 'data-selection-start', + /** + * Present when the year is the last year of the selected range. + */ + selectionEnd = 'data-selection-end', + /** + * Present when the year is within the selected range and is not its first or last year. + */ + insideSelection = 'data-inside-selection', + /** + * Present when the year is within the preview range. + */ + previewed = 'data-previewed', + /** + * Present when the year is the first year of the preview range. + */ + previewStart = 'data-preview-start', + /** + * Present when the year is the last year of the preview range. + */ + previewEnd = 'data-preview-end', + /** + * Present when the year is within the preview range and is not its first or last year. + */ + insidePreview = 'data-inside-preview', /** * Present when the year is disabled. */ @@ -12,7 +40,7 @@ export enum RangeCalendarYearsCellDataAttributes { */ invalid = 'data-invalid', /** - * Present when the year contains the current date. + * Present when the year is the current date. */ current = 'data-current', } diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-cell/useRangeCalendarMonthsCellWrapper.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-cell/useRangeCalendarMonthsCellWrapper.ts new file mode 100644 index 0000000000000..4f487bc427cd4 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-cell/useRangeCalendarMonthsCellWrapper.ts @@ -0,0 +1,36 @@ +import * as React from 'react'; +import useForkRef from '@mui/utils/useForkRef'; +// eslint-disable-next-line no-restricted-imports +import { useBaseCalendarYearsCellWrapper } from '@mui/x-date-pickers/internals/base/utils/base-calendar/years-cell/useBaseCalendarYearsCellWrapper'; +import type { useRangeCalendarYearsCell } from './useRangeCalendarYearsCell'; +import { useRangeCellWrapper } from '../utils/useRangeCellWrapper'; + +export function useRangeCalendarYearsCellWrapper( + parameters: useRangeCalendarYearsCellWrapper.Parameters, +): useRangeCalendarYearsCellWrapper.ReturnValue { + const { value } = parameters; + const { ref: baseRef, ctx: baseCtx } = useBaseCalendarYearsCellWrapper(parameters); + const { cellRef, ctx: rangeCellCtx } = useRangeCellWrapper({ value, section: 'year' }); + const ref = useForkRef(baseRef, cellRef); + + const ctx = React.useMemo( + () => ({ + ...baseCtx, + ...rangeCellCtx, + }), + [baseCtx, rangeCellCtx], + ); + + return { ref, ctx }; +} + +export namespace useRangeCalendarYearsCellWrapper { + export interface Parameters extends useBaseCalendarYearsCellWrapper.Parameters {} + + export interface ReturnValue extends Omit { + /** + * The memoized context to forward to the memoized component so that it does not need to subscribe to any context. + */ + ctx: useRangeCalendarYearsCell.Context; + } +} diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-cell/useRangeCalendarYearsCell.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-cell/useRangeCalendarYearsCell.tsx new file mode 100644 index 0000000000000..65b136bc7ce91 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-cell/useRangeCalendarYearsCell.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +// eslint-disable-next-line no-restricted-imports +import { useBaseCalendarYearsCell } from '@mui/x-date-pickers/internals/base/utils/base-calendar/years-cell/useBaseCalendarYearsCell'; +// eslint-disable-next-line no-restricted-imports +import { GenericHTMLProps } from '@mui/x-date-pickers/internals/base/base-utils/types'; +// eslint-disable-next-line no-restricted-imports +import { mergeReactProps } from '@mui/x-date-pickers/internals/base/base-utils/mergeReactProps'; +import { useRangeCell } from '../utils/useRangeCell'; + +export function useRangeCalendarYearsCell(parameters: useRangeCalendarYearsCell.Parameters) { + const { ctx, value } = parameters; + const rangeCellProps = useRangeCell({ ctx, value, section: 'year' }); + + const { getYearsCellProps: getBaseYearsCellProps } = useBaseCalendarYearsCell(parameters); + + const getYearsCellProps = React.useCallback( + (externalProps: GenericHTMLProps) => { + return mergeReactProps(externalProps, rangeCellProps, getBaseYearsCellProps(externalProps)); + }, + [rangeCellProps, getBaseYearsCellProps], + ); + + return React.useMemo(() => ({ getYearsCellProps }), [getYearsCellProps]); +} + +export namespace useRangeCalendarYearsCell { + export interface Parameters extends useBaseCalendarYearsCell.Parameters { + /** + * The memoized context forwarded by the wrapper component so that this component does not need to subscribe to any context. + */ + ctx: Context; + } + + export interface Context extends useBaseCalendarYearsCell.Context, useRangeCell.Context {} +} diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-grid/RangeCalendarYearsGrid.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-grid/RangeCalendarYearsGrid.tsx index 4aae3f48d8f98..27a2251582a32 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-grid/RangeCalendarYearsGrid.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-grid/RangeCalendarYearsGrid.tsx @@ -50,7 +50,7 @@ export namespace RangeCalendarYearsGrid { export interface Props extends Omit, 'children'>, - useBaseCalendarYearsGrid.Parameters {} + useBaseCalendarYearsGrid.PublicParameters {} } export { RangeCalendarYearsGrid }; diff --git a/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx index ea03b5deb3d2b..febad223e2d1c 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/days-cell/CalendarDaysCell.tsx @@ -30,7 +30,7 @@ const InnerCalendarDaysCell = React.forwardRef(function CalendarDaysGrid( forwardedRef: React.ForwardedRef, ) { const { className, render, value, ctx, ...otherProps } = props; - const { getDaysCellProps, isCurrent } = useBaseCalendarDaysCell({ value, ctx }); + const { getDaysCellProps } = useBaseCalendarDaysCell({ value, ctx }); const state: CalendarDaysCell.State = React.useMemo( () => ({ @@ -38,9 +38,9 @@ const InnerCalendarDaysCell = React.forwardRef(function CalendarDaysGrid( disabled: ctx.isDisabled, invalid: ctx.isInvalid, outsideMonth: ctx.isOutsideCurrentMonth, - current: isCurrent, + current: ctx.isCurrent, }), - [ctx.isSelected, ctx.isDisabled, ctx.isInvalid, ctx.isOutsideCurrentMonth, isCurrent], + [ctx.isSelected, ctx.isDisabled, ctx.isInvalid, ctx.isOutsideCurrentMonth, ctx.isCurrent], ); const { renderElement } = useComponentRenderer({ diff --git a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx index 375964fa5159e..bfda869dcc450 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/months-cell/CalendarMonthsCell.tsx @@ -10,16 +10,16 @@ const InnerCalendarMonthsCell = React.forwardRef(function InnerCalendarMonthsCel forwardedRef: React.ForwardedRef, ) { const { className, render, value, format, ctx, ...otherProps } = props; - const { getMonthsCellProps, isCurrent } = useBaseCalendarMonthsCell({ value, format, ctx }); + const { getMonthsCellProps } = useBaseCalendarMonthsCell({ value, format, ctx }); const state: CalendarMonthsCell.State = React.useMemo( () => ({ selected: ctx.isSelected, disabled: ctx.isDisabled, invalid: ctx.isInvalid, - current: isCurrent, + current: ctx.isCurrent, }), - [ctx.isSelected, ctx.isDisabled, ctx.isInvalid, isCurrent], + [ctx.isSelected, ctx.isDisabled, ctx.isInvalid, ctx.isCurrent], ); const { renderElement } = useComponentRenderer({ diff --git a/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx b/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx index cd7ce5a39d972..d03f014a46444 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/years-cell/CalendarYearsCell.tsx @@ -10,20 +10,20 @@ const InnerCalendarYearsCell = React.forwardRef(function InnerCalendarYearsCell( forwardedRef: React.ForwardedRef, ) { const { className, render, value, format, ctx, ...otherProps } = props; - const { getYearCellProps, isCurrent } = useBaseCalendarYearsCell({ value, format, ctx }); + const { getYearsCellProps } = useBaseCalendarYearsCell({ value, format, ctx }); const state: CalendarYearsCell.State = React.useMemo( () => ({ selected: ctx.isSelected, disabled: ctx.isDisabled, invalid: ctx.isInvalid, - current: isCurrent, + current: ctx.isCurrent, }), - [ctx.isSelected, ctx.isDisabled, ctx.isInvalid, isCurrent], + [ctx.isSelected, ctx.isDisabled, ctx.isInvalid, ctx.isCurrent], ); const { renderElement } = useComponentRenderer({ - propGetter: getYearCellProps, + propGetter: getYearsCellProps, render: render ?? 'button', ref: forwardedRef, className, diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCell.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCell.ts index 193d7bf7c6cf4..4e2f5b16d36bc 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCell.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCell.ts @@ -14,8 +14,6 @@ export function useBaseCalendarDaysCell(parameters: useBaseCalendarDaysCell.Para [utils, value, format], ); - const isCurrent = React.useMemo(() => utils.isSameDay(value, utils.date()), [utils, value]); - const onClick = useEventCallback(() => { ctx.selectDay(value); }); @@ -25,7 +23,7 @@ export function useBaseCalendarDaysCell(parameters: useBaseCalendarDaysCell.Para return mergeReactProps(externalProps, { role: 'gridcell', 'aria-selected': ctx.isSelected, - 'aria-current': isCurrent ? 'date' : undefined, + 'aria-current': ctx.isCurrent ? 'date' : undefined, 'aria-colindex': ctx.colIndex + 1, children: formattedValue, disabled: ctx.isDisabled, @@ -39,12 +37,12 @@ export function useBaseCalendarDaysCell(parameters: useBaseCalendarDaysCell.Para ctx.isDisabled, ctx.isTabbable, ctx.colIndex, - isCurrent, + ctx.isCurrent, onClick, ], ); - return React.useMemo(() => ({ getDaysCellProps, isCurrent }), [getDaysCellProps, isCurrent]); + return React.useMemo(() => ({ getDaysCellProps }), [getDaysCellProps]); } export namespace useBaseCalendarDaysCell { @@ -70,6 +68,7 @@ export namespace useBaseCalendarDaysCell { isDisabled: boolean; isInvalid: boolean; isTabbable: boolean; + isCurrent: boolean; isOutsideCurrentMonth: boolean; selectDay: (value: PickerValidDate) => void; } diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCellWrapper.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCellWrapper.ts index aac201f45b615..378be44b474ab 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCellWrapper.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/days-cell/useBaseCalendarDaysCellWrapper.ts @@ -21,6 +21,8 @@ export function useBaseCalendarDaysCellWrapper( [baseRootContext.selectedDates, value, utils], ); + const isCurrent = React.useMemo(() => utils.isSameDay(value, utils.date()), [utils, value]); + const isOutsideCurrentMonth = React.useMemo( () => baseDaysGridContext.currentMonth == null @@ -52,6 +54,7 @@ export function useBaseCalendarDaysCellWrapper( isDisabled, isInvalid, isTabbable, + isCurrent, isOutsideCurrentMonth, selectDay: baseDaysGridContext.selectDay, }), @@ -60,6 +63,7 @@ export function useBaseCalendarDaysCellWrapper( isDisabled, isInvalid, isTabbable, + isCurrent, isOutsideCurrentMonth, baseDaysGridContext.selectDay, colIndex, diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-cell/useBaseCalendarMonthsCell.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-cell/useBaseCalendarMonthsCell.ts index a38a9946a55b2..93972df6cd45b 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-cell/useBaseCalendarMonthsCell.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-cell/useBaseCalendarMonthsCell.ts @@ -14,8 +14,6 @@ export function useBaseCalendarMonthsCell(parameters: useBaseCalendarMonthsCell. [utils, value, format], ); - const isCurrent = React.useMemo(() => utils.isSameMonth(value, utils.date()), [utils, value]); - const onClick = useEventCallback(() => { ctx.selectMonth(value); }); @@ -26,17 +24,17 @@ export function useBaseCalendarMonthsCell(parameters: useBaseCalendarMonthsCell. type: 'button' as const, role: 'radio', 'aria-checked': ctx.isSelected, - 'aria-current': isCurrent ? 'date' : undefined, + 'aria-current': ctx.isCurrent ? 'date' : undefined, disabled: ctx.isDisabled, tabIndex: ctx.isTabbable ? 0 : -1, children: formattedValue, onClick, }); }, - [formattedValue, ctx.isSelected, ctx.isDisabled, ctx.isTabbable, onClick, isCurrent], + [formattedValue, ctx.isSelected, ctx.isDisabled, ctx.isTabbable, onClick, ctx.isCurrent], ); - return React.useMemo(() => ({ getMonthsCellProps, isCurrent }), [getMonthsCellProps, isCurrent]); + return React.useMemo(() => ({ getMonthsCellProps }), [getMonthsCellProps]); } export namespace useBaseCalendarMonthsCell { @@ -58,6 +56,7 @@ export namespace useBaseCalendarMonthsCell { isDisabled: boolean; isInvalid: boolean; isTabbable: boolean; + isCurrent: boolean; selectMonth: (value: PickerValidDate) => void; } } diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-cell/useBaseCalendarMonthsCellWrapper.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-cell/useBaseCalendarMonthsCellWrapper.ts index 8d4490888b9f6..c89c31c39d6fc 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-cell/useBaseCalendarMonthsCellWrapper.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/months-cell/useBaseCalendarMonthsCellWrapper.ts @@ -25,6 +25,8 @@ export function useBaseCalendarMonthsCellWrapper( [baseRootContext.selectedDates, value, utils], ); + const isCurrent = React.useMemo(() => utils.isSameMonth(value, utils.date()), [utils, value]); + const isInvalid = React.useMemo(() => { const firstEnabledMonth = utils.startOfMonth( baseRootContext.dateValidationProps.disablePast && @@ -110,9 +112,10 @@ export function useBaseCalendarMonthsCellWrapper( isDisabled, isInvalid, isTabbable, + isCurrent, selectMonth, }), - [isSelected, isDisabled, isInvalid, isTabbable, selectMonth], + [isSelected, isDisabled, isInvalid, isTabbable, isCurrent, selectMonth], ); return { ref: mergedRef, ctx }; diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/years-cell/useBaseCalendarYearsCell.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/years-cell/useBaseCalendarYearsCell.ts index c7f6091708ca4..94b4f78b025a7 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/years-cell/useBaseCalendarYearsCell.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/years-cell/useBaseCalendarYearsCell.ts @@ -14,29 +14,27 @@ export function useBaseCalendarYearsCell(parameters: useBaseCalendarYearsCell.Pa [utils, value, format], ); - const isCurrent = React.useMemo(() => utils.isSameYear(value, utils.date()), [utils, value]); - const onClick = useEventCallback(() => { ctx.selectYear(value); }); - const getYearCellProps = React.useCallback( + const getYearsCellProps = React.useCallback( (externalProps: GenericHTMLProps) => { return mergeReactProps(externalProps, { type: 'button' as const, role: 'radio', 'aria-checked': ctx.isSelected, - 'aria-current': isCurrent ? 'date' : undefined, + 'aria-current': ctx.isCurrent ? 'date' : undefined, disabled: ctx.isDisabled, tabIndex: ctx.isTabbable ? 0 : -1, children: formattedValue, onClick, }); }, - [formattedValue, ctx.isSelected, ctx.isDisabled, ctx.isTabbable, onClick, isCurrent], + [formattedValue, ctx.isSelected, ctx.isDisabled, ctx.isTabbable, onClick, ctx.isCurrent], ); - return React.useMemo(() => ({ getYearCellProps, isCurrent }), [getYearCellProps, isCurrent]); + return React.useMemo(() => ({ getYearsCellProps }), [getYearsCellProps]); } export namespace useBaseCalendarYearsCell { @@ -58,6 +56,7 @@ export namespace useBaseCalendarYearsCell { isDisabled: boolean; isInvalid: boolean; isTabbable: boolean; + isCurrent: boolean; selectYear: (value: PickerValidDate) => void; } } diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/years-cell/useBaseCalendarYearsCellWrapper.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/years-cell/useBaseCalendarYearsCellWrapper.ts index 8e91c7c8ed212..4e3972f304a83 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/years-cell/useBaseCalendarYearsCellWrapper.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/years-cell/useBaseCalendarYearsCellWrapper.ts @@ -25,6 +25,8 @@ export function useBaseCalendarYearsCellWrapper( [baseRootContext.selectedDates, value, utils], ); + const isCurrent = React.useMemo(() => utils.isSameYear(value, utils.date()), [utils, value]); + const isInvalid = React.useMemo(() => { if (baseRootContext.dateValidationProps.disablePast && utils.isBeforeYear(value, now)) { return true; @@ -105,9 +107,10 @@ export function useBaseCalendarYearsCellWrapper( isDisabled, isInvalid, isTabbable, + isCurrent, selectYear, }), - [isSelected, isDisabled, isInvalid, isTabbable, selectYear], + [isSelected, isDisabled, isInvalid, isTabbable, isCurrent, selectYear], ); return { ref: mergedRef, ctx }; From d3af922da8a4dcfb64aef77458e21817b67101e1 Mon Sep 17 00:00:00 2001 From: flavien Date: Tue, 14 Jan 2025 13:21:43 +0100 Subject: [PATCH 124/136] Work --- .../DayRangeCalendarAirbnbRecipe.js | 1 + .../root/useRangeCalendarRoot.tsx | 22 +++++++++---------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/docs/data/date-pickers/base-calendar/DayRangeCalendarAirbnbRecipe.js b/docs/data/date-pickers/base-calendar/DayRangeCalendarAirbnbRecipe.js index aa8881c339677..e3c87a2ac26b9 100644 --- a/docs/data/date-pickers/base-calendar/DayRangeCalendarAirbnbRecipe.js +++ b/docs/data/date-pickers/base-calendar/DayRangeCalendarAirbnbRecipe.js @@ -162,6 +162,7 @@ function BookingCalendar() { return ( (value); - if (value !== prevValue) { - setPrevValue(value); - let targetDate: PickerValidDate | null = null; - if (utils.isValid(value[0])) { - targetDate = value[0]; - } else if (utils.isValid(value[1])) { - targetDate = value[1]; - } - if (targetDate != null && isDateCellVisible(targetDate)) { - setVisibleDate(targetDate); + const [prevState, setPrevState] = React.useState<{ + value: PickerRangeValue; + rangePosition: RangePosition; + }>({ value, rangePosition }); + if (prevState.value !== value || prevState.rangePosition !== rangePosition) { + setPrevState({ value, rangePosition }); + if (rangePosition === 'start' && utils.isValid(value[0]) && !isDateCellVisible(value[0])) { + setVisibleDate(value[0]); + } else if (rangePosition === 'end' && utils.isValid(value[1]) && !isDateCellVisible(value[1])) { + setVisibleDate(value[1]); } } From d9305826943d84f2dc0e3bfaf464bd434a5a63ed Mon Sep 17 00:00:00 2001 From: flavien Date: Tue, 14 Jan 2025 13:47:30 +0100 Subject: [PATCH 125/136] Work on range --- .../root/useRangeCalendarRoot.tsx | 52 ++++----- .../base/RangeCalendar/utils/range.ts | 105 +++++++++++++++++- .../base-calendar/root/useBaseCalendarRoot.ts | 5 + 3 files changed, 135 insertions(+), 27 deletions(-) diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx index f2ef7052f8ae3..b5c13a8416d35 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx @@ -23,11 +23,10 @@ import { ValidateDateRangeProps, ExportedValidateDateRangeProps, } from '../../../../validation/validateDateRange'; -import { calculateRangeChange, calculateRangePreview } from '../../../utils/date-range-manager'; import { isRangeValid } from '../../../utils/date-utils'; import { useRangePosition, UseRangePositionProps } from '../../../hooks/useRangePosition'; import { RangeCalendarRootContext } from './RangeCalendarRootContext'; -import { getRoundedRange } from '../utils/range'; +import { applySelectedDateOnRange, createPreviewRange, getRoundedRange } from '../utils/range'; const DEFAULT_AVAILABLE_RANGE_POSITIONS: RangePosition[] = ['start', 'end']; @@ -97,28 +96,30 @@ export function useRangeCalendarRoot(parameters: useRangeCalendarRoot.Parameters selectedDate, referenceDate, allowRangeFlip, + section, }: useBaseCalendarRoot.GetNewValueFromNewSelectedDateParameters & { allowRangeFlip?: boolean; }): useBaseCalendarRoot.GetNewValueFromNewSelectedDateReturnValue => { - const { nextSelection, newRange } = calculateRangeChange({ - newDate: selectedDate, + const changes = applySelectedDateOnRange({ + selectedDate, utils, range: prevValue, - rangePosition, - allowRangeFlip, + position: rangePosition, + allowRangeFlip: allowRangeFlip ?? false, shouldMergeDateAndTime: true, referenceDate, + section, }); - const isNextSectionAvailable = availableRangePositions.includes(nextSelection); + const isNextSectionAvailable = availableRangePositions.includes(changes.position); if (isNextSectionAvailable) { - onRangePositionChange(nextSelection); + onRangePositionChange(changes.position); } - const isFullRangeSelected = rangePosition === 'end' && isRangeValid(utils, newRange); + const isFullRangeSelected = rangePosition === 'end' && isRangeValid(utils, changes.range); return { - value: newRange, + value: changes.range, changeImportance: isFullRangeSelected || !isNextSectionAvailable ? 'set' : 'accept', }; }, @@ -271,7 +272,7 @@ function useBuildRangeCalendarRootContext(parameters: UseBuildRangeCalendarRootC valueOrElement instanceof HTMLElement ? cellToDateMapRef.current.get(valueOrElement) : valueOrElement; - if (selectedDate == null) { + if (selectedDate == null || state.draggedSection == null) { return; } @@ -280,6 +281,7 @@ function useBuildRangeCalendarRootContext(parameters: UseBuildRangeCalendarRootC selectedDate, referenceDate, allowRangeFlip: true, + section: state.draggedSection, }); setValue(response.value, { changeImportance: response.changeImportance, section: 'day' }); @@ -330,32 +332,30 @@ function useBuildRangeCalendarRootContext(parameters: UseBuildRangeCalendarRootC return roundedRange; } - const rangeAfterDragAndDrop = calculateRangeChange({ + const rangeAfterDragAndDrop = applySelectedDateOnRange({ utils, range: roundedRange, - newDate: state.targetDate, - rangePosition, + selectedDate: state.targetDate, + position: rangePosition, allowRangeFlip: true, - }).newRange; + shouldMergeDateAndTime: false, + referenceDate, + section: state.draggedSection, + }).range; return getRoundedRange({ utils, range: rangeAfterDragAndDrop, section: state.draggedSection }); - }, [rangePosition, state.targetDate, state.draggedSection, utils, value]); + }, [rangePosition, state.targetDate, state.draggedSection, utils, value, referenceDate]); const previewRange = React.useMemo(() => { - if (disableHoverPreview || state.hoveredDate == null) { + if (disableHoverPreview) { return [null, null]; } - const roundedRange = getRoundedRange({ + return createPreviewRange({ utils, - range: value, - section: state.hoveredDate?.section, - }); - return calculateRangePreview({ - utils, - range: roundedRange, - newDate: state.hoveredDate.value, - rangePosition, + value, + hoveredDate: state.hoveredDate, + position: rangePosition, }); }, [utils, rangePosition, state.hoveredDate, value, disableHoverPreview]); diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/range.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/range.ts index fcfa719888513..fddfadcb4e6ef 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/range.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/range.ts @@ -1,4 +1,4 @@ -import { PickerRangeValue } from '@mui/x-date-pickers/internals'; +import { mergeDateAndTime, PickerRangeValue, RangePosition } from '@mui/x-date-pickers/internals'; import { MuiPickersAdapter, PickerValidDate } from '@mui/x-date-pickers/models'; // eslint-disable-next-line no-restricted-imports import { BaseCalendarSection } from '@mui/x-date-pickers/internals/base/utils/base-calendar/utils/types'; @@ -56,6 +56,109 @@ export function getDatePositionInRange({ }; } +export function applySelectedDateOnRange({ + utils, + range, + selectedDate, + position, + allowRangeFlip, + shouldMergeDateAndTime, + referenceDate, + section, +}: ApplySelectedDateOnRangeParameters): ApplySelectedDateOnRangeReturnValue { + const start = !utils.isValid(range[0]) ? null : range[0]; + const end = !utils.isValid(range[1]) ? null : range[1]; + + if (shouldMergeDateAndTime && selectedDate) { + // If there is a date already selected, then we want to keep its time + if (start && position === 'start') { + selectedDate = mergeDateAndTime(utils, selectedDate, start); + } + if (end && position === 'end') { + selectedDate = mergeDateAndTime(utils, selectedDate, end); + } + } + + const newSelectedDate = + referenceDate && selectedDate && shouldMergeDateAndTime + ? mergeDateAndTime(utils, selectedDate, referenceDate) + : selectedDate; + + if (position === 'start') { + const truthyResult: ApplySelectedDateOnRangeReturnValue = allowRangeFlip + ? { position: 'start', range: [end!, newSelectedDate] } + : { position: 'end', range: [newSelectedDate, null] }; + + return Boolean(end) && utils.isAfter(newSelectedDate!, end!) + ? truthyResult + : { position: 'end', range: [newSelectedDate, end] }; + } + + const truthyResult: ApplySelectedDateOnRangeReturnValue = allowRangeFlip + ? { position: 'end', range: [newSelectedDate, start!] } + : { position: 'end', range: [newSelectedDate, null] }; + + const sectionMethods = getCalendarSectionMethods(utils, section); + return Boolean(start) && utils.isBefore(newSelectedDate!, sectionMethods.startOf(start!)) + ? truthyResult + : { position: 'start', range: [start, newSelectedDate] }; +} + +interface ApplySelectedDateOnRangeParameters { + utils: MuiPickersAdapter; + range: PickerRangeValue; + selectedDate: PickerValidDate; + position: RangePosition; + allowRangeFlip: boolean; + shouldMergeDateAndTime: boolean; + referenceDate: PickerValidDate; + section: BaseCalendarSection; +} + +interface ApplySelectedDateOnRangeReturnValue { + position: RangePosition; + range: PickerRangeValue; +} + +export function createPreviewRange(parameters: CreatePreviewRangeParameters): PickerRangeValue { + const { utils, value, hoveredDate, position } = parameters; + if (hoveredDate == null) { + return [null, null]; + } + + const roundedValue = getRoundedRange({ + utils, + range: value, + section: hoveredDate.section, + }); + + const [start, end] = roundedValue; + const changes = applySelectedDateOnRange({ + utils, + range: value, + selectedDate: hoveredDate.value, + position, + allowRangeFlip: false, + shouldMergeDateAndTime: false, + referenceDate: hoveredDate.value, + section: hoveredDate.section, + }); + + if (!start || !end) { + return changes.range; + } + + const [previewStart, previewEnd] = changes.range; + return position === 'end' ? [end, previewEnd] : [previewStart, start]; +} + +interface CreatePreviewRangeParameters { + utils: MuiPickersAdapter; + value: PickerRangeValue; + hoveredDate: { value: PickerValidDate; section: BaseCalendarSection } | null; + position: RangePosition; +} + /** * Create a range going for the start of the first selected cell to the end of the last selected cell. * This makes sure that `isWithinRange` works with any time in the start and end day. diff --git a/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarRoot.ts b/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarRoot.ts index dde46573bd3f9..3590d13d4ac12 100644 --- a/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarRoot.ts +++ b/packages/x-date-pickers/src/internals/base/utils/base-calendar/root/useBaseCalendarRoot.ts @@ -178,6 +178,7 @@ export function useBaseCalendarRoot< prevValue: value, selectedDate, referenceDate, + section: options.section, }); return setValue(response.value, { @@ -399,6 +400,10 @@ export namespace useBaseCalendarRoot { * The reference date. */ referenceDate: PickerValidDate; + /** + * The section handled by the UI that triggered the change. + */ + section: BaseCalendarSection; } export interface GetNewValueFromNewSelectedDateReturnValue { From 4f7c47f105a54eef4af3dfa6732adaf17aabf673 Mon Sep 17 00:00:00 2001 From: flavien Date: Tue, 14 Jan 2025 15:31:03 +0100 Subject: [PATCH 126/136] Work --- .../root/useRangeCalendarRoot.tsx | 92 +++++++++++-------- .../base/RangeCalendar/utils/range.ts | 17 ++-- 2 files changed, 65 insertions(+), 44 deletions(-) diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx index b5c13a8416d35..3ad953e8b4ad2 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx @@ -1,9 +1,15 @@ import * as React from 'react'; +import { useMediaQuery } from '@base-ui-components/react/unstable-use-media-query'; import useEnhancedEffect from '@mui/utils/useEnhancedEffect'; import useEventCallback from '@mui/utils/useEventCallback'; import { PickerValidDate } from '@mui/x-date-pickers/models'; import { ValidateDateProps } from '@mui/x-date-pickers/validation'; -import { PickerRangeValue, RangePosition, useUtils } from '@mui/x-date-pickers/internals'; +import { + DEFAULT_DESKTOP_MODE_MEDIA_QUERY, + PickerRangeValue, + RangePosition, + useUtils, +} from '@mui/x-date-pickers/internals'; // eslint-disable-next-line no-restricted-imports import { BaseCalendarSection } from '@mui/x-date-pickers/internals/base/utils/base-calendar/utils/types'; // eslint-disable-next-line no-restricted-imports @@ -31,6 +37,10 @@ import { applySelectedDateOnRange, createPreviewRange, getRoundedRange } from '. const DEFAULT_AVAILABLE_RANGE_POSITIONS: RangePosition[] = ['start', 'end']; export function useRangeCalendarRoot(parameters: useRangeCalendarRoot.Parameters) { + const isPointerFine = useMediaQuery(DEFAULT_DESKTOP_MODE_MEDIA_QUERY, { + defaultMatches: false, + }); + const { // Validation props minDate, @@ -45,7 +55,7 @@ export function useRangeCalendarRoot(parameters: useRangeCalendarRoot.Parameters availableRangePositions = DEFAULT_AVAILABLE_RANGE_POSITIONS, // Other range-specific parameters disableDragEditing = false, - disableHoverPreview = false, + disableHoverPreview = !isPointerFine, // Parameters forwarded to `useBaseCalendarRoot` ...baseParameters } = parameters; @@ -206,11 +216,10 @@ export namespace useRangeCalendarRoot { * @default false */ disableDragEditing?: boolean; - // TODO: Apply smart behavior based on the media que /** * If `true`, the hover preview is disabled. * The cells that would be selected if clicking on the hovered cell won't receive a data-preview attribute. - * @default false + * @default useMediaQuery('@media (pointer: fine)') */ disableHoverPreview?: boolean; } @@ -235,11 +244,17 @@ function useBuildRangeCalendarRootContext(parameters: UseBuildRangeCalendarRootC const utils = useUtils(); const cellToDateMapRef = React.useRef(new Map()); - const [state, setState] = React.useState({ + const [dragAndDropState, setDragAndDropState] = React.useState<{ + draggedSection: BaseCalendarSection | null; + targetDate: PickerValidDate | null; + }>({ draggedSection: null, targetDate: null, - hoveredDate: null, }); + const [previewState, setPreviewState] = React.useState<{ + hoveredDate: PickerValidDate; + section: BaseCalendarSection; + } | null>(null); const registerCell: RangeCalendarRootContext['registerCell'] = useEventCallback( (element, valueToRegister) => { @@ -252,18 +267,16 @@ function useBuildRangeCalendarRootContext(parameters: UseBuildRangeCalendarRootC const startDragging: RangeCalendarRootContext['startDragging'] = useEventCallback( (position, section) => { - setState((prev) => ({ ...prev, draggedSection: section })); + setDragAndDropState((prev) => ({ ...prev, draggedSection: section })); onRangePositionChange(position); }, ); const stopDragging: RangeCalendarRootContext['stopDragging'] = useEventCallback(() => { - setState((prev) => ({ - ...prev, + setDragAndDropState({ draggedSection: null, - draggedDate: null, targetDate: null, - })); + }); }); const selectDateFromDrag: RangeCalendarRootContext['selectDateFromDrag'] = useEventCallback( @@ -272,7 +285,7 @@ function useBuildRangeCalendarRootContext(parameters: UseBuildRangeCalendarRootC valueOrElement instanceof HTMLElement ? cellToDateMapRef.current.get(valueOrElement) : valueOrElement; - if (selectedDate == null || state.draggedSection == null) { + if (selectedDate == null || dragAndDropState.draggedSection == null) { return; } @@ -281,7 +294,7 @@ function useBuildRangeCalendarRootContext(parameters: UseBuildRangeCalendarRootC selectedDate, referenceDate, allowRangeFlip: true, - section: state.draggedSection, + section: dragAndDropState.draggedSection, }); setValue(response.value, { changeImportance: response.changeImportance, section: 'day' }); @@ -293,10 +306,7 @@ function useBuildRangeCalendarRootContext(parameters: UseBuildRangeCalendarRootC if (disableHoverPreview) { return; } - setState((prev) => ({ - ...prev, - hoveredDate: date == null ? null : { value: date, section }, - })); + setPreviewState(date == null ? null : { hoveredDate: date, section }); }, ); @@ -307,11 +317,11 @@ function useBuildRangeCalendarRootContext(parameters: UseBuildRangeCalendarRootC ? cellToDateMapRef.current.get(valueOrElement) : valueOrElement; - if (newTargetDate == null || utils.isEqual(newTargetDate, state.targetDate)) { + if (newTargetDate == null || utils.isEqual(newTargetDate, dragAndDropState.targetDate)) { return; } - setState((prev) => ({ ...prev, targetDate: newTargetDate })); + setDragAndDropState((prev) => ({ ...prev, targetDate: newTargetDate })); // TODO: Buggy if (value[0] && utils.isBeforeDay(newTargetDate, value[0])) { @@ -323,11 +333,15 @@ function useBuildRangeCalendarRootContext(parameters: UseBuildRangeCalendarRootC ); const selectedRange = React.useMemo(() => { - if (!state.targetDate || state.draggedSection == null) { + if (!dragAndDropState.targetDate || dragAndDropState.draggedSection == null) { return value; } - const roundedRange = getRoundedRange({ utils, range: value, section: state.draggedSection }); + const roundedRange = getRoundedRange({ + utils, + range: value, + section: dragAndDropState.draggedSection, + }); if (roundedRange[0] == null || roundedRange[1] == null) { return roundedRange; } @@ -335,29 +349,41 @@ function useBuildRangeCalendarRootContext(parameters: UseBuildRangeCalendarRootC const rangeAfterDragAndDrop = applySelectedDateOnRange({ utils, range: roundedRange, - selectedDate: state.targetDate, + selectedDate: dragAndDropState.targetDate, position: rangePosition, allowRangeFlip: true, shouldMergeDateAndTime: false, referenceDate, - section: state.draggedSection, + section: dragAndDropState.draggedSection, }).range; - return getRoundedRange({ utils, range: rangeAfterDragAndDrop, section: state.draggedSection }); - }, [rangePosition, state.targetDate, state.draggedSection, utils, value, referenceDate]); + return getRoundedRange({ + utils, + range: rangeAfterDragAndDrop, + section: dragAndDropState.draggedSection, + }); + }, [ + rangePosition, + dragAndDropState.targetDate, + dragAndDropState.draggedSection, + utils, + value, + referenceDate, + ]); const previewRange = React.useMemo(() => { - if (disableHoverPreview) { + if (disableHoverPreview || previewState == null) { return [null, null]; } return createPreviewRange({ utils, value, - hoveredDate: state.hoveredDate, + hoveredDate: previewState.hoveredDate, + section: previewState.section, position: rangePosition, }); - }, [utils, rangePosition, state.hoveredDate, value, disableHoverPreview]); + }, [utils, rangePosition, value, disableHoverPreview, previewState]); const emptyDragImgRef = React.useRef(null); React.useEffect(() => { @@ -367,9 +393,9 @@ function useBuildRangeCalendarRootContext(parameters: UseBuildRangeCalendarRootC 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; }, []); - const draggedSectionRef = React.useRef(state.draggedSection); + const draggedSectionRef = React.useRef(dragAndDropState.draggedSection); useEnhancedEffect(() => { - draggedSectionRef.current = state.draggedSection; + draggedSectionRef.current = dragAndDropState.draggedSection; }); const context: RangeCalendarRootContext = { @@ -405,9 +431,3 @@ interface UseBuildRangeCalendarRootContextParameters { rangePosition: RangePosition; disableHoverPreview: boolean; } - -interface UseBuildRangeCalendarRootContextState { - draggedSection: BaseCalendarSection | null; - targetDate: PickerValidDate | null; - hoveredDate: { value: PickerValidDate; section: BaseCalendarSection } | null; -} diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/range.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/range.ts index fddfadcb4e6ef..a21e52d1416e9 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/range.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/range.ts @@ -121,7 +121,7 @@ interface ApplySelectedDateOnRangeReturnValue { } export function createPreviewRange(parameters: CreatePreviewRangeParameters): PickerRangeValue { - const { utils, value, hoveredDate, position } = parameters; + const { utils, value, hoveredDate, section, position } = parameters; if (hoveredDate == null) { return [null, null]; } @@ -129,19 +129,19 @@ export function createPreviewRange(parameters: CreatePreviewRangeParameters): Pi const roundedValue = getRoundedRange({ utils, range: value, - section: hoveredDate.section, + section, }); const [start, end] = roundedValue; const changes = applySelectedDateOnRange({ utils, - range: value, - selectedDate: hoveredDate.value, + section, position, + range: value, + selectedDate: hoveredDate, allowRangeFlip: false, shouldMergeDateAndTime: false, - referenceDate: hoveredDate.value, - section: hoveredDate.section, + referenceDate: hoveredDate, }); if (!start || !end) { @@ -155,13 +155,14 @@ export function createPreviewRange(parameters: CreatePreviewRangeParameters): Pi interface CreatePreviewRangeParameters { utils: MuiPickersAdapter; value: PickerRangeValue; - hoveredDate: { value: PickerValidDate; section: BaseCalendarSection } | null; + hoveredDate: PickerValidDate; + section: BaseCalendarSection; position: RangePosition; } /** * Create a range going for the start of the first selected cell to the end of the last selected cell. - * This makes sure that `isWithinRange` works with any time in the start and end day. + * This makes sure that methods like `isWithinRange` works with any time in the start and end day. */ export function getRoundedRange({ utils, From c870fd00369d9bbff68e967f18df370616ff705f Mon Sep 17 00:00:00 2001 From: flavien Date: Tue, 14 Jan 2025 15:47:57 +0100 Subject: [PATCH 127/136] clean --- packages/x-date-pickers-pro/package.json | 1 + .../days-cell/useRangeCalendarDaysCell.tsx | 3 +- .../useRangeCalendarMonthsCell.tsx | 1 - .../root/RangeCalendarRootContext.ts | 11 + .../root/useBuildRangeCalendarRootContext.ts | 224 ++++++++++++++++++ .../root/useRangeCalendarRoot.tsx | 221 +---------------- .../base/RangeCalendar/utils/useRangeCell.ts | 5 +- .../years-cell/useRangeCalendarYearsCell.tsx | 3 +- pnpm-lock.yaml | 3 + 9 files changed, 250 insertions(+), 222 deletions(-) create mode 100644 packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useBuildRangeCalendarRootContext.ts diff --git a/packages/x-date-pickers-pro/package.json b/packages/x-date-pickers-pro/package.json index eba4d65d6b1b9..4df1eec44ff1d 100644 --- a/packages/x-date-pickers-pro/package.json +++ b/packages/x-date-pickers-pro/package.json @@ -43,6 +43,7 @@ }, "dependencies": { "@babel/runtime": "^7.26.0", + "@base-ui-components/react": "1.0.0-alpha.4", "@mui/utils": "^5.16.6 || ^6.0.0", "@mui/x-date-pickers": "workspace:*", "@mui/x-internals": "workspace:*", diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx index 593bb9def66c7..a332127d1310d 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-cell/useRangeCalendarDaysCell.tsx @@ -9,9 +9,8 @@ import { useRangeCell } from '../utils/useRangeCell'; export function useRangeCalendarDaysCell(parameters: useRangeCalendarDaysCell.Parameters) { const { ctx, value } = parameters; - const rangeCellProps = useRangeCell({ ctx, value, section: 'day' }); - const { getDaysCellProps: getBaseDaysCellProps } = useBaseCalendarDaysCell(parameters); + const rangeCellProps = useRangeCell({ ctx, value, section: 'day' }); const getDaysCellProps = React.useCallback( (externalProps: GenericHTMLProps) => { diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/useRangeCalendarMonthsCell.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/useRangeCalendarMonthsCell.tsx index 8bfe58faa9287..811588a63489c 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/useRangeCalendarMonthsCell.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/useRangeCalendarMonthsCell.tsx @@ -10,7 +10,6 @@ import { useRangeCell } from '../utils/useRangeCell'; export function useRangeCalendarMonthsCell(parameters: useRangeCalendarMonthsCell.Parameters) { const { ctx, value } = parameters; const rangeCellProps = useRangeCell({ ctx, value, section: 'month' }); - const { getMonthsCellProps: getBaseMonthsCellProps } = useBaseCalendarMonthsCell(parameters); const getMonthsCellProps = React.useCallback( diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts index 1ef44fa097c37..9c2f206d9b74f 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRootContext.ts @@ -14,6 +14,9 @@ export interface RangeCalendarRootContext { * This is used to check if the user is dragging in event handlers without causing re-renders. */ draggedSectionRef: React.RefObject; + /** + * If `true`, the drag editing feature is disabled. + */ disableDragEditing: boolean; selectDateFromDrag: (valueOrElement: PickerValidDate | HTMLElement) => void; startDragging: (position: RangePosition, section: BaseCalendarSection) => void; @@ -21,8 +24,16 @@ export interface RangeCalendarRootContext { setDragTarget: (valueOrElement: PickerValidDate | HTMLElement) => void; emptyDragImgRef: React.RefObject; registerCell: (element: HTMLElement, value: PickerValidDate) => () => void; + /** + * The range that should be visually selected. + * When there is no ongoing dragging, it is equal to the current value. + * When there is ongoing dragging, it is equal to the value that would be selected if the user dropped the cell in its existing location. + */ selectedRange: PickerRangeValue; setHoveredDate: (value: PickerValidDate | null, section: BaseCalendarSection) => void; + /** + * That range should would be selected if the user clicking on the currently hovered cell. + */ previewRange: PickerRangeValue; } diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useBuildRangeCalendarRootContext.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useBuildRangeCalendarRootContext.ts new file mode 100644 index 0000000000000..f76830d8c6b74 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useBuildRangeCalendarRootContext.ts @@ -0,0 +1,224 @@ +import * as React from 'react'; +import useEnhancedEffect from '@mui/utils/useEnhancedEffect'; +import useEventCallback from '@mui/utils/useEventCallback'; +import { PickerValidDate } from '@mui/x-date-pickers/models'; +import { PickerRangeValue, RangePosition, useUtils } from '@mui/x-date-pickers/internals'; +// eslint-disable-next-line no-restricted-imports +import { BaseCalendarSection } from '@mui/x-date-pickers/internals/base/utils/base-calendar/utils/types'; +// eslint-disable-next-line no-restricted-imports +import { useBaseCalendarRoot } from '@mui/x-date-pickers/internals/base/utils/base-calendar/root/useBaseCalendarRoot'; +// eslint-disable-next-line no-restricted-imports +import { BaseCalendarRootContext } from '@mui/x-date-pickers/internals/base/utils/base-calendar/root/BaseCalendarRootContext'; +import { RangeCalendarRootContext } from './RangeCalendarRootContext'; +import { applySelectedDateOnRange, createPreviewRange, getRoundedRange } from '../utils/range'; + +export function useBuildRangeCalendarRootContext( + parameters: useBuildRangeCalendarRootContext.Parameters, +) { + const { + value, + setValue, + referenceDate, + baseContext, + rangePosition, + onRangePositionChange, + getNewValueFromNewSelectedDate, + disableDragEditing: disableDragEditingProp, + disableHoverPreview: disableHoverPreviewProp, + } = parameters; + + const disableDragEditing = disableDragEditingProp || baseContext.disabled || baseContext.readOnly; + const disableHoverPreview = + disableHoverPreviewProp || baseContext.disabled || baseContext.readOnly; + + const utils = useUtils(); + const cellToDateMapRef = React.useRef(new Map()); + const [dragAndDropState, setDragAndDropState] = React.useState<{ + draggedSection: BaseCalendarSection | null; + targetDate: PickerValidDate | null; + }>({ + draggedSection: null, + targetDate: null, + }); + const [previewState, setPreviewState] = React.useState<{ + hoveredDate: PickerValidDate; + section: BaseCalendarSection; + } | null>(null); + + const registerCell: RangeCalendarRootContext['registerCell'] = useEventCallback( + (element, valueToRegister) => { + cellToDateMapRef.current.set(element, valueToRegister); + return () => { + cellToDateMapRef.current.delete(element); + }; + }, + ); + + const startDragging: RangeCalendarRootContext['startDragging'] = useEventCallback( + (position, section) => { + setDragAndDropState((prev) => ({ ...prev, draggedSection: section })); + onRangePositionChange(position); + }, + ); + + const stopDragging: RangeCalendarRootContext['stopDragging'] = useEventCallback(() => { + setDragAndDropState({ + draggedSection: null, + targetDate: null, + }); + }); + + const selectDateFromDrag: RangeCalendarRootContext['selectDateFromDrag'] = useEventCallback( + (valueOrElement) => { + const selectedDate = + valueOrElement instanceof HTMLElement + ? cellToDateMapRef.current.get(valueOrElement) + : valueOrElement; + if (selectedDate == null || dragAndDropState.draggedSection == null) { + return; + } + + const response = getNewValueFromNewSelectedDate({ + prevValue: value, + selectedDate, + referenceDate, + allowRangeFlip: true, + section: dragAndDropState.draggedSection, + }); + + setValue(response.value, { changeImportance: response.changeImportance, section: 'day' }); + }, + ); + + const setHoveredDate: RangeCalendarRootContext['setHoveredDate'] = useEventCallback( + (date, section) => { + if (disableHoverPreview) { + return; + } + setPreviewState(date == null ? null : { hoveredDate: date, section }); + }, + ); + + const setDragTarget: RangeCalendarRootContext['setDragTarget'] = useEventCallback( + (valueOrElement) => { + const newTargetDate = + valueOrElement instanceof HTMLElement + ? cellToDateMapRef.current.get(valueOrElement) + : valueOrElement; + + if (newTargetDate == null || utils.isEqual(newTargetDate, dragAndDropState.targetDate)) { + return; + } + + setDragAndDropState((prev) => ({ ...prev, targetDate: newTargetDate })); + + // TODO: Buggy + if (value[0] && utils.isBeforeDay(newTargetDate, value[0])) { + onRangePositionChange('start'); + } else if (value[1] && utils.isAfterDay(newTargetDate, value[1])) { + onRangePositionChange('end'); + } + }, + ); + + const selectedRange = React.useMemo(() => { + if (!dragAndDropState.targetDate || dragAndDropState.draggedSection == null) { + return value; + } + + const roundedRange = getRoundedRange({ + utils, + range: value, + section: dragAndDropState.draggedSection, + }); + if (roundedRange[0] == null || roundedRange[1] == null) { + return roundedRange; + } + + const rangeAfterDragAndDrop = applySelectedDateOnRange({ + utils, + range: roundedRange, + selectedDate: dragAndDropState.targetDate, + position: rangePosition, + allowRangeFlip: true, + shouldMergeDateAndTime: false, + referenceDate, + section: dragAndDropState.draggedSection, + }).range; + + return getRoundedRange({ + utils, + range: rangeAfterDragAndDrop, + section: dragAndDropState.draggedSection, + }); + }, [ + rangePosition, + dragAndDropState.targetDate, + dragAndDropState.draggedSection, + utils, + value, + referenceDate, + ]); + + const previewRange = React.useMemo(() => { + if (disableHoverPreview || previewState == null) { + return [null, null]; + } + + return createPreviewRange({ + utils, + value, + hoveredDate: previewState.hoveredDate, + section: previewState.section, + position: rangePosition, + }); + }, [utils, rangePosition, value, disableHoverPreview, previewState]); + + const emptyDragImgRef = React.useRef(null); + React.useEffect(() => { + // Preload the image - required for Safari support: https://stackoverflow.com/a/40923520/3303436 + emptyDragImgRef.current = document.createElement('img'); + emptyDragImgRef.current.src = + 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; + }, []); + + const draggedSectionRef = React.useRef(dragAndDropState.draggedSection); + useEnhancedEffect(() => { + draggedSectionRef.current = dragAndDropState.draggedSection; + }); + + const context: RangeCalendarRootContext = { + value, + selectedRange, + previewRange, + disableDragEditing, + draggedSectionRef, + emptyDragImgRef, + selectDateFromDrag, + startDragging, + stopDragging, + setDragTarget, + setHoveredDate, + registerCell, + }; + + return context; +} + +export namespace useBuildRangeCalendarRootContext { + export interface Parameters { + value: PickerRangeValue; + referenceDate: PickerValidDate; + setValue: useBaseCalendarRoot.ReturnValue['setValue']; + baseContext: BaseCalendarRootContext; + disableDragEditing: boolean; + getNewValueFromNewSelectedDate: ( + parameters: useBaseCalendarRoot.GetNewValueFromNewSelectedDateParameters & { + allowRangeFlip?: boolean; + }, + ) => useBaseCalendarRoot.GetNewValueFromNewSelectedDateReturnValue; + onRangePositionChange: (position: RangePosition) => void; + rangePosition: RangePosition; + disableHoverPreview: boolean; + } +} diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx index 3ad953e8b4ad2..c3e0d628fdd05 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; import { useMediaQuery } from '@base-ui-components/react/unstable-use-media-query'; -import useEnhancedEffect from '@mui/utils/useEnhancedEffect'; import useEventCallback from '@mui/utils/useEventCallback'; import { PickerValidDate } from '@mui/x-date-pickers/models'; import { ValidateDateProps } from '@mui/x-date-pickers/validation'; @@ -11,15 +10,11 @@ import { useUtils, } from '@mui/x-date-pickers/internals'; // eslint-disable-next-line no-restricted-imports -import { BaseCalendarSection } from '@mui/x-date-pickers/internals/base/utils/base-calendar/utils/types'; -// eslint-disable-next-line no-restricted-imports import { useAddDefaultsToBaseDateValidationProps, useBaseCalendarRoot, } from '@mui/x-date-pickers/internals/base/utils/base-calendar/root/useBaseCalendarRoot'; // eslint-disable-next-line no-restricted-imports -import { BaseCalendarRootContext } from '@mui/x-date-pickers/internals/base/utils/base-calendar/root/BaseCalendarRootContext'; -// eslint-disable-next-line no-restricted-imports import { mergeReactProps } from '@mui/x-date-pickers/internals/base/base-utils/mergeReactProps'; // eslint-disable-next-line no-restricted-imports import { GenericHTMLProps } from '@mui/x-date-pickers/internals/base/base-utils/types'; @@ -31,12 +26,14 @@ import { } from '../../../../validation/validateDateRange'; import { isRangeValid } from '../../../utils/date-utils'; import { useRangePosition, UseRangePositionProps } from '../../../hooks/useRangePosition'; -import { RangeCalendarRootContext } from './RangeCalendarRootContext'; -import { applySelectedDateOnRange, createPreviewRange, getRoundedRange } from '../utils/range'; +import { applySelectedDateOnRange } from '../utils/range'; +import { useBuildRangeCalendarRootContext } from './useBuildRangeCalendarRootContext'; const DEFAULT_AVAILABLE_RANGE_POSITIONS: RangePosition[] = ['start', 'end']; export function useRangeCalendarRoot(parameters: useRangeCalendarRoot.Parameters) { + const utils = useUtils(); + const manager = useDateRangeManager(); const isPointerFine = useMediaQuery(DEFAULT_DESKTOP_MODE_MEDIA_QUERY, { defaultMatches: false, }); @@ -59,8 +56,6 @@ export function useRangeCalendarRoot(parameters: useRangeCalendarRoot.Parameters // Parameters forwarded to `useBaseCalendarRoot` ...baseParameters } = parameters; - const utils = useUtils(); - const manager = useDateRangeManager(); // TODO: Add support for range position from the context when implementing the Picker Base UI X component. const { rangePosition, onRangePositionChange } = useRangePosition({ @@ -100,6 +95,7 @@ export function useRangeCalendarRoot(parameters: useRangeCalendarRoot.Parameters [baseDateValidationProps, shouldDisableDate], ); + // TODO: Clean this part of the code which is hard to follow. const getNewValueFromNewSelectedDate = useEventCallback( ({ prevValue, @@ -224,210 +220,3 @@ export namespace useRangeCalendarRoot { disableHoverPreview?: boolean; } } - -function useBuildRangeCalendarRootContext(parameters: UseBuildRangeCalendarRootContextParameters) { - const { - value, - setValue, - referenceDate, - baseContext, - rangePosition, - onRangePositionChange, - getNewValueFromNewSelectedDate, - disableDragEditing: disableDragEditingProp, - disableHoverPreview: disableHoverPreviewProp, - } = parameters; - - const disableDragEditing = disableDragEditingProp || baseContext.disabled || baseContext.readOnly; - const disableHoverPreview = - disableHoverPreviewProp || baseContext.disabled || baseContext.readOnly; - - const utils = useUtils(); - const cellToDateMapRef = React.useRef(new Map()); - const [dragAndDropState, setDragAndDropState] = React.useState<{ - draggedSection: BaseCalendarSection | null; - targetDate: PickerValidDate | null; - }>({ - draggedSection: null, - targetDate: null, - }); - const [previewState, setPreviewState] = React.useState<{ - hoveredDate: PickerValidDate; - section: BaseCalendarSection; - } | null>(null); - - const registerCell: RangeCalendarRootContext['registerCell'] = useEventCallback( - (element, valueToRegister) => { - cellToDateMapRef.current.set(element, valueToRegister); - return () => { - cellToDateMapRef.current.delete(element); - }; - }, - ); - - const startDragging: RangeCalendarRootContext['startDragging'] = useEventCallback( - (position, section) => { - setDragAndDropState((prev) => ({ ...prev, draggedSection: section })); - onRangePositionChange(position); - }, - ); - - const stopDragging: RangeCalendarRootContext['stopDragging'] = useEventCallback(() => { - setDragAndDropState({ - draggedSection: null, - targetDate: null, - }); - }); - - const selectDateFromDrag: RangeCalendarRootContext['selectDateFromDrag'] = useEventCallback( - (valueOrElement) => { - const selectedDate = - valueOrElement instanceof HTMLElement - ? cellToDateMapRef.current.get(valueOrElement) - : valueOrElement; - if (selectedDate == null || dragAndDropState.draggedSection == null) { - return; - } - - const response = getNewValueFromNewSelectedDate({ - prevValue: value, - selectedDate, - referenceDate, - allowRangeFlip: true, - section: dragAndDropState.draggedSection, - }); - - setValue(response.value, { changeImportance: response.changeImportance, section: 'day' }); - }, - ); - - const setHoveredDate: RangeCalendarRootContext['setHoveredDate'] = useEventCallback( - (date, section) => { - if (disableHoverPreview) { - return; - } - setPreviewState(date == null ? null : { hoveredDate: date, section }); - }, - ); - - const setDragTarget: RangeCalendarRootContext['setDragTarget'] = useEventCallback( - (valueOrElement) => { - const newTargetDate = - valueOrElement instanceof HTMLElement - ? cellToDateMapRef.current.get(valueOrElement) - : valueOrElement; - - if (newTargetDate == null || utils.isEqual(newTargetDate, dragAndDropState.targetDate)) { - return; - } - - setDragAndDropState((prev) => ({ ...prev, targetDate: newTargetDate })); - - // TODO: Buggy - if (value[0] && utils.isBeforeDay(newTargetDate, value[0])) { - onRangePositionChange('start'); - } else if (value[1] && utils.isAfterDay(newTargetDate, value[1])) { - onRangePositionChange('end'); - } - }, - ); - - const selectedRange = React.useMemo(() => { - if (!dragAndDropState.targetDate || dragAndDropState.draggedSection == null) { - return value; - } - - const roundedRange = getRoundedRange({ - utils, - range: value, - section: dragAndDropState.draggedSection, - }); - if (roundedRange[0] == null || roundedRange[1] == null) { - return roundedRange; - } - - const rangeAfterDragAndDrop = applySelectedDateOnRange({ - utils, - range: roundedRange, - selectedDate: dragAndDropState.targetDate, - position: rangePosition, - allowRangeFlip: true, - shouldMergeDateAndTime: false, - referenceDate, - section: dragAndDropState.draggedSection, - }).range; - - return getRoundedRange({ - utils, - range: rangeAfterDragAndDrop, - section: dragAndDropState.draggedSection, - }); - }, [ - rangePosition, - dragAndDropState.targetDate, - dragAndDropState.draggedSection, - utils, - value, - referenceDate, - ]); - - const previewRange = React.useMemo(() => { - if (disableHoverPreview || previewState == null) { - return [null, null]; - } - - return createPreviewRange({ - utils, - value, - hoveredDate: previewState.hoveredDate, - section: previewState.section, - position: rangePosition, - }); - }, [utils, rangePosition, value, disableHoverPreview, previewState]); - - const emptyDragImgRef = React.useRef(null); - React.useEffect(() => { - // Preload the image - required for Safari support: https://stackoverflow.com/a/40923520/3303436 - emptyDragImgRef.current = document.createElement('img'); - emptyDragImgRef.current.src = - 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; - }, []); - - const draggedSectionRef = React.useRef(dragAndDropState.draggedSection); - useEnhancedEffect(() => { - draggedSectionRef.current = dragAndDropState.draggedSection; - }); - - const context: RangeCalendarRootContext = { - value, - selectedRange, - previewRange, - disableDragEditing, - draggedSectionRef, - emptyDragImgRef, - selectDateFromDrag, - startDragging, - stopDragging, - setDragTarget, - setHoveredDate, - registerCell, - }; - - return context; -} - -interface UseBuildRangeCalendarRootContextParameters { - value: PickerRangeValue; - referenceDate: PickerValidDate; - setValue: useBaseCalendarRoot.ReturnValue['setValue']; - baseContext: BaseCalendarRootContext; - disableDragEditing: boolean; - getNewValueFromNewSelectedDate: ( - parameters: useBaseCalendarRoot.GetNewValueFromNewSelectedDateParameters & { - allowRangeFlip?: boolean; - }, - ) => useBaseCalendarRoot.GetNewValueFromNewSelectedDateReturnValue; - onRangePositionChange: (position: RangePosition) => void; - rangePosition: RangePosition; - disableHoverPreview: boolean; -} diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/useRangeCell.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/useRangeCell.ts index 10bbfa0e6525e..591c45d365adc 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/useRangeCell.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/utils/useRangeCell.ts @@ -6,7 +6,7 @@ import { BaseCalendarSection } from '@mui/x-date-pickers/internals/base/utils/ba import { RangeCalendarRootContext } from '../root/RangeCalendarRootContext'; /** - * Add support for drag&drop and preview to the cell components of the Range Calendar. + * Add support for drag and drop and hover preview to the cell components of the Range Calendar. */ export function useRangeCell(parameters: useRangeCell.Parameters) { const { ctx, value, section } = parameters; @@ -204,6 +204,9 @@ function resolveButtonElement(element: Element | null): HTMLButtonElement | null if (element.children.length) { const allButtons = element.querySelectorAll('button:not(:disabled)'); + + // If there are several buttons inside the element, + // Then we the coordinates probably point between two cells and the element is probably a parent that we don't want to drop on. if (allButtons.length > 1) { return null; } diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-cell/useRangeCalendarYearsCell.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-cell/useRangeCalendarYearsCell.tsx index 65b136bc7ce91..bf2ef6e58dfa5 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-cell/useRangeCalendarYearsCell.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/years-cell/useRangeCalendarYearsCell.tsx @@ -9,9 +9,8 @@ import { useRangeCell } from '../utils/useRangeCell'; export function useRangeCalendarYearsCell(parameters: useRangeCalendarYearsCell.Parameters) { const { ctx, value } = parameters; - const rangeCellProps = useRangeCell({ ctx, value, section: 'year' }); - const { getYearsCellProps: getBaseYearsCellProps } = useBaseCalendarYearsCell(parameters); + const rangeCellProps = useRangeCell({ ctx, value, section: 'year' }); const getYearsCellProps = React.useCallback( (externalProps: GenericHTMLProps) => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4513b4e26898a..a083f37c93e05 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1317,6 +1317,9 @@ importers: '@babel/runtime': specifier: ^7.26.0 version: 7.26.0 + '@base-ui-components/react': + specifier: 1.0.0-alpha.4 + version: 1.0.0-alpha.4(@types/react@19.0.6)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@emotion/react': specifier: ^11.9.0 version: 11.14.0(@types/react@19.0.6)(react@19.0.0) From 990a4dfff48feceb13de12fccd2f40d4cbe4a4ea Mon Sep 17 00:00:00 2001 From: flavien Date: Tue, 14 Jan 2025 16:18:31 +0100 Subject: [PATCH 128/136] Add render function to the root components --- .../base-calendar/DayCalendarDemo.js | 109 +++++++------ .../base-calendar/DayCalendarDemo.tsx | 109 +++++++------ .../DayCalendarWithFixedWeekNumberDemo.js | 109 +++++++------ .../DayCalendarWithFixedWeekNumberDemo.tsx | 109 +++++++------ .../DayCalendarWithValidationDemo.js | 110 +++++++------ .../DayCalendarWithValidationDemo.tsx | 110 +++++++------ .../DayCalendarWithWeekNumberDemo.js | 145 +++++++++--------- .../DayCalendarWithWeekNumberDemo.tsx | 145 +++++++++--------- .../base-calendar/DayRangeCalendarDemo.js | 115 +++++++------- .../base-calendar/DayRangeCalendarDemo.tsx | 115 +++++++------- .../base-calendar/MonthCalendarDemo.js | 63 ++++---- .../base-calendar/MonthCalendarDemo.tsx | 63 ++++---- .../MonthCalendarDemo.tsx.preview | 14 -- .../MonthCalendarWithCustomCellFormatDemo.js | 65 ++++---- .../MonthCalendarWithCustomCellFormatDemo.tsx | 65 ++++---- .../MonthCalendarWithListLayoutDemo.js | 63 ++++---- .../MonthCalendarWithListLayoutDemo.tsx | 63 ++++---- ...onthCalendarWithListLayoutDemo.tsx.preview | 14 -- .../base-calendar/MonthRangeCalendarDemo.js | 69 ++++----- .../base-calendar/MonthRangeCalendarDemo.tsx | 69 ++++----- .../MonthRangeCalendarDemo.tsx.preview | 14 -- .../YearCalendarWithDecadeNavigationDemo.js | 88 +++++------ .../YearCalendarWithDecadeNavigationDemo.tsx | 88 +++++------ .../useRangeCalendarDaysGridBody.ts | 3 +- .../useRangeCalendarMonthsCell.tsx | 2 +- .../RangeCalendar/root/RangeCalendarRoot.tsx | 7 +- .../root/useRangeCalendarRoot.tsx | 26 +++- .../base/Calendar/root/CalendarRoot.tsx | 7 +- .../base/Calendar/root/useCalendarRoot.ts | 31 +++- 29 files changed, 966 insertions(+), 1024 deletions(-) delete mode 100644 docs/data/date-pickers/base-calendar/MonthCalendarDemo.tsx.preview delete mode 100644 docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.tsx.preview delete mode 100644 docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.tsx.preview diff --git a/docs/data/date-pickers/base-calendar/DayCalendarDemo.js b/docs/data/date-pickers/base-calendar/DayCalendarDemo.js index f79edb7eff8d4..e3d5a2df5af6f 100644 --- a/docs/data/date-pickers/base-calendar/DayCalendarDemo.js +++ b/docs/data/date-pickers/base-calendar/DayCalendarDemo.js @@ -2,67 +2,66 @@ import * as React from 'react'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports -import { - Calendar, - useCalendarContext, -} from '@mui/x-date-pickers/internals/base/Calendar'; +import { Calendar } from '@mui/x-date-pickers/internals/base/Calendar'; import styles from './calendar.module.css'; -function Header() { - const { visibleDate } = useCalendarContext(); - - return ( -
- - ◀ - - {visibleDate.format('MMMM YYYY')} - - ▶ - -
- ); -} - export default function DayCalendarDemo() { return ( -
- - - {({ days }) => - days.map((day) => ( - - )) - } - - - {({ weeks }) => - weeks.map((week) => ( - - {({ days }) => - days.map((day) => ( - - )) - } - - )) - } - - + {({ visibleDate }) => ( + +
+ + ◀ + + {visibleDate.format('MMMM YYYY')} + + ▶ + +
+ + + {({ days }) => + days.map((day) => ( + + )) + } + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => + days.map((day) => ( + + )) + } + + )) + } + + +
+ )} ); diff --git a/docs/data/date-pickers/base-calendar/DayCalendarDemo.tsx b/docs/data/date-pickers/base-calendar/DayCalendarDemo.tsx index f79edb7eff8d4..e3d5a2df5af6f 100644 --- a/docs/data/date-pickers/base-calendar/DayCalendarDemo.tsx +++ b/docs/data/date-pickers/base-calendar/DayCalendarDemo.tsx @@ -2,67 +2,66 @@ import * as React from 'react'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports -import { - Calendar, - useCalendarContext, -} from '@mui/x-date-pickers/internals/base/Calendar'; +import { Calendar } from '@mui/x-date-pickers/internals/base/Calendar'; import styles from './calendar.module.css'; -function Header() { - const { visibleDate } = useCalendarContext(); - - return ( -
- - ◀ - - {visibleDate.format('MMMM YYYY')} - - ▶ - -
- ); -} - export default function DayCalendarDemo() { return ( -
- - - {({ days }) => - days.map((day) => ( - - )) - } - - - {({ weeks }) => - weeks.map((week) => ( - - {({ days }) => - days.map((day) => ( - - )) - } - - )) - } - - + {({ visibleDate }) => ( + +
+ + ◀ + + {visibleDate.format('MMMM YYYY')} + + ▶ + +
+ + + {({ days }) => + days.map((day) => ( + + )) + } + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => + days.map((day) => ( + + )) + } + + )) + } + + +
+ )} ); diff --git a/docs/data/date-pickers/base-calendar/DayCalendarWithFixedWeekNumberDemo.js b/docs/data/date-pickers/base-calendar/DayCalendarWithFixedWeekNumberDemo.js index 750d0fb6404f5..97c0c2234bd19 100644 --- a/docs/data/date-pickers/base-calendar/DayCalendarWithFixedWeekNumberDemo.js +++ b/docs/data/date-pickers/base-calendar/DayCalendarWithFixedWeekNumberDemo.js @@ -2,67 +2,66 @@ import * as React from 'react'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports -import { - Calendar, - useCalendarContext, -} from '@mui/x-date-pickers/internals/base/Calendar'; +import { Calendar } from '@mui/x-date-pickers/internals/base/Calendar'; import styles from './calendar.module.css'; -function Header() { - const { visibleDate } = useCalendarContext(); - - return ( -
- - ◀ - - {visibleDate.format('MMMM YYYY')} - - ▶ - -
- ); -} - export default function DayCalendarWithFixedWeekNumberDemo() { return ( -
- - - {({ days }) => - days.map((day) => ( - - )) - } - - - {({ weeks }) => - weeks.map((week) => ( - - {({ days }) => - days.map((day) => ( - - )) - } - - )) - } - - + {({ visibleDate }) => ( + +
+ + ◀ + + {visibleDate.format('MMMM YYYY')} + + ▶ + +
+ + + {({ days }) => + days.map((day) => ( + + )) + } + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => + days.map((day) => ( + + )) + } + + )) + } + + +
+ )} ); diff --git a/docs/data/date-pickers/base-calendar/DayCalendarWithFixedWeekNumberDemo.tsx b/docs/data/date-pickers/base-calendar/DayCalendarWithFixedWeekNumberDemo.tsx index 750d0fb6404f5..97c0c2234bd19 100644 --- a/docs/data/date-pickers/base-calendar/DayCalendarWithFixedWeekNumberDemo.tsx +++ b/docs/data/date-pickers/base-calendar/DayCalendarWithFixedWeekNumberDemo.tsx @@ -2,67 +2,66 @@ import * as React from 'react'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports -import { - Calendar, - useCalendarContext, -} from '@mui/x-date-pickers/internals/base/Calendar'; +import { Calendar } from '@mui/x-date-pickers/internals/base/Calendar'; import styles from './calendar.module.css'; -function Header() { - const { visibleDate } = useCalendarContext(); - - return ( -
- - ◀ - - {visibleDate.format('MMMM YYYY')} - - ▶ - -
- ); -} - export default function DayCalendarWithFixedWeekNumberDemo() { return ( -
- - - {({ days }) => - days.map((day) => ( - - )) - } - - - {({ weeks }) => - weeks.map((week) => ( - - {({ days }) => - days.map((day) => ( - - )) - } - - )) - } - - + {({ visibleDate }) => ( + +
+ + ◀ + + {visibleDate.format('MMMM YYYY')} + + ▶ + +
+ + + {({ days }) => + days.map((day) => ( + + )) + } + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => + days.map((day) => ( + + )) + } + + )) + } + + +
+ )} ); diff --git a/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.js b/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.js index d50a9cd346143..942908dc880f8 100644 --- a/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.js +++ b/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.js @@ -1,69 +1,67 @@ import * as React from 'react'; - import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports -import { - Calendar, - useCalendarContext, -} from '@mui/x-date-pickers/internals/base/Calendar'; +import { Calendar } from '@mui/x-date-pickers/internals/base/Calendar'; import styles from './calendar.module.css'; -function Header() { - const { visibleDate } = useCalendarContext(); - - return ( -
- - ◀ - - {visibleDate.format('MMMM YYYY')} - - ▶ - -
- ); -} - export default function DayCalendarWithValidationDemo() { return ( -
- - - {({ days }) => - days.map((day) => ( - - )) - } - - - {({ weeks }) => - weeks.map((week) => ( - - {({ days }) => - days.map((day) => ( - - )) - } - - )) - } - - + {({ visibleDate }) => ( + +
+ + ◀ + + {visibleDate.format('MMMM YYYY')} + + ▶ + +
{' '} + + + {({ days }) => + days.map((day) => ( + + )) + } + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => + days.map((day) => ( + + )) + } + + )) + } + + +
+ )} ); diff --git a/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.tsx b/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.tsx index f06afa8db50c4..942908dc880f8 100644 --- a/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.tsx +++ b/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.tsx @@ -1,69 +1,67 @@ import * as React from 'react'; -import dayjs from 'dayjs'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports -import { - Calendar, - useCalendarContext, -} from '@mui/x-date-pickers/internals/base/Calendar'; +import { Calendar } from '@mui/x-date-pickers/internals/base/Calendar'; import styles from './calendar.module.css'; -function Header() { - const { visibleDate } = useCalendarContext(); - - return ( -
- - ◀ - - {visibleDate.format('MMMM YYYY')} - - ▶ - -
- ); -} - export default function DayCalendarWithValidationDemo() { return ( -
- - - {({ days }) => - days.map((day) => ( - - )) - } - - - {({ weeks }) => - weeks.map((week) => ( - - {({ days }) => - days.map((day) => ( - - )) - } - - )) - } - - + {({ visibleDate }) => ( + +
+ + ◀ + + {visibleDate.format('MMMM YYYY')} + + ▶ + +
{' '} + + + {({ days }) => + days.map((day) => ( + + )) + } + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => + days.map((day) => ( + + )) + } + + )) + } + + +
+ )} ); diff --git a/docs/data/date-pickers/base-calendar/DayCalendarWithWeekNumberDemo.js b/docs/data/date-pickers/base-calendar/DayCalendarWithWeekNumberDemo.js index f0f8db396db0d..8839cfa4fff39 100644 --- a/docs/data/date-pickers/base-calendar/DayCalendarWithWeekNumberDemo.js +++ b/docs/data/date-pickers/base-calendar/DayCalendarWithWeekNumberDemo.js @@ -4,28 +4,9 @@ import clsx from 'clsx'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports -import { - Calendar, - useCalendarContext, -} from '@mui/x-date-pickers/internals/base/Calendar'; +import { Calendar } from '@mui/x-date-pickers/internals/base/Calendar'; import styles from './calendar.module.css'; -function Header() { - const { visibleDate } = useCalendarContext(); - - return ( -
- - ◀ - - {visibleDate.format('MMMM YYYY')} - - ▶ - -
- ); -} - function DayCalendar(props) { return ( @@ -33,59 +14,77 @@ function DayCalendar(props) { {...props} className={clsx(styles.Root, styles.RootWithWeekNumber)} > -
- - - {({ days }) => ( - - - # - - {days.map((day) => ( - - ))} - - )} - - - {({ weeks }) => - weeks.map((week) => ( - - {({ days }) => ( - - - {days[0].week()} - - {days.map((day) => ( - - ))} - - )} - - )) - } - - + {({ visibleDate }) => ( + +
+ + ◀ + + {visibleDate.format('MMMM YYYY')} + + ▶ + +
{' '} + + + {({ days }) => ( + + + # + + {days.map((day) => ( + + ))} + + )} + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => ( + + + {days[0].week()} + + {days.map((day) => ( + + ))} + + )} + + )) + } + + +
+ )} ); diff --git a/docs/data/date-pickers/base-calendar/DayCalendarWithWeekNumberDemo.tsx b/docs/data/date-pickers/base-calendar/DayCalendarWithWeekNumberDemo.tsx index b58264f112eca..1ca6cd0996411 100644 --- a/docs/data/date-pickers/base-calendar/DayCalendarWithWeekNumberDemo.tsx +++ b/docs/data/date-pickers/base-calendar/DayCalendarWithWeekNumberDemo.tsx @@ -4,28 +4,9 @@ import { Dayjs } from 'dayjs'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports -import { - Calendar, - useCalendarContext, -} from '@mui/x-date-pickers/internals/base/Calendar'; +import { Calendar } from '@mui/x-date-pickers/internals/base/Calendar'; import styles from './calendar.module.css'; -function Header() { - const { visibleDate } = useCalendarContext(); - - return ( -
- - ◀ - - {visibleDate.format('MMMM YYYY')} - - ▶ - -
- ); -} - function DayCalendar(props: Omit) { return ( @@ -33,59 +14,77 @@ function DayCalendar(props: Omit) { {...props} className={clsx(styles.Root, styles.RootWithWeekNumber)} > -
- - - {({ days }) => ( - - - # - - {days.map((day) => ( - - ))} - - )} - - - {({ weeks }) => - weeks.map((week) => ( - - {({ days }) => ( - - - {days[0].week()} - - {days.map((day) => ( - - ))} - - )} - - )) - } - - + {({ visibleDate }) => ( + +
+ + ◀ + + {visibleDate.format('MMMM YYYY')} + + ▶ + +
{' '} + + + {({ days }) => ( + + + # + + {days.map((day) => ( + + ))} + + )} + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => ( + + + {days[0].week()} + + {days.map((day) => ( + + ))} + + )} + + )) + } + + +
+ )} ); diff --git a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.js b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.js index 7274228d02023..35819d87a12bc 100644 --- a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.js +++ b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.js @@ -3,73 +3,66 @@ import clsx from 'clsx'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports -import { - RangeCalendar, - useRangeCalendarContext, -} from '@mui/x-date-pickers-pro/internals/base/RangeCalendar'; +import { RangeCalendar } from '@mui/x-date-pickers-pro/internals/base/RangeCalendar'; import styles from './calendar.module.css'; -function Header() { - const { visibleDate } = useRangeCalendarContext(); - - return ( -
- - ◀ - - {visibleDate.format('MMMM YYYY')} - - ▶ - -
- ); -} - export default function DayRangeCalendarDemo() { return ( -
- - - {({ days }) => - days.map((day) => ( - - )) - } - - - {({ weeks }) => - weeks.map((week) => ( - - {({ days }) => - days.map((day) => ( - - )) - } - - )) - } - - + {({ visibleDate }) => ( + +
+ + ◀ + + {visibleDate.format('MMMM YYYY')} + + ▶ + +
+ + + {({ days }) => + days.map((day) => ( + + )) + } + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => + days.map((day) => ( + + )) + } + + )) + } + + +
+ )} ); diff --git a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx index 7274228d02023..35819d87a12bc 100644 --- a/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx +++ b/docs/data/date-pickers/base-calendar/DayRangeCalendarDemo.tsx @@ -3,73 +3,66 @@ import clsx from 'clsx'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports -import { - RangeCalendar, - useRangeCalendarContext, -} from '@mui/x-date-pickers-pro/internals/base/RangeCalendar'; +import { RangeCalendar } from '@mui/x-date-pickers-pro/internals/base/RangeCalendar'; import styles from './calendar.module.css'; -function Header() { - const { visibleDate } = useRangeCalendarContext(); - - return ( -
- - ◀ - - {visibleDate.format('MMMM YYYY')} - - ▶ - -
- ); -} - export default function DayRangeCalendarDemo() { return ( -
- - - {({ days }) => - days.map((day) => ( - - )) - } - - - {({ weeks }) => - weeks.map((week) => ( - - {({ days }) => - days.map((day) => ( - - )) - } - - )) - } - - + {({ visibleDate }) => ( + +
+ + ◀ + + {visibleDate.format('MMMM YYYY')} + + ▶ + +
+ + + {({ days }) => + days.map((day) => ( + + )) + } + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => + days.map((day) => ( + + )) + } + + )) + } + + +
+ )} ); diff --git a/docs/data/date-pickers/base-calendar/MonthCalendarDemo.js b/docs/data/date-pickers/base-calendar/MonthCalendarDemo.js index 4dcb327f74126..62e160b15c694 100644 --- a/docs/data/date-pickers/base-calendar/MonthCalendarDemo.js +++ b/docs/data/date-pickers/base-calendar/MonthCalendarDemo.js @@ -3,46 +3,45 @@ import * as React from 'react'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports -import { - Calendar, - useCalendarContext, -} from '@mui/x-date-pickers/internals/base/Calendar'; +import { Calendar } from '@mui/x-date-pickers/internals/base/Calendar'; import styles from './calendar.module.css'; -function Header() { - const { visibleDate } = useCalendarContext(); - - return ( -
- - ◀ - - {visibleDate.format('YYYY')} - - ▶ - -
- ); -} - export default function MonthCalendarDemo() { const [value, setValue] = React.useState(null); return ( -
- - {({ months }) => - months.map((month) => ( - - )) - } - + {({ visibleDate }) => ( + +
+ + ◀ + + {visibleDate.format('YYYY')} + + ▶ + +
{' '} + + {({ months }) => + months.map((month) => ( + + )) + } + +
+ )} ); diff --git a/docs/data/date-pickers/base-calendar/MonthCalendarDemo.tsx b/docs/data/date-pickers/base-calendar/MonthCalendarDemo.tsx index 61146fe490457..7d53087a9f7f6 100644 --- a/docs/data/date-pickers/base-calendar/MonthCalendarDemo.tsx +++ b/docs/data/date-pickers/base-calendar/MonthCalendarDemo.tsx @@ -3,46 +3,45 @@ import { Dayjs } from 'dayjs'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports -import { - Calendar, - useCalendarContext, -} from '@mui/x-date-pickers/internals/base/Calendar'; +import { Calendar } from '@mui/x-date-pickers/internals/base/Calendar'; import styles from './calendar.module.css'; -function Header() { - const { visibleDate } = useCalendarContext(); - - return ( -
- - ◀ - - {visibleDate.format('YYYY')} - - ▶ - -
- ); -} - export default function MonthCalendarDemo() { const [value, setValue] = React.useState(null); return ( -
- - {({ months }) => - months.map((month) => ( - - )) - } - + {({ visibleDate }) => ( + +
+ + ◀ + + {visibleDate.format('YYYY')} + + ▶ + +
{' '} + + {({ months }) => + months.map((month) => ( + + )) + } + +
+ )} ); diff --git a/docs/data/date-pickers/base-calendar/MonthCalendarDemo.tsx.preview b/docs/data/date-pickers/base-calendar/MonthCalendarDemo.tsx.preview deleted file mode 100644 index 2dcd2a095dd1b..0000000000000 --- a/docs/data/date-pickers/base-calendar/MonthCalendarDemo.tsx.preview +++ /dev/null @@ -1,14 +0,0 @@ - -
- - {({ months }) => - months.map((month) => ( - - )) - } - - \ No newline at end of file diff --git a/docs/data/date-pickers/base-calendar/MonthCalendarWithCustomCellFormatDemo.js b/docs/data/date-pickers/base-calendar/MonthCalendarWithCustomCellFormatDemo.js index 1d22853691634..0f171906260d6 100644 --- a/docs/data/date-pickers/base-calendar/MonthCalendarWithCustomCellFormatDemo.js +++ b/docs/data/date-pickers/base-calendar/MonthCalendarWithCustomCellFormatDemo.js @@ -4,28 +4,9 @@ import clsx from 'clsx'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports -import { - Calendar, - useCalendarContext, -} from '@mui/x-date-pickers/internals/base/Calendar'; +import { Calendar } from '@mui/x-date-pickers/internals/base/Calendar'; import styles from './calendar.module.css'; -function Header() { - const { visibleDate } = useCalendarContext(); - - return ( -
- - ◀ - - {visibleDate.format('YYYY')} - - ▶ - -
- ); -} - export default function MonthCalendarWithCustomCellFormatDemo() { const [value, setValue] = React.useState(null); @@ -36,19 +17,37 @@ export default function MonthCalendarWithCustomCellFormatDemo() { onValueChange={setValue} className={clsx(styles.Root, styles.RootShort)} > -
- - {({ months }) => - months.map((month) => ( - - )) - } - + {({ visibleDate }) => ( + +
+ + ◀ + + {visibleDate.format('YYYY')} + + ▶ + +
+ + {({ months }) => + months.map((month) => ( + + )) + } + +
+ )} ); diff --git a/docs/data/date-pickers/base-calendar/MonthCalendarWithCustomCellFormatDemo.tsx b/docs/data/date-pickers/base-calendar/MonthCalendarWithCustomCellFormatDemo.tsx index e4f45d97071fb..24262e27e4132 100644 --- a/docs/data/date-pickers/base-calendar/MonthCalendarWithCustomCellFormatDemo.tsx +++ b/docs/data/date-pickers/base-calendar/MonthCalendarWithCustomCellFormatDemo.tsx @@ -4,28 +4,9 @@ import { Dayjs } from 'dayjs'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports -import { - Calendar, - useCalendarContext, -} from '@mui/x-date-pickers/internals/base/Calendar'; +import { Calendar } from '@mui/x-date-pickers/internals/base/Calendar'; import styles from './calendar.module.css'; -function Header() { - const { visibleDate } = useCalendarContext(); - - return ( -
- - ◀ - - {visibleDate.format('YYYY')} - - ▶ - -
- ); -} - export default function MonthCalendarWithCustomCellFormatDemo() { const [value, setValue] = React.useState(null); @@ -36,19 +17,37 @@ export default function MonthCalendarWithCustomCellFormatDemo() { onValueChange={setValue} className={clsx(styles.Root, styles.RootShort)} > -
- - {({ months }) => - months.map((month) => ( - - )) - } - + {({ visibleDate }) => ( + +
+ + ◀ + + {visibleDate.format('YYYY')} + + ▶ + +
+ + {({ months }) => + months.map((month) => ( + + )) + } + +
+ )} ); diff --git a/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.js b/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.js index 9f9c72e5f1695..e9c30e397055a 100644 --- a/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.js +++ b/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.js @@ -3,46 +3,45 @@ import * as React from 'react'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports -import { - Calendar, - useCalendarContext, -} from '@mui/x-date-pickers/internals/base/Calendar'; +import { Calendar } from '@mui/x-date-pickers/internals/base/Calendar'; import styles from './calendar.module.css'; -function Header() { - const { visibleDate } = useCalendarContext(); - - return ( -
- - ◀ - - {visibleDate.format('YYYY')} - - ▶ - -
- ); -} - export default function MonthCalendarWithListLayoutDemo() { const [value, setValue] = React.useState(null); return ( -
- - {({ months }) => - months.map((month) => ( - - )) - } - + {({ visibleDate }) => ( + +
+ + ◀ + + {visibleDate.format('YYYY')} + + ▶ + +
{' '} + + {({ months }) => + months.map((month) => ( + + )) + } + +
+ )} ); diff --git a/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.tsx b/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.tsx index b64fea1a3de45..76ac6dd27bf93 100644 --- a/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.tsx +++ b/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.tsx @@ -3,46 +3,45 @@ import { Dayjs } from 'dayjs'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports -import { - Calendar, - useCalendarContext, -} from '@mui/x-date-pickers/internals/base/Calendar'; +import { Calendar } from '@mui/x-date-pickers/internals/base/Calendar'; import styles from './calendar.module.css'; -function Header() { - const { visibleDate } = useCalendarContext(); - - return ( -
- - ◀ - - {visibleDate.format('YYYY')} - - ▶ - -
- ); -} - export default function MonthCalendarWithListLayoutDemo() { const [value, setValue] = React.useState(null); return ( -
- - {({ months }) => - months.map((month) => ( - - )) - } - + {({ visibleDate }) => ( + +
+ + ◀ + + {visibleDate.format('YYYY')} + + ▶ + +
{' '} + + {({ months }) => + months.map((month) => ( + + )) + } + +
+ )} ); diff --git a/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.tsx.preview b/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.tsx.preview deleted file mode 100644 index 28bf2a9fcbb53..0000000000000 --- a/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.tsx.preview +++ /dev/null @@ -1,14 +0,0 @@ - -
- - {({ months }) => - months.map((month) => ( - - )) - } - - \ No newline at end of file diff --git a/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.js b/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.js index ddddc14041b16..9176585aef37b 100644 --- a/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.js +++ b/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.js @@ -3,50 +3,43 @@ import clsx from 'clsx'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports -import { - RangeCalendar, - useRangeCalendarContext, -} from '@mui/x-date-pickers-pro/internals/base/RangeCalendar'; +import { RangeCalendar } from '@mui/x-date-pickers-pro/internals/base/RangeCalendar'; import styles from './calendar.module.css'; -function Header() { - const { visibleDate } = useRangeCalendarContext(); - - return ( -
- - ◀ - - {visibleDate.format('YYYY')} - - ▶ - -
- ); -} - export default function MonthRangeCalendarDemo() { return ( -
- - {({ months }) => - months.map((month) => ( - - )) - } - + {({ visibleDate }) => ( + +
+ + ◀ + + {visibleDate.format('YYYY')} + + ▶ + +
{' '} + + {({ months }) => + months.map((month) => ( + + )) + } + +
+ )} ); diff --git a/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.tsx b/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.tsx index ddddc14041b16..9176585aef37b 100644 --- a/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.tsx +++ b/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.tsx @@ -3,50 +3,43 @@ import clsx from 'clsx'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports -import { - RangeCalendar, - useRangeCalendarContext, -} from '@mui/x-date-pickers-pro/internals/base/RangeCalendar'; +import { RangeCalendar } from '@mui/x-date-pickers-pro/internals/base/RangeCalendar'; import styles from './calendar.module.css'; -function Header() { - const { visibleDate } = useRangeCalendarContext(); - - return ( -
- - ◀ - - {visibleDate.format('YYYY')} - - ▶ - -
- ); -} - export default function MonthRangeCalendarDemo() { return ( -
- - {({ months }) => - months.map((month) => ( - - )) - } - + {({ visibleDate }) => ( + +
+ + ◀ + + {visibleDate.format('YYYY')} + + ▶ + +
{' '} + + {({ months }) => + months.map((month) => ( + + )) + } + +
+ )} ); diff --git a/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.tsx.preview b/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.tsx.preview deleted file mode 100644 index 0657052bc9a13..0000000000000 --- a/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.tsx.preview +++ /dev/null @@ -1,14 +0,0 @@ - -
- - {({ months }) => - months.map((month) => ( - - )) - } - - \ No newline at end of file diff --git a/docs/data/date-pickers/base-calendar/YearCalendarWithDecadeNavigationDemo.js b/docs/data/date-pickers/base-calendar/YearCalendarWithDecadeNavigationDemo.js index 6a3d3c8768382..07f7d35eb6c31 100644 --- a/docs/data/date-pickers/base-calendar/YearCalendarWithDecadeNavigationDemo.js +++ b/docs/data/date-pickers/base-calendar/YearCalendarWithDecadeNavigationDemo.js @@ -1,41 +1,10 @@ import * as React from 'react'; - import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports -import { - Calendar, - useCalendarContext, -} from '@mui/x-date-pickers/internals/base/Calendar'; +import { Calendar } from '@mui/x-date-pickers/internals/base/Calendar'; import styles from './calendar.module.css'; -function Header() { - const { visibleDate } = useCalendarContext(); - - const startOfDecade = React.useMemo( - () => visibleDate.set('year', Math.floor(visibleDate.year() / 10) * 10), - [visibleDate], - ); - - return ( -
- - ◀ - - {startOfDecade.format('YYYY')}s - - ▶ - -
- ); -} - const getYearsInDecade = ({ visibleDate }) => { const reference = visibleDate.startOf('year'); const decade = Math.floor(reference.year() / 10) * 10; @@ -48,22 +17,45 @@ export default function YearCalendarWithDecadeNavigationDemo() { return ( -
- - {({ years }) => - years.map((year) => ( - - )) - } - + {({ visibleDate }) => ( + +
+ + ◀ + + + {visibleDate + .set('year', Math.floor(visibleDate.year() / 10) * 10) + .format('YYYY')} + s + + + ▶ + +
{' '} + + {({ years }) => + years.map((year) => ( + + )) + } + +
+ )} ); diff --git a/docs/data/date-pickers/base-calendar/YearCalendarWithDecadeNavigationDemo.tsx b/docs/data/date-pickers/base-calendar/YearCalendarWithDecadeNavigationDemo.tsx index e3ab80b298838..fec096af6faed 100644 --- a/docs/data/date-pickers/base-calendar/YearCalendarWithDecadeNavigationDemo.tsx +++ b/docs/data/date-pickers/base-calendar/YearCalendarWithDecadeNavigationDemo.tsx @@ -1,41 +1,10 @@ import * as React from 'react'; -import { Dayjs } from 'dayjs'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; // eslint-disable-next-line no-restricted-imports -import { - Calendar, - useCalendarContext, -} from '@mui/x-date-pickers/internals/base/Calendar'; +import { Calendar } from '@mui/x-date-pickers/internals/base/Calendar'; import styles from './calendar.module.css'; -function Header() { - const { visibleDate } = useCalendarContext(); - - const startOfDecade = React.useMemo( - () => visibleDate.set('year', Math.floor(visibleDate.year() / 10) * 10), - [visibleDate], - ); - - return ( -
- - ◀ - - {startOfDecade.format('YYYY')}s - - ▶ - -
- ); -} - const getYearsInDecade: Calendar.YearsGrid.Props['getItems'] = ({ visibleDate }) => { const reference = visibleDate.startOf('year'); const decade = Math.floor(reference.year() / 10) * 10; @@ -48,22 +17,45 @@ export default function YearCalendarWithDecadeNavigationDemo() { return ( -
- - {({ years }) => - years.map((year) => ( - - )) - } - + {({ visibleDate }) => ( + +
+ + ◀ + + + {visibleDate + .set('year', Math.floor(visibleDate.year() / 10) * 10) + .format('YYYY')} + s + + + ▶ + +
{' '} + + {({ years }) => + years.map((year) => ( + + )) + } + +
+ )} ); diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid-body/useRangeCalendarDaysGridBody.ts b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid-body/useRangeCalendarDaysGridBody.ts index c80df2d9efa53..affd547ba5556 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid-body/useRangeCalendarDaysGridBody.ts +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/days-grid-body/useRangeCalendarDaysGridBody.ts @@ -17,8 +17,9 @@ export function useRangeCalendarDaysGridBody(parameters: useRangeCalendarDaysGri const rootContext = useRangeCalendarRootContext(); + // TODO: Add the same of year and month list and year. const onMouseLeave = useEventCallback(() => { - rootContext.setHoveredDate(null); + rootContext.setHoveredDate(null, 'day'); }); const getDaysGridBodyProps = React.useCallback( diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/useRangeCalendarMonthsCell.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/useRangeCalendarMonthsCell.tsx index 811588a63489c..76163d31ce8ca 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/useRangeCalendarMonthsCell.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/months-cell/useRangeCalendarMonthsCell.tsx @@ -9,8 +9,8 @@ import { useRangeCell } from '../utils/useRangeCell'; export function useRangeCalendarMonthsCell(parameters: useRangeCalendarMonthsCell.Parameters) { const { ctx, value } = parameters; - const rangeCellProps = useRangeCell({ ctx, value, section: 'month' }); const { getMonthsCellProps: getBaseMonthsCellProps } = useBaseCalendarMonthsCell(parameters); + const rangeCellProps = useRangeCell({ ctx, value, section: 'month' }); const getMonthsCellProps = React.useCallback( (externalProps: GenericHTMLProps) => { diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRoot.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRoot.tsx index 7d81b1d85243d..f2c343a7b4541 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRoot.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/RangeCalendarRoot.tsx @@ -43,6 +43,8 @@ const RangeCalendarRoot = React.forwardRef(function RangeCalendarRoot( disablePast, disableFuture, shouldDisableDate, + // Children + children, // Range position props rangePosition, defaultRangePosition, @@ -70,6 +72,7 @@ const RangeCalendarRoot = React.forwardRef(function RangeCalendarRoot( defaultVisibleDate, onError, shouldDisableDate, + children, disablePast, disableFuture, minDate, @@ -116,9 +119,7 @@ export namespace RangeCalendarRoot { export interface Props extends useRangeCalendarRoot.Parameters, - Omit, 'value' | 'defaultValue' | 'onError'> { - children: React.ReactNode; - } + Omit, 'value' | 'defaultValue' | 'onError' | 'children'> {} export interface ValueChangeHandlerContext extends useBaseCalendarRoot.ValueChangeHandlerContext {} diff --git a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx index c3e0d628fdd05..9571117f63ac0 100644 --- a/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx +++ b/packages/x-date-pickers-pro/src/internals/base/RangeCalendar/root/useRangeCalendarRoot.tsx @@ -45,6 +45,8 @@ export function useRangeCalendarRoot(parameters: useRangeCalendarRoot.Parameters disablePast, disableFuture, shouldDisableDate, + // Children + children: childrenProp, // Range position props rangePosition: rangePositionProp, defaultRangePosition: defaultRangePositionProp, @@ -182,9 +184,18 @@ export function useRangeCalendarRoot(parameters: useRangeCalendarRoot.Parameters getNewValueFromNewSelectedDate, }); - const getRootProps = React.useCallback((externalProps: GenericHTMLProps) => { - return mergeReactProps(externalProps, {}); - }, []); + const getRootProps = React.useCallback( + (externalProps: GenericHTMLProps) => { + let children: React.ReactNode; + if (!React.isValidElement(childrenProp) && typeof childrenProp === 'function') { + children = childrenProp({ visibleDate: baseContext.visibleDate }); + } else { + children = childrenProp; + } + return mergeReactProps(externalProps, { children }); + }, + [childrenProp, baseContext.visibleDate], + ); const isEmpty = value[0] == null && value[1] == null; @@ -218,5 +229,14 @@ export namespace useRangeCalendarRoot { * @default useMediaQuery('@media (pointer: fine)') */ disableHoverPreview?: boolean; + /** + * The children of the calendar. + * If a function is provided, it will be called with the public context as its parameter. + */ + children?: React.ReactNode | ((parameters: ChildrenParameters) => React.ReactNode); + } + + export interface ChildrenParameters { + visibleDate: PickerValidDate; } } diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRoot.tsx b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRoot.tsx index 6128b1913ff6e..f0c6e6cd7ece7 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRoot.tsx +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/CalendarRoot.tsx @@ -31,6 +31,8 @@ const CalendarRoot = React.forwardRef(function CalendarRoot( onVisibleDateChange, visibleDate, defaultVisibleDate, + // Children + children, // Validation props onError, minDate, @@ -64,6 +66,7 @@ const CalendarRoot = React.forwardRef(function CalendarRoot( disableFuture, minDate, maxDate, + children, }); const state: CalendarRoot.State = React.useMemo(() => ({ empty: isEmpty }), [isEmpty]); @@ -89,9 +92,7 @@ export namespace CalendarRoot { export interface Props extends useCalendarRoot.Parameters, - Omit, 'value' | 'defaultValue' | 'onError'> { - children: React.ReactNode; - } + Omit, 'value' | 'defaultValue' | 'onError' | 'children'> {} export interface ValueChangeHandlerContext extends useBaseCalendarRoot.ValueChangeHandlerContext {} diff --git a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts index c5fb35123d14b..4dc1f7dec08ad 100644 --- a/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts +++ b/packages/x-date-pickers/src/internals/base/Calendar/root/useCalendarRoot.ts @@ -1,5 +1,5 @@ import * as React from 'react'; -import { DateValidationError } from '../../../../models'; +import { DateValidationError, PickerValidDate } from '../../../../models'; import { useDateManager } from '../../../../managers'; import { ExportedValidateDateProps, ValidateDateProps } from '../../../../validation/validateDate'; import { useUtils } from '../../../hooks/useUtils'; @@ -31,6 +31,8 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { shouldDisableDate, shouldDisableMonth, shouldDisableYear, + // Children + children: childrenProp, // Parameters forwarded to `useBaseCalendarRoot` ...baseParameters } = parameters; @@ -75,9 +77,18 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { } } - const getRootProps = React.useCallback((externalProps: GenericHTMLProps) => { - return mergeReactProps(externalProps, {}); - }, []); + const getRootProps = React.useCallback( + (externalProps: GenericHTMLProps) => { + let children: React.ReactNode; + if (!React.isValidElement(childrenProp) && typeof childrenProp === 'function') { + children = childrenProp({ visibleDate: baseContext.visibleDate }); + } else { + children = childrenProp; + } + return mergeReactProps(externalProps, { children }); + }, + [childrenProp, baseContext.visibleDate], + ); const isEmpty = value == null; @@ -90,5 +101,15 @@ export function useCalendarRoot(parameters: useCalendarRoot.Parameters) { export namespace useCalendarRoot { export interface Parameters extends useBaseCalendarRoot.PublicParameters, - ExportedValidateDateProps {} + ExportedValidateDateProps { + /** + * The children of the calendar. + * If a function is provided, it will be called with the public context as its parameter. + */ + children?: React.ReactNode | ((parameters: ChildrenParameters) => React.ReactNode); + } + + export interface ChildrenParameters { + visibleDate: PickerValidDate; + } } From 437913f2e4cf056d7d4f2f34895f621a60f3024a Mon Sep 17 00:00:00 2001 From: flavien Date: Tue, 14 Jan 2025 16:27:16 +0100 Subject: [PATCH 129/136] Add anatomy to the doc --- .../DayCalendarWithValidationDemo.js | 2 +- .../DayCalendarWithValidationDemo.tsx | 2 +- .../DayCalendarWithWeekNumberDemo.js | 2 +- .../DayCalendarWithWeekNumberDemo.tsx | 2 +- .../base-calendar/MonthCalendarDemo.js | 2 +- .../base-calendar/MonthCalendarDemo.tsx | 2 +- .../MonthCalendarWithListLayoutDemo.js | 2 +- .../MonthCalendarWithListLayoutDemo.tsx | 2 +- .../base-calendar/MonthRangeCalendarDemo.js | 2 +- .../base-calendar/MonthRangeCalendarDemo.tsx | 2 +- .../YearCalendarWithDecadeNavigationDemo.js | 2 +- .../YearCalendarWithDecadeNavigationDemo.tsx | 2 +- .../base-calendar/base-calendar.md | 68 +++++++++++++++++++ 13 files changed, 80 insertions(+), 12 deletions(-) diff --git a/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.js b/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.js index 942908dc880f8..2de5f34ca9882 100644 --- a/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.js +++ b/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.js @@ -25,7 +25,7 @@ export default function DayCalendarWithValidationDemo() { > ▶ -
{' '} +
{({ days }) => diff --git a/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.tsx b/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.tsx index 942908dc880f8..2de5f34ca9882 100644 --- a/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.tsx +++ b/docs/data/date-pickers/base-calendar/DayCalendarWithValidationDemo.tsx @@ -25,7 +25,7 @@ export default function DayCalendarWithValidationDemo() { > ▶ -
{' '} +
{({ days }) => diff --git a/docs/data/date-pickers/base-calendar/DayCalendarWithWeekNumberDemo.js b/docs/data/date-pickers/base-calendar/DayCalendarWithWeekNumberDemo.js index 8839cfa4fff39..23c14bd215d4f 100644 --- a/docs/data/date-pickers/base-calendar/DayCalendarWithWeekNumberDemo.js +++ b/docs/data/date-pickers/base-calendar/DayCalendarWithWeekNumberDemo.js @@ -30,7 +30,7 @@ function DayCalendar(props) { > ▶ -
{' '} +
{({ days }) => ( diff --git a/docs/data/date-pickers/base-calendar/DayCalendarWithWeekNumberDemo.tsx b/docs/data/date-pickers/base-calendar/DayCalendarWithWeekNumberDemo.tsx index 1ca6cd0996411..6fbd022964033 100644 --- a/docs/data/date-pickers/base-calendar/DayCalendarWithWeekNumberDemo.tsx +++ b/docs/data/date-pickers/base-calendar/DayCalendarWithWeekNumberDemo.tsx @@ -30,7 +30,7 @@ function DayCalendar(props: Omit) { > ▶ -
{' '} +
{({ days }) => ( diff --git a/docs/data/date-pickers/base-calendar/MonthCalendarDemo.js b/docs/data/date-pickers/base-calendar/MonthCalendarDemo.js index 62e160b15c694..60134a7b2af02 100644 --- a/docs/data/date-pickers/base-calendar/MonthCalendarDemo.js +++ b/docs/data/date-pickers/base-calendar/MonthCalendarDemo.js @@ -28,7 +28,7 @@ export default function MonthCalendarDemo() { > ▶ -
{' '} +
{({ months }) => months.map((month) => ( diff --git a/docs/data/date-pickers/base-calendar/MonthCalendarDemo.tsx b/docs/data/date-pickers/base-calendar/MonthCalendarDemo.tsx index 7d53087a9f7f6..7f844a4ca8f09 100644 --- a/docs/data/date-pickers/base-calendar/MonthCalendarDemo.tsx +++ b/docs/data/date-pickers/base-calendar/MonthCalendarDemo.tsx @@ -28,7 +28,7 @@ export default function MonthCalendarDemo() { > ▶ -
{' '} +
{({ months }) => months.map((month) => ( diff --git a/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.js b/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.js index e9c30e397055a..800ef557b1829 100644 --- a/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.js +++ b/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.js @@ -28,7 +28,7 @@ export default function MonthCalendarWithListLayoutDemo() { > ▶ -
{' '} +
{({ months }) => months.map((month) => ( diff --git a/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.tsx b/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.tsx index 76ac6dd27bf93..a0f461727e096 100644 --- a/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.tsx +++ b/docs/data/date-pickers/base-calendar/MonthCalendarWithListLayoutDemo.tsx @@ -28,7 +28,7 @@ export default function MonthCalendarWithListLayoutDemo() { > ▶ -
{' '} +
{({ months }) => months.map((month) => ( diff --git a/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.js b/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.js index 9176585aef37b..e1ccc288214b8 100644 --- a/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.js +++ b/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.js @@ -26,7 +26,7 @@ export default function MonthRangeCalendarDemo() { > ▶ -
{' '} +
{({ months }) => months.map((month) => ( diff --git a/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.tsx b/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.tsx index 9176585aef37b..e1ccc288214b8 100644 --- a/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.tsx +++ b/docs/data/date-pickers/base-calendar/MonthRangeCalendarDemo.tsx @@ -26,7 +26,7 @@ export default function MonthRangeCalendarDemo() { > ▶ -
{' '} +
{({ months }) => months.map((month) => ( diff --git a/docs/data/date-pickers/base-calendar/YearCalendarWithDecadeNavigationDemo.js b/docs/data/date-pickers/base-calendar/YearCalendarWithDecadeNavigationDemo.js index 07f7d35eb6c31..20d62bbcb808f 100644 --- a/docs/data/date-pickers/base-calendar/YearCalendarWithDecadeNavigationDemo.js +++ b/docs/data/date-pickers/base-calendar/YearCalendarWithDecadeNavigationDemo.js @@ -38,7 +38,7 @@ export default function YearCalendarWithDecadeNavigationDemo() { > ▶ -
{' '} +
▶ -
{' '} +
POC of a Calendar component using the Base UI DX.

+## Anatomy + +### Days + +```tsx + + + + {({ days }) => days.map((day) => )} + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => days.map((day) => )} + + )) + } + + + +``` + +### Months + +```tsx +// Grid layout + + + {({ months }) => months.map((month) => )} + + + +// List layout + + + {({ months }) => months.map((month) => )} + + +``` + +### Years + +```tsx +// Grid layout + + + {({ years }) => years.map((year) => )} + + + +// List layout + + + {({ years }) => years.map((year) => )} + + +``` + +### Navigation + +```tsx + + + + +``` + ## Day Calendar ### Single visible month From 7ff238cd75465b7f590df7b2c317b2adc62b73ff Mon Sep 17 00:00:00 2001 From: flavien Date: Wed, 15 Jan 2025 11:39:18 +0100 Subject: [PATCH 130/136] Work on a full MD2 recipe --- .../DateCalendarWithMaterialDesignDemo.js | 396 +++++++++++++++++ .../DateCalendarWithMaterialDesignDemo.tsx | 409 ++++++++++++++++++ ...CalendarWithMaterialDesignDemo.tsx.preview | 1 + .../base-calendar/base-calendar.md | 8 +- docs/package.json | 2 + .../useRangeCalendarContext.ts | 1 + .../useCalendarContext/useCalendarContext.ts | 1 + pnpm-lock.yaml | 6 + 8 files changed, 822 insertions(+), 2 deletions(-) create mode 100644 docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.js create mode 100644 docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.tsx create mode 100644 docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.tsx.preview diff --git a/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.js b/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.js new file mode 100644 index 0000000000000..296aac5d7e0f6 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.js @@ -0,0 +1,396 @@ +import * as React from 'react'; +import { TransitionGroup } from 'react-transition-group'; +import { useRtl } from '@mui/system/RtlProvider'; +import Fade from '@mui/material/Fade'; +import IconButton from '@mui/material/IconButton'; +import { styled, alpha, useTheme } from '@mui/material/styles'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { usePickerTranslations } from '@mui/x-date-pickers/hooks'; +import { + ArrowDropDownIcon, + ArrowRightIcon, + ArrowLeftIcon, +} from '@mui/x-date-pickers/icons'; +// eslint-disable-next-line no-restricted-imports +import { + Calendar, + useCalendarContext, +} from '@mui/x-date-pickers/internals/base/Calendar'; +import useId from '@mui/utils/useId'; +import Typography from '@mui/material/Typography'; + +export const DIALOG_WIDTH = 320; +export const MAX_CALENDAR_HEIGHT = 280; + +const CalendarHeaderRoot = styled('div')({ + display: 'flex', + alignItems: 'center', + marginTop: 12, + marginBottom: 4, + paddingLeft: 24, + paddingRight: 12, + // prevent jumping in safari + maxHeight: 40, + minHeight: 40, +}); + +const CalendarHeaderLabelContainer = styled('div')(({ theme }) => ({ + display: 'flex', + overflow: 'hidden', + alignItems: 'center', + cursor: 'pointer', + marginRight: 'auto', + ...theme.typography.body1, + fontWeight: theme.typography.fontWeightMedium, +})); + +const CalendarHeaderLabelContent = styled(TransitionGroup)({ + display: 'block', + position: 'relative', +}); + +const CalendarHeaderLabel = styled('div')({ + marginRight: 6, +}); + +const CalendarHeaderSwitchViewButton = styled(IconButton)({ + marginRight: 'auto', +}); + +const CalendarHeaderSwitchViewIcon = styled(ArrowDropDownIcon)(({ theme }) => ({ + willChange: 'transform', + transition: theme.transitions.create('transform'), + transform: 'rotate(0deg)', + '&[data-view="year"]': { + transform: 'rotate(180deg)', + }, +})); + +const CalendarHeaderNavigation = styled('div')({ + display: 'flex', +}); + +const CalendarHeaderNavigationButton = styled(IconButton)({ + '&[data-hidden]': { + visibility: 'hidden', + }, +}); + +const CalendarHeaderNavigationSpacier = styled('div')(({ theme }) => ({ + width: theme.spacing(3), +})); + +const YearsGrid = styled(Calendar.YearsGrid)({ + display: 'flex', + flexWrap: 'wrap', + justifyContent: 'space-evenly', + rowGap: 12, + columnGap: 24, + padding: '6px 0', + overflowY: 'auto', + height: '100%', + width: DIALOG_WIDTH, + maxHeight: MAX_CALENDAR_HEIGHT, + // avoid padding increasing width over defined + boxSizing: 'border-box', + position: 'relative', +}); + +const YearsCell = styled(Calendar.YearsCell)(({ theme }) => ({ + color: 'unset', + backgroundColor: 'transparent', + border: 0, + outline: 0, + ...theme.typography.subtitle1, + height: 36, + width: 72, + borderRadius: 18, + cursor: 'pointer', + '&:focus': { + backgroundColor: theme.vars + ? `rgba(${theme.vars.palette.action.activeChannel} / ${theme.vars.palette.action.focusOpacity})` + : alpha(theme.palette.action.active, theme.palette.action.focusOpacity), + }, + '&:hover': { + backgroundColor: theme.vars + ? `rgba(${theme.vars.palette.action.activeChannel} / ${theme.vars.palette.action.hoverOpacity})` + : alpha(theme.palette.action.active, theme.palette.action.hoverOpacity), + }, + '&:disabled': { + cursor: 'auto', + pointerEvents: 'none', + }, + '&[data-disabled]': { + color: (theme.vars || theme).palette.text.secondary, + }, + '&[data-selected]': { + color: (theme.vars || theme).palette.primary.contrastText, + backgroundColor: (theme.vars || theme).palette.primary.main, + '&:focus, &:hover': { + backgroundColor: (theme.vars || theme).palette.primary.dark, + }, + }, +})); + +const MonthsGrid = styled(Calendar.MonthsGrid)({ + display: 'flex', + flexWrap: 'wrap', + justifyContent: 'space-evenly', + rowGap: 16, + columnGap: 24, + padding: '8px 0', + width: DIALOG_WIDTH, + // avoid padding increasing width over defined + boxSizing: 'border-box', +}); + +const MonthsCell = styled(Calendar.MonthsCell)(({ theme }) => ({ + color: 'unset', + backgroundColor: 'transparent', + border: 0, + outline: 0, + ...theme.typography.subtitle1, + height: 36, + width: 72, + borderRadius: 18, + cursor: 'pointer', + '&:focus': { + backgroundColor: theme.vars + ? `rgba(${theme.vars.palette.action.activeChannel} / ${theme.vars.palette.action.hoverOpacity})` + : alpha(theme.palette.action.active, theme.palette.action.hoverOpacity), + }, + '&:hover': { + backgroundColor: theme.vars + ? `rgba(${theme.vars.palette.action.activeChannel} / ${theme.vars.palette.action.hoverOpacity})` + : alpha(theme.palette.action.active, theme.palette.action.hoverOpacity), + }, + '&:disabled': { + cursor: 'auto', + pointerEvents: 'none', + }, + '&[data-disabled]': { + color: (theme.vars || theme).palette.text.secondary, + }, + '&[data-selected]': { + color: (theme.vars || theme).palette.primary.contrastText, + backgroundColor: (theme.vars || theme).palette.primary.main, + '&:focus, &:hover': { + backgroundColor: (theme.vars || theme).palette.primary.dark, + }, + }, +})); + +const CalendarDaysGridHeader = styled(Calendar.DaysGridHeader)({ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', +}); + +const CalendarDaysGridWeekNumberHeaderCell = styled(Typography)(({ theme }) => ({ + width: 36, + height: 40, + margin: '0 2px', + textAlign: 'center', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + color: theme.palette.text.disabled, +})); + +const CalendarDaysGridHeaderCell = styled(Typography)(({ theme }) => ({ + width: 36, + height: 40, + margin: '0 2px', + textAlign: 'center', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + color: (theme.vars || theme).palette.text.secondary, +})); + +function CalendarHeader(props) { + const { view, onViewChange, views, labelId } = props; + const translations = usePickerTranslations(); + const theme = useTheme(); + const isRtl = useRtl(); + const { visibleDate, disabled } = useCalendarContext(); + + const handleToggleView = () => { + if (view === 'year' && views.month) { + onViewChange('month'); + } else if (view === 'year' && views.day) { + onViewChange('day'); + } else if (view === 'month' && views.day) { + onViewChange('day'); + } else if (view === 'month' && views.year) { + onViewChange('year'); + } else if (view === 'day' && views.month) { + onViewChange('month'); + } + }; + + const viewCount = Object.values(views).filter(Boolean).length; + + if (!views.day && !views.month && views.year) { + return null; + } + + const label = visibleDate.format('MMMM YYYY'); + + return ( + + + + + + {label} + + + + {viewCount > 1 && !disabled && ( + + + + )} + + + + + } + > + {isRtl ? ( + + ) : ( + + )} + + + + } + > + {isRtl ? ( + + ) : ( + + )} + + + + + ); +} + +const DEFAULT_VIEWS = { year: true, month: false, day: true }; + +function DateCalendar(props) { + const { views = DEFAULT_VIEWS, openTo, displayWeekNumber } = props; + const [view, setView] = React.useState(() => + openTo != null && views[openTo] ? openTo : 'day', + ); + const translations = usePickerTranslations(); + const id = useId(); + const gridLabelId = `${id}-grid-label`; + + return ( + + +
+
+ {view === 'year' && ( + + {({ years }) => + years.map((year) => ) + } + + )} + {view === 'month' && ( + + {({ months }) => + months.map((month) => ( + + )) + } + + )} + {view === 'day' && ( + + + {({ days }) => ( + + {displayWeekNumber && ( + + {translations.calendarWeekNumberHeaderText} + + )} + {days.map((day) => ( + } + /> + ))} + + )} + + + )} +
+
+
+ ); +} + +export default function DateCalendarWithMaterialDesignDemo() { + return ( + + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.tsx b/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.tsx new file mode 100644 index 0000000000000..0fa0f9b5bd38e --- /dev/null +++ b/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.tsx @@ -0,0 +1,409 @@ +import * as React from 'react'; +import { TransitionGroup } from 'react-transition-group'; +import { useRtl } from '@mui/system/RtlProvider'; +import Fade from '@mui/material/Fade'; +import IconButton from '@mui/material/IconButton'; +import { styled, alpha, useTheme } from '@mui/material/styles'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { usePickerTranslations } from '@mui/x-date-pickers/hooks'; +import { + ArrowDropDownIcon, + ArrowRightIcon, + ArrowLeftIcon, +} from '@mui/x-date-pickers/icons'; +// eslint-disable-next-line no-restricted-imports +import { + Calendar, + useCalendarContext, +} from '@mui/x-date-pickers/internals/base/Calendar'; +import useId from '@mui/utils/useId'; +import Typography from '@mui/material/Typography'; + +export const DIALOG_WIDTH = 320; +export const MAX_CALENDAR_HEIGHT = 280; + +const CalendarHeaderRoot = styled('div')({ + display: 'flex', + alignItems: 'center', + marginTop: 12, + marginBottom: 4, + paddingLeft: 24, + paddingRight: 12, + // prevent jumping in safari + maxHeight: 40, + minHeight: 40, +}); + +const CalendarHeaderLabelContainer = styled('div')(({ theme }) => ({ + display: 'flex', + overflow: 'hidden', + alignItems: 'center', + cursor: 'pointer', + marginRight: 'auto', + ...theme.typography.body1, + fontWeight: theme.typography.fontWeightMedium, +})); + +const CalendarHeaderLabelContent = styled(TransitionGroup)({ + display: 'block', + position: 'relative', +}); + +const CalendarHeaderLabel = styled('div')({ + marginRight: 6, +}); + +const CalendarHeaderSwitchViewButton = styled(IconButton)({ + marginRight: 'auto', +}); + +const CalendarHeaderSwitchViewIcon = styled(ArrowDropDownIcon)(({ theme }) => ({ + willChange: 'transform', + transition: theme.transitions.create('transform'), + transform: 'rotate(0deg)', + '&[data-view="year"]': { + transform: 'rotate(180deg)', + }, +})); + +const CalendarHeaderNavigation = styled('div')({ + display: 'flex', +}); + +const CalendarHeaderNavigationButton = styled(IconButton)({ + '&[data-hidden]': { + visibility: 'hidden', + }, +}); + +const CalendarHeaderNavigationSpacier = styled('div')(({ theme }) => ({ + width: theme.spacing(3), +})); + +const YearsGrid = styled(Calendar.YearsGrid)({ + display: 'flex', + flexWrap: 'wrap', + justifyContent: 'space-evenly', + rowGap: 12, + columnGap: 24, + padding: '6px 0', + overflowY: 'auto', + height: '100%', + width: DIALOG_WIDTH, + maxHeight: MAX_CALENDAR_HEIGHT, + // avoid padding increasing width over defined + boxSizing: 'border-box', + position: 'relative', +}); + +const YearsCell = styled(Calendar.YearsCell)(({ theme }) => ({ + color: 'unset', + backgroundColor: 'transparent', + border: 0, + outline: 0, + ...theme.typography.subtitle1, + height: 36, + width: 72, + borderRadius: 18, + cursor: 'pointer', + '&:focus': { + backgroundColor: theme.vars + ? `rgba(${theme.vars.palette.action.activeChannel} / ${theme.vars.palette.action.focusOpacity})` + : alpha(theme.palette.action.active, theme.palette.action.focusOpacity), + }, + '&:hover': { + backgroundColor: theme.vars + ? `rgba(${theme.vars.palette.action.activeChannel} / ${theme.vars.palette.action.hoverOpacity})` + : alpha(theme.palette.action.active, theme.palette.action.hoverOpacity), + }, + '&:disabled': { + cursor: 'auto', + pointerEvents: 'none', + }, + '&[data-disabled]': { + color: (theme.vars || theme).palette.text.secondary, + }, + '&[data-selected]': { + color: (theme.vars || theme).palette.primary.contrastText, + backgroundColor: (theme.vars || theme).palette.primary.main, + '&:focus, &:hover': { + backgroundColor: (theme.vars || theme).palette.primary.dark, + }, + }, +})); + +const MonthsGrid = styled(Calendar.MonthsGrid)({ + display: 'flex', + flexWrap: 'wrap', + justifyContent: 'space-evenly', + rowGap: 16, + columnGap: 24, + padding: '8px 0', + width: DIALOG_WIDTH, + // avoid padding increasing width over defined + boxSizing: 'border-box', +}); + +const MonthsCell = styled(Calendar.MonthsCell)(({ theme }) => ({ + color: 'unset', + backgroundColor: 'transparent', + border: 0, + outline: 0, + ...theme.typography.subtitle1, + height: 36, + width: 72, + borderRadius: 18, + cursor: 'pointer', + '&:focus': { + backgroundColor: theme.vars + ? `rgba(${theme.vars.palette.action.activeChannel} / ${theme.vars.palette.action.hoverOpacity})` + : alpha(theme.palette.action.active, theme.palette.action.hoverOpacity), + }, + '&:hover': { + backgroundColor: theme.vars + ? `rgba(${theme.vars.palette.action.activeChannel} / ${theme.vars.palette.action.hoverOpacity})` + : alpha(theme.palette.action.active, theme.palette.action.hoverOpacity), + }, + '&:disabled': { + cursor: 'auto', + pointerEvents: 'none', + }, + '&[data-disabled]': { + color: (theme.vars || theme).palette.text.secondary, + }, + '&[data-selected]': { + color: (theme.vars || theme).palette.primary.contrastText, + backgroundColor: (theme.vars || theme).palette.primary.main, + '&:focus, &:hover': { + backgroundColor: (theme.vars || theme).palette.primary.dark, + }, + }, +})); + +const CalendarDaysGridHeader = styled(Calendar.DaysGridHeader)({ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', +}); + +const CalendarDaysGridWeekNumberHeaderCell = styled(Typography)(({ theme }) => ({ + width: 36, + height: 40, + margin: '0 2px', + textAlign: 'center', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + color: theme.palette.text.disabled, +})); + +const CalendarDaysGridHeaderCell = styled(Typography)(({ theme }) => ({ + width: 36, + height: 40, + margin: '0 2px', + textAlign: 'center', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + color: (theme.vars || theme).palette.text.secondary, +})); + +function CalendarHeader(props: { + view: DateCalendarView; + onViewChange: (view: DateCalendarView) => void; + views: Record; + labelId: string; +}) { + const { view, onViewChange, views, labelId } = props; + const translations = usePickerTranslations(); + const theme = useTheme(); + const isRtl = useRtl(); + const { visibleDate, disabled } = useCalendarContext(); + + const handleToggleView = () => { + if (view === 'year' && views.month) { + onViewChange('month'); + } else if (view === 'year' && views.day) { + onViewChange('day'); + } else if (view === 'month' && views.day) { + onViewChange('day'); + } else if (view === 'month' && views.year) { + onViewChange('year'); + } else if (view === 'day' && views.month) { + onViewChange('month'); + } + }; + + const viewCount = Object.values(views).filter(Boolean).length; + + if (!views.day && !views.month && views.year) { + return null; + } + + const label = visibleDate.format('MMMM YYYY'); + + return ( + + + + + + {label} + + + + {viewCount > 1 && !disabled && ( + + + + )} + + + + + } + > + {isRtl ? ( + + ) : ( + + )} + + + + } + > + {isRtl ? ( + + ) : ( + + )} + + + + + ); +} + +const DEFAULT_VIEWS = { year: true, month: false, day: true }; + +function DateCalendar(props: DateCalendarProps) { + const { views = DEFAULT_VIEWS, openTo, displayWeekNumber } = props; + const [view, setView] = React.useState(() => + openTo != null && views[openTo] ? openTo! : 'day', + ); + const translations = usePickerTranslations(); + const id = useId(); + const gridLabelId = `${id}-grid-label`; + + return ( + + +
+
+ {view === 'year' && ( + + {({ years }) => + years.map((year) => ) + } + + )} + {view === 'month' && ( + + {({ months }) => + months.map((month) => ( + + )) + } + + )} + {view === 'day' && ( + + + {({ days }) => ( + + {displayWeekNumber && ( + + {translations.calendarWeekNumberHeaderText} + + )} + {days.map((day) => ( + } + /> + ))} + + )} + + + )} +
+
+
+ ); +} + +type DateCalendarView = 'day' | 'month' | 'year'; + +interface DateCalendarProps { + views?: Record; + openTo?: DateCalendarView; + displayWeekNumber?: boolean; +} + +export default function DateCalendarWithMaterialDesignDemo() { + return ( + + + + ); +} diff --git a/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.tsx.preview b/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.tsx.preview new file mode 100644 index 0000000000000..301ea681acac9 --- /dev/null +++ b/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.tsx.preview @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/data/date-pickers/base-calendar/base-calendar.md b/docs/data/date-pickers/base-calendar/base-calendar.md index dc286d664ce7c..caf3dba2caa43 100644 --- a/docs/data/date-pickers/base-calendar/base-calendar.md +++ b/docs/data/date-pickers/base-calendar/base-calendar.md @@ -237,14 +237,18 @@ Using the `getItems` prop instead of manually providing a list of ` Date: Wed, 15 Jan 2025 12:40:06 +0100 Subject: [PATCH 131/136] Work on MD recipe --- .../DateCalendarWithMaterialDesignDemo.tsx | 394 ++++++++++++++---- 1 file changed, 311 insertions(+), 83 deletions(-) diff --git a/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.tsx b/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.tsx index 0fa0f9b5bd38e..aee363e01b46e 100644 --- a/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.tsx +++ b/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.tsx @@ -1,8 +1,10 @@ import * as React from 'react'; -import { TransitionGroup } from 'react-transition-group'; +import { CSSTransition, TransitionGroup } from 'react-transition-group'; +import { TransitionGroupProps } from 'react-transition-group/TransitionGroup'; import { useRtl } from '@mui/system/RtlProvider'; import Fade from '@mui/material/Fade'; import IconButton from '@mui/material/IconButton'; +import ButtonBase from '@mui/material/ButtonBase'; import { styled, alpha, useTheme } from '@mui/material/styles'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; @@ -20,10 +22,30 @@ import { import useId from '@mui/utils/useId'; import Typography from '@mui/material/Typography'; -export const DIALOG_WIDTH = 320; -export const MAX_CALENDAR_HEIGHT = 280; +const DIALOG_WIDTH = 320; +const MAX_CALENDAR_HEIGHT = 280; +const DAY_MARGIN = 2; +const DAY_SIZE = 36; +const VIEW_HEIGHT = 336; +const WEEKS_CONTAINER_HEIGHT = (DAY_SIZE + DAY_MARGIN * 2) * 6; +const DEFAULT_VIEWS = { year: true, month: false, day: true }; + +const Root = styled(Calendar.Root)({ + overflow: 'hidden', + width: DIALOG_WIDTH, + maxHeight: VIEW_HEIGHT, + display: 'flex', + flexDirection: 'column', + margin: '0 auto', + height: VIEW_HEIGHT, +}); + +const Content = styled(TransitionGroup)({ + display: 'block', + position: 'relative', +}); -const CalendarHeaderRoot = styled('div')({ +const HeaderRoot = styled('div')({ display: 'flex', alignItems: 'center', marginTop: 12, @@ -35,7 +57,7 @@ const CalendarHeaderRoot = styled('div')({ minHeight: 40, }); -const CalendarHeaderLabelContainer = styled('div')(({ theme }) => ({ +const HeaderLabelContainer = styled('div')(({ theme }) => ({ display: 'flex', overflow: 'hidden', alignItems: 'center', @@ -45,20 +67,20 @@ const CalendarHeaderLabelContainer = styled('div')(({ theme }) => ({ fontWeight: theme.typography.fontWeightMedium, })); -const CalendarHeaderLabelContent = styled(TransitionGroup)({ +const HeaderLabelContent = styled(TransitionGroup)({ display: 'block', position: 'relative', }); -const CalendarHeaderLabel = styled('div')({ +const HeaderLabel = styled('div')({ marginRight: 6, }); -const CalendarHeaderSwitchViewButton = styled(IconButton)({ +const HeaderSwitchViewButton = styled(IconButton)({ marginRight: 'auto', }); -const CalendarHeaderSwitchViewIcon = styled(ArrowDropDownIcon)(({ theme }) => ({ +const HeaderSwitchViewIcon = styled(ArrowDropDownIcon)(({ theme }) => ({ willChange: 'transform', transition: theme.transitions.create('transform'), transform: 'rotate(0deg)', @@ -67,17 +89,11 @@ const CalendarHeaderSwitchViewIcon = styled(ArrowDropDownIcon)(({ theme }) => ({ }, })); -const CalendarHeaderNavigation = styled('div')({ +const HeaderNavigation = styled('div')({ display: 'flex', }); -const CalendarHeaderNavigationButton = styled(IconButton)({ - '&[data-hidden]': { - visibility: 'hidden', - }, -}); - -const CalendarHeaderNavigationSpacier = styled('div')(({ theme }) => ({ +const NavigationSpacier = styled('div')(({ theme }) => ({ width: theme.spacing(3), })); @@ -181,13 +197,13 @@ const MonthsCell = styled(Calendar.MonthsCell)(({ theme }) => ({ }, })); -const CalendarDaysGridHeader = styled(Calendar.DaysGridHeader)({ +const DaysGridHeader = styled(Calendar.DaysGridHeader)({ display: 'flex', justifyContent: 'center', alignItems: 'center', }); -const CalendarDaysGridWeekNumberHeaderCell = styled(Typography)(({ theme }) => ({ +const DaysGridWeekNumberHeaderCell = styled(Typography)(({ theme }) => ({ width: 36, height: 40, margin: '0 2px', @@ -198,7 +214,7 @@ const CalendarDaysGridWeekNumberHeaderCell = styled(Typography)(({ theme }) => ( color: theme.palette.text.disabled, })); -const CalendarDaysGridHeaderCell = styled(Typography)(({ theme }) => ({ +const DaysGridHeaderCell = styled(Typography)(({ theme }) => ({ width: 36, height: 40, margin: '0 2px', @@ -209,6 +225,128 @@ const CalendarDaysGridHeaderCell = styled(Typography)(({ theme }) => ({ color: (theme.vars || theme).palette.text.secondary, })); +const DaysGridBodyContainer = styled(TransitionGroup)(({ + theme, +}) => { + const slideTransition = theme.transitions.create('transform', { + duration: theme.transitions.duration.complex, + easing: 'cubic-bezier(0.35, 0.8, 0.4, 1)', + }); + return { + minHeight: WEEKS_CONTAINER_HEIGHT, + display: 'block', + position: 'relative', + overflowX: 'hidden', + '& > *': { + position: 'absolute', + top: 0, + right: 0, + left: 0, + }, + '& .day-grid-enter[data-direction="left"]': { + willChange: 'transform', + transform: 'translate(100%)', + zIndex: 1, + }, + '& .day-grid-enter[data-direction="right"]': { + willChange: 'transform', + transform: 'translate(-100%)', + zIndex: 1, + }, + '& .day-grid-enter-active': { + transform: 'translate(0%)', + transition: slideTransition, + }, + '& .day-grid-exit': { + transform: 'translate(0%)', + }, + '& .day-grid-exit-active[data-direction="left"]': { + willChange: 'transform', + transform: 'translate(-100%)', + transition: slideTransition, + zIndex: 0, + }, + '& .day-grid-exit-active[data-direction="right"]': { + willChange: 'transform', + transform: 'translate(100%)', + transition: slideTransition, + zIndex: 0, + }, + }; +}); + +const DaysGridBody = styled(Calendar.DaysGridBody)({ overflow: 'hidden' }); + +const DaysWeekRow = styled(Calendar.DaysWeekRow)({ + margin: `${DAY_MARGIN}px 0`, + display: 'flex', + justifyContent: 'center', +}); + +const DaysGridWeekNumberCell = styled(Typography)(({ theme }) => ({ + ...theme.typography.caption, + width: DAY_SIZE, + height: DAY_SIZE, + padding: 0, + margin: `0 ${DAY_MARGIN}px`, + color: theme.palette.text.disabled, + fontSize: '0.75rem', + alignItems: 'center', + justifyContent: 'center', + display: 'inline-flex', +})); + +const DaysCell = styled(ButtonBase)(({ theme }) => ({ + ...theme.typography.caption, + width: DAY_SIZE, + height: DAY_SIZE, + borderRadius: '50%', + padding: 0, + // explicitly setting to `transparent` to avoid potentially getting impacted by change from the overridden component + backgroundColor: 'transparent', + transition: theme.transitions.create('background-color', { + duration: theme.transitions.duration.short, + }), + color: (theme.vars || theme).palette.text.primary, + '@media (pointer: fine)': { + '&:hover': { + backgroundColor: theme.vars + ? `rgba(${theme.vars.palette.primary.mainChannel} / ${theme.vars.palette.action.hoverOpacity})` + : alpha(theme.palette.primary.main, theme.palette.action.hoverOpacity), + }, + }, + '&:focus': { + backgroundColor: theme.vars + ? `rgba(${theme.vars.palette.primary.mainChannel} / ${theme.vars.palette.action.focusOpacity})` + : alpha(theme.palette.primary.main, theme.palette.action.focusOpacity), + '&[data-selected]': { + willChange: 'background-color', + backgroundColor: (theme.vars || theme).palette.primary.dark, + }, + }, + '&[data-selected]': { + color: (theme.vars || theme).palette.primary.contrastText, + backgroundColor: (theme.vars || theme).palette.primary.main, + fontWeight: theme.typography.fontWeightMedium, + '&:hover': { + willChange: 'background-color', + backgroundColor: (theme.vars || theme).palette.primary.dark, + }, + }, + '&[data-disabled]:not([data-selected])': { + color: (theme.vars || theme).palette.text.disabled, + }, + '&[data-disabled][data-selected]': { + opacity: 0.6, + }, + '&[data-outside-month': { + color: (theme.vars || theme).palette.text.secondary, + }, + '&[data-current]:not([data-selected])': { + border: `1px solid ${(theme.vars || theme).palette.text.secondary}`, + }, +})); + function CalendarHeader(props: { view: DateCalendarView; onViewChange: (view: DateCalendarView) => void; @@ -232,6 +370,8 @@ function CalendarHeader(props: { onViewChange('year'); } else if (view === 'day' && views.month) { onViewChange('month'); + } else if (view === 'day' && views.year) { + onViewChange('year'); } }; @@ -244,14 +384,14 @@ function CalendarHeader(props: { const label = visibleDate.format('MMMM YYYY'); return ( - - + - + - + {label} - + - + {viewCount > 1 && !disabled && ( - - - + + )} - + - + )} - + )} - + - + ); } -const DEFAULT_VIEWS = { year: true, month: false, day: true }; +function DayCalendar(props: { displayWeekNumber: boolean }) { + const { displayWeekNumber } = props; + const translations = usePickerTranslations(); + const theme = useTheme(); + const { visibleDate } = useCalendarContext(); -function DateCalendar(props: DateCalendarProps) { - const { views = DEFAULT_VIEWS, openTo, displayWeekNumber } = props; - const [view, setView] = React.useState(() => - openTo != null && views[openTo] ? openTo! : 'day', + // We need a new ref whenever the `key` of the transition changes: https://reactcommunity.org/react-transition-group/transition/#Transition-prop-nodeRef. + const transitionKey = visibleDate.format('MMMM YYYY'); + const daysGridBodyNodeRef = React.useMemo( + () => React.createRef(), + // eslint-disable-next-line react-hooks/exhaustive-deps + [transitionKey], ); - const translations = usePickerTranslations(); - const id = useId(); - const gridLabelId = `${id}-grid-label`; + + const slideDirection = 'left'; + const dayGridTransitionClasses = { + exit: 'day-grid-exit', + enterActive: 'day-grid-enter-active', + enter: `day-grid-enter-${slideDirection}`, + exitActive: `day-grid-exit-active-${slideDirection}`, + }; + + const handleMonthSwitchingAnimationEnd = React.useCallback(() => {}, []); return ( - - -
+ + + {({ days }) => ( + + {displayWeekNumber && ( + + {translations.calendarWeekNumberHeaderText} + + )} + {days.map((day) => ( + } + /> + ))} + + )} + + ) => + React.cloneElement(element, { + classNames: dayGridTransitionClasses, + }) + } + role="presentation" + > + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => ( + + {displayWeekNumber && ( + + {translations.calendarWeekNumberText(week[0].week())} + + )} + {days.map((day) => ( + } /> + ))} + + )} + + )) + } + + + + + ); +} + +function CalendarContent(props: { + view: DateCalendarView; + displayWeekNumber: boolean; +}) { + const { view, displayWeekNumber } = props; + const theme = useTheme(); + + return ( + +
{view === 'year' && ( @@ -360,35 +592,31 @@ function DateCalendar(props: DateCalendarProps) { } )} - {view === 'day' && ( - - - {({ days }) => ( - - {displayWeekNumber && ( - - {translations.calendarWeekNumberHeaderText} - - )} - {days.map((day) => ( - } - /> - ))} - - )} - - - )} + {view === 'day' && }
-
-
+ + + ); +} + +function DateCalendar(props: DateCalendarProps) { + const { views = DEFAULT_VIEWS, openTo, displayWeekNumber = false } = props; + const [view, setView] = React.useState(() => + openTo != null && views[openTo] ? openTo! : 'day', + ); + const id = useId(); + const gridLabelId = `${id}-grid-label`; + + return ( + + + + ); } From bb8dafa65de88c995cbf3113f2989daa47a3c43d Mon Sep 17 00:00:00 2001 From: flavien Date: Wed, 15 Jan 2025 17:50:27 +0100 Subject: [PATCH 132/136] Slide work but super slow --- .../DateCalendarWithMaterialDesignDemo.js | 396 ++++++++++++++---- .../DateCalendarWithMaterialDesignDemo.tsx | 30 +- 2 files changed, 331 insertions(+), 95 deletions(-) diff --git a/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.js b/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.js index 296aac5d7e0f6..e51c307fe97c3 100644 --- a/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.js +++ b/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.js @@ -1,8 +1,10 @@ import * as React from 'react'; -import { TransitionGroup } from 'react-transition-group'; +import { CSSTransition, TransitionGroup } from 'react-transition-group'; + import { useRtl } from '@mui/system/RtlProvider'; import Fade from '@mui/material/Fade'; import IconButton from '@mui/material/IconButton'; +import ButtonBase from '@mui/material/ButtonBase'; import { styled, alpha, useTheme } from '@mui/material/styles'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; @@ -19,11 +21,32 @@ import { } from '@mui/x-date-pickers/internals/base/Calendar'; import useId from '@mui/utils/useId'; import Typography from '@mui/material/Typography'; +import useEnhancedEffect from '@mui/utils/useEnhancedEffect'; + +const DIALOG_WIDTH = 320; +const MAX_CALENDAR_HEIGHT = 280; +const DAY_MARGIN = 2; +const DAY_SIZE = 36; +const VIEW_HEIGHT = 336; +const WEEKS_CONTAINER_HEIGHT = (DAY_SIZE + DAY_MARGIN * 2) * 6; +const DEFAULT_VIEWS = { year: true, month: false, day: true }; + +const Root = styled(Calendar.Root)({ + overflow: 'hidden', + width: DIALOG_WIDTH, + maxHeight: VIEW_HEIGHT, + display: 'flex', + flexDirection: 'column', + margin: '0 auto', + height: VIEW_HEIGHT, +}); -export const DIALOG_WIDTH = 320; -export const MAX_CALENDAR_HEIGHT = 280; +const Content = styled(TransitionGroup)({ + display: 'block', + position: 'relative', +}); -const CalendarHeaderRoot = styled('div')({ +const HeaderRoot = styled('div')({ display: 'flex', alignItems: 'center', marginTop: 12, @@ -35,7 +58,7 @@ const CalendarHeaderRoot = styled('div')({ minHeight: 40, }); -const CalendarHeaderLabelContainer = styled('div')(({ theme }) => ({ +const HeaderLabelContainer = styled('div')(({ theme }) => ({ display: 'flex', overflow: 'hidden', alignItems: 'center', @@ -45,20 +68,20 @@ const CalendarHeaderLabelContainer = styled('div')(({ theme }) => ({ fontWeight: theme.typography.fontWeightMedium, })); -const CalendarHeaderLabelContent = styled(TransitionGroup)({ +const HeaderLabelContent = styled(TransitionGroup)({ display: 'block', position: 'relative', }); -const CalendarHeaderLabel = styled('div')({ +const HeaderLabel = styled('div')({ marginRight: 6, }); -const CalendarHeaderSwitchViewButton = styled(IconButton)({ +const HeaderSwitchViewButton = styled(IconButton)({ marginRight: 'auto', }); -const CalendarHeaderSwitchViewIcon = styled(ArrowDropDownIcon)(({ theme }) => ({ +const HeaderSwitchViewIcon = styled(ArrowDropDownIcon)(({ theme }) => ({ willChange: 'transform', transition: theme.transitions.create('transform'), transform: 'rotate(0deg)', @@ -67,17 +90,11 @@ const CalendarHeaderSwitchViewIcon = styled(ArrowDropDownIcon)(({ theme }) => ({ }, })); -const CalendarHeaderNavigation = styled('div')({ +const HeaderNavigation = styled('div')({ display: 'flex', }); -const CalendarHeaderNavigationButton = styled(IconButton)({ - '&[data-hidden]': { - visibility: 'hidden', - }, -}); - -const CalendarHeaderNavigationSpacier = styled('div')(({ theme }) => ({ +const NavigationSpacier = styled('div')(({ theme }) => ({ width: theme.spacing(3), })); @@ -181,13 +198,13 @@ const MonthsCell = styled(Calendar.MonthsCell)(({ theme }) => ({ }, })); -const CalendarDaysGridHeader = styled(Calendar.DaysGridHeader)({ +const DaysGridHeader = styled(Calendar.DaysGridHeader)({ display: 'flex', justifyContent: 'center', alignItems: 'center', }); -const CalendarDaysGridWeekNumberHeaderCell = styled(Typography)(({ theme }) => ({ +const DaysGridWeekNumberHeaderCell = styled(Typography)(({ theme }) => ({ width: 36, height: 40, margin: '0 2px', @@ -198,7 +215,7 @@ const CalendarDaysGridWeekNumberHeaderCell = styled(Typography)(({ theme }) => ( color: theme.palette.text.disabled, })); -const CalendarDaysGridHeaderCell = styled(Typography)(({ theme }) => ({ +const DaysGridHeaderCell = styled(Typography)(({ theme }) => ({ width: 36, height: 40, margin: '0 2px', @@ -209,6 +226,127 @@ const CalendarDaysGridHeaderCell = styled(Typography)(({ theme }) => ({ color: (theme.vars || theme).palette.text.secondary, })); +const DaysGridBodyContainer = styled(TransitionGroup)(({ theme }) => { + const slideTransition = theme.transitions.create('transform', { + duration: theme.transitions.duration.complex, + easing: 'cubic-bezier(0.35, 0.8, 0.4, 1)', + }); + return { + minHeight: WEEKS_CONTAINER_HEIGHT, + display: 'block', + position: 'relative', + overflow: 'hidden', + '& > *': { + position: 'absolute', + top: 0, + right: 0, + left: 0, + }, + '& .day-grid-enter-left': { + willChange: 'transform', + transform: 'translate(100%)', + zIndex: 1, + }, + '& .day-grid-enter-right': { + willChange: 'transform', + transform: 'translate(-100%)', + zIndex: 1, + }, + '& .day-grid-enter-active': { + transform: 'translate(0%)', + transition: slideTransition, + }, + '& .day-grid-exit': { + transform: 'translate(0%)', + }, + '& .day-grid-exit-active-left': { + willChange: 'transform', + transform: 'translate(-100%)', + transition: slideTransition, + zIndex: 0, + }, + '& .day-grid-exit-active-right': { + willChange: 'transform', + transform: 'translate(100%)', + transition: slideTransition, + zIndex: 0, + }, + }; +}); + +const DaysGridBody = styled(Calendar.DaysGridBody)({ overflow: 'hidden' }); + +const DaysWeekRow = styled(Calendar.DaysWeekRow)({ + margin: `${DAY_MARGIN}px 0`, + display: 'flex', + justifyContent: 'center', +}); + +const DaysGridWeekNumberCell = styled(Typography)(({ theme }) => ({ + ...theme.typography.caption, + width: DAY_SIZE, + height: DAY_SIZE, + padding: 0, + margin: `0 ${DAY_MARGIN}px`, + color: theme.palette.text.disabled, + fontSize: '0.75rem', + alignItems: 'center', + justifyContent: 'center', + display: 'inline-flex', +})); + +const DaysCell = styled(ButtonBase)(({ theme }) => ({ + ...theme.typography.caption, + width: DAY_SIZE, + height: DAY_SIZE, + borderRadius: '50%', + padding: 0, + // explicitly setting to `transparent` to avoid potentially getting impacted by change from the overridden component + backgroundColor: 'transparent', + transition: theme.transitions.create('background-color', { + duration: theme.transitions.duration.short, + }), + color: (theme.vars || theme).palette.text.primary, + '@media (pointer: fine)': { + '&:hover': { + backgroundColor: theme.vars + ? `rgba(${theme.vars.palette.primary.mainChannel} / ${theme.vars.palette.action.hoverOpacity})` + : alpha(theme.palette.primary.main, theme.palette.action.hoverOpacity), + }, + }, + '&:focus': { + backgroundColor: theme.vars + ? `rgba(${theme.vars.palette.primary.mainChannel} / ${theme.vars.palette.action.focusOpacity})` + : alpha(theme.palette.primary.main, theme.palette.action.focusOpacity), + '&[data-selected]': { + willChange: 'background-color', + backgroundColor: (theme.vars || theme).palette.primary.dark, + }, + }, + '&[data-selected]': { + color: (theme.vars || theme).palette.primary.contrastText, + backgroundColor: (theme.vars || theme).palette.primary.main, + fontWeight: theme.typography.fontWeightMedium, + '&:hover': { + willChange: 'background-color', + backgroundColor: (theme.vars || theme).palette.primary.dark, + }, + }, + '&[data-disabled]:not([data-selected])': { + color: (theme.vars || theme).palette.text.disabled, + }, + '&[data-disabled][data-selected]': { + opacity: 0.6, + }, + '&[data-outside-month]': { + color: (theme.vars || theme).palette.text.secondary, + pointerEvents: 'none', + }, + '&[data-current]:not([data-selected])': { + border: `1px solid ${(theme.vars || theme).palette.text.secondary}`, + }, +})); + function CalendarHeader(props) { const { view, onViewChange, views, labelId } = props; const translations = usePickerTranslations(); @@ -227,6 +365,8 @@ function CalendarHeader(props) { onViewChange('year'); } else if (view === 'day' && views.month) { onViewChange('month'); + } else if (view === 'day' && views.year) { + onViewChange('year'); } }; @@ -239,14 +379,14 @@ function CalendarHeader(props) { const label = visibleDate.format('MMMM YYYY'); return ( - - + - + - + {label} - + - + {viewCount > 1 && !disabled && ( - - - + + )} - + - + )} - + )} - + - + ); } -const DEFAULT_VIEWS = { year: true, month: false, day: true }; +function DayCalendar(props) { + const { displayWeekNumber } = props; + const translations = usePickerTranslations(); + const theme = useTheme(); + const { visibleDate } = useCalendarContext(); -function DateCalendar(props) { - const { views = DEFAULT_VIEWS, openTo, displayWeekNumber } = props; - const [view, setView] = React.useState(() => - openTo != null && views[openTo] ? openTo : 'day', + // We need a new ref whenever the `key` of the transition changes: https://reactcommunity.org/react-transition-group/transition/#Transition-prop-nodeRef. + const transitionKey = visibleDate.format('MMMM YYYY'); + const daysGridBodyNodeRef = React.useMemo( + () => React.createRef(), + // eslint-disable-next-line react-hooks/exhaustive-deps + [transitionKey], ); - const translations = usePickerTranslations(); - const id = useId(); - const gridLabelId = `${id}-grid-label`; + + const prevVisibleDate = React.useRef(visibleDate); + const slideDirection = prevVisibleDate.current.isBefore(visibleDate) + ? 'left' + : 'right'; + + useEnhancedEffect(() => { + prevVisibleDate.current = visibleDate; + }, [visibleDate]); + + const dayGridTransitionClasses = { + exit: 'day-grid-exit', + enterActive: 'day-grid-enter-active', + enter: `day-grid-enter-${slideDirection}`, + exitActive: `day-grid-exit-active-${slideDirection}`, + }; return ( - - -
+ + + {({ days }) => ( + + {displayWeekNumber && ( + + {translations.calendarWeekNumberHeaderText} + + )} + {days.map((day) => ( + } + /> + ))} + + )} + + + React.cloneElement(element, { + classNames: dayGridTransitionClasses, + }) + } + role="presentation" + > + + + {({ weeks }) => + weeks.map((week) => ( + + {({ days }) => ( + + {displayWeekNumber && ( + + {translations.calendarWeekNumberText(days[0].week())} + + )} + {days.map((day) => ( + } /> + ))} + + )} + + )) + } + + + + + ); +} + +function CalendarContent(props) { + const { view, displayWeekNumber } = props; + const theme = useTheme(); + + return ( + +
{view === 'year' && ( @@ -355,35 +589,31 @@ function DateCalendar(props) { } )} - {view === 'day' && ( - - - {({ days }) => ( - - {displayWeekNumber && ( - - {translations.calendarWeekNumberHeaderText} - - )} - {days.map((day) => ( - } - /> - ))} - - )} - - - )} + {view === 'day' && }
-
-
+ + + ); +} + +function DateCalendar(props) { + const { views = DEFAULT_VIEWS, openTo, displayWeekNumber = false } = props; + const [view, setView] = React.useState(() => + openTo != null && views[openTo] ? openTo : 'day', + ); + const id = useId(); + const gridLabelId = `${id}-grid-label`; + + return ( + + + + ); } diff --git a/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.tsx b/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.tsx index aee363e01b46e..741ea9eccc64b 100644 --- a/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.tsx +++ b/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.tsx @@ -21,6 +21,7 @@ import { } from '@mui/x-date-pickers/internals/base/Calendar'; import useId from '@mui/utils/useId'; import Typography from '@mui/material/Typography'; +import useEnhancedEffect from '@mui/utils/useEnhancedEffect'; const DIALOG_WIDTH = 320; const MAX_CALENDAR_HEIGHT = 280; @@ -236,19 +237,19 @@ const DaysGridBodyContainer = styled(TransitionGroup)(({ minHeight: WEEKS_CONTAINER_HEIGHT, display: 'block', position: 'relative', - overflowX: 'hidden', + overflow: 'hidden', '& > *': { position: 'absolute', top: 0, right: 0, left: 0, }, - '& .day-grid-enter[data-direction="left"]': { + '& .day-grid-enter-left': { willChange: 'transform', transform: 'translate(100%)', zIndex: 1, }, - '& .day-grid-enter[data-direction="right"]': { + '& .day-grid-enter-right': { willChange: 'transform', transform: 'translate(-100%)', zIndex: 1, @@ -260,13 +261,13 @@ const DaysGridBodyContainer = styled(TransitionGroup)(({ '& .day-grid-exit': { transform: 'translate(0%)', }, - '& .day-grid-exit-active[data-direction="left"]': { + '& .day-grid-exit-active-left': { willChange: 'transform', transform: 'translate(-100%)', transition: slideTransition, zIndex: 0, }, - '& .day-grid-exit-active[data-direction="right"]': { + '& .day-grid-exit-active-right': { willChange: 'transform', transform: 'translate(100%)', transition: slideTransition, @@ -339,8 +340,9 @@ const DaysCell = styled(ButtonBase)(({ theme }) => ({ '&[data-disabled][data-selected]': { opacity: 0.6, }, - '&[data-outside-month': { + '&[data-outside-month]': { color: (theme.vars || theme).palette.text.secondary, + pointerEvents: 'none', }, '&[data-current]:not([data-selected])': { border: `1px solid ${(theme.vars || theme).palette.text.secondary}`, @@ -474,7 +476,14 @@ function DayCalendar(props: { displayWeekNumber: boolean }) { [transitionKey], ); - const slideDirection = 'left'; + const prevVisibleDate = React.useRef(visibleDate); + const slideDirection = prevVisibleDate.current.isBefore(visibleDate) ? 'left' : 'right'; + + useEnhancedEffect(() => { + prevVisibleDate.current = visibleDate; + }, [visibleDate]) + + const dayGridTransitionClasses = { exit: 'day-grid-exit', enterActive: 'day-grid-enter-active', @@ -482,8 +491,6 @@ function DayCalendar(props: { displayWeekNumber: boolean }) { exitActive: `day-grid-exit-active-${slideDirection}`, }; - const handleMonthSwitchingAnimationEnd = React.useCallback(() => {}, []); - return ( @@ -521,7 +528,6 @@ function DayCalendar(props: { displayWeekNumber: boolean }) { unmountOnExit key={transitionKey} timeout={theme.transitions.duration.complex} - onExited={handleMonthSwitchingAnimationEnd} nodeRef={daysGridBodyNodeRef} > @@ -534,10 +540,10 @@ function DayCalendar(props: { displayWeekNumber: boolean }) { - {translations.calendarWeekNumberText(week[0].week())} + {translations.calendarWeekNumberText(days[0].week())} )} {days.map((day) => ( From 8352fa06bce284240e571a6d0e1dd791f7a2576c Mon Sep 17 00:00:00 2001 From: flavien Date: Thu, 16 Jan 2025 10:58:26 +0100 Subject: [PATCH 133/136] Work --- .../DateCalendarWithMaterialDesignDemo.js | 1 + .../DateCalendarWithMaterialDesignDemo.tsx | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.js b/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.js index e51c307fe97c3..f2420a716a166 100644 --- a/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.js +++ b/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.js @@ -301,6 +301,7 @@ const DaysCell = styled(ButtonBase)(({ theme }) => ({ height: DAY_SIZE, borderRadius: '50%', padding: 0, + margin: `0 ${DAY_MARGIN}px`, // explicitly setting to `transparent` to avoid potentially getting impacted by change from the overridden component backgroundColor: 'transparent', transition: theme.transitions.create('background-color', { diff --git a/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.tsx b/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.tsx index 741ea9eccc64b..23690eac05df4 100644 --- a/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.tsx +++ b/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.tsx @@ -22,6 +22,7 @@ import { import useId from '@mui/utils/useId'; import Typography from '@mui/material/Typography'; import useEnhancedEffect from '@mui/utils/useEnhancedEffect'; +import { boxSizing } from '@mui/system'; const DIALOG_WIDTH = 320; const MAX_CALENDAR_HEIGHT = 280; @@ -303,6 +304,8 @@ const DaysCell = styled(ButtonBase)(({ theme }) => ({ height: DAY_SIZE, borderRadius: '50%', padding: 0, + margin: `0 ${DAY_MARGIN}px`, + // explicitly setting to `transparent` to avoid potentially getting impacted by change from the overridden component backgroundColor: 'transparent', transition: theme.transitions.create('background-color', { @@ -477,12 +480,13 @@ function DayCalendar(props: { displayWeekNumber: boolean }) { ); const prevVisibleDate = React.useRef(visibleDate); - const slideDirection = prevVisibleDate.current.isBefore(visibleDate) ? 'left' : 'right'; + const slideDirection = prevVisibleDate.current.isBefore(visibleDate) + ? 'left' + : 'right'; useEnhancedEffect(() => { prevVisibleDate.current = visibleDate; - }, [visibleDate]) - + }, [visibleDate]); const dayGridTransitionClasses = { exit: 'day-grid-exit', From 9fafcd430a6058edb5df22aa99e0ab857556e2e6 Mon Sep 17 00:00:00 2001 From: flavien Date: Thu, 16 Jan 2025 11:00:57 +0100 Subject: [PATCH 134/136] Add explainations --- docs/data/date-pickers/base-calendar/base-calendar.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/data/date-pickers/base-calendar/base-calendar.md b/docs/data/date-pickers/base-calendar/base-calendar.md index caf3dba2caa43..eb919e6a1e9ea 100644 --- a/docs/data/date-pickers/base-calendar/base-calendar.md +++ b/docs/data/date-pickers/base-calendar/base-calendar.md @@ -247,6 +247,12 @@ Using the `getItems` prop instead of manually providing a list of ` Date: Thu, 16 Jan 2025 15:24:08 +0100 Subject: [PATCH 135/136] Fix view management --- .../DateCalendarWithMaterialDesignDemo.js | 22 ++++++++++++++-- .../DateCalendarWithMaterialDesignDemo.tsx | 25 ++++++++++++++++--- .../base-calendar/base-calendar.md | 1 + 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.js b/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.js index f2420a716a166..3634347c9e298 100644 --- a/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.js +++ b/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.js @@ -598,15 +598,33 @@ function CalendarContent(props) { } function DateCalendar(props) { - const { views = DEFAULT_VIEWS, openTo, displayWeekNumber = false } = props; + const { + views = DEFAULT_VIEWS, + openTo, + displayWeekNumber = false, + onValueChange, + ...other + } = props; const [view, setView] = React.useState(() => openTo != null && views[openTo] ? openTo : 'day', ); const id = useId(); const gridLabelId = `${id}-grid-label`; + const handleValueChange = React.useCallback( + (value, ctx) => { + onValueChange?.(value, ctx); + if (ctx.section === 'year' && views.month) { + setView('month'); + } else if (ctx.section !== 'day' && views.day) { + setView('day'); + } + }, + [onValueChange], + ); + return ( - + (() => openTo != null && views[openTo] ? openTo! : 'day', ); const id = useId(); const gridLabelId = `${id}-grid-label`; + const handleValueChange = React.useCallback( + (value: Dayjs, ctx: Calendar.Root.ValueChangeHandlerContext) => { + onValueChange?.(value, ctx); + if (ctx.section === 'year' && views.month) { + setView('month'); + } else if (ctx.section !== 'day' && views.day) { + setView('day'); + } + }, + [onValueChange], + ); + return ( - + { views?: Record; openTo?: DateCalendarView; displayWeekNumber?: boolean; diff --git a/docs/data/date-pickers/base-calendar/base-calendar.md b/docs/data/date-pickers/base-calendar/base-calendar.md index eb919e6a1e9ea..4ee12bcf73c05 100644 --- a/docs/data/date-pickers/base-calendar/base-calendar.md +++ b/docs/data/date-pickers/base-calendar/base-calendar.md @@ -248,6 +248,7 @@ Using the `getItems` prop instead of manually providing a list of ` Date: Thu, 16 Jan 2025 17:23:04 +0100 Subject: [PATCH 136/136] Improve MUI demo --- .../DateCalendarWithMaterialDesignDemo.js | 61 +++++++++++++------ .../DateCalendarWithMaterialDesignDemo.tsx | 60 ++++++++++++------ 2 files changed, 85 insertions(+), 36 deletions(-) diff --git a/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.js b/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.js index 3634347c9e298..adf32b9d87408 100644 --- a/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.js +++ b/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.js @@ -1,9 +1,15 @@ import * as React from 'react'; + import { CSSTransition, TransitionGroup } from 'react-transition-group'; import { useRtl } from '@mui/system/RtlProvider'; +import useEnhancedEffect from '@mui/utils/useEnhancedEffect'; +import useEventCallback from '@mui/utils/useEventCallback'; +import useId from '@mui/utils/useId'; +import useControlled from '@mui/utils/useControlled'; import Fade from '@mui/material/Fade'; import IconButton from '@mui/material/IconButton'; +import Typography from '@mui/material/Typography'; import ButtonBase from '@mui/material/ButtonBase'; import { styled, alpha, useTheme } from '@mui/material/styles'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; @@ -19,9 +25,6 @@ import { Calendar, useCalendarContext, } from '@mui/x-date-pickers/internals/base/Calendar'; -import useId from '@mui/utils/useId'; -import Typography from '@mui/material/Typography'; -import useEnhancedEffect from '@mui/utils/useEnhancedEffect'; const DIALOG_WIDTH = 320; const MAX_CALENDAR_HEIGHT = 280; @@ -479,6 +482,7 @@ function DayCalendar(props) { prevVisibleDate.current = visibleDate; }, [visibleDate]); + // TODO: Try to move slide direction to a data-direction attribute const dayGridTransitionClasses = { exit: 'day-grid-exit', enterActive: 'day-grid-enter-active', @@ -600,35 +604,44 @@ function CalendarContent(props) { function DateCalendar(props) { const { views = DEFAULT_VIEWS, - openTo, + defaultView = getDefaultView(views), + view: viewProp, + onViewChange, displayWeekNumber = false, onValueChange, ...other } = props; - const [view, setView] = React.useState(() => - openTo != null && views[openTo] ? openTo : 'day', - ); + + const [view, setView] = useControlled({ + name: 'DateCalendar', + state: 'view', + default: defaultView, + controlled: viewProp, + }); + const id = useId(); const gridLabelId = `${id}-grid-label`; - const handleValueChange = React.useCallback( - (value, ctx) => { - onValueChange?.(value, ctx); - if (ctx.section === 'year' && views.month) { - setView('month'); - } else if (ctx.section !== 'day' && views.day) { - setView('day'); - } - }, - [onValueChange], - ); + const handleViewChange = useEventCallback((newView) => { + setView(newView); + onViewChange?.(newView); + }); + + const handleValueChange = useEventCallback((value, ctx) => { + onValueChange?.(value, ctx); + if (ctx.section === 'year' && views.month) { + handleViewChange('month'); + } else if (ctx.section !== 'day' && views.day) { + handleViewChange('day'); + } + }); return ( @@ -636,6 +649,16 @@ function DateCalendar(props) { ); } +function getDefaultView(views) { + if (views.day) { + return 'day'; + } + if (views.month) { + return 'month'; + } + return 'year'; +} + export default function DateCalendarWithMaterialDesignDemo() { return ( diff --git a/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.tsx b/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.tsx index 54949aee7cfb9..193043c6deae8 100644 --- a/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.tsx +++ b/docs/data/date-pickers/base-calendar/DateCalendarWithMaterialDesignDemo.tsx @@ -1,9 +1,15 @@ import * as React from 'react'; +import { Dayjs } from 'dayjs'; import { CSSTransition, TransitionGroup } from 'react-transition-group'; import { TransitionGroupProps } from 'react-transition-group/TransitionGroup'; import { useRtl } from '@mui/system/RtlProvider'; +import useEnhancedEffect from '@mui/utils/useEnhancedEffect'; +import useEventCallback from '@mui/utils/useEventCallback'; +import useId from '@mui/utils/useId'; +import useControlled from '@mui/utils/useControlled'; import Fade from '@mui/material/Fade'; import IconButton from '@mui/material/IconButton'; +import Typography from '@mui/material/Typography'; import ButtonBase from '@mui/material/ButtonBase'; import { styled, alpha, useTheme } from '@mui/material/styles'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; @@ -19,11 +25,6 @@ import { Calendar, useCalendarContext, } from '@mui/x-date-pickers/internals/base/Calendar'; -import useId from '@mui/utils/useId'; -import Typography from '@mui/material/Typography'; -import useEnhancedEffect from '@mui/utils/useEnhancedEffect'; -import { boxSizing } from '@mui/system'; -import { Dayjs } from 'dayjs'; const DIALOG_WIDTH = 320; const MAX_CALENDAR_HEIGHT = 280; @@ -356,7 +357,7 @@ const DaysCell = styled(ButtonBase)(({ theme }) => ({ function CalendarHeader(props: { view: DateCalendarView; onViewChange: (view: DateCalendarView) => void; - views: Record; + views: { [key in DateCalendarView]?: boolean }; labelId: string; }) { const { view, onViewChange, views, labelId } = props; @@ -489,6 +490,7 @@ function DayCalendar(props: { displayWeekNumber: boolean }) { prevVisibleDate.current = visibleDate; }, [visibleDate]); + // TODO: Try to move slide direction to a data-direction attribute const dayGridTransitionClasses = { exit: 'day-grid-exit', enterActive: 'day-grid-enter-active', @@ -613,27 +615,38 @@ function CalendarContent(props: { function DateCalendar(props: DateCalendarProps) { const { views = DEFAULT_VIEWS, - openTo, + defaultView = getDefaultView(views), + view: viewProp, + onViewChange, displayWeekNumber = false, onValueChange, ...other } = props; - const [view, setView] = React.useState(() => - openTo != null && views[openTo] ? openTo! : 'day', - ); + + const [view, setView] = useControlled({ + name: 'DateCalendar', + state: 'view', + default: defaultView, + controlled: viewProp, + }); + const id = useId(); const gridLabelId = `${id}-grid-label`; - const handleValueChange = React.useCallback( + const handleViewChange = useEventCallback((newView: DateCalendarView) => { + setView(newView); + onViewChange?.(newView); + }); + + const handleValueChange = useEventCallback( (value: Dayjs, ctx: Calendar.Root.ValueChangeHandlerContext) => { onValueChange?.(value, ctx); if (ctx.section === 'year' && views.month) { - setView('month'); + handleViewChange('month'); } else if (ctx.section !== 'day' && views.day) { - setView('day'); + handleViewChange('day'); } }, - [onValueChange], ); return ( @@ -641,7 +654,7 @@ function DateCalendar(props: DateCalendarProps) { @@ -652,9 +665,22 @@ function DateCalendar(props: DateCalendarProps) { type DateCalendarView = 'day' | 'month' | 'year'; interface DateCalendarProps extends Omit { - views?: Record; - openTo?: DateCalendarView; + views?: { [key in DateCalendarView]?: boolean }; + view?: DateCalendarView; + onViewChange?: (view: DateCalendarView) => void; + defaultView?: DateCalendarView; displayWeekNumber?: boolean; + //TODO: Add reduceAnimations prop +} + +function getDefaultView(views: { [key in DateCalendarView]?: boolean }) { + if (views.day) { + return 'day'; + } + if (views.month) { + return 'month'; + } + return 'year'; } export default function DateCalendarWithMaterialDesignDemo() {