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

fix: correctly format date/time in RTL #7423

Merged
merged 53 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
bbe6191
bdo on timefield, reverse segments on timefield in datefield
yihuiliao Nov 21, 2024
0716ad5
fix lint
yihuiliao Nov 21, 2024
76b8d0e
make things inline
yihuiliao Nov 26, 2024
47e2af5
use unicode character to wrap segments
yihuiliao Dec 4, 2024
ef0b637
fix test
yihuiliao Dec 4, 2024
b3e2f70
append unicode to text in hooks, update rac
yihuiliao Dec 6, 2024
7b77f1e
Merge branch 'main' into bidi-override
yihuiliao Dec 6, 2024
fd5a902
add comment
yihuiliao Dec 17, 2024
d9b69a8
Merge branch 'main' into bidi-override
yihuiliao Dec 18, 2024
a9f732e
skip failing test for now
yihuiliao Dec 20, 2024
8df5ad6
update keyboard nav
yihuiliao Dec 20, 2024
679e355
update logic of how unicode is applied
yihuiliao Dec 20, 2024
bedf713
fix spacing
yihuiliao Dec 20, 2024
c2df442
add comments
yihuiliao Jan 7, 2025
9080188
update tests
yihuiliao Jan 8, 2025
2b5fb13
undo some previous changes
yihuiliao Jan 8, 2025
91173c3
wrap time segments in lri, wrap fields in unicode isolate
yihuiliao Jan 8, 2025
79a8da3
fix ssr test
yihuiliao Jan 8, 2025
b923904
fix spacing
yihuiliao Jan 10, 2025
ae03f3b
fix css logic
yihuiliao Jan 10, 2025
2bbde19
fix lint
yihuiliao Jan 10, 2025
81560bd
fix keyboard nav in rac datepicker popover
yihuiliao Jan 14, 2025
dad9ceb
fix lint
yihuiliao Jan 14, 2025
ec8fe1d
prevent overflow in date range picker
yihuiliao Jan 14, 2025
a863e18
Merge branch 'main' into bidi-override
yihuiliao Jan 14, 2025
891dce9
move overflow hidden to separate new div to fix weird focus ring arou…
yihuiliao Jan 14, 2025
4b56394
this time actually fix the overflow and focus ring issue
yihuiliao Jan 14, 2025
1a9eaa9
update var names to be nicer
yihuiliao Jan 14, 2025
f50b4b1
fix japanese placeholder for extra space
yihuiliao Jan 14, 2025
e39f1ea
fix css positioning
yihuiliao Jan 14, 2025
05ed94d
Merge branch 'main' into bidi-override
yihuiliao Jan 14, 2025
ccf7b85
fix custom width
yihuiliao Jan 15, 2025
b1915c2
small css changes so that rtl will format properly
yihuiliao Jan 15, 2025
af3ab18
memo ordering of segments for keyboard navigation
yihuiliao Jan 15, 2025
50bb4df
add chromatic tests
yihuiliao Jan 16, 2025
1bd410e
fix lint
yihuiliao Jan 16, 2025
623219d
add tests to rsp date components
yihuiliao Jan 17, 2025
ccf7def
add tests to rac
yihuiliao Jan 17, 2025
ccce891
fix tests
yihuiliao Jan 17, 2025
2e682e2
Merge branch 'main' into bidi-override
yihuiliao Jan 17, 2025
6efe67f
remove comment
yihuiliao Jan 17, 2025
0fdb98d
fix chromatic stories
yihuiliao Jan 17, 2025
ab2a67a
add chromatic story
yihuiliao Jan 17, 2025
83d2727
remove style tests
yihuiliao Jan 21, 2025
9e74c07
fix lint
yihuiliao Jan 21, 2025
e48425b
update to uselayouteffect and update keyboard nav test
yihuiliao Jan 22, 2025
8e10996
make date input more consistent with using display inline
yihuiliao Jan 22, 2025
13d47a9
update timefield docs css to use display inline
yihuiliao Jan 22, 2025
4d1adff
fix showFormatHelpText
yihuiliao Jan 24, 2025
9acdf22
small change
yihuiliao Jan 24, 2025
c02ec95
fix lint
yihuiliao Jan 24, 2025
e564b9c
add divs to keyboard navigation so it works with older versions
yihuiliao Jan 24, 2025
77a6bb9
fix lint + fix tests
yihuiliao Jan 24, 2025
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
6 changes: 3 additions & 3 deletions packages/@react-aria/datepicker/docs/useDateField.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,12 @@ function DateSegment({segment, state}) {
let {segmentProps} = useDateSegment(segment, state, ref);

return (
<div
<span
{...segmentProps}
ref={ref}
className={`segment ${segment.isPlaceholder ? 'placeholder' : ''}`}>
{segment.text}
</div>
</span>
);
}

Expand All @@ -153,7 +153,7 @@ function DateSegment({segment, state}) {
}

.field {
display: inline-flex;
display: block;
padding: 2px 4px;
border-radius: 2px;
border: 1px solid var(--gray);
Expand Down
3 changes: 3 additions & 0 deletions packages/@react-aria/datepicker/src/useDateField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,9 @@ export function useDateField<T extends DateValue>(props: AriaDateFieldOptions<T>
if (props.onKeyUp) {
props.onKeyUp(e);
}
},
style: {
unicodeBidi: 'isolate'
Copy link
Member Author

@yihuiliao yihuiliao Jan 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added for DateRangePicker because things were looking a little funky without it. wraps around each datefield. see codepen for reproduction

i could update this so that it only applies in rtl locales but it also doesn't seem to have any affect for ltr locales.

}
}),
inputProps,
Expand Down
73 changes: 69 additions & 4 deletions packages/@react-aria/datepicker/src/useDatePickerGroup.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,48 @@
import {createFocusManager, getFocusableTreeWalker} from '@react-aria/focus';
import {DateFieldState, DatePickerState, DateRangePickerState} from '@react-stately/datepicker';
import {FocusableElement, KeyboardEvent, RefObject} from '@react-types/shared';
import {mergeProps} from '@react-aria/utils';
import {mergeProps, useLayoutEffect} from '@react-aria/utils';
import {useLocale} from '@react-aria/i18n';
import {useMemo} from 'react';
import {useMemo, useRef} from 'react';
import {usePress} from '@react-aria/interactions';

export function useDatePickerGroup(state: DatePickerState | DateRangePickerState | DateFieldState, ref: RefObject<Element | null>, disableArrowNavigation?: boolean) {
let {direction} = useLocale();
let focusManager = useMemo(() => createFocusManager(ref), [ref]);
let segments = useRef<FocusableElement[]>(undefined);
useLayoutEffect(() => {
if (ref?.current) {

let update = () => {
if (ref.current) {
// TODO: For now, just querying this list of elements. However, it's possible that either through hooks or RAC that some users may include other focusable items that they would want to able to keyboard navigate to. In that case, we might want to utilize focusableElements in isFocusable.ts
let editableSegments: NodeListOf<Element> | undefined = ref.current?.querySelectorAll('span[role="spinbutton"], span[role="textbox"], button, div[role="spinbutton"], div[role="textbox"]');

let segmentsArr = Array.from(editableSegments as NodeListOf<Element>).filter(Boolean).map(node => {
return {
element: node as FocusableElement,
rectX: node.getBoundingClientRect().left
};
});

let orderedSegments = segmentsArr.sort((a, b) => a.rectX - b.rectX).map((item => item.element));
segments.current = orderedSegments;
}
};

update();

let observer = new MutationObserver(update);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need the MutationObserver? When would the segments change order without the date picker itself re-rendering?

Copy link
Member Author

@yihuiliao yihuiliao Jan 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm good point,,,i think it should be fine if we were to get rid of it

Copy link
Member

@snowystinger snowystinger Jan 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it was for if users added more buttons to control the field, Yihui had an example with a << and < button for jumping month and year I think

I don't know if those buttons would ever be removed or added dynamically

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah right, I think it was from the example in this issue #7645

observer.observe(ref.current, {
subtree: true,
childList: true
});

return () => {
observer.disconnect();
};
}
}, []);

// Open the popover on alt + arrow down
let onKeyDown = (e: KeyboardEvent) => {
Expand All @@ -31,7 +65,21 @@ export function useDatePickerGroup(state: DatePickerState | DateRangePickerState
e.preventDefault();
e.stopPropagation();
if (direction === 'rtl') {
focusManager.focusNext();
if (segments.current) {
let orderedSegments = segments.current;
let target = e.target as FocusableElement;
let index = orderedSegments.indexOf(target);

if (index === 0) {
target = orderedSegments[0] || target;
} else {
target = orderedSegments[index - 1] || target;
}

if (target) {
target.focus();
}
}
} else {
focusManager.focusPrevious();
}
Expand All @@ -40,7 +88,24 @@ export function useDatePickerGroup(state: DatePickerState | DateRangePickerState
e.preventDefault();
e.stopPropagation();
if (direction === 'rtl') {
focusManager.focusPrevious();
if (segments.current) {
let orderedSegments = segments.current;
let target = e.target as FocusableElement;
let index = orderedSegments.indexOf(target);

if (index === orderedSegments.length - 1) {
target = orderedSegments[orderedSegments.length - 1] || target;
} else {
target = orderedSegments[index - 1] || target;
}


target = orderedSegments[index + 1] || target;

if (target) {
target.focus();
}
}
} else {
focusManager.focusNext();
}
Expand Down
18 changes: 13 additions & 5 deletions packages/@react-aria/datepicker/src/useDateSegment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {DateFieldState, DateSegment} from '@react-stately/datepicker';
import {getScrollParent, isIOS, isMac, mergeProps, scrollIntoViewport, useEvent, useId, useLabels, useLayoutEffect} from '@react-aria/utils';
import {hookData} from './useDateField';
import {NumberParser} from '@internationalized/number';
import React, {useMemo, useRef} from 'react';
import React, {CSSProperties, useMemo, useRef} from 'react';
import {RefObject} from '@react-types/shared';
import {useDateFormatter, useFilter, useLocale} from '@react-aria/i18n';
import {useDisplayNames} from './useDisplayNames';
Expand All @@ -33,7 +33,7 @@ export interface DateSegmentAria {
*/
export function useDateSegment(segment: DateSegment, state: DateFieldState, ref: RefObject<HTMLElement | null>): DateSegmentAria {
let enteredKeys = useRef('');
let {locale} = useLocale();
let {locale, direction} = useLocale();
let displayNames = useDisplayNames();
let {ariaLabel, ariaLabelledBy, ariaDescribedBy, focusManager} = hookData.get(state)!;

Expand Down Expand Up @@ -385,6 +385,16 @@ export function useDateSegment(segment: DateSegment, state: DateFieldState, ref:
};
}

let dateSegments = ['day', 'month', 'year'];
let segmentStyle : CSSProperties = {caretColor: 'transparent'};
if (direction === 'rtl') {
if (dateSegments.includes(segment.type)) {
segmentStyle = {caretColor: 'transparent', direction: 'ltr', unicodeBidi: 'embed'};
} else if (segment.type === 'timeZoneName') {
segmentStyle = {caretColor: 'transparent', unicodeBidi: 'embed'};
}
}

return {
segmentProps: mergeProps(spinButtonProps, labelProps, {
id,
Expand All @@ -403,9 +413,7 @@ export function useDateSegment(segment: DateSegment, state: DateFieldState, ref:
tabIndex: state.isDisabled ? undefined : 0,
onKeyDown,
onFocus,
style: {
caretColor: 'transparent'
},
style: segmentStyle,
// Prevent pointer events from reaching useDatePickerGroup, and allow native browser behavior to focus the segment.
onPointerDown(e) {
e.stopPropagation();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default {
title: 'DateField',
parameters: {
chromaticProvider: {
locales: ['en-US', 'ar-EG', 'ja-JP']
locales: ['en-US', 'ar-EG', 'ja-JP', 'he-IL']
}
}
};
Expand All @@ -41,6 +41,16 @@ PlaceholderFocus.parameters = {
}
};

export const PlaceholderFocusRTL = () => <DateField label="Date" placeholderValue={date} autoFocus />;
PlaceholderFocusRTL.parameters = {
chromaticProvider: {
locales: ['he-IL'],
scales: ['medium'],
colorSchemes: ['light'],
express: false
}
};

export const PlaceholderFocusExpress = () => <DateField label="Date" placeholderValue={date} autoFocus />;
PlaceholderFocusExpress.parameters = {
chromaticProvider: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ import {ContextualHelp} from '@react-spectrum/contextualhelp';
import {DatePicker} from '../';
import {Heading} from '@react-spectrum/text';
import React from 'react';
import {userEvent, within} from '@storybook/testing-library';

export default {
title: 'DatePicker',
parameters: {
chromaticProvider: {
locales: ['en-US'/* , 'ar-EG', 'ja-JP' */]
locales: ['en-US', 'ar-EG', 'ja-JP', 'he-IL']
}
}
};
Expand All @@ -37,6 +38,17 @@ const focusParams = {

const openParams = {
chromaticProvider: {
locales: ['en-US'],
colorSchemes: ['light'],
scales: ['medium'],
disableAnimations: true,
express: false
}
};

const openParamsRTL = {
chromaticProvider: {
locales: ['he-IL'],
colorSchemes: ['light'],
scales: ['medium'],
disableAnimations: true,
Expand All @@ -54,6 +66,16 @@ export const Placeholder = () => <DatePicker label="Date" placeholderValue={date
export const PlaceholderFocus = () => <DatePicker label="Date" placeholderValue={date} autoFocus />;
PlaceholderFocus.parameters = focusParams;

export const PlaceholderFocusRTL = () => <DatePicker label="Date" placeholderValue={date} autoFocus />;
PlaceholderFocusRTL.parameters = {
chromaticProvider: {
locales: ['ar-EG'],
scales: ['medium'],
colorSchemes: ['light'],
express: false
}
};

export const PlaceholderFocusExpress = () => <DatePicker label="Date" placeholderValue={date} autoFocus />;
PlaceholderFocusExpress.parameters = {
chromaticProvider: {
Expand Down Expand Up @@ -110,10 +132,18 @@ export const OpenPlaceholder = () => <DatePicker label="Date" placeholderValue={
OpenPlaceholder.parameters = openParams;
OpenPlaceholder.decorators = openDecorators;

export const OpenPlaceholderRTL = () => <DatePicker label="Date" placeholderValue={date} isOpen shouldFlip={false} />;
OpenPlaceholderRTL.parameters = openParamsRTL;
OpenPlaceholderRTL.decorators = openDecorators;

export const OpenValue = () => <DatePicker label="Date" value={date} isOpen shouldFlip={false} />;
OpenValue.parameters = openParams;
OpenValue.decorators = openDecorators;

export const OpenValueRTL = () => <DatePicker label="Date" value={date} isOpen shouldFlip={false} />;
OpenValueRTL.parameters = openParamsRTL;
OpenValueRTL.decorators = openDecorators;

export const OpenTime = () => <DatePicker label="Date" value={dateTime} isOpen shouldFlip={false} />;
OpenTime.parameters = openParams;
OpenTime.decorators = openDecorators;
Expand Down Expand Up @@ -147,6 +177,50 @@ OpenExpress.parameters = {
};
OpenExpress.decorators = openDecorators;

export const OpenLTRInteractions = () => <DatePicker label="Date" value={date} />;
OpenLTRInteractions.parameters = {
chromaticProvider: {
locales: ['en-US'],
scales: ['medium'],
colorSchemes: ['light'],
express: false
}
};
OpenLTRInteractions.decorators = openDecorators;

OpenLTRInteractions.play = async ({canvasElement}) => {
await userEvent.tab();
await userEvent.keyboard('[ArrowRight]');
await userEvent.keyboard('[ArrowRight]');
await userEvent.keyboard('[ArrowRight]');
await userEvent.keyboard('[Enter]]');
let body = canvasElement.ownerDocument.body;
await within(body).findByRole('dialog');
await userEvent.keyboard('[ArrowRight]');
};

export const OpenRTLInteractions = () => <DatePicker label="Date" value={date} />;
OpenRTLInteractions.parameters = {
chromaticProvider: {
locales: ['ar-EG'],
scales: ['medium'],
colorSchemes: ['light'],
express: false
}
};
OpenRTLInteractions.decorators = openDecorators;

OpenRTLInteractions.play = async ({canvasElement}) => {
await userEvent.tab();
await userEvent.keyboard('[ArrowLeft]');
await userEvent.keyboard('[ArrowLeft]');
await userEvent.keyboard('[ArrowLeft]');
await userEvent.keyboard('[Enter]]');
let body = canvasElement.ownerDocument.body;
await within(body).findByRole('dialog');
await userEvent.keyboard('[ArrowLeft]');
};

export const MultipleMonths = () => <DatePicker label="Date" value={date} isOpen shouldFlip={false} maxVisibleMonths={3} />;
MultipleMonths.parameters = openParams;
MultipleMonths.decorators = [Story => <div style={{height: 550, width: 1000}}><Story /></div>];
Expand Down
Loading
Loading