Refactor RTL support in contexts and NavBar; update CSS for improved direction handling
This commit is contained in:
@@ -8,8 +8,10 @@ import ThemeContext from './ThemeContext'
|
||||
const Contexts = ({ children }) => {
|
||||
const contexts = [
|
||||
AlertsProvider,
|
||||
ThemeContext,
|
||||
// Above ThemeContext: the theme reads the active language to pick the
|
||||
// text direction and the matching emotion cache.
|
||||
LocalizationProvider,
|
||||
ThemeContext,
|
||||
QueryContext,
|
||||
NotificationProvider,
|
||||
RouterContext,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import useStickyState from '@/hooks/useStickyState'
|
||||
import moment from 'moment'
|
||||
import { createContext, useContext, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import useStickyState from '@/hooks/useStickyState'
|
||||
|
||||
const LocalizationContext = createContext()
|
||||
|
||||
export const DATE_FORMATS = {
|
||||
@@ -21,6 +22,7 @@ export const TIME_FORMATS = {
|
||||
export const RTL_LANGUAGES = ['ar', 'he', 'fa', 'ur']
|
||||
|
||||
export const AVAILABLE_LANGUAGES = [
|
||||
{ code: 'ar', name: 'Arabic', nativeName: 'العربية' },
|
||||
{ code: 'de', name: 'German', nativeName: 'Deutsch' },
|
||||
{ code: 'en', name: 'English', nativeName: 'English' },
|
||||
{ code: 'es', name: 'Spanish', nativeName: 'Español' },
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import createCache from '@emotion/cache'
|
||||
import { CacheProvider } from '@emotion/react'
|
||||
import { CssBaseline } from '@mui/joy'
|
||||
import { CssVarsProvider, extendTheme } from '@mui/joy/styles'
|
||||
import PropType from 'prop-types'
|
||||
import { useMemo } from 'react'
|
||||
import { prefixer } from 'stylis'
|
||||
import rtlPlugin from 'stylis-plugin-rtl'
|
||||
|
||||
import { COLORS, THEME_BACKGROUND } from '@/constants/theme'
|
||||
|
||||
import { useLocalization } from './LocalizationContext'
|
||||
|
||||
const primaryColor = 'cyan'
|
||||
const shades = [
|
||||
'50',
|
||||
@@ -24,7 +31,7 @@ const primaryPalette = getPalette(primaryColor)
|
||||
const CONTROL_RADIUS = '12px'
|
||||
const ICON_BUTTON_RADIUS = '10px'
|
||||
|
||||
const theme = extendTheme({
|
||||
const themeConfig = {
|
||||
radius: {
|
||||
xs: '6px',
|
||||
sm: '8px',
|
||||
@@ -140,20 +147,83 @@ const theme = extendTheme({
|
||||
},
|
||||
JoyButtonGroup: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
root: ({ ownerState, theme }) => ({
|
||||
'--ButtonGroup-radius': CONTROL_RADIUS,
|
||||
},
|
||||
...(theme.direction === 'rtl' && buttonGroupRtlGeometry(ownerState)),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const ThemeContext = ({ children }) => (
|
||||
<CssVarsProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
{children}
|
||||
</CssVarsProvider>
|
||||
)
|
||||
// Joy packs a ButtonGroup's direction-sensitive geometry into CSS custom
|
||||
// properties: --Button-radius as a four-corner shorthand, --Button-margin as the
|
||||
// negative overlap that collapses the seam between siblings. stylis-plugin-rtl
|
||||
// mirrors real properties only — it cannot know what a custom property will end
|
||||
// up feeding — so those two survive into RTL still in LTR order, while the
|
||||
// separator borders declared alongside them *do* flip. The result is rounded
|
||||
// corners on the wrong ends and the overlap pulling the wrong way. Re-mirror
|
||||
// them here. Vertical groups have no horizontal geometry, so they are left be.
|
||||
const GROUP_RADIUS = 'var(--ButtonGroup-radius)'
|
||||
const CHILD_RADIUS = 'var(--unstable_childRadius)'
|
||||
const OVERLAP = 'calc(var(--ButtonGroup-separatorSize) * -1)'
|
||||
|
||||
// Corners read clockwise from top-left. [data-first-child] is the DOM-first
|
||||
// button, which in RTL renders at the *right* end of the group, so it is the one
|
||||
// that needs its right corners rounded — and vice versa for [data-last-child].
|
||||
const ROUNDED_RIGHT = `${CHILD_RADIUS} ${GROUP_RADIUS} ${GROUP_RADIUS} ${CHILD_RADIUS}`
|
||||
const ROUNDED_LEFT = `${GROUP_RADIUS} ${CHILD_RADIUS} ${CHILD_RADIUS} ${GROUP_RADIUS}`
|
||||
|
||||
const buttonGroupRtlGeometry = ownerState => {
|
||||
if (ownerState.orientation === 'vertical') return {}
|
||||
return {
|
||||
'& > [data-first-child]': {
|
||||
'--Button-radius': ROUNDED_RIGHT,
|
||||
'--IconButton-radius': ROUNDED_RIGHT,
|
||||
},
|
||||
'& > [data-last-child]': {
|
||||
'--Button-radius': ROUNDED_LEFT,
|
||||
'--IconButton-radius': ROUNDED_LEFT,
|
||||
},
|
||||
// Each non-first button overlaps the sibling to its right in RTL.
|
||||
'& > :not([data-first-child]):not(:only-child)': {
|
||||
'--Button-margin': `0 ${OVERLAP} 0 0`,
|
||||
'--IconButton-margin': `0 ${OVERLAP} 0 0`,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// One theme and one emotion cache per direction, built once and reused. The RTL
|
||||
// cache runs every rule through stylis-plugin-rtl, which mirrors the physical
|
||||
// properties (margin-left, left, text-align, translateX, …) that `sx` emits, so
|
||||
// components written for LTR lay out correctly without per-component overrides.
|
||||
const byDirection = {
|
||||
ltr: {
|
||||
theme: extendTheme({ ...themeConfig, direction: 'ltr' }),
|
||||
cache: createCache({ key: 'dt', stylisPlugins: [prefixer] }),
|
||||
},
|
||||
rtl: {
|
||||
theme: extendTheme({ ...themeConfig, direction: 'rtl' }),
|
||||
cache: createCache({ key: 'dt-rtl', stylisPlugins: [prefixer, rtlPlugin] }),
|
||||
},
|
||||
}
|
||||
|
||||
const ThemeContext = ({ children }) => {
|
||||
const { isRTL } = useLocalization()
|
||||
const { cache, theme } = useMemo(
|
||||
() => byDirection[isRTL ? 'rtl' : 'ltr'],
|
||||
[isRTL],
|
||||
)
|
||||
|
||||
return (
|
||||
<CacheProvider value={cache}>
|
||||
<CssVarsProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
{children}
|
||||
</CssVarsProvider>
|
||||
</CacheProvider>
|
||||
)
|
||||
}
|
||||
|
||||
ThemeContext.propTypes = {
|
||||
children: PropType.node,
|
||||
|
||||
@@ -55,69 +55,17 @@ html {
|
||||
will-change: auto;
|
||||
}
|
||||
|
||||
/* RTL Support */
|
||||
[dir='rtl'] {
|
||||
direction: rtl;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
[dir='rtl'] .rtl-mirror {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
/* Handle margins and paddings for RTL */
|
||||
[dir='rtl'] .ml-auto {
|
||||
margin-left: 0;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
[dir='rtl'] .mr-auto {
|
||||
margin-right: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Flip icons and arrows in RTL */
|
||||
/*
|
||||
* RTL Support
|
||||
*
|
||||
* Component styling is mirrored by stylis-plugin-rtl (see ThemeContext), which
|
||||
* flips the physical properties emitted by `sx`. Do not add per-component
|
||||
* `[dir='rtl']` overrides here — they fight the plugin and double-flip.
|
||||
*
|
||||
* Only opt-in utilities belong in this block:
|
||||
* .rtl-flip — mirror an icon that encodes a direction (arrows, chevrons).
|
||||
* Do NOT use it on symbols that read the same either way.
|
||||
*/
|
||||
[dir='rtl'] .rtl-flip {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
/* Ensure proper text alignment in RTL */
|
||||
[dir='rtl'] input,
|
||||
[dir='rtl'] textarea {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Handle border radius for RTL */
|
||||
[dir='rtl'] .rounded-l-none {
|
||||
border-radius: 0 0.375rem 0.375rem 0;
|
||||
}
|
||||
|
||||
[dir='rtl'] .rounded-r-none {
|
||||
border-radius: 0.375rem 0 0 0.375rem;
|
||||
}
|
||||
|
||||
/* Fix flex alignment for RTL */
|
||||
[dir='rtl'] .flex {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
/* Ensure cards and containers align properly in RTL */
|
||||
[dir='rtl'] .MuiCard-root,
|
||||
[dir='rtl'] .MuiBox-root,
|
||||
[dir='rtl'] .MuiStack-root {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Fix list item alignment in RTL */
|
||||
[dir='rtl'] .MuiListItem-root,
|
||||
[dir='rtl'] .MuiListItemButton-root {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
/* Fix gap alignment in RTL */
|
||||
[dir='rtl'] .gap-1,
|
||||
[dir='rtl'] .gap-2,
|
||||
[dir='rtl'] .gap-3,
|
||||
[dir='rtl'] .gap-4 {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { version } from '../../../package.json'
|
||||
import UserProfileAvatar from '../../components/UserProfileAvatar'
|
||||
import Z_INDEX from '../../constants/zIndex'
|
||||
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||
import { useResource } from '../../queries/ResourceQueries'
|
||||
import { useGlobalSearch } from '../../search/GlobalSearchContext'
|
||||
import { apiClient } from '../../utils/ApiClient'
|
||||
@@ -41,7 +40,6 @@ import SyncStatusIndicator from './SyncStatusIndicator'
|
||||
const publicPages = ['/landing', '/privacy', '/terms']
|
||||
const NavBar = () => {
|
||||
const { t } = useTranslation('common')
|
||||
const { isRTL } = useLocalization()
|
||||
const { data: resource } = useResource()
|
||||
const { openSearch } = useGlobalSearch()
|
||||
|
||||
@@ -224,14 +222,19 @@ const NavBar = () => {
|
||||
<Drawer
|
||||
open={drawerOpen}
|
||||
onClose={closeDrawer}
|
||||
anchor={isRTL ? 'right' : 'left'}
|
||||
// Always 'left'. Joy bakes the anchor into emotion CSS (`left: 0` plus a
|
||||
// translateX for the slide), so stylis-plugin-rtl already mirrors it to
|
||||
// the right edge under RTL. Branching on isRTL here would flip it twice
|
||||
// and land the drawer back on the left, half off-screen.
|
||||
anchor='left'
|
||||
size='sm'
|
||||
onClick={closeDrawer}
|
||||
sx={{
|
||||
'& .MuiDrawer-content': {
|
||||
position: 'fixed',
|
||||
// pt: 'calc(var(--safe-area-inset-top, 0px))',
|
||||
...(isRTL ? { right: 0 } : { left: 0 }),
|
||||
// Physical on purpose, so it is mirrored in step with the anchor.
|
||||
left: 0,
|
||||
// pb: 'calc(var(--safe-area-inset-bottom, 0px))',
|
||||
// height:
|
||||
// 'calc(100vh - var(--safe-area-inset-top, 0px) - var(--safe-area-inset-bottom, 0px))',
|
||||
|
||||
Reference in New Issue
Block a user