Skip to content

Commit 0c3e1d6

Browse files
authored
Avoid using rem for icons for safari compatibility (#56304)
* Avoid using rem for icons for safari compatibility * Fix poorly sized custom icon
1 parent 8847e64 commit 0c3e1d6

15 files changed

Lines changed: 179 additions & 27 deletions

File tree

airflow-core/src/airflow/ui/eslint.config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { jsoncRules } from "./rules/jsonc.js";
2727
import { perfectionistRules } from "./rules/perfectionist.js";
2828
import { prettierRules } from "./rules/prettier.js";
2929
import { reactRules } from "./rules/react.js";
30+
import { remRules } from "./rules/rem.js";
3031
import { stylisticRules } from "./rules/stylistic.js";
3132
import { typescriptRules } from "./rules/typescript.js";
3233
import { unicornRules } from "./rules/unicorn.js";
@@ -46,6 +47,7 @@ export default /** @type {const} @satisfies {ReadonlyArray<FlatConfig.Config>} *
4647
prettierRules,
4748
reactRules,
4849
stylisticRules,
50+
remRules,
4951
unicornRules,
5052
i18nextRules,
5153
i18nRules,
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*!
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
import { AST_NODE_TYPES } from "@typescript-eslint/utils";
20+
21+
export const remNamespace = "rem";
22+
23+
/**
24+
* Check if a value contains rem units
25+
* @param {string} value
26+
* @returns {boolean}
27+
*/
28+
const containsRem = (value) => /\d+\.?\d*rem/u.test(value);
29+
30+
/**
31+
* Convert rem value to pixels (1rem = 16px)
32+
* @param {string} value
33+
* @returns {string}
34+
*/
35+
const convertRemToPixels = (value) =>
36+
value.replaceAll(/(?<temp1>\d+\.?\d*)rem/gu, (_, number) => {
37+
if (typeof number === "string") {
38+
const pixels = parseFloat(number) * 16;
39+
40+
return `${pixels}px`;
41+
}
42+
43+
return value;
44+
});
45+
46+
export const remPlugin = {
47+
rules: {
48+
"no-rem-in-props": {
49+
/** @param {import('@typescript-eslint/utils').TSESLint.RuleContext<'noRemInProps', []>} context */
50+
create(context) {
51+
/** @param {import('@typescript-eslint/utils').TSESTree.JSXOpeningElement} node */
52+
const checkAttributes = (node) => {
53+
// Check all attributes for rem values in size, width, height props (but not style)
54+
node.attributes.forEach((attr) => {
55+
if (attr.type !== AST_NODE_TYPES.JSXAttribute || !attr.value) {
56+
return;
57+
}
58+
59+
const attrName = attr.name.name;
60+
61+
// Skip style attributes - rem is allowed there
62+
if (attrName === "style") {
63+
return;
64+
}
65+
66+
// Only check size, width, height attributes
67+
if (attrName !== "height" && attrName !== "size" && attrName !== "width") {
68+
return;
69+
}
70+
71+
let attrValue = undefined;
72+
73+
// Handle different attribute value types
74+
if (attr.value.type === AST_NODE_TYPES.Literal) {
75+
attrValue = attr.value.value;
76+
} else if (
77+
attr.value.type === AST_NODE_TYPES.JSXExpressionContainer &&
78+
attr.value.expression.type === AST_NODE_TYPES.Literal
79+
) {
80+
attrValue = attr.value.expression.value;
81+
}
82+
83+
// Check for rem values
84+
if (typeof attrValue === "string" && containsRem(attrValue)) {
85+
const fixedValue = convertRemToPixels(attrValue);
86+
87+
context.report({
88+
data: {
89+
attribute: attrName,
90+
fixedValue,
91+
value: attrValue,
92+
},
93+
fix(fixer) {
94+
// For string literals, replace the entire value
95+
if (attr.value !== null && attr.value.type === AST_NODE_TYPES.Literal) {
96+
return fixer.replaceText(attr.value, `{${fixedValue}}`);
97+
}
98+
// For JSX expressions with literal values, replace just the literal
99+
if (
100+
attr.value !== null &&
101+
attr.value.type === AST_NODE_TYPES.JSXExpressionContainer &&
102+
attr.value.expression.type === AST_NODE_TYPES.Literal
103+
) {
104+
return fixer.replaceText(attr.value.expression, fixedValue);
105+
}
106+
107+
// eslint-disable-next-line unicorn/no-null
108+
return null;
109+
},
110+
messageId: "noRemInProps",
111+
node: attr,
112+
});
113+
}
114+
});
115+
};
116+
117+
return {
118+
/** @param {import('@typescript-eslint/utils').TSESTree.JSXOpeningElement} node */
119+
JSXOpeningElement(node) {
120+
checkAttributes(node);
121+
},
122+
};
123+
},
124+
meta: {
125+
docs: {
126+
category: "Best Practices",
127+
description: "Disallow rem units in size, width, and height attributes (but allow in style)",
128+
recommended: "error",
129+
},
130+
fixable: "code",
131+
messages: {
132+
noRemInProps:
133+
"Avoid using rem units in {{attribute}} attribute. Use numeric pixel values instead of '{{value}}'. Auto-fix available: {{fixedValue}}",
134+
},
135+
type: "problem",
136+
},
137+
},
138+
},
139+
};
140+
141+
/** @type {import("@typescript-eslint/utils/ts-eslint").FlatConfig.Config} */
142+
export const remRules = {
143+
files: ["**/*.tsx"],
144+
plugins: {
145+
[remNamespace]: remPlugin,
146+
},
147+
rules: {
148+
[`${remNamespace}/no-rem-in-props`]: "error",
149+
},
150+
};

airflow-core/src/airflow/ui/src/components/SearchBar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export const SearchBar = ({
8585
/>
8686
) : undefined}
8787
{Boolean(hideAdvanced) ? undefined : (
88-
<Button fontWeight="normal" height="1.75rem" variant="ghost" width={140} {...buttonProps}>
88+
<Button fontWeight="normal" height={28} variant="ghost" width={140} {...buttonProps}>
8989
{translate("search.advanced")}
9090
</Button>
9191
)}

airflow-core/src/airflow/ui/src/layouts/Details/Grid/TaskNames.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ export const TaskNames = ({ nodes, onRowClick }: Props) => {
117117
px={1}
118118
>
119119
<FiChevronUp
120-
size="1rem"
120+
size={16}
121121
style={{
122122
transform: `rotate(${node.isOpen ? 0 : 180}deg)`,
123123
transition: "transform 0.5s",

airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ export const PanelButtons = ({
262262
<Popover.Trigger asChild>
263263
<Button size="sm" variant="outline">
264264
{translate("dag:panel.buttons.options")}
265-
<FiChevronDown size="0.5rem" />
265+
<FiChevronDown size={8} />
266266
</Button>
267267
</Popover.Trigger>
268268
<Portal>

airflow-core/src/airflow/ui/src/layouts/Nav/AdminButton.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ export const AdminButton = ({
7979
return (
8080
<Menu.Root positioning={{ placement: "right" }}>
8181
<Menu.Trigger asChild>
82-
<NavButton icon={<FiSettings size="1.75rem" />} title={translate("nav.admin")} />
82+
<NavButton icon={<FiSettings size={28} />} title={translate("nav.admin")} />
8383
</Menu.Trigger>
8484
<Menu.Content>
8585
{menuItems}

airflow-core/src/airflow/ui/src/layouts/Nav/BrowseButton.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export const BrowseButton = ({
7070
return (
7171
<Menu.Root positioning={{ placement: "right" }}>
7272
<Menu.Trigger asChild>
73-
<NavButton icon={<FiGlobe size="1.75rem" />} title={translate("nav.browse")} />
73+
<NavButton icon={<FiGlobe size={28} />} title={translate("nav.browse")} />
7474
</Menu.Trigger>
7575
<Menu.Content>
7676
{menuItems}

airflow-core/src/airflow/ui/src/layouts/Nav/DocsButton.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export const DocsButton = ({
6161
return (
6262
<Menu.Root positioning={{ placement: "right" }}>
6363
<Menu.Trigger asChild>
64-
<NavButton icon={<FiBookOpen size="1.75rem" />} title={translate("nav.docs")} />
64+
<NavButton icon={<FiBookOpen size={28} />} title={translate("nav.docs")} />
6565
</Menu.Trigger>
6666
<Menu.Content>
6767
{links

airflow-core/src/airflow/ui/src/layouts/Nav/Nav.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,16 +151,16 @@ export const Nav = () => {
151151
<AirflowPin height="35px" width="35px" />
152152
</NavLink>
153153
</Box>
154-
<NavButton icon={<FiHome size="1.75rem" />} title={translate("nav.home")} to="/" />
154+
<NavButton icon={<FiHome size="28px" />} title={translate("nav.home")} to="/" />
155155
<NavButton
156156
disabled={!authLinks?.authorized_menu_items.includes("Dags")}
157-
icon={<DagIcon height="1.75rem" width="1.75rem" />}
157+
icon={<DagIcon height="28px" width="28px" />}
158158
title={translate("nav.dags")}
159159
to="dags"
160160
/>
161161
<NavButton
162162
disabled={!authLinks?.authorized_menu_items.includes("Assets")}
163-
icon={<FiDatabase size="1.75rem" />}
163+
icon={<FiDatabase size="28px" />}
164164
title={translate("nav.assets")}
165165
to="assets"
166166
/>

airflow-core/src/airflow/ui/src/layouts/Nav/PluginMenuItem.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,11 @@ export const PluginMenuItem = ({
4646
const displayIcon = colorMode === "dark" && typeof iconDarkMode === "string" ? iconDarkMode : icon;
4747
const pluginIcon =
4848
typeof displayIcon === "string" ? (
49-
<Image height="1.25rem" mr={topLevel ? 0 : 2} src={displayIcon} width="1.25rem" />
49+
<Image height="20px" mr={topLevel ? 0 : 2} src={displayIcon} width="20px" />
5050
) : urlRoute === "legacy-fab-views" ? (
51-
<RiArchiveStackLine size="1.25rem" style={{ marginRight: topLevel ? 0 : "8px" }} />
51+
<RiArchiveStackLine size="20px" style={{ marginRight: topLevel ? 0 : "8px" }} />
5252
) : (
53-
<LuPlug size="1.25rem" style={{ marginRight: topLevel ? 0 : "8px" }} />
53+
<LuPlug size="20px" style={{ marginRight: topLevel ? 0 : "8px" }} />
5454
);
5555

5656
const isExternal = urlRoute === undefined || urlRoute === null;

0 commit comments

Comments
 (0)