Skip to content

Commit d3bf506

Browse files
chore(site): convert more components from Emotion to TailwindCSS (coder#19719)
## Changes made - Patched React `CSSProperties` type to add support for custom CSS properties - Updated several of the components in the `components` directory to Tailwind - Updated most of the `WorkspacePageBuildView` component to Tailwind to account for CSS specificity changes - Updated `Search` to address accessibility violation and removed all MUI logic - Updated `Search` stories (added new story, decoupled all stories from single decorator) - Updated `autoFocus` behavior in `SearchField` - Updated the styling for `WorkspacePageBuildView` to make sure the tabs had enough padding - Fixed layout effect in `WorkspacePageBuildView` to fire correctly
1 parent 679179f commit d3bf506

File tree

16 files changed

+280
-432
lines changed

16 files changed

+280
-432
lines changed

site/src/@types/react.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
declare module "react" {
2+
interface CSSProperties {
3+
[key: `--${string}`]: string | number | undefined;
4+
}
5+
}
6+
7+
export {};

site/src/components/Filter/SelectFilter.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ export const SelectFilter: FC<SelectFilterProps> = ({
5252
<SelectMenuTrigger>
5353
<SelectMenuButton
5454
startIcon={selectedOption?.startIcon}
55-
css={{ flexBasis: width, flexGrow: 1 }}
56-
className="shrink-0"
55+
className="shrink-0 grow"
56+
style={{ flexBasis: width }}
5757
aria-label={label}
5858
>
5959
{selectedOption?.label ?? placeholder}

site/src/components/MultiSelectCombobox/MultiSelectCombobox.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -506,7 +506,7 @@ export const MultiSelectCombobox = forwardRef<
506506
<Badge
507507
key={option.value}
508508
className={cn(
509-
"data-[disabled]:bg-content-disabled data-[disabled]:text-surface-tertiarydata-[disabled]:hover:bg-content-disabled",
509+
"data-[disabled]:bg-content-disabled data-[disabled]:text-surface-tertiary data-[disabled]:hover:bg-content-disabled",
510510
"data-[fixed]:bg-content-disabled data-[fixed]:text-surface-tertiary data-[fixed]:hover:bg-surface-secondary",
511511
badgeClassName,
512512
)}
Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,46 @@
11
import type { Meta, StoryObj } from "@storybook/react-vite";
2-
import { Search, SearchInput } from "./Search";
2+
import { Search, SearchEmpty, SearchInput } from "./Search";
33

44
const meta: Meta<typeof SearchInput> = {
55
title: "components/Search",
66
component: SearchInput,
7-
decorators: [
8-
(Story) => (
9-
<Search>
10-
<Story />
11-
</Search>
12-
),
13-
],
147
};
158

169
export default meta;
1710
type Story = StoryObj<typeof SearchInput>;
1811

19-
export const Example: Story = {};
12+
export const Example: Story = {
13+
render: (props) => (
14+
<Search>
15+
<SearchInput {...props} />
16+
</Search>
17+
),
18+
};
2019

21-
export const WithPlaceholder: Story = {
20+
export const WithCustomPlaceholder: Story = {
2221
args: {
2322
label: "uwu",
2423
placeholder: "uwu",
2524
},
25+
render: (props) => (
26+
<Search>
27+
<SearchInput {...props} />
28+
</Search>
29+
),
30+
};
31+
32+
export const WithSearchEmpty: Story = {
33+
args: {
34+
label: "I crave the certainty of steel",
35+
placeholder: "Alas, I am empty",
36+
},
37+
render: (props) => (
38+
<div className="flex flex-col gap-2">
39+
<Search>
40+
<SearchInput {...props} />
41+
</Search>
42+
43+
<SearchEmpty />
44+
</div>
45+
),
2646
};
Lines changed: 28 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
1-
import type { Interpolation, Theme } from "@emotion/react";
2-
// biome-ignore lint/style/noRestrictedImports: use it to have the component prop
3-
import Box, { type BoxProps } from "@mui/material/Box";
4-
import visuallyHidden from "@mui/utils/visuallyHidden";
51
import { SearchIcon } from "lucide-react";
62
import type { FC, HTMLAttributes, InputHTMLAttributes, Ref } from "react";
3+
import { cn } from "utils/cn";
74

8-
interface SearchProps extends Omit<BoxProps, "ref"> {
9-
$$ref?: Ref<unknown>;
5+
interface SearchProps extends HTMLAttributes<HTMLDivElement> {
6+
ref?: Ref<HTMLDivElement>;
107
}
118

129
/**
@@ -18,100 +15,63 @@ interface SearchProps extends Omit<BoxProps, "ref"> {
1815
* </Search>
1916
* ```
2017
*/
21-
export const Search: FC<SearchProps> = ({ children, $$ref, ...boxProps }) => {
18+
export const Search: FC<SearchProps> = ({
19+
children,
20+
ref,
21+
className,
22+
...props
23+
}) => {
2224
return (
23-
<Box ref={$$ref} {...boxProps} css={SearchStyles.container}>
24-
<SearchIcon className="size-icon-xs" css={SearchStyles.icon} />
25+
<div
26+
ref={ref}
27+
{...props}
28+
className={cn(
29+
"flex items-center h-10 pl-4 border-0 border-b border-solid border-border",
30+
className,
31+
)}
32+
>
33+
<SearchIcon className="size-icon-xs text-sm text-content-secondary" />
2534
{children}
26-
</Box>
35+
</div>
2736
);
2837
};
2938

30-
const SearchStyles = {
31-
container: (theme) => ({
32-
display: "flex",
33-
alignItems: "center",
34-
paddingLeft: 16,
35-
height: 40,
36-
borderBottom: `1px solid ${theme.palette.divider}`,
37-
}),
38-
39-
icon: (theme) => ({
40-
fontSize: 14,
41-
color: theme.palette.text.secondary,
42-
}),
43-
} satisfies Record<string, Interpolation<Theme>>;
44-
4539
type SearchInputProps = InputHTMLAttributes<HTMLInputElement> & {
4640
label?: string;
47-
$$ref?: Ref<HTMLInputElement>;
41+
ref?: Ref<HTMLInputElement>;
4842
};
4943

5044
export const SearchInput: FC<SearchInputProps> = ({
5145
label,
52-
$$ref,
46+
ref,
47+
id,
5348
...inputProps
5449
}) => {
5550
return (
5651
<>
57-
<label css={{ ...visuallyHidden }} htmlFor={inputProps.id}>
52+
<label className="sr-only" htmlFor={id}>
5853
{label}
5954
</label>
6055
<input
61-
ref={$$ref}
62-
tabIndex={-1}
56+
ref={ref}
57+
id={id}
58+
tabIndex={0}
6359
type="text"
6460
placeholder="Search..."
65-
css={SearchInputStyles.input}
61+
className="text-inherit h-full border-0 bg-transparent grow basis-0 outline-none pl-4 placeholder:text-content-secondary"
6662
{...inputProps}
6763
/>
6864
</>
6965
);
7066
};
7167

72-
const SearchInputStyles = {
73-
input: (theme) => ({
74-
color: "inherit",
75-
height: "100%",
76-
border: 0,
77-
background: "none",
78-
flex: 1,
79-
marginLeft: 16,
80-
outline: 0,
81-
"&::placeholder": {
82-
color: theme.palette.text.secondary,
83-
},
84-
}),
85-
} satisfies Record<string, Interpolation<Theme>>;
86-
8768
export const SearchEmpty: FC<HTMLAttributes<HTMLDivElement>> = ({
8869
children = "Not found",
8970
...props
9071
}) => {
9172
return (
92-
<div css={SearchEmptyStyles.empty} {...props}>
73+
<div className="text-sm text-content-secondary text-center py-2" {...props}>
9374
{children}
9475
</div>
9576
);
9677
};
97-
98-
const SearchEmptyStyles = {
99-
empty: (theme) => ({
100-
fontSize: 13,
101-
color: theme.palette.text.secondary,
102-
textAlign: "center",
103-
paddingTop: 8,
104-
paddingBottom: 8,
105-
}),
106-
} satisfies Record<string, Interpolation<Theme>>;
107-
108-
/**
109-
* Reusable styles for consumers of the base components
110-
*/
111-
export const searchStyles = {
112-
content: {
113-
width: 320,
114-
padding: 0,
115-
borderRadius: 4,
116-
},
117-
} satisfies Record<string, Interpolation<Theme>>;

site/src/components/SearchField/SearchField.tsx

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,49 @@
1-
import { useTheme } from "@emotion/react";
21
import IconButton from "@mui/material/IconButton";
32
import InputAdornment from "@mui/material/InputAdornment";
43
import TextField, { type TextFieldProps } from "@mui/material/TextField";
54
import Tooltip from "@mui/material/Tooltip";
6-
import visuallyHidden from "@mui/utils/visuallyHidden";
5+
import { useEffectEvent } from "hooks/hookPolyfills";
76
import { SearchIcon, XIcon } from "lucide-react";
8-
import { type FC, useEffect, useRef } from "react";
7+
import { type FC, useLayoutEffect, useRef } from "react";
98

109
export type SearchFieldProps = Omit<TextFieldProps, "onChange"> & {
1110
onChange: (query: string) => void;
1211
autoFocus?: boolean;
1312
};
1413

1514
export const SearchField: FC<SearchFieldProps> = ({
16-
value = "",
15+
InputProps,
1716
onChange,
17+
value = "",
1818
autoFocus = false,
19-
InputProps,
2019
...textFieldProps
2120
}) => {
22-
const theme = useTheme();
23-
const inputRef = useRef<HTMLInputElement>(null);
24-
25-
useEffect(() => {
21+
// MUI's autoFocus behavior is wonky. If you set autoFocus=true, the
22+
// component will keep getting focus on every single render, even if there
23+
// are other input elements on screen. We want this to be one-time logic
24+
const inputRef = useRef<HTMLInputElement | null>(null);
25+
const focusOnMount = useEffectEvent((): void => {
2626
if (autoFocus) {
2727
inputRef.current?.focus();
2828
}
2929
});
30+
useLayoutEffect(() => {
31+
focusOnMount();
32+
}, [focusOnMount]);
3033

3134
return (
3235
<TextField
33-
// Specifying `minWidth` so that the text box can't shrink so much
36+
inputRef={inputRef}
37+
// Specifying min width so that the text box can't shrink so much
3438
// that it becomes un-clickable as we add more filter controls
35-
css={{ minWidth: "280px" }}
39+
className="min-w-[280px]"
3640
size="small"
3741
value={value}
3842
onChange={(e) => onChange(e.target.value)}
39-
inputRef={inputRef}
4043
InputProps={{
4144
startAdornment: (
4245
<InputAdornment position="start">
43-
<SearchIcon
44-
className="size-icon-xs"
45-
css={{
46-
color: theme.palette.text.secondary,
47-
}}
48-
/>
46+
<SearchIcon className="size-icon-xs text-content-secondary" />
4947
</InputAdornment>
5048
),
5149
endAdornment: value !== "" && (
@@ -58,7 +56,7 @@ export const SearchField: FC<SearchFieldProps> = ({
5856
}}
5957
>
6058
<XIcon className="size-icon-xs" />
61-
<span css={{ ...visuallyHidden }}>Clear search</span>
59+
<span className="sr-only">Clear search</span>
6260
</IconButton>
6361
</Tooltip>
6462
</InputAdornment>

site/src/components/SelectMenu/SelectMenu.tsx

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ import {
2222
} from "react";
2323
import { cn } from "utils/cn";
2424

25-
const SIDE_PADDING = 16;
26-
2725
export const SelectMenu = Popover;
2826

2927
export const SelectMenuTrigger = PopoverTrigger;
@@ -37,13 +35,20 @@ type SelectMenuButtonProps = ButtonProps & {
3735
export const SelectMenuButton = forwardRef<
3836
HTMLButtonElement,
3937
SelectMenuButtonProps
40-
>((props, ref) => {
41-
const { startIcon, ...restProps } = props;
38+
>(({ className, startIcon, children, ...props }, ref) => {
4239
return (
43-
<Button variant="outline" size="lg" ref={ref} {...restProps}>
40+
<Button
41+
variant="outline"
42+
size="lg"
43+
ref={ref}
44+
// Shrink padding right slightly to account for visual weight of
45+
// the chevron
46+
className={cn("flex flex-row gap-2 pr-1.5", className)}
47+
{...props}
48+
>
4449
{startIcon}
4550
<span className="text-left block overflow-hidden text-ellipsis flex-grow">
46-
{props.children}
51+
{children}
4752
</span>
4853
<ChevronDownIcon />
4954
</Button>
@@ -55,22 +60,7 @@ export const SelectMenuSearch: FC<SearchFieldProps> = (props) => {
5560
<SearchField
5661
fullWidth
5762
size="medium"
58-
css={(theme) => ({
59-
borderBottom: `1px solid ${theme.palette.divider}`,
60-
"& input": {
61-
fontSize: 14,
62-
},
63-
"& fieldset": {
64-
border: 0,
65-
borderRadius: 0,
66-
},
67-
"& .MuiInputBase-root": {
68-
padding: `12px ${SIDE_PADDING}px`,
69-
},
70-
"& .MuiInputAdornment-positionStart": {
71-
marginRight: SIDE_PADDING,
72-
},
73-
})}
63+
className="border border-solid border-border [&_input]:text-sm [&_fieldset]:border-0 [&_fieldset]:rounded-none [&_.MuiInputBase-root]:px-4 [&_.MuiInputBase-root]:py-3"
7464
{...props}
7565
inputProps={{ autoFocus: true, ...props.inputProps }}
7666
/>

0 commit comments

Comments
 (0)