Skip to content

Commit aa42701

Browse files
committed
Add noPhysicalCssProperties eslint rule
1 parent 22128d9 commit aa42701

13 files changed

Lines changed: 105 additions & 0 deletions

File tree

eslint.config.mjs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,83 @@ const noUnstableMethods = {
171171
},
172172
};
173173

174+
const noPhysicalCssProperties = {
175+
meta: {
176+
type: 'problem',
177+
docs: {
178+
description: 'Enforce use of CSS logical properties instead of physical properties for RTL support',
179+
recommended: false,
180+
},
181+
messages: {
182+
useLogicalProperty:
183+
'Use logical CSS property "{{logical}}" instead of physical property "{{physical}}" for RTL support.',
184+
useLogicalTextAlign:
185+
'Use logical textAlign value "{{logical}}" instead of physical value "{{physical}}" for RTL support.',
186+
},
187+
schema: [],
188+
},
189+
create(context) {
190+
// Mapping of physical properties to logical equivalents
191+
const propertyMap = {
192+
left: 'insetInlineStart',
193+
right: 'insetInlineEnd',
194+
marginLeft: 'marginInlineStart',
195+
marginRight: 'marginInlineEnd',
196+
paddingLeft: 'paddingInlineStart',
197+
paddingRight: 'paddingInlineEnd',
198+
borderLeft: 'borderInlineStart',
199+
borderRight: 'borderInlineEnd',
200+
borderLeftWidth: 'borderInlineStartWidth',
201+
borderRightWidth: 'borderInlineEndWidth',
202+
borderLeftStyle: 'borderInlineStartStyle',
203+
borderRightStyle: 'borderInlineEndStyle',
204+
borderLeftColor: 'borderInlineStartColor',
205+
borderRightColor: 'borderInlineEndColor',
206+
borderTopLeftRadius: 'borderStartStartRadius',
207+
borderTopRightRadius: 'borderStartEndRadius',
208+
borderBottomLeftRadius: 'borderEndStartRadius',
209+
borderBottomRightRadius: 'borderEndEndRadius',
210+
};
211+
212+
const checkProperty = (key, value) => {
213+
const keyName = key.type === 'Identifier' ? key.name : key.value;
214+
215+
// Check for physical property names
216+
if (propertyMap[keyName]) {
217+
context.report({
218+
node: key,
219+
messageId: 'useLogicalProperty',
220+
data: {
221+
physical: keyName,
222+
logical: propertyMap[keyName],
223+
},
224+
});
225+
}
226+
227+
// Check for textAlign with physical values
228+
if (keyName === 'textAlign' && value) {
229+
if (value.type === 'Literal' && (value.value === 'left' || value.value === 'right')) {
230+
const logicalValue = value.value === 'left' ? 'start' : 'end';
231+
context.report({
232+
node: value,
233+
messageId: 'useLogicalTextAlign',
234+
data: {
235+
physical: value.value,
236+
logical: logicalValue,
237+
},
238+
});
239+
}
240+
}
241+
};
242+
243+
return {
244+
Property(node) {
245+
checkProperty(node.key, node.value);
246+
},
247+
};
248+
},
249+
};
250+
174251
export default tseslint.config([
175252
{
176253
name: 'repo/ignores',
@@ -248,6 +325,7 @@ export default tseslint.config([
248325
rules: {
249326
'no-global-object': noGlobalObject,
250327
'no-unstable-methods': noUnstableMethods,
328+
'no-physical-css-properties': noPhysicalCssProperties,
251329
},
252330
},
253331
'simple-import-sort': pluginSimpleImportSort,
@@ -458,6 +536,13 @@ export default tseslint.config([
458536
'custom-rules/no-unstable-methods': 'error',
459537
},
460538
},
539+
{
540+
name: 'packages/ui',
541+
files: ['packages/ui/src/**/*'],
542+
rules: {
543+
'custom-rules/no-physical-css-properties': 'error',
544+
},
545+
},
461546
{
462547
name: 'packages - vitest',
463548
files: ['packages/*/src/**/*.test.{ts,tsx}'],

packages/ui/src/common/InfiniteListSpinner.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const InfiniteListSpinner = forwardRef<HTMLDivElement>((_, ref) => {
1616
sx={{
1717
margin: 'auto',
1818
position: 'absolute',
19+
// eslint-disable-next-line custom-rules/no-physical-css-properties -- Centering with transform: translateX(-50%)
1920
left: '50%',
2021
top: '50%',
2122
transform: 'translateY(-50%) translateX(-50%)',

packages/ui/src/common/PrintableComponent.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export const PrintableComponent = (props: UsePrintableReturn['printableProps'] &
2424
return (
2525
<div
2626
ref={ref}
27+
// eslint-disable-next-line custom-rules/no-physical-css-properties -- Off-screen hide for print functionality
2728
style={{ position: 'fixed', left: '-9999px', top: 0, display: 'none' }}
2829
>
2930
{children}

packages/ui/src/common/organizations/OrganizationPreview.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export const OrganizationPreviewSpinner = forwardRef<HTMLDivElement>((_, ref) =>
9696
sx={{
9797
margin: 'auto',
9898
position: 'absolute',
99+
// eslint-disable-next-line custom-rules/no-physical-css-properties -- Centering with transform: translateX(-50%)
99100
left: '50%',
100101
top: '50%',
101102
transform: 'translateY(-50%) translateX(-50%)',

packages/ui/src/components/ImpersonationFab/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ const ImpersonationFabInternal = () => {
199199
position: 'fixed',
200200
overflow: 'hidden',
201201
top: `var(${topProperty}, ${defaultTop}px)`,
202+
// eslint-disable-next-line custom-rules/no-physical-css-properties -- Complex JS-based positioning via CSS custom properties
202203
right: `var(${rightProperty}, ${defaultRight}px)`,
203204
zIndex: t.zIndices.$fab,
204205
boxShadow: t.shadows.$fabShadow,

packages/ui/src/components/OrganizationProfile/DomainList.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ export const DomainList = withProtect(
168168
display: 'flex',
169169
margin: 'auto',
170170
position: 'absolute',
171+
// eslint-disable-next-line custom-rules/no-physical-css-properties -- Centering with transform: translateX(-50%)
171172
left: '50%',
172173
top: '50%',
173174
transform: 'translateY(-50%) translateX(-50%)',

packages/ui/src/components/devPrompts/KeylessPrompt/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,7 @@ const KeylessPromptInternal = (_props: KeylessPromptProps) => {
520520
href={`#${buttonIdentifier}`}
521521
css={css`
522522
position: fixed;
523+
/* eslint-disable-next-line custom-rules/no-physical-css-properties -- Skip link - visually hidden pattern */
523524
left: -999px;
524525
top: 1rem;
525526
z-index: 999999;
@@ -531,6 +532,7 @@ const KeylessPromptInternal = (_props: KeylessPromptProps) => {
531532
text-decoration: underline;
532533
533534
&:focus {
535+
/* eslint-disable-next-line custom-rules/no-physical-css-properties -- Skip link - visually hidden pattern */
534536
left: 1rem;
535537
outline: 2px solid;
536538
outline-offset: 2px;

packages/ui/src/elements/Avatar.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ export const Avatar = (props: AvatarProps) => {
160160
background: t.colors.$colorShimmer,
161161
position: 'absolute',
162162
top: 0,
163+
// eslint-disable-next-line custom-rules/no-physical-css-properties -- Shimmer effect uses translateX animation
163164
left: 0,
164165
width: '25%',
165166
height: '100%',
@@ -171,6 +172,7 @@ export const Avatar = (props: AvatarProps) => {
171172
content: "''",
172173
position: 'absolute',
173174
top: 0,
175+
// eslint-disable-next-line custom-rules/no-physical-css-properties -- Shimmer effect uses translateX animation
174176
left: 0,
175177
width: '400%',
176178
height: '100%',

packages/ui/src/elements/Drawer.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,7 +535,9 @@ const Confirmation = React.forwardRef<HTMLDivElement, ConfirmationProps>(
535535
willChange: 'transform',
536536
position: 'absolute',
537537
bottom: 0,
538+
// eslint-disable-next-line custom-rules/no-physical-css-properties -- Full-width overlay positioning
538539
left: 0,
540+
// eslint-disable-next-line custom-rules/no-physical-css-properties -- Full-width overlay positioning
539541
right: 0,
540542
background: common.mergedColorsBackground(
541543
colors.setAlpha(t.colors.$colorBackground, 1),

packages/ui/src/elements/Modal.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ export const Modal = withFloatingTree((props: ModalProps) => {
8181
width: '100vw',
8282
height: ['100vh', '-webkit-fill-available'],
8383
position: 'fixed',
84+
// eslint-disable-next-line custom-rules/no-physical-css-properties -- Full-viewport centering (used with right: 0)
8485
left: 0,
8586
top: 0,
8687
}),

0 commit comments

Comments
 (0)