Skip to content

[TimePicker] Refactor digital clock selected item scrolling #17072

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/x-data-grid/src/models/api/gridEditingApi.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { MuiBaseEvent } from '@mui/x-internals/types';
import { GridCellMode, GridRowMode } from '../gridCell';
import { GridCellModes, GridRowModes } from '../gridEditRowModel';
import { GridRowId, GridRowModel } from '../gridRows';
import { GridCellParams } from '../params/gridCellParams';
import { GridEditCellValueParams } from '../params/gridEditCellParams';
import { MuiBaseEvent } from '../muiEvent';

export type GridCellModesModelProps =
| ({ mode: GridCellModes.View } & Omit<GridStopCellEditModeParams, 'id' | 'field'>)
Expand Down
2 changes: 1 addition & 1 deletion packages/x-data-grid/src/models/api/gridFocusApi.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { MuiBaseEvent } from '@mui/x-internals/types';
import { GridRowId } from '../gridRows';
import { MuiBaseEvent } from '../muiEvent';
import { GridColumnGroupIdentifier } from '../../hooks/features/focus';

export interface GridFocusApi {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { MuiBaseEvent, MuiEvent } from '../muiEvent';
import type { MuiBaseEvent, MuiEvent } from '@mui/x-internals/types';
import type { GridCallbackDetails } from '../api/gridCallbackDetails';
import type { GridEventLookup, GridEvents } from './gridEventLookup';

Expand Down
2 changes: 1 addition & 1 deletion packages/x-data-grid/src/models/events/gridEventLookup.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from 'react';
import type { MuiBaseEvent } from '@mui/x-internals/types';
import type {
GridColumnHeaderParams,
GridColumnOrderChangeParams,
Expand All @@ -18,7 +19,6 @@ import type { GridFilterModel } from '../gridFilterModel';
import type { GridSortModel } from '../gridSortModel';
import type { GridRowSelectionModel } from '../gridRowSelectionModel';
import type { ElementSize } from '../elementSize';
import type { MuiBaseEvent } from '../muiEvent';
import type { GridGroupNode } from '../gridRows';
import type { GridColumnVisibilityModel } from '../../hooks/features/columns';
import type { GridStrategyProcessorName } from '../../hooks/core/strategyProcessing';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { MuiBaseEvent } from '../muiEvent';
import type { MuiBaseEvent } from '@mui/x-internals/types';
import type { GridEventLookup, GridEvents } from './gridEventLookup';

type PublisherArgsNoEvent<E extends GridEvents, T extends { params: any }> = [E, T['params']];
Expand Down
2 changes: 1 addition & 1 deletion packages/x-data-grid/src/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export type { GridSlotsComponent } from './gridSlotsComponent';
export * from './gridSlotsComponentsProps';
export * from './gridDensity';
export * from './logger';
export * from './muiEvent';
export type { MuiBaseEvent, MuiEvent } from '@mui/x-internals/types';
export * from './events';
export type {
GridSortCellParams,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';
import { SlotComponentProps } from '@mui/utils';
import MenuItem from '@mui/material/MenuItem';
import { MenuItemProps } from '@mui/material/MenuItem';
import { MuiEvent, SlotComponentPropsFromProps } from '@mui/x-internals/types';
import { MultiSectionDigitalClockClasses } from './multiSectionDigitalClockClasses';
import {
BaseClockProps,
Expand All @@ -9,6 +9,7 @@ import {
} from '../internals/models/props/time';
import { MultiSectionDigitalClockSectionProps } from './MultiSectionDigitalClockSection';
import { TimeViewWithMeridiem } from '../internals/models';
import { PickerOwnerState } from '../models/pickers';

export interface MultiSectionDigitalClockOption<TSectionValue extends number | string> {
isDisabled?: (value: TSectionValue) => boolean;
Expand All @@ -26,16 +27,31 @@ export interface ExportedMultiSectionDigitalClockProps
export interface MultiSectionDigitalClockViewProps<TSectionValue extends number | string>
extends Pick<MultiSectionDigitalClockSectionProps<TSectionValue>, 'onChange' | 'items'> {}

export interface MultiSectionDigitalClockSectionItemProps extends MenuItemProps {
onClick?: (event: MuiEvent<React.MouseEvent<HTMLLIElement>>) => void;
}

export interface MultiSectionDigitalClockSlots {
/**
* Component responsible for rendering a single multi section digital clock section item.
* @default MenuItem from '@mui/material'
*/
digitalClockSectionItem?: React.ElementType;
digitalClockSectionItem?: React.ElementType<MultiSectionDigitalClockSectionItemProps>;
}

export interface MultiSectionDigitalClockSectionOwnerState extends PickerOwnerState {
/**
* `true` if this is not the initial render of the digital clock.
*/
hasDigitalClockAlreadyBeenRendered: boolean;
}

export interface MultiSectionDigitalClockSlotProps {
digitalClockSectionItem?: SlotComponentProps<typeof MenuItem, {}, Record<string, any>>;
digitalClockSectionItem?: SlotComponentPropsFromProps<
MultiSectionDigitalClockSectionItemProps,
{},
MultiSectionDigitalClockSectionOwnerState
>;
}

export interface MultiSectionDigitalClockProps
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import composeClasses from '@mui/utils/composeClasses';
import MenuList from '@mui/material/MenuList';
import MenuItem from '@mui/material/MenuItem';
import useForkRef from '@mui/utils/useForkRef';
import { MuiEvent } from '@mui/x-internals/types';
import useSlotProps from '@mui/utils/useSlotProps';
import {
MultiSectionDigitalClockSectionClasses,
getMultiSectionDigitalClockSectionUtilityClass,
Expand All @@ -14,14 +16,15 @@ import type {
MultiSectionDigitalClockOption,
MultiSectionDigitalClockSlots,
MultiSectionDigitalClockSlotProps,
MultiSectionDigitalClockSectionOwnerState,
MultiSectionDigitalClockSectionItemProps,
} from './MultiSectionDigitalClock.types';
import {
DIGITAL_CLOCK_VIEW_HEIGHT,
MULTI_SECTION_CLOCK_SECTION_WIDTH,
} from '../internals/constants/dimensions';
import { getFocusedListItemIndex } from '../internals/utils/utils';
import { FormProps } from '../internals/models/formProps';
import { PickerOwnerState } from '../models/pickers';
import { usePickerPrivateContext } from '../internals/hooks/usePickerPrivateContext';
import { MultiSectionDigitalClockClasses } from './multiSectionDigitalClockClasses';

Expand All @@ -43,13 +46,6 @@ export interface MultiSectionDigitalClockSectionProps<TSectionValue extends numb
role?: string;
}

interface MultiSectionDigitalClockSectionOwnerState extends PickerOwnerState {
/**
* `true` if this is not the initial render of the digital clock.
*/
hasDigitalClockAlreadyBeenRendered: boolean;
}

const useUtilityClasses = (classes: Partial<MultiSectionDigitalClockClasses> | undefined) => {
const slots = {
root: ['root'],
Expand Down Expand Up @@ -178,25 +174,75 @@ export const MultiSectionDigitalClockSection = React.forwardRef(
const DigitalClockSectionItem =
slots?.digitalClockSectionItem ?? MultiSectionDigitalClockSectionItem;

const digitalClockSectionProps: MultiSectionDigitalClockSectionItemProps = useSlotProps({
elementType: DigitalClockSectionItem,
externalSlotProps: slotProps?.digitalClockSectionItem,
additionalProps: {
disableRipple: readOnly,
role: 'option',
},
ownerState,
className: classes.item,
});

const handleFocusingAndScrolling = React.useCallback(
(optionIndex?: number) => {
if (containerRef.current === null) {
return;
}
let activeItem: HTMLElement | null = null;
if (optionIndex !== undefined) {
activeItem = containerRef.current.querySelector<HTMLElement>(
`[role="option"]:nth-child(${optionIndex + 1})`,
);
} else {
activeItem = containerRef.current.querySelector<HTMLElement>(
'[role="option"][tabindex="0"], [role="option"][aria-selected="true"]',
);
}

if (active && autoFocus && activeItem) {
activeItem.focus();
}
if (!activeItem || previousActive.current === activeItem) {
return;
}
previousActive.current = activeItem;
const offsetTop = activeItem.offsetTop;

// Subtracting the 4px of extra margin intended for the first visible section item
containerRef.current.scrollTop = offsetTop - 4;
},
[active, autoFocus],
);

React.useEffect(() => {
if (containerRef.current === null) {
return;
}
const activeItem = containerRef.current.querySelector<HTMLElement>(
'[role="option"][tabindex="0"], [role="option"][aria-selected="true"]',
);
if (active && autoFocus && activeItem) {
activeItem.focus();
}
if (!activeItem || previousActive.current === activeItem) {
return;
}
previousActive.current = activeItem;
const offsetTop = activeItem.offsetTop;
handleFocusingAndScrolling();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

// Subtracting the 4px of extra margin intended for the first visible section item
containerRef.current.scrollTop = offsetTop - 4;
});
const handleChange = React.useCallback(
(
event: MuiEvent<React.MouseEvent<HTMLLIElement>>,
sectionValue: TSectionValue,
index: number,
) => {
if (readOnly) {
return;
}

if (digitalClockSectionProps.onClick) {
digitalClockSectionProps.onClick(event);
}

onChange(sectionValue);

if (!event.defaultMuiPrevented) {
handleFocusingAndScrolling(index);
}
},
[digitalClockSectionProps, handleFocusingAndScrolling, onChange, readOnly],
);

const focusedOptionIndex = items.findIndex((item) => item.isFocused(item.value));

Expand Down Expand Up @@ -252,18 +298,17 @@ export const MultiSectionDigitalClockSection = React.forwardRef(
return (
<DigitalClockSectionItem
key={option.label}
onClick={() => !readOnly && onChange(option.value)}
{...digitalClockSectionProps}
onClick={(event: React.MouseEvent<HTMLLIElement>) =>
handleChange(event, option.value, index)
}
selected={isSelected}
disabled={isDisabled}
disableRipple={readOnly}
role="option"
// aria-readonly is not supported here and does not have any effect
aria-disabled={readOnly || isDisabled || undefined}
aria-label={option.ariaLabel}
aria-selected={isSelected}
tabIndex={tabIndex}
className={classes.item}
{...slotProps?.digitalClockSectionItem}
>
{option.label}
</DigitalClockSectionItem>
Expand Down
1 change: 1 addition & 0 deletions packages/x-internals/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './AppendKeys';
export * from './DefaultizedProps';
export * from './MakeOptional';
export * from './MakeRequired';
export * from './MuiEvent';
export * from './PrependKeys';
export * from './RefObject';
export * from './SlotComponentPropsFromProps';