Skip to content

Commit

Permalink
[fields] Support RTL out of the box (#6715)
Browse files Browse the repository at this point in the history
Co-authored-by: Flavien DELANGLE <[email protected]>
  • Loading branch information
alexfauquette and flaviendelangle authored Dec 12, 2022
1 parent 2c282ea commit 5ed7bb6
Show file tree
Hide file tree
Showing 21 changed files with 389 additions and 110 deletions.
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

0 comments on commit 5ed7bb6

Please sign in to comment.