Skip to content

Commit

Permalink
Inner state for picker modal (mui#1095)
Browse files Browse the repository at this point in the history
* Use inner state for displayed date in calendar

* Make inline pickers controlled by default

* Fix tests

* Fix deadlock on rendering with usePickerState

* Fix crashing on utils change

* Fix not applying keydown listener, closes mui#1090

* Update packages 08.06.2019 (mui#1096)

* Update packages 08.06.2019

* Fix prettier
  • Loading branch information
dmtrKovalenko authored Jun 9, 2019
1 parent 14e2033 commit 48cd17a
Show file tree
Hide file tree
Showing 17 changed files with 247 additions and 258 deletions.
7 changes: 6 additions & 1 deletion docs/_shared/Example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import GithubIcon from '_shared/svgIcons/GithubIcon';
import React, { useState, useCallback, useMemo, useContext } from 'react';
import { copy } from 'utils/helpers';
import { GITHUB_EDIT_URL } from '_constants';
import { useUtils } from '@material-ui/pickers';
import { replaceGetFormatStrings } from 'utils/utilsService';
import { withSnackbar, InjectedNotistackProps } from 'notistack';
import { withUtilsService, UtilsContext } from './UtilsServiceContext';
Expand Down Expand Up @@ -66,6 +67,7 @@ function Example({ source, enqueueSnackbar }: Props) {
);
}

const utils = useUtils();
const classes = useStyles();
const currentLib = useContext(UtilsContext).lib;
const [expanded, setExpanded] = useState(false);
Expand Down Expand Up @@ -121,7 +123,10 @@ function Example({ source, enqueueSnackbar }: Props) {
</IconButton>
</Tooltip>

<Component />
<Component
// remount component when utils changed
key={utils.constructor.name}
/>
</div>
</>
);
Expand Down
2 changes: 1 addition & 1 deletion docs/_shared/UtilsServiceContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const UtilsContext = React.createContext<UtilsService>(createUtilsService
export const withUtilsService = <P extends UtilsService>(Component: React.ComponentType<P>) => {
const withUtilsService: React.SFC<Omit<P, keyof UtilsService>> = props => (
<UtilsContext.Consumer>
{service => <Component {...service} {...props as any} />}
{service => <Component {...service} {...(props as any)} />}
</UtilsContext.Consumer>
);

Expand Down
1 change: 0 additions & 1 deletion docs/pages/guides/StaticComponents.example.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ function StaticPickers() {
{ value, onChange: handleDateChange },
{
getDefaultFormat: () => 'MM/dd/yyyy',
getValidationError: () => null,
}
);

Expand Down
6 changes: 4 additions & 2 deletions lib/src/Picker/WrappedKeyboardPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,17 @@ export type WrappedKeyboardPickerProps = DateValidationProps &
BaseKeyboardPickerProps &
ExtendWrapper<KeyboardDateInputProps>;

export interface MakePickerOptions<T> {
export interface MakePickerOptions {
useOptions: (props: any) => StateHookOptions;
ToolbarComponent: React.ComponentType<ToolbarComponentProps>;
}

// Mostly duplicate of ./WrappedPurePicker.tsx to enable tree-shaking of keyboard logic
// TODO investigate how to reduce duplications
export function makeKeyboardPicker<T extends any>({
useOptions,
ToolbarComponent,
}: MakePickerOptions<T>): React.FC<WrappedKeyboardPickerProps & T> {
}: MakePickerOptions): React.FC<WrappedKeyboardPickerProps & T> {
function WrappedKeyboardPicker(props: WrappedKeyboardPickerProps & T) {
const {
allowKeyboardControl,
Expand Down
2 changes: 1 addition & 1 deletion lib/src/Picker/WrappedPurePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export type WrappedPurePickerProps = DateValidationProps &
export function makePurePicker<T extends any>({
useOptions,
ToolbarComponent,
}: MakePickerOptions<T>): React.FC<WrappedPurePickerProps & T> {
}: MakePickerOptions): React.FC<WrappedPurePickerProps & T> {
function WrappedPurePicker(props: WrappedPurePickerProps & T) {
const {
allowKeyboardControl,
Expand Down
36 changes: 0 additions & 36 deletions lib/src/__tests__/DatePicker/Calendar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,39 +49,3 @@ describe('Calendar - disabled selected date on mount', () => {
expect(onChange).toHaveBeenCalledWith(utilsToUse.date('01-01-2018'), false);
});
});

describe('Calendar - pop and push loading queue', () => {
let component: ShallowWrapper<any, any, any>;

beforeEach(() => {
component = shallowRender(props => (
<Calendar
date={utilsToUse.date('01-01-2017')}
minDate={new Date('01-01-2018')}
onChange={jest.fn()}
utils={utilsToUse}
{...props}
/>
));
});

it('Push two times to loading queue', () => {
(component.instance() as Calendar).pushToLoadingQueue();
(component.instance() as Calendar).pushToLoadingQueue();

expect(component.state('loadingQueue')).toEqual(2);
});

it('Pop from empty loading queue', () => {
(component.instance() as Calendar).popFromLoadingQueue();

expect(component.state('loadingQueue')).toEqual(0);
});

it('Push and pop loading queue', () => {
(component.instance() as Calendar).pushToLoadingQueue();
(component.instance() as Calendar).popFromLoadingQueue();

expect(component.state('loadingQueue')).toEqual(0);
});
});
9 changes: 7 additions & 2 deletions lib/src/__tests__/e2e/DatePickerRoot.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as React from 'react';
import { ReactWrapper } from 'enzyme';
import { clickOKButton } from './commands';
import { mount, utilsToUse } from '../test-utils';
import { DatePicker, DatePickerProps } from '../../DatePicker';

Expand All @@ -12,6 +13,7 @@ describe('e2e - DatePicker', () => {
component = mount(
<DatePicker
open
autoOk
openTo="date"
animateYearScrolling={false}
value={utilsToUse.date('2018-01-01T00:00:00.000Z')}
Expand Down Expand Up @@ -48,6 +50,8 @@ describe('e2e - DatePicker', () => {
.find('Year')
.at(1)
.simulate('click');

clickOKButton(component);
expect(onChangeMock).toHaveBeenCalled();
});
});
Expand Down Expand Up @@ -78,7 +82,7 @@ describe('e2e -- DatePicker views year', () => {
.at(1)
.simulate('click');

expect(onChangeMock).toHaveBeenCalled();
clickOKButton(component);
expect(onYearChangeMock).toHaveBeenCalled();
});
});
Expand All @@ -93,6 +97,7 @@ describe('e2e -- DatePicker views year and month', () => {
component = mount(
<DatePicker
open
autoOk
value={utilsToUse.date('2018-01-01T00:00:00.000Z')}
onChange={onChangeMock}
onMonthChange={onMonthChangeMock}
Expand All @@ -109,7 +114,6 @@ describe('e2e -- DatePicker views year and month', () => {
.first()
.simulate('click');

expect(onChangeMock).toHaveBeenCalled();
expect(onMonthChangeMock).toHaveBeenCalled();
});

Expand All @@ -135,6 +139,7 @@ describe('e2e -- DatePicker views year and month open from year', () => {
component = mount(
<DatePicker
open
autoOk
value={utilsToUse.date('2018-01-01T00:00:00.000Z')}
onChange={onChangeMock}
views={['year', 'month']}
Expand Down
16 changes: 7 additions & 9 deletions lib/src/__tests__/e2e/DateTimePickerRoot.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from 'react';
import { ReactWrapper } from 'enzyme';
import { mount, utilsToUse } from '../test-utils';
import { clickOKButton } from './commands';
import { mount, utilsToUse, toHaveBeenCalledExceptMoment } from '../test-utils';
import { DateTimePicker, DateTimePickerProps } from '../../DateTimePicker/DateTimePicker';

describe('e2e - DateTimePicker', () => {
Expand All @@ -27,18 +28,19 @@ describe('e2e - DateTimePicker', () => {
expect(component).toBeTruthy();
});

it('Should render year selection', () => {
it('Should display year view', () => {
component
.find('ToolbarButton')
.first()
.simulate('click');

expect(component.find('Year').length).toBe(201);

component
.find('Year')
.at(1)
.simulate('click');

clickOKButton(component);
expect(onChangeMock).toHaveBeenCalled();
});

Expand All @@ -64,11 +66,7 @@ describe('e2e - DateTimePicker', () => {
.at(5)
.simulate('click');

if (process.env.UTILS === 'moment') {
expect(onChangeMock).toHaveBeenCalled();
return;
}

expect(onChangeMock).toHaveBeenCalledWith(utilsToUse.date('2018-01-01T12:00:00.000Z'));
clickOKButton(component);
toHaveBeenCalledExceptMoment(onChangeMock, [utilsToUse.date('2018-01-01T12:00:00.000Z')]);
});
});
40 changes: 16 additions & 24 deletions lib/src/__tests__/e2e/TimePicker.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as React from 'react';
import { ReactWrapper } from 'enzyme';
import { clickOKButton } from './commands';
import { TimePicker, TimePickerProps } from '../../TimePicker/TimePicker';
import { mount, utilsToUse, toHaveBeenCalledExceptMoment } from '../test-utils';

Expand Down Expand Up @@ -28,24 +29,20 @@ describe('e2e - TimePicker', () => {

it('Should submit onChange on moving', () => {
component.find('Clock div[role="menu"]').simulate('mouseMove', fakeTouchEvent);

expect(onChangeMock).toHaveBeenCalled();
});

it('Should submit hourview (mouse move)', () => {
component
.find('WithStyles(ToolbarButton)')
.at(1)
.simulate('click');
component.find('Clock div[role="menu"]').simulate('mouseUp', fakeTouchEvent);

expect(onChangeMock).toHaveBeenCalled();
expect(
component
.find('WithStyles(ToolbarButton)')
.at(0)
.text()
).toBe('11');
});

it('Should change minutes (touch)', () => {
component
.find('WithStyles(ToolbarButton)')
.at(2)
.at(1)
.simulate('click');

component.find('Clock div[role="menu"]').simulate('touchMove', {
Expand All @@ -58,19 +55,12 @@ describe('e2e - TimePicker', () => {
],
});

expect(onChangeMock).toHaveBeenCalled();

component.find('Clock div[role="menu"]').simulate('touchEnd', {
buttons: 1,
changedTouches: [
{
clientX: 20,
clientY: 15,
},
],
});

expect(onChangeMock).toHaveBeenCalled();
expect(
component
.find('WithStyles(ToolbarButton)')
.at(1)
.text()
).toBe('53');
});

it('Should change meridiem mode', () => {
Expand All @@ -79,6 +69,7 @@ describe('e2e - TimePicker', () => {
.at(3)
.simulate('click');

clickOKButton(component);
toHaveBeenCalledExceptMoment(onChangeMock, [utilsToUse.date('2018-01-01T12:00:00.000')]);
});
});
Expand Down Expand Up @@ -124,6 +115,7 @@ describe('e2e - TimePicker with seconds', () => {
],
});

clickOKButton(component);
toHaveBeenCalledExceptMoment(onChangeMock, [utilsToUse.date('2018-01-01T00:00:53.000')]);
});
});
8 changes: 8 additions & 0 deletions lib/src/__tests__/e2e/commands.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ReactWrapper } from 'enzyme';

export const clickOKButton = (component: ReactWrapper<any>) => {
component
.find('ForwardRef(DialogActions) WithStyles(ForwardRef(Button))')
.at(1)
.simulate('click');
};
2 changes: 1 addition & 1 deletion lib/src/_shared/WithUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export interface WithUtilsProps {
export const withUtils = () => <P extends WithUtilsProps>(Component: React.ComponentType<P>) => {
const WithUtils: React.SFC<Omit<P, keyof WithUtilsProps>> = props => {
const utils = useUtils();
return <Component utils={utils} {...props as any} />;
return <Component utils={utils} {...(props as any)} />;
};

WithUtils.displayName = `WithUtils(${Component.displayName || Component.name})`;
Expand Down
2 changes: 1 addition & 1 deletion lib/src/_shared/hooks/useKeyDown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,5 @@ export function useKeyDown(active: boolean, keyHandlers: KeyHandlers) {
window.removeEventListener('keydown', handleKeyDown);
};
}
}, [active]);
}, [active, keyHandlers]);
}
6 changes: 3 additions & 3 deletions lib/src/_shared/hooks/useKeyboardPickerState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,21 +40,21 @@ export function useKeyboardPickerState(props: BaseKeyboardPickerProps, options:
}
}, [format, props, props.value, utils]);

function handleChange(date: MaterialUiPickersDate) {
function handleKeyboardChange(date: MaterialUiPickersDate) {
const dateString = date === null ? null : utils.format(date, format);

props.onChange(date, dateString);
}

const { inputProps: innerInputProps, wrapperProps, pickerProps } = usePickerState(
// Extend props interface
{ ...props, value: dateValue, onChange: handleChange },
{ ...props, value: dateValue, onChange: handleKeyboardChange },
options
);

const inputProps = useMemo(
() => ({
...innerInputProps,
...innerInputProps, // reuse validation and open/close logic
format: wrapperProps.format,
inputValue: props.inputValue || innerInputValue,
onChange: (value: string) => {
Expand Down
30 changes: 30 additions & 0 deletions lib/src/_shared/hooks/useOpenState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { useCallback, useState } from 'react';
import { BasePickerProps } from '../../typings/BasePicker';

function makeControlledOpenProps(props: BasePickerProps) {
return {
isOpen: props.open!,
setIsOpen: (newIsOpen: boolean) => {
return newIsOpen ? props.onOpen && props.onOpen() : props.onClose && props.onClose();
},
};
}

export function useOpenState(props: BasePickerProps) {
if (props.open !== undefined && props.open !== null) {
return makeControlledOpenProps(props);
}

const [isOpen, setIsOpenState] = useState(false);
// prettier-ignore
const setIsOpen = useCallback((newIsOpen: boolean) => {
setIsOpenState(newIsOpen);

return newIsOpen
? props.onOpen && props.onOpen()
: props.onClose && props.onClose();
}, [props]);

return { isOpen, setIsOpen };
}
Loading

0 comments on commit 48cd17a

Please sign in to comment.