Skip to content

Commit 0105bef

Browse files
userquindanielroe
andauthored
feat: add force left/right rules to RTL preset (#1179)
Co-authored-by: Daniel Roe <daniel@roe.dev>
1 parent 281877c commit 0105bef

File tree

2 files changed

+113
-16
lines changed

2 files changed

+113
-16
lines changed

test/unit/uno-preset-rtl.spec.ts

Lines changed: 53 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -52,22 +52,60 @@ describe('uno-preset-rtl', () => {
5252
const warnings = warnSpy.mock.calls.flat()
5353
expect(warnings).toMatchInlineSnapshot(`
5454
[
55-
"[RTL] Avoid using 'left-0', use 'inset-is-0' instead.",
56-
"[RTL] Avoid using 'right-0', use 'inset-ie-0' instead.",
57-
"[RTL] Avoid using 'pl-1', use 'ps-1' instead.",
58-
"[RTL] Avoid using 'ml-1', use 'ms-1' instead.",
59-
"[RTL] Avoid using 'pr-1', use 'pe-1' instead.",
60-
"[RTL] Avoid using 'mr-1', use 'me-1' instead.",
61-
"[RTL] Avoid using 'border-l', use 'border-is' instead.",
62-
"[RTL] Avoid using 'border-r', use 'border-ie' instead.",
63-
"[RTL] Avoid using 'rounded-l', use 'rounded-is' instead.",
64-
"[RTL] Avoid using 'rounded-r', use 'rounded-ie' instead.",
65-
"[RTL] Avoid using 'position-left-4', use 'inset-is-4' instead.",
66-
"[RTL] Avoid using 'sm:pl-2', use 'sm:ps-2' instead.",
67-
"[RTL] Avoid using 'text-left', use 'text-start' instead.",
68-
"[RTL] Avoid using 'text-right', use 'text-end' instead.",
69-
"[RTL] Avoid using 'hover:text-right', use 'hover:text-end' instead.",
55+
"[RTL] Avoid using 'left-0', use 'inset-is-0' instead, or 'force-left-0' to keep physical direction.",
56+
"[RTL] Avoid using 'right-0', use 'inset-ie-0' instead, or 'force-right-0' to keep physical direction.",
57+
"[RTL] Avoid using 'pl-1', use 'ps-1' instead, or 'force-pl-1' to keep physical direction.",
58+
"[RTL] Avoid using 'ml-1', use 'ms-1' instead, or 'force-ml-1' to keep physical direction.",
59+
"[RTL] Avoid using 'pr-1', use 'pe-1' instead, or 'force-pr-1' to keep physical direction.",
60+
"[RTL] Avoid using 'mr-1', use 'me-1' instead, or 'force-mr-1' to keep physical direction.",
61+
"[RTL] Avoid using 'border-l', use 'border-is' instead, or 'force-border-l' to keep physical direction.",
62+
"[RTL] Avoid using 'border-r', use 'border-ie' instead, or 'force-border-r' to keep physical direction.",
63+
"[RTL] Avoid using 'rounded-l', use 'rounded-is' instead, or 'force-rounded-l' to keep physical direction.",
64+
"[RTL] Avoid using 'rounded-r', use 'rounded-ie' instead, or 'force-rounded-r' to keep physical direction.",
65+
"[RTL] Avoid using 'position-left-4', use 'inset-is-4' instead, or 'force-position-left-4' to keep physical direction.",
66+
"[RTL] Avoid using 'sm:pl-2', use 'sm:ps-2' instead, or 'force-sm:pl-2' to keep physical direction.",
67+
"[RTL] Avoid using 'text-left', use 'text-start' instead, or 'force-text-left' to keep physical direction.",
68+
"[RTL] Avoid using 'text-right', use 'text-end' instead, or 'force-text-right' to keep physical direction.",
69+
"[RTL] Avoid using 'hover:text-right', use 'hover:text-end' instead, or 'force-hover:text-right' to keep physical direction.",
7070
]
7171
`)
7272
})
73+
74+
it('force-* (non rtl rules) use original css styles correctly', async () => {
75+
const uno = await createGenerator({
76+
presets: [presetWind4(), presetRtl()],
77+
})
78+
79+
const { css } = await uno.generate(
80+
'force-left-0 force-right-0 force-pl-1 force-ml-1 force-pr-1 force-mr-1 force-text-left force-text-right force-border-l force-border-r force-rounded-l force-rounded-r sm:force-pl-2 hover:force-text-right force-position-left-4',
81+
)
82+
83+
expect(css).toMatchInlineSnapshot(`
84+
"/* layer: theme */
85+
:root, :host { --font-sans: ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; --font-mono: ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; --default-font-family: var(--font-sans); --default-monoFont-family: var(--font-mono); }
86+
/* layer: base */
87+
*, ::after, ::before, ::backdrop, ::file-selector-button { box-sizing: border-box; margin: 0; padding: 0; border: 0 solid; } html, :host { line-height: 1.5; -webkit-text-size-adjust: 100%; tab-size: 4; font-family: var( --default-font-family, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji' ); font-feature-settings: var(--default-font-featureSettings, normal); font-variation-settings: var(--default-font-variationSettings, normal); -webkit-tap-highlight-color: transparent; } hr { height: 0; color: inherit; border-top-width: 1px; } abbr:where([title]) { -webkit-text-decoration: underline dotted; text-decoration: underline dotted; } h1, h2, h3, h4, h5, h6 { font-size: inherit; font-weight: inherit; } a { color: inherit; -webkit-text-decoration: inherit; text-decoration: inherit; } b, strong { font-weight: bolder; } code, kbd, samp, pre { font-family: var( --default-monoFont-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace ); font-feature-settings: var(--default-monoFont-featureSettings, normal); font-variation-settings: var(--default-monoFont-variationSettings, normal); font-size: 1em; } small { font-size: 80%; } sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } sub { bottom: -0.25em; } sup { top: -0.5em; } table { text-indent: 0; border-color: inherit; border-collapse: collapse; } :-moz-focusring { outline: auto; } progress { vertical-align: baseline; } summary { display: list-item; } ol, ul, menu { list-style: none; } img, svg, video, canvas, audio, iframe, embed, object { display: block; vertical-align: middle; } img, video { max-width: 100%; height: auto; } button, input, select, optgroup, textarea, ::file-selector-button { font: inherit; font-feature-settings: inherit; font-variation-settings: inherit; letter-spacing: inherit; color: inherit; border-radius: 0; background-color: transparent; opacity: 1; } :where(select:is([multiple], [size])) optgroup { font-weight: bolder; } :where(select:is([multiple], [size])) optgroup option { padding-inline-start: 20px; } ::file-selector-button { margin-inline-end: 4px; } ::placeholder { opacity: 1; } @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) { ::placeholder { color: color-mix(in oklab, currentcolor 50%, transparent); } } textarea { resize: vertical; } ::-webkit-search-decoration { -webkit-appearance: none; } ::-webkit-date-and-time-value { min-height: 1lh; text-align: inherit; } ::-webkit-datetime-edit { display: inline-flex; } ::-webkit-datetime-edit-fields-wrapper { padding: 0; } ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field { padding-block: 0; } ::-webkit-calendar-picker-indicator { line-height: 1; } :-moz-ui-invalid { box-shadow: none; } button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-button { appearance: button; } ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { height: auto; } [hidden]:where(:not([hidden~='until-found'])) { display: none !important; }
88+
/* layer: default */
89+
.force-pl-1{padding-left:0.25rem;}
90+
.force-pr-1{padding-right:0.25rem;}
91+
.force-ml-1{margin-left:0.25rem;}
92+
.force-mr-1{margin-right:0.25rem;}
93+
.force-left-0{left:0;}
94+
.force-position-left-4{left:1rem;}
95+
.force-right-0{right:0;}
96+
.force-text-left{text-align:left;}
97+
.force-text-right{text-align:right;}
98+
.hover\\:force-text-right:hover{text-align:right;}
99+
.force-rounded-l{border-top-left-radius:0.25rem;border-bottom-left-radius:0.25rem;}
100+
.force-rounded-r{border-top-right-radius:0.25rem;border-bottom-right-radius:0.25rem;}
101+
.force-border-l{border-left-width:1px;}
102+
.force-border-r{border-right-width:1px;}
103+
@media (min-width: 40rem){
104+
.sm\\:force-pl-2{padding-left:0.5rem;}
105+
}"
106+
`)
107+
108+
const warnings = warnSpy.mock.calls.flat()
109+
expect(warnings).toMatchInlineSnapshot(`[]`)
110+
})
73111
})

uno-preset-rtl.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export function resetRtlWarnings() {
2020
}
2121

2222
function reportWarning(match: string, suggestedClass: string, checker?: CollectorChecker) {
23-
const message = `${checker ? 'a' : 'A'}void using '${match}', use '${suggestedClass}' instead.`
23+
const message = `${checker ? 'a' : 'A'}void using '${match}', use '${suggestedClass}' instead, or 'force-${match}' to keep physical direction.`
2424
if (checker) {
2525
checker(message, match)
2626
} else {
@@ -90,6 +90,20 @@ function handlerBorderSize([, a = '', b = '1']: string[]): CSSEntries | undefine
9090
if (directions && v != null) return directions.map(i => [`border${i}-width`, v])
9191
}
9292

93+
function handlerForceDirectionSize(
94+
propertyPrefix: string,
95+
[, direction, size]: string[],
96+
{ theme }: RuleContext<any>,
97+
): CSSEntries | undefined {
98+
const v =
99+
theme.spacing?.[size || 'DEFAULT'] ?? h.bracket?.cssvar?.global?.auto?.fraction?.rem?.(size!)
100+
const directions = directionMap[direction!]
101+
102+
if (v != null && directions) {
103+
return directions.map(i => [`${propertyPrefix}${i}`, v])
104+
}
105+
}
106+
93107
/**
94108
* CSS RTL support to detect, replace and warn wrong left/right usages.
95109
*/
@@ -101,6 +115,51 @@ export function presetRtl(checker?: CollectorChecker): Preset {
101115
['text-right', 'text-end x-rtl-end'],
102116
],
103117
rules: [
118+
// Force physical directions (bypass RTL logic)
119+
[
120+
/^force-p([rl])-(.+)?$/,
121+
(match, context) => handlerForceDirectionSize('padding', match, context),
122+
{ autocomplete: 'force-p(l|r)-<num>' },
123+
],
124+
[
125+
/^force-m([rl])-(.+)?$/,
126+
(match, context) => handlerForceDirectionSize('margin', match, context),
127+
{ autocomplete: 'force-m(l|r)-<num>' },
128+
],
129+
[
130+
/^force-(?:position-|pos-)?(left|right)-(.+)$/,
131+
([_, direction, size], context) => {
132+
// Map 'left'/'right' to 'l'/'r' for directionMap lookup if needed,
133+
// but directionMap has 'left'/'right' keys? No, it has 'l'/'r'.
134+
// Wait, directionMap keys are 'l', 'r'.
135+
// But inset usually uses 'left', 'right' properties directly.
136+
// Let's use a custom handler for inset to be safe.
137+
const v =
138+
(context.theme as unknown as any).spacing?.[size || 'DEFAULT'] ??
139+
h.bracket?.cssvar?.global?.auto?.fraction?.rem?.(size!)
140+
if (v != null) {
141+
return [[direction === 'left' ? 'left' : 'right', v]]
142+
}
143+
},
144+
{ autocomplete: 'force-(left|right)-<num>' },
145+
],
146+
[
147+
/^force-text-(left|right)$/,
148+
([, direction]) => ({ 'text-align': direction }),
149+
{ autocomplete: 'force-text-(left|right)' },
150+
],
151+
[
152+
/^force-rounded-([rl])(?:-(.+))?$/,
153+
([, direction, size], context) =>
154+
handlerRounded(['', direction!, size ?? 'DEFAULT'], context),
155+
{ autocomplete: 'force-rounded-(l|r)-<num>' },
156+
],
157+
[
158+
/^force-border-([rl])(?:-(.+))?$/,
159+
([, direction, size]) => handlerBorderSize(['', direction!, size || '1']),
160+
{ autocomplete: 'force-border-(l|r)-<num>' },
161+
],
162+
104163
// RTL overrides
105164
// We need to move the dash out of the capturing group to avoid capturing it in the direction
106165
[

0 commit comments

Comments
 (0)