Skip to content
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

[fields] Support RTL out of the box #6715

Merged
merged 25 commits into from
Dec 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
72e62fa
add dependencies
alexfauquette Nov 2, 2022
e048044
uninteresting modifications
alexfauquette Nov 3, 2022
f7cf39d
comput the section order for keyboard navigation
alexfauquette Nov 3, 2022
59f3c64
isolate elements
alexfauquette Nov 3, 2022
f546fa0
fix ts
alexfauquette Nov 3, 2022
ebff63a
keyboard fixes
alexfauquette Nov 3, 2022
f5e7dd4
support digit update
alexfauquette Nov 4, 2022
9b949c4
fix tests
alexfauquette Nov 4, 2022
945af6a
fix ts
alexfauquette Nov 7, 2022
7f07e65
Merge remote-tracking branch 'upstream/next' into support-rtl
alexfauquette Nov 28, 2022
73d997d
Merge remote-tracking branch 'upstream/next' into support-rtl
alexfauquette Nov 30, 2022
0fbb531
documents types
alexfauquette Nov 30, 2022
7de245d
fix alone selection bare
alexfauquette Nov 30, 2022
50623f8
prettier
alexfauquette Nov 30, 2022
b95e470
fix tests
alexfauquette Nov 30, 2022
afd0175
lukas typo fix
alexfauquette Nov 30, 2022
d325937
fix replaceALl
alexfauquette Nov 30, 2022
43b0f43
[fields] Fix usage with moment-jalaali (#7)
flaviendelangle Dec 1, 2022
15314e5
Merge remote-tracking branch 'upstream/next' into support-rtl
alexfauquette Dec 2, 2022
5129f01
Merge branch 'support-rtl' of github.com:alexfauquette/mui-x into sup…
alexfauquette Dec 2, 2022
9615e04
fix tests
alexfauquette Dec 2, 2022
423c966
add testing caveat
alexfauquette Dec 2, 2022
1e7f60f
remove only
alexfauquette Dec 2, 2022
4040e64
add error on hijri docs
alexfauquette Dec 7, 2022
dd181c8
Merge remote-tracking branch 'upstream/next' into support-rtl
alexfauquette Dec 7, 2022
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
5 changes: 5 additions & 0 deletions docs/data/date-pickers/calendar-systems/calendar-systems.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ The following demo shows how to use the date-fns plugin:

## Hijri

:::error
The adapter with `moment-hijri` does not support the new fields components because the date library seems buggy when parsing a month only.
If you want to help on the support of hijri calendar, please have a look at [this PR](https://github.com/xsoh/moment-hijri/issues/83).
:::

You can use the `AdapterMomentHijri` adapter, which is based on [moment-hijri](https://www.npmjs.com/package/moment-hijri):

{{"demo": "AdapterHijri.js"}}
Expand Down
20 changes: 20 additions & 0 deletions docs/data/date-pickers/getting-started/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,29 @@ Native date (`type="date"`), time (`type="time"`) and date&time (`type="datetime

## Testing caveats

### Responsive components

:::info
Some test environments (i.e. `jsdom`) do not support media query. In such cases, components will be rendered in desktop mode. To modify this behavior you can fake the `window.matchMedia`.
:::

Be aware that running tests in headless browsers might not pass the default mediaQuery (`pointer: fine`).
In such case you can [force pointer precision](https://github.com/microsoft/playwright/issues/7769#issuecomment-1205106311) via browser flags or preferences.

### Field components

:::info
To support RTL and some keyboard interactions, field components add some Unicode character that are invisible, but appears in the input value.
:::

To add tests about a field value without having to care about those characters, you can remove the specific character before testing the equality.
Here is an example about how to do it.

```js
// Helper removing specific characters
const cleanText = (string) =>
string.replace(/\u200e|\u2066|\u2067|\u2068|\u2069/g, '');

// Example of a test using the helper
expect(cleanText(input.value)).to.equal('10-11-2021');
```
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
splitFormatIntoSections,
addPositionPropertiesToSections,
createDateStrFromSections,
getSectionOrder,
} from '@mui/x-date-pickers/internals';
import { DateRange, DateRangeFieldSection } from '../models/range';
import { splitDateRangeSections, removeLastSeparator } from './date-fields-utils';
Expand Down Expand Up @@ -142,4 +143,6 @@ export const rangeFieldValueManager: FieldValueManager<
};
},
hasError: (error) => error[0] != null || error[1] != null,
getSectionOrder: (utils, localeText, format, isRTL) =>
getSectionOrder(splitFormatIntoSections(utils, localeText, format, null), isRTL),
};
23 changes: 23 additions & 0 deletions packages/x-date-pickers/src/AdapterMomentJalaali/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import BaseAdapterMomentJalaali from '@date-io/jalaali';
import defaultMoment, { LongDateFormatKey } from 'moment-jalaali';
import { MuiFormatTokenMap, MuiPickersAdapter } from '../internals/models';

type Moment = defaultMoment.Moment;

// From https://momentjs.com/docs/#/displaying/format/
const formatTokenMap: MuiFormatTokenMap = {
// Month
Expand Down Expand Up @@ -89,4 +91,25 @@ export class AdapterMomentJalaali
public getWeekNumber = (date: defaultMoment.Moment) => {
return date.jWeek();
};

public addYears = (date: Moment, count: number) => {
return count < 0
? date.clone().subtract(Math.abs(count), 'jYear')
: date.clone().add(count, 'jYear');
};

public addMonths = (date: Moment, count: number) => {
return count < 0
? date.clone().subtract(Math.abs(count), 'jMonth')
: date.clone().add(count, 'jMonth');
};

public isValid = (value: any) => {
// We can't to `this.moment(value)` because moment-jalaali looses the invalidity information when creating a new moment object from an existing one
if (!this.moment.isMoment(value)) {
return false;
}

return value.isValid(value);
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ describe('<DateField /> - Describes', () => {
setNewValue: (value) => {
const newValue = adapterToUse.addDays(value, 1);
const input = screen.getByRole('textbox');
clickOnInput(input, 5); // Update the day
clickOnInput(input, 10); // Update the day
userEvent.keyPress(input, { key: 'ArrowUp' });
return newValue;
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,9 @@ import {
createPickerRenderer,
adapterToUse,
buildFieldInteractions,
expectInputValue,
} from 'test/utils/pickers-utils';

const expectInputValue = (input: HTMLInputElement, expectedValue: string) =>
expect(input.value.replace(/‎/g, '')).to.equal(expectedValue);

describe('<DateField /> - Editing', () => {
const { render, clock } = createPickerRenderer({
clock: 'fake',
Expand All @@ -24,11 +22,23 @@ describe('<DateField /> - Editing', () => {
key,
expectedValue,
cursorPosition = 1,
valueToSelect,
...props
}: DateFieldProps<TDate> & { key: string; expectedValue: string; cursorPosition?: number }) => {
}: DateFieldProps<TDate> & {
key: string;
expectedValue: string;
cursorPosition?: number;
valueToSelect?: string;
}) => {
render(<DateField {...props} />);
const input = screen.getByRole('textbox');
clickOnInput(input, cursorPosition);
const clickPosition = valueToSelect ? input.value.indexOf(valueToSelect) : cursorPosition;
if (clickPosition === -1) {
throw new Error(
`Failed to find value to select "${valueToSelect}" in input value: ${input.value}`,
);
}
clickOnInput(input, clickPosition);
userEvent.keyPress(input, { key });
expectInputValue(input, expectedValue);
};
Expand Down Expand Up @@ -118,7 +128,7 @@ describe('<DateField /> - Editing', () => {
key: 'ArrowDown',
expectedValue: 'May 31',
// To select the date and not the month
cursorPosition: 5,
valueToSelect: '1',
});
});

Expand Down Expand Up @@ -210,7 +220,7 @@ describe('<DateField /> - Editing', () => {
key: 'ArrowUp',
expectedValue: 'July 1',
// To select the date and not the month
cursorPosition: 5,
valueToSelect: '30',
});
});

Expand Down Expand Up @@ -638,7 +648,7 @@ describe('<DateField /> - Editing', () => {
/>,
);
const input = screen.getByRole('textbox');
clickOnInput(input, 10);
clickOnInput(input, input.value.indexOf('2010'));
userEvent.keyPress(input, { key: 'ArrowDown' });

expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2009, 3, 3, 3, 3, 3));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,29 @@ import * as React from 'react';
import { expect } from 'chai';
import { Unstable_DateField as DateField } from '@mui/x-date-pickers/DateField';
import { screen, act, userEvent, fireEvent } from '@mui/monorepo/test/utils';
import { createPickerRenderer } from 'test/utils/pickers-utils';

const getSelectedContent = (input: HTMLInputElement) =>
input.value.slice(input.selectionStart ?? 0, input.selectionEnd ?? 0);
import {
createPickerRenderer,
expectInputValue,
getCleanedSelectedContent,
} from 'test/utils/pickers-utils';

describe('<DateField /> - Selection', () => {
const { render, clock } = createPickerRenderer({ clock: 'fake' });

const clickOnInput = (input: HTMLInputElement, cursorPosition: number) => {
const clickOnInput = (input: HTMLInputElement, position: number | string) => {
const clickPosition = typeof position === 'string' ? input.value.indexOf(position) : position;
if (clickPosition === -1) {
throw new Error(
`Failed to find value to select "${position}" in input value: ${input.value}`,
);
}
act(() => {
fireEvent.mouseDown(input);
if (document.activeElement !== input) {
input.focus();
}
fireEvent.mouseUp(input);
input.setSelectionRange(cursorPosition, cursorPosition);
input.setSelectionRange(clickPosition, clickPosition);
fireEvent.click(input);

clock.runToLast();
Expand All @@ -34,7 +41,7 @@ describe('<DateField /> - Selection', () => {
input.select();
});

expect(input.value).to.equal('MM / DD / YYYY');
expectInputValue(input, 'MM / DD / YYYY');
expect(input.selectionStart).to.equal(0);
expect(input.selectionEnd).to.equal(input.value.length);
});
Expand All @@ -45,44 +52,44 @@ describe('<DateField /> - Selection', () => {
// Simulate a touch focus interaction on mobile
act(() => {
input.focus();
input.setSelectionRange(6, 8);
input.setSelectionRange(9, 10);
clock.runToLast();
});

expect(input.value).to.equal('MM / DD / YYYY');
expect(input.selectionStart).to.equal(5);
expect(input.selectionEnd).to.equal(7);
expectInputValue(input, 'MM / DD / YYYY');
expect(input.selectionStart).to.equal(9);
expect(input.selectionEnd).to.equal(11);
});

it('should select day on desktop', () => {
render(<DateField />);
const input = screen.getByRole('textbox');
clickOnInput(input, 6);
clickOnInput(input, 'DD');

expect(input.value).to.equal('MM / DD / YYYY');
expect(getSelectedContent(input)).to.equal('DD');
expectInputValue(input, 'MM / DD / YYYY');
expect(getCleanedSelectedContent(input)).to.equal('DD');
});
});

describe('Click', () => {
it('should select the clicked selection when the input is already focused', () => {
render(<DateField />);
const input = screen.getByRole('textbox');
clickOnInput(input, 7);
expect(getSelectedContent(input)).to.equal('DD');
clickOnInput(input, 'DD');
expect(getCleanedSelectedContent(input)).to.equal('DD');

clickOnInput(input, 1);
expect(getSelectedContent(input)).to.equal('MM');
clickOnInput(input, 'MM');
expect(getCleanedSelectedContent(input)).to.equal('MM');
});

it('should not change the selection when clicking on the only already selected section', () => {
render(<DateField />);
const input = screen.getByRole('textbox');
clickOnInput(input, 7);
expect(getSelectedContent(input)).to.equal('DD');
clickOnInput(input, 'DD');
expect(getCleanedSelectedContent(input)).to.equal('DD');

clickOnInput(input, 8);
expect(getSelectedContent(input)).to.equal('DD');
clickOnInput(input, input.value.indexOf('DD') + 1);
expect(getCleanedSelectedContent(input)).to.equal('DD');
});
});

Expand All @@ -93,27 +100,27 @@ describe('<DateField /> - Selection', () => {
clickOnInput(input, 1);

userEvent.keyPress(input, { key: 'a', ctrlKey: true });
expect(getSelectedContent(input)).to.equal('MM / DD / YYYY');
expect(getCleanedSelectedContent(input)).to.equal('MM / DD / YYYY');
});
});

describe('key: ArrowRight', () => {
it('should move selection to the next section when one section is selected', () => {
render(<DateField />);
const input = screen.getByRole('textbox');
clickOnInput(input, 7);
expect(getSelectedContent(input)).to.equal('DD');
clickOnInput(input, 'DD');
expect(getCleanedSelectedContent(input)).to.equal('DD');
userEvent.keyPress(input, { key: 'ArrowRight' });
expect(getSelectedContent(input)).to.equal('YYYY');
expect(getCleanedSelectedContent(input)).to.equal('YYYY');
});

it('should stay on the current section when the last section is selected', () => {
render(<DateField />);
const input = screen.getByRole('textbox');
clickOnInput(input, 11);
expect(getSelectedContent(input)).to.equal('YYYY');
clickOnInput(input, 'YYYY');
expect(getCleanedSelectedContent(input)).to.equal('YYYY');
userEvent.keyPress(input, { key: 'ArrowRight' });
expect(getSelectedContent(input)).to.equal('YYYY');
expect(getCleanedSelectedContent(input)).to.equal('YYYY');
});

it('should select the last section when all the sections are selected', () => {
Expand All @@ -123,30 +130,30 @@ describe('<DateField /> - Selection', () => {

// Select all sections
userEvent.keyPress(input, { key: 'a', ctrlKey: true });
expect(getSelectedContent(input)).to.equal('MM / DD / YYYY');
expect(getCleanedSelectedContent(input)).to.equal('MM / DD / YYYY');

userEvent.keyPress(input, { key: 'ArrowRight' });
expect(getSelectedContent(input)).to.equal('YYYY');
expect(getCleanedSelectedContent(input)).to.equal('YYYY');
});
});

describe('key: ArrowLeft', () => {
it('should move selection to the previous section when one section is selected', () => {
render(<DateField />);
const input = screen.getByRole('textbox');
clickOnInput(input, 7);
expect(getSelectedContent(input)).to.equal('DD');
clickOnInput(input, 'DD');
expect(getCleanedSelectedContent(input)).to.equal('DD');
userEvent.keyPress(input, { key: 'ArrowLeft' });
expect(getSelectedContent(input)).to.equal('MM');
expect(getCleanedSelectedContent(input)).to.equal('MM');
});

it('should stay on the current section when the first section is selected', () => {
render(<DateField />);
const input = screen.getByRole('textbox');
clickOnInput(input, 1);
expect(getSelectedContent(input)).to.equal('MM');
expect(getCleanedSelectedContent(input)).to.equal('MM');
userEvent.keyPress(input, { key: 'ArrowLeft' });
expect(getSelectedContent(input)).to.equal('MM');
expect(getCleanedSelectedContent(input)).to.equal('MM');
});

it('should select the first section when all the sections are selected', () => {
Expand All @@ -156,10 +163,10 @@ describe('<DateField /> - Selection', () => {

// Select all sections
userEvent.keyPress(input, { key: 'a', ctrlKey: true });
expect(getSelectedContent(input)).to.equal('MM / DD / YYYY');
expect(getCleanedSelectedContent(input)).to.equal('MM / DD / YYYY');

userEvent.keyPress(input, { key: 'ArrowLeft' });
expect(getSelectedContent(input)).to.equal('MM');
expect(getCleanedSelectedContent(input)).to.equal('MM');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ describe('<DateTimeField /> - Describes', () => {
setNewValue: (value) => {
const newValue = adapterToUse.addDays(value, 1);
const input = screen.getByRole('textbox');
clickOnInput(input, 5); // Update the day
clickOnInput(input, 10); // Update the day
userEvent.keyPress(input, { key: 'ArrowUp' });
return newValue;
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import { inputBaseClasses } from '@mui/material/InputBase';
import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon';
import { fireEvent, screen, userEvent } from '@mui/monorepo/test/utils';
import { Unstable_DesktopNextDatePicker as DesktopNextDatePicker } from '@mui/x-date-pickers/DesktopNextDatePicker';
import { createPickerRenderer, adapterToUse, openPicker } from 'test/utils/pickers-utils';
import {
createPickerRenderer,
adapterToUse,
openPicker,
expectInputValue,
} from 'test/utils/pickers-utils';

describe('<DesktopNextDatePicker />', () => {
const { render } = createPickerRenderer({ clock: 'fake' });
Expand Down Expand Up @@ -65,14 +70,15 @@ describe('<DesktopNextDatePicker />', () => {
const handleChange = spy();

render(<DesktopNextDatePicker onChange={handleChange} />);
const input = screen.getByRole('textbox');

fireEvent.change(screen.getByRole('textbox'), {
fireEvent.change(input, {
target: {
value: '10/11/2018',
},
});

expect(screen.getByRole('textbox')).to.have.value('10 / 11 / 2018');
expectInputValue(input, '10 / 11 / 2018');
expect(handleChange.callCount).to.equal(1);
});

Expand Down
Loading