-
Notifications
You must be signed in to change notification settings - Fork 1.2k
docs: rtl date time format blog #7936
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
base: main
Are you sure you want to change the base?
Conversation
|
||
To format the segments according to the user locale, we rely on the browser’s [Unicode Bidirectional Algorithm](https://unicode.org/reports/tr9/). However, we found that some of our CSS styles were interferring with algorithm's application, leading to incorrect formating. For instance, in `he-IL`, the proper numeric date format should be `DD.MM.YYYY`, but our date component was displaying `YYYY.MM.DD`. This issue varied across different RTL languages for date fields, but for time fields, we observed a consistent problem across all RTL languages where time segments were flipped, rendering `MM:HH` instead of the correct `HH:MM` format. | ||
|
||
<RTLTimefield /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i have some things i'd like to fix this image so it'll get eventually get replaced but the jist is there. wanted to have something so people could see how the blog flows with this image
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some screen recordings have the keyboard events displayed, and some don't. Could we add them to all? otherwise it's hard to know what's going on
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mainly stylistic suggestions and small typos/grammar, feel free to push back on any of them! Otherwise great work, loved the break down of the numerous issues and the fixes you found when investigating this
|
||
<Video src={keyboardVideoURL} loop autoPlay muted /> | ||
|
||
As a result, we updated the keyboard navigation in right-to-left langauges to rely on the positioning of the different segments to determine which node to receive focus for an intuitive keyboard experience. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not entirely sure what this fix entails, mind clarifying?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
videos are much easier for me to follow now
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a great deep dive! I wonder if it might help to have a short section at the start that gives a high level overview of RTL languages and how support for it is commonly implemented (mirroring, bidirectional text with mixed rtl/ltr, etc). I have a feeling that most developers are not familiar with it, and we get pretty deep into the weeds very quickly here, so we might need to explain some stuff more. Then later on, don't assume devs know what <bdo>
is, what these different unicode control characters are, etc. The links are helpful, but most people won't click them so we might need just a bit more explanation of our own.
</div> | ||
``` | ||
|
||
Through much trial and error, we discovered that appplying the [left-to-right embedding (LRE) Unicode](https://unicode.org/reports/tr9/#Explicit_Directional_Embeddings) on the date segments allowed us to to treat the text as embedded left-to-right while preserving the right-to-left mark on the separators, ensuring that Arabic dates display in the correct format. While we could have added Unicode to the segments like we did with the time fields, we opted for the [equivalent CSS](https://unicode.org/reports/tr9/#Markup_And_Formatting) approach instead to avoid modifying the DOM. This CSS is applied on date segments with placeholder or actual values to avoid the behavior discussed earlier with shifting segments. Through additional testing, we found that we should only apply left-to-right embedding on numeric values. If the value was displayed as text (e.g. "November" instead of "11") we did not apply this CSS. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We might need to show how it was rendering in the incorrect cases again here. I lost track of which problem this was solving when reading it. If I remember correctly it was so that the segments were ordered in LTR but the contents of the individual segments were RTL (so as not to mess up the placeholder text)?
## API Changes
react-aria-components/react-aria-components:DropZone DropZone {
aria-describedby?: string
aria-details?: string
aria-label?: string
aria-labelledby?: string
children?: ReactNode | ((DropZoneRenderProps & {
defaultChildren: ReactNode | undefined
})) => ReactNode
className?: string | ((DropZoneRenderProps & {
defaultClassName: string | undefined
})) => string
getDropOperation?: (DragTypes, Array<DropOperation>) => DropOperation
isDisabled?: boolean
onDrop?: (DropEvent) => void
- onDropActivate?: (DropActivateEvent) => void
onDropEnter?: (DropEnterEvent) => void
onDropExit?: (DropExitEvent) => void
onDropMove?: (DropMoveEvent) => void
onHoverChange?: (boolean) => void
onHoverStart?: (HoverEvent) => void
slot?: string | null
style?: CSSProperties | ((DropZoneRenderProps & {
defaultStyle: CSSProperties
})) => CSSProperties | undefined
} /react-aria-components:Tree Tree <T extends {}> {
aria-describedby?: string
aria-details?: string
aria-label?: string
aria-labelledby?: string
autoFocus?: boolean | FocusStrategy
children?: ReactNode | ({}) => ReactNode
className?: string | ((TreeRenderProps & {
defaultClassName: string | undefined
})) => string
defaultExpandedKeys?: Iterable<Key>
defaultSelectedKeys?: 'all' | Iterable<Key>
dependencies?: ReadonlyArray<any>
disabledBehavior?: DisabledBehavior = 'all'
disabledKeys?: Iterable<Key>
disallowEmptySelection?: boolean
- dragAndDropHooks?: DragAndDropHooks
escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
expandedKeys?: Iterable<Key>
id?: string
items?: Iterable<T>
- keyboardDelegate?: KeyboardDelegate
onAction?: (Key) => void
onExpandedChange?: (Set<Key>) => any
onScroll?: (UIEvent<Element>) => void
onSelectionChange?: (Selection) => void
selectedKeys?: 'all' | Iterable<Key>
selectionBehavior?: SelectionBehavior
selectionMode?: SelectionMode
shouldSelectOnPressUp?: boolean
slot?: string | null
style?: CSSProperties | ((TreeRenderProps & {
defaultStyle: CSSProperties
})) => CSSProperties | undefined
} /react-aria-components:DropZoneProps DropZoneProps {
aria-describedby?: string
aria-details?: string
aria-label?: string
aria-labelledby?: string
children?: ReactNode | ((DropZoneRenderProps & {
defaultChildren: ReactNode | undefined
})) => ReactNode
className?: string | ((DropZoneRenderProps & {
defaultClassName: string | undefined
})) => string
getDropOperation?: (DragTypes, Array<DropOperation>) => DropOperation
isDisabled?: boolean
onDrop?: (DropEvent) => void
- onDropActivate?: (DropActivateEvent) => void
onDropEnter?: (DropEnterEvent) => void
onDropExit?: (DropExitEvent) => void
onDropMove?: (DropMoveEvent) => void
onHoverChange?: (boolean) => void
onHoverStart?: (HoverEvent) => void
slot?: string | null
style?: CSSProperties | ((DropZoneRenderProps & {
defaultStyle: CSSProperties
})) => CSSProperties | undefined
} /react-aria-components:TreeProps TreeProps <T> {
aria-describedby?: string
aria-details?: string
aria-label?: string
aria-labelledby?: string
autoFocus?: boolean | FocusStrategy
children?: ReactNode | (T) => ReactNode
className?: string | ((TreeRenderProps & {
defaultClassName: string | undefined
})) => string
defaultExpandedKeys?: Iterable<Key>
defaultSelectedKeys?: 'all' | Iterable<Key>
dependencies?: ReadonlyArray<any>
disabledBehavior?: DisabledBehavior = 'all'
disabledKeys?: Iterable<Key>
disallowEmptySelection?: boolean
- dragAndDropHooks?: DragAndDropHooks
escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
expandedKeys?: Iterable<Key>
id?: string
items?: Iterable<T>
- keyboardDelegate?: KeyboardDelegate
onAction?: (Key) => void
onExpandedChange?: (Set<Key>) => any
onScroll?: (UIEvent<Element>) => void
onSelectionChange?: (Selection) => void
selectedKeys?: 'all' | Iterable<Key>
selectionBehavior?: SelectionBehavior
selectionMode?: SelectionMode
shouldSelectOnPressUp?: boolean
slot?: string | null
style?: CSSProperties | ((TreeRenderProps & {
defaultStyle: CSSProperties
})) => CSSProperties | undefined
} @react-aria/dnd/@react-aria/dnd:DroppableCollectionOptions DroppableCollectionOptions {
acceptedDragTypes?: 'all' | Array<string | symbol> = 'all'
dropTargetDelegate: DropTargetDelegate
getDropOperation?: (DropTarget, DragTypes, Array<DropOperation>) => DropOperation
keyboardDelegate: KeyboardDelegate
onDrop?: (DroppableCollectionDropEvent) => void
onDropEnter?: (DroppableCollectionEnterEvent) => void
onDropExit?: (DroppableCollectionExitEvent) => void
onInsert?: (DroppableCollectionInsertDropEvent) => void
onItemDrop?: (DroppableCollectionOnItemDropEvent) => void
- onKeyDown?: KeyboardEvents['onKeyDown']
onReorder?: (DroppableCollectionReorderEvent) => void
onRootDrop?: (DroppableCollectionRootDropEvent) => void
shouldAcceptItemDrop?: (ItemDropTarget, DragTypes) => boolean
} /@react-aria/dnd:DropOptions DropOptions {
getDropOperation?: (DragTypes, Array<DropOperation>) => DropOperation
getDropOperationForPoint?: (DragTypes, Array<DropOperation>, number, number) => DropOperation
hasDropButton?: boolean
isDisabled?: boolean
onDrop?: (DropEvent) => void
- onDropActivate?: (DropActivateEvent) => void
onDropEnter?: (DropEnterEvent) => void
onDropExit?: (DropExitEvent) => void
onDropMove?: (DropMoveEvent) => void
ref: RefObject<FocusableElement | null> @react-spectrum/dropzone/@react-spectrum/dropzone:DropZone DropZone {
UNSAFE_className?: string
UNSAFE_style?: CSSProperties
alignSelf?: Responsive<'auto' | 'normal' | 'start' | 'end' | 'center' | 'flex-start' | 'flex-end' | 'self-start' | 'self-end' | 'stretch'>
aria-describedby?: string
aria-details?: string
aria-label?: string
aria-labelledby?: string
bottom?: Responsive<DimensionValue>
children: ReactNode
end?: Responsive<DimensionValue>
flex?: Responsive<string | number | boolean>
flexBasis?: Responsive<number | string>
flexGrow?: Responsive<number>
flexShrink?: Responsive<number>
getDropOperation?: (DragTypes, Array<DropOperation>) => DropOperation
gridArea?: Responsive<string>
gridColumn?: Responsive<string>
gridColumnEnd?: Responsive<string>
gridColumnStart?: Responsive<string>
gridRow?: Responsive<string>
gridRowEnd?: Responsive<string>
gridRowStart?: Responsive<string>
height?: Responsive<DimensionValue>
id?: string
isFilled?: boolean
isHidden?: Responsive<boolean>
justifySelf?: Responsive<'auto' | 'normal' | 'start' | 'end' | 'flex-start' | 'flex-end' | 'self-start' | 'self-end' | 'center' | 'left' | 'right' | 'stretch'>
left?: Responsive<DimensionValue>
margin?: Responsive<DimensionValue>
marginBottom?: Responsive<DimensionValue>
marginEnd?: Responsive<DimensionValue>
marginStart?: Responsive<DimensionValue>
marginTop?: Responsive<DimensionValue>
marginX?: Responsive<DimensionValue>
marginY?: Responsive<DimensionValue>
maxHeight?: Responsive<DimensionValue>
maxWidth?: Responsive<DimensionValue>
minHeight?: Responsive<DimensionValue>
minWidth?: Responsive<DimensionValue>
onDrop?: (DropEvent) => void
- onDropActivate?: (DropActivateEvent) => void
onDropEnter?: (DropEnterEvent) => void
onDropExit?: (DropExitEvent) => void
onDropMove?: (DropMoveEvent) => void
order?: Responsive<number>
replaceMessage?: string
right?: Responsive<DimensionValue>
slot?: string | null
start?: Responsive<DimensionValue>
top?: Responsive<DimensionValue>
width?: Responsive<DimensionValue>
zIndex?: Responsive<number>
} /@react-spectrum/dropzone:SpectrumDropZoneProps SpectrumDropZoneProps {
UNSAFE_className?: string
UNSAFE_style?: CSSProperties
alignSelf?: Responsive<'auto' | 'normal' | 'start' | 'end' | 'center' | 'flex-start' | 'flex-end' | 'self-start' | 'self-end' | 'stretch'>
aria-describedby?: string
aria-details?: string
aria-label?: string
aria-labelledby?: string
bottom?: Responsive<DimensionValue>
children: ReactNode
end?: Responsive<DimensionValue>
flex?: Responsive<string | number | boolean>
flexBasis?: Responsive<number | string>
flexGrow?: Responsive<number>
flexShrink?: Responsive<number>
getDropOperation?: (DragTypes, Array<DropOperation>) => DropOperation
gridArea?: Responsive<string>
gridColumn?: Responsive<string>
gridColumnEnd?: Responsive<string>
gridColumnStart?: Responsive<string>
gridRow?: Responsive<string>
gridRowEnd?: Responsive<string>
gridRowStart?: Responsive<string>
height?: Responsive<DimensionValue>
id?: string
isFilled?: boolean
isHidden?: Responsive<boolean>
justifySelf?: Responsive<'auto' | 'normal' | 'start' | 'end' | 'flex-start' | 'flex-end' | 'self-start' | 'self-end' | 'center' | 'left' | 'right' | 'stretch'>
left?: Responsive<DimensionValue>
margin?: Responsive<DimensionValue>
marginBottom?: Responsive<DimensionValue>
marginEnd?: Responsive<DimensionValue>
marginStart?: Responsive<DimensionValue>
marginTop?: Responsive<DimensionValue>
marginX?: Responsive<DimensionValue>
marginY?: Responsive<DimensionValue>
maxHeight?: Responsive<DimensionValue>
maxWidth?: Responsive<DimensionValue>
minHeight?: Responsive<DimensionValue>
minWidth?: Responsive<DimensionValue>
onDrop?: (DropEvent) => void
- onDropActivate?: (DropActivateEvent) => void
onDropEnter?: (DropEnterEvent) => void
onDropExit?: (DropExitEvent) => void
onDropMove?: (DropMoveEvent) => void
order?: Responsive<number>
replaceMessage?: string
right?: Responsive<DimensionValue>
slot?: string | null
start?: Responsive<DimensionValue>
top?: Responsive<DimensionValue>
width?: Responsive<DimensionValue>
zIndex?: Responsive<number>
} @react-spectrum/s2/@react-spectrum/s2:DropZone DropZone {
UNSAFE_className?: UnsafeClassName
UNSAFE_style?: CSSProperties
aria-describedby?: string
aria-details?: string
aria-label?: string
aria-labelledby?: string
children: ReactNode
getDropOperation?: (DragTypes, Array<DropOperation>) => DropOperation
id?: string
isFilled?: boolean
onDrop?: (DropEvent) => void
- onDropActivate?: (DropActivateEvent) => void
onDropEnter?: (DropEnterEvent) => void
onDropExit?: (DropExitEvent) => void
onDropMove?: (DropMoveEvent) => void
replaceMessage?: string
slot?: string | null
styles?: StylesPropWithHeight
} /@react-spectrum/s2:TreeView TreeView {
UNSAFE_className?: UnsafeClassName
UNSAFE_style?: CSSProperties
aria-describedby?: string
aria-details?: string
aria-label?: string
aria-labelledby?: string
autoFocus?: boolean | FocusStrategy
children?: ReactNode | (T) => ReactNode
defaultExpandedKeys?: Iterable<Key>
defaultSelectedKeys?: 'all' | Iterable<Key>
dependencies?: ReadonlyArray<any>
disabledBehavior?: DisabledBehavior = 'all'
disabledKeys?: Iterable<Key>
disallowEmptySelection?: boolean
escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
expandedKeys?: Iterable<Key>
id?: string
isDetached?: boolean
isEmphasized?: boolean
items?: Iterable<T>
- keyboardDelegate?: KeyboardDelegate
onAction?: (Key) => void
onExpandedChange?: (Set<Key>) => any
onSelectionChange?: (Selection) => void
renderEmptyState?: (TreeEmptyStateRenderProps) => ReactNode
selectionMode?: SelectionMode
shouldSelectOnPressUp?: boolean
slot?: string | null
styles?: StylesPropWithHeight
} /@react-spectrum/s2:DropZoneProps DropZoneProps {
UNSAFE_className?: UnsafeClassName
UNSAFE_style?: CSSProperties
aria-describedby?: string
aria-details?: string
aria-label?: string
aria-labelledby?: string
children: ReactNode
getDropOperation?: (DragTypes, Array<DropOperation>) => DropOperation
id?: string
isFilled?: boolean
onDrop?: (DropEvent) => void
- onDropActivate?: (DropActivateEvent) => void
onDropEnter?: (DropEnterEvent) => void
onDropExit?: (DropExitEvent) => void
onDropMove?: (DropMoveEvent) => void
replaceMessage?: string
slot?: string | null
styles?: StylesPropWithHeight
} /@react-spectrum/s2:TreeViewProps TreeViewProps {
UNSAFE_className?: UnsafeClassName
UNSAFE_style?: CSSProperties
aria-describedby?: string
aria-details?: string
aria-label?: string
aria-labelledby?: string
autoFocus?: boolean | FocusStrategy
children?: ReactNode | (T) => ReactNode
defaultExpandedKeys?: Iterable<Key>
defaultSelectedKeys?: 'all' | Iterable<Key>
dependencies?: ReadonlyArray<any>
disabledBehavior?: DisabledBehavior = 'all'
disabledKeys?: Iterable<Key>
disallowEmptySelection?: boolean
escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
expandedKeys?: Iterable<Key>
id?: string
isDetached?: boolean
isEmphasized?: boolean
items?: Iterable<T>
- keyboardDelegate?: KeyboardDelegate
onAction?: (Key) => void
onExpandedChange?: (Set<Key>) => any
onSelectionChange?: (Selection) => void
renderEmptyState?: (TreeEmptyStateRenderProps) => ReactNode
selectionMode?: SelectionMode
shouldSelectOnPressUp?: boolean
slot?: string | null
styles?: StylesPropWithHeight
} @react-stately/data/@react-stately/data:TreeData TreeData <T extends {}> {
append: (Key | null, Array<{}>) => void
getItem: (Key) => TreeNode<{}> | undefined
insert: (Key | null, number, Array<{}>) => void
insertAfter: (Key, Array<{}>) => void
insertBefore: (Key, Array<{}>) => void
items: Array<TreeNode<{}>>
move: (Key, Key | null, number) => void
- moveAfter: (Key, Iterable<Key>) => void
- moveBefore: (Key, Iterable<Key>) => void
prepend: (Key | null, Array<{}>) => void
remove: (Array<Key>) => void
removeSelectedItems: () => void
selectedKeys: Set<Key>
update: (Key, {}) => void
} |
Closes
✅ Pull Request Checklist:
📝 Test Instructions:
🧢 Your Project: