diff --git a/.eslintrc.js b/.eslintrc.js index 5236f9b5..8bb3c280 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -94,6 +94,32 @@ module.exports = { "testing-library/no-node-access": "off", }, }, + { + files: ["./**/*.stories.{ts,tsx}"], + rules: { + "@typescript-eslint/no-restricted-imports": [ + "error", + { + paths: [ + { + name: "@faker-js/faker", + message: "Please use @faker-js/faker/locale/en instead.", + allowTypeImports: true, + }, + { + name: "@mui/material", + message: "Please use @mui/material/ instead.", + allowTypeImports: true, + }, + // Note: @emotion/styled restrictions are not applied to story files + ], + patterns: [ + // Note: @emotion/styled pattern restrictions are not applied to story files + ], + }, + ], + }, + }, ], } @@ -137,9 +163,25 @@ function restrictedImports({ paths = [], patterns = [] } = {}) { message: "Please use @mui/material/ instead.", allowTypeImports: true, }, + { + name: "@emotion/styled", + importNames: ["styled"], + message: + "Do not import 'styled' from @emotion/styled. Use 'styled' from '../StyleIsolation/StyleIsolation' (or relative path to StyleIsolation) instead. For components that need shouldForwardProp, use 'import { default as emotionStyled } from \"@emotion/styled\"'.", + allowTypeImports: true, + }, ...paths, ], - patterns: [...patterns], + patterns: [ + { + group: ["@emotion/styled"], + importNames: ["default"], + message: + "Do not use default import from @emotion/styled as 'styled'. Use 'styled' from '../StyleIsolation/StyleIsolation' instead. For components that need shouldForwardProp, use 'import { default as emotionStyled } from \"@emotion/styled\"'.", + allowTypeImports: true, + }, + ...patterns, + ], }, ], } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f7f368d..7cff6acc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,7 +45,7 @@ jobs: cache-dependency-path: yarn.lock - name: Install dependencies - run: yarn install + run: yarn install --immutable && yarn msw init - name: Build Storybook run: yarn build-storybook diff --git a/.github/workflows/publish-pages.yml b/.github/workflows/publish-pages.yml index fb10b61e..5580155a 100644 --- a/.github/workflows/publish-pages.yml +++ b/.github/workflows/publish-pages.yml @@ -3,7 +3,7 @@ name: Publish Storybook on: # Runs on pushes targeting the default branch push: - branches: [main, cc/initial] + branches: [main] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: @@ -21,8 +21,8 @@ jobs: cache: yarn cache-dependency-path: yarn.lock - - name: Install dependencies - run: yarn install + - name: Install dependencies & Initialize + run: yarn install --immutable && yarn msw init - name: Build Storybook run: yarn build-storybook diff --git a/.storybook/main.ts b/.storybook/main.ts index f01fba11..7907f261 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -2,12 +2,26 @@ import { StorybookConfig } from "@storybook/react-webpack5" import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin" import { exec as execCb } from "child_process" import { promisify } from "util" +import * as fs from "fs" +import * as path from "path" const exec = promisify(execCb) const getGitSha = async (): Promise => { const { stdout } = await exec("git rev-parse HEAD") return stdout.trim() } +// We have postinstall hooks disabled, so ensure that MSW is initialized +const serviceWorkerPath = path.join( + process.cwd(), + "storybook-public", + "mockServiceWorker.js", +) +if (!fs.existsSync(serviceWorkerPath)) { + console.error( + "mockServiceWorker.js not found at storybook-public/mockServiceWorker.js. Please run 'yarn msw init storybook-public' to initialize MSW.", + ) + process.exit(1) +} const config: StorybookConfig = { stories: ["../src/**/*.mdx", "../src/**/*.stories.tsx"], diff --git a/bundle-preview/bundle-ai-chat-canvas-styles.html b/bundle-preview/bundle-ai-chat-canvas-styles.html new file mode 100644 index 00000000..995d0714 --- /dev/null +++ b/bundle-preview/bundle-ai-chat-canvas-styles.html @@ -0,0 +1,55 @@ + + + + + + AI Chat - Canvas Styles Isolation + + + + + +
+ + + diff --git a/bundle-preview/bundle-ai-drawer-canvas-styles.html b/bundle-preview/bundle-ai-drawer-canvas-styles.html new file mode 100644 index 00000000..098b5a4c --- /dev/null +++ b/bundle-preview/bundle-ai-drawer-canvas-styles.html @@ -0,0 +1,42 @@ + + + + + Home + + + + + + + + + + diff --git a/bundle-preview/bundle-demo-legacy.html b/bundle-preview/bundle-ai-drawer-legacy.html similarity index 100% rename from bundle-preview/bundle-demo-legacy.html rename to bundle-preview/bundle-ai-drawer-legacy.html diff --git a/bundle-preview/bundle-demo.html b/bundle-preview/bundle-ai-drawer.html similarity index 100% rename from bundle-preview/bundle-demo.html rename to bundle-preview/bundle-ai-drawer.html diff --git a/src/bundles/AiDrawer/AiDrawer.tsx b/src/bundles/AiDrawer/AiDrawer.tsx index 069c567a..11f20a4a 100644 --- a/src/bundles/AiDrawer/AiDrawer.tsx +++ b/src/bundles/AiDrawer/AiDrawer.tsx @@ -1,7 +1,10 @@ // @format import * as React from "react" import { FC, useEffect, useState, useRef, useMemo } from "react" -import styled from "@emotion/styled" +import { + styled, + StyleIsolation, +} from "../../components/StyleIsolation/StyleIsolation" import Markdown from "react-markdown" import rehypeRaw from "rehype-raw" import { RiCloseLine, RiSparkling2Line } from "@remixicon/react" @@ -423,105 +426,107 @@ const AiDrawer: FC = ({ aria-modal="true" keepMounted > -
- - {title ? <RiSparkling2Line /> : null} - <Typography variant="body1" component="h1"> - {title?.includes("AskTIM") ? ( - <> - Ask<strong>TIM</strong> - {title.replace("AskTIM", "")} - </> - ) : ( - title - )} - </Typography> - - - - -
- {blockType === "problem" ? ( - - ) : null} - {blockType === "video" ? ( - - { - setTab(tab) - onTrackingEvent?.({ - type: TrackingEventType.TabChange, - data: { - value: tab, - }, - }) - }} + +
+ + {title ? <RiSparkling2Line /> : null} + <Typography variant="body1" component="h1"> + {title?.includes("AskTIM") ? ( + <> + Ask<strong>TIM</strong> + {title.replace("AskTIM", "")} + </> + ) : ( + title + )} + </Typography> + + - - {response?.flashcards?.length ? ( - + +
+ {blockType === "problem" ? ( + + ) : null} + {blockType === "video" ? ( + + { + setTab(tab) + onTrackingEvent?.({ + type: TrackingEventType.TabChange, + data: { + value: tab, + }, + }) + }} + > + + {response?.flashcards?.length ? ( + + ) : null} + + + + + + {response?.flashcards?.length ? ( + + + ) : null} - -
- - - - {response?.flashcards?.length ? ( - - + + + + + {response?.summary ?? ""} + + - ) : null} - - - - - {response?.summary ?? ""} - - - -
- ) : null} + + ) : null} + ) } diff --git a/src/bundles/AiDrawer/FlashcardsScreen.tsx b/src/bundles/AiDrawer/FlashcardsScreen.tsx index a5f91a1b..3874542e 100644 --- a/src/bundles/AiDrawer/FlashcardsScreen.tsx +++ b/src/bundles/AiDrawer/FlashcardsScreen.tsx @@ -2,7 +2,7 @@ import { ActionButton } from "../../components/Button/ActionButton" import Typography from "@mui/material/Typography" import * as React from "react" import { useState, useCallback, useEffect, useRef } from "react" -import styled from "@emotion/styled" +import { styled } from "../../components/StyleIsolation/StyleIsolation" import { RiArrowRightLine, RiArrowLeftLine } from "@remixicon/react" export type Flashcard = { diff --git a/src/bundles/aiChat.tsx b/src/bundles/aiChat.tsx index 2989459c..cb66e78f 100644 --- a/src/bundles/aiChat.tsx +++ b/src/bundles/aiChat.tsx @@ -3,6 +3,7 @@ import { createRoot } from "react-dom/client" import { AiChat } from "../components/AiChat/AiChat" import { ThemeProvider } from "../components/ThemeProvider/ThemeProvider" import type { AiChatProps } from "../ai" +import { StyleIsolation } from "../components/StyleIsolation/StyleIsolation" const createAndAppend = () => { const newContainer = document.createElement("div") @@ -19,7 +20,9 @@ const init = (props: AiChatProps, { container }: InitOptions) => { const root = createRoot(rootEl) root.render( - + + + , ) } diff --git a/src/components/AiChat/AiChat.stories.tsx b/src/components/AiChat/AiChat.stories.tsx index 6af9f346..78a46617 100644 --- a/src/components/AiChat/AiChat.stories.tsx +++ b/src/components/AiChat/AiChat.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from "@storybook/nextjs" import { http, HttpResponse } from "msw" import { AiChat } from "./AiChat" import type { AiChatProps } from "./types" -import styled from "@emotion/styled" +import { styled } from "../StyleIsolation/StyleIsolation" import { handlers } from "./test-utils/api" import { FC, useEffect, useRef, useState } from "react" diff --git a/src/components/AiChat/AiChat.tsx b/src/components/AiChat/AiChat.tsx index 78700df4..015d9681 100644 --- a/src/components/AiChat/AiChat.tsx +++ b/src/components/AiChat/AiChat.tsx @@ -1,7 +1,7 @@ import * as React from "react" import { useEffect, useRef, useState, useCallback } from "react" import type { FC } from "react" -import styled from "@emotion/styled" +import { styled } from "../StyleIsolation/StyleIsolation" import Typography from "@mui/material/Typography" import classNames from "classnames" import { diff --git a/src/components/AiChat/AiChatContext.stories.tsx b/src/components/AiChat/AiChatContext.stories.tsx index 97481db0..f367b4a4 100644 --- a/src/components/AiChat/AiChatContext.stories.tsx +++ b/src/components/AiChat/AiChatContext.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from "@storybook/nextjs" import { AiChatDisplay } from "./AiChat" import { AiChatProvider, useAiChat } from "./AiChatContext" import type { AiChatProps } from "./types" -import styled from "@emotion/styled" +import { styled } from "../StyleIsolation/StyleIsolation" import { handlers } from "./test-utils/api" import Typography from "@mui/material/Typography" diff --git a/src/components/AiChat/AiChatMarkdown.stories.tsx b/src/components/AiChat/AiChatMarkdown.stories.tsx index c068a52c..17921a50 100644 --- a/src/components/AiChat/AiChatMarkdown.stories.tsx +++ b/src/components/AiChat/AiChatMarkdown.stories.tsx @@ -1,7 +1,7 @@ import * as React from "react" import type { Meta, StoryObj } from "@storybook/nextjs" import { AiChat } from "./AiChat" -import styled from "@emotion/styled" +import { styled } from "../StyleIsolation/StyleIsolation" import { handlers } from "./test-utils/api" const TEST_API_STREAMING = "http://localhost:4567/streaming" diff --git a/src/components/AiChat/ChatTitle.tsx b/src/components/AiChat/ChatTitle.tsx index 10c79053..e07f8539 100644 --- a/src/components/AiChat/ChatTitle.tsx +++ b/src/components/AiChat/ChatTitle.tsx @@ -1,5 +1,5 @@ import * as React from "react" -import styled from "@emotion/styled" +import { styled } from "../StyleIsolation/StyleIsolation" import Typography from "@mui/material/Typography" import { RiSparkling2Line } from "@remixicon/react" diff --git a/src/components/AiChat/EntryScreen.tsx b/src/components/AiChat/EntryScreen.tsx index 3c5ac554..99f9dab3 100644 --- a/src/components/AiChat/EntryScreen.tsx +++ b/src/components/AiChat/EntryScreen.tsx @@ -1,10 +1,10 @@ import * as React from "react" import { RiSparkling2Line, RiSendPlaneFill } from "@remixicon/react" -import styled from "@emotion/styled" import Typography from "@mui/material/Typography" import { AdornmentButton, Input } from "../Input/Input" import TimLogo from "./TimLogo" import { useState } from "react" +import { styled } from "../StyleIsolation/StyleIsolation" const Container = styled.form(({ theme }) => ({ display: "flex", diff --git a/src/components/Alert/Alert.tsx b/src/components/Alert/Alert.tsx index b37c8bcf..4006ba51 100644 --- a/src/components/Alert/Alert.tsx +++ b/src/components/Alert/Alert.tsx @@ -2,7 +2,7 @@ import * as React from "react" -import styled from "@emotion/styled" +import { styled } from "../StyleIsolation/StyleIsolation" import { default as MuiAlert } from "@mui/material/Alert" import type { AlertColor } from "@mui/material/Alert" import { Theme } from "@emotion/react" diff --git a/src/components/Button/ActionButton.tsx b/src/components/Button/ActionButton.tsx index 284636ec..d14e227b 100644 --- a/src/components/Button/ActionButton.tsx +++ b/src/components/Button/ActionButton.tsx @@ -1,4 +1,5 @@ import * as React from "react" +// eslint-disable-next-line @typescript-eslint/no-restricted-imports import styled from "@emotion/styled" import { pxToRem } from "../ThemeProvider/typography" import { @@ -9,6 +10,7 @@ import { } from "./Button" import type { ButtonStyleProps, ButtonSize } from "./Button" import type { LinkAdapterPropsOverrides } from "../LinkAdapter/LinkAdapter" +import { useStyleIsolation } from "../StyleIsolation/StyleIsolation" type ActionButtonStyleProps = Omit type ActionButtonProps = ActionButtonStyleProps & React.ComponentProps<"button"> @@ -56,9 +58,11 @@ const ActionButton = styled( ), )(({ size = DEFAULT_PROPS.size, responsive, theme }) => { return [ - actionStyles(size), + useStyleIsolation(actionStyles(size)), responsive && { - [theme.breakpoints.down("sm")]: actionStyles(RESPONSIVE_SIZES[size]), + [theme.breakpoints.down("sm")]: useStyleIsolation( + actionStyles(RESPONSIVE_SIZES[size]), + ), }, ] }) diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index d1c96a5b..371f9133 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -1,6 +1,7 @@ import * as React from "react" -import styled from "@emotion/styled" -import { css } from "@emotion/react" +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { default as emotionStyled } from "@emotion/styled" +import { styled, useStyleIsolation } from "../StyleIsolation/StyleIsolation" import { pxToRem } from "../ThemeProvider/typography" import type { Theme, ThemeOptions } from "@mui/material/styles" import CircularProgress from "@mui/material/CircularProgress" @@ -95,14 +96,19 @@ const sizeStyles = ( ] } -const buttonStyles = (props: ButtonStyleProps & { theme: Theme }) => { +const buttonStyles = ( + props: ButtonStyleProps & { + theme: Theme + }, +) => { const { size, variant, edge, theme, color, responsive } = { ...DEFAULT_PROPS, ...props, } const { colors } = theme.custom const hasBorder = variant === "secondary" || variant === "bordered" - return css([ + + return [ { color: theme.palette.text.primary, textAlign: "center", @@ -212,15 +218,16 @@ const buttonStyles = (props: ButtonStyleProps & { theme: Theme }) => { backgroundColor: theme.custom.colors.lightGray1, }, }, - ]) + ] } -const ButtonRoot = styled("button", { +const ButtonRoot = emotionStyled("button", { shouldForwardProp: shouldForwardButtonProp, -})(buttonStyles) -const ButtonLinkRoot = styled(LinkAdapter, { +})((props) => useStyleIsolation(buttonStyles(props))) + +const ButtonLinkRoot = emotionStyled(LinkAdapter, { shouldForwardProp: shouldForwardButtonProp, -})(buttonStyles) +})((props) => useStyleIsolation(buttonStyles(props))) const iconSizeStyles = (size: ButtonSize) => ({ "& > *": { diff --git a/src/components/Checkbox/Checkbox.tsx b/src/components/Checkbox/Checkbox.tsx index 11bb99c8..0981fc24 100644 --- a/src/components/Checkbox/Checkbox.tsx +++ b/src/components/Checkbox/Checkbox.tsx @@ -1,5 +1,5 @@ import * as React from "react" -import styled from "@emotion/styled" +import { styled } from "../StyleIsolation/StyleIsolation" import { css } from "@emotion/react" import type { Theme } from "@mui/material/styles" diff --git a/src/components/CheckboxChoiceField/CheckboxChoiceField.tsx b/src/components/CheckboxChoiceField/CheckboxChoiceField.tsx index 0483467c..4fc77ece 100644 --- a/src/components/CheckboxChoiceField/CheckboxChoiceField.tsx +++ b/src/components/CheckboxChoiceField/CheckboxChoiceField.tsx @@ -2,7 +2,7 @@ import * as React from "react" import { Checkbox, CheckboxProps } from "../Checkbox/Checkbox" import FormControl from "@mui/material/FormControl" import FormLabel from "@mui/material/FormLabel" -import styled from "@emotion/styled" +import { styled } from "../StyleIsolation/StyleIsolation" type CheckboxChoice = Omit & { value: string diff --git a/src/components/FormHelpers/FormHelpers.tsx b/src/components/FormHelpers/FormHelpers.tsx index 0e741927..61b48c2c 100644 --- a/src/components/FormHelpers/FormHelpers.tsx +++ b/src/components/FormHelpers/FormHelpers.tsx @@ -1,5 +1,5 @@ import * as React from "react" -import styled from "@emotion/styled" +import { styled } from "../StyleIsolation/StyleIsolation" import { RiErrorWarningLine } from "@remixicon/react" import Typography from "@mui/material/Typography" diff --git a/src/components/Input/Input.stories.tsx b/src/components/Input/Input.stories.tsx index 6766b23c..087ab637 100644 --- a/src/components/Input/Input.stories.tsx +++ b/src/components/Input/Input.stories.tsx @@ -1,6 +1,5 @@ import * as React from "react" import type { Meta, StoryObj } from "@storybook/nextjs" -import styled from "@emotion/styled" import { Input, AdornmentButton } from "./Input" import type { InputProps } from "./Input" import Stack from "@mui/material/Stack" @@ -8,7 +7,6 @@ import Grid from "@mui/material/Grid2" import { RiCalendarLine, RiCloseLine, RiSearchLine } from "@remixicon/react" import { fn } from "storybook/test" import { enumValues } from "../../story-utils" -import Typography from "@mui/material/Typography" const StatefulInput = (props: InputProps) => { const [value, setValue] = React.useState(props.value || "") @@ -180,89 +178,3 @@ Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu ) }, } - -const PageStyles = styled.div(` - input { - background-color: red; - border: 2px solid blue; - } - - input[type="text"] { - background: red; - } - - input:disabled { - background-image: linear-gradient(135deg, #2196F3 0%, #21CBF3 100%); - } - - .MuiInputBase-input { - background: red; - } -`) - -/** - * Tests that the Input component maintains its intended styling across all states - * even when parent page styles attempt to override it. The PageStyles wrapper - * includes potentially conflicting CSS that might exist in a consuming application. - */ -export const StatesAndParentStyleResistance: Story = { - render: (args) => { - return ( - - - - Placeholder - - - - - - Default - - - - - - Initially Focused - - - - - - Error - - - - - - Disabled - - - - - - Password - - - - - - - ) - }, - args: { - placeholder: "This is placeholder text.", - value: "Some value", - }, - argTypes: { - placeholder: { table: { disable: true } }, - value: { table: { disable: true } }, - error: { table: { disable: true } }, - disabled: { table: { disable: true } }, - }, -} diff --git a/src/components/Input/Input.tsx b/src/components/Input/Input.tsx index 9e1c44b1..be3a5d1b 100644 --- a/src/components/Input/Input.tsx +++ b/src/components/Input/Input.tsx @@ -1,5 +1,7 @@ import * as React from "react" -import styled from "@emotion/styled" +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { default as emotionStyled } from "@emotion/styled" +import { styled, useStyleIsolation } from "../StyleIsolation/StyleIsolation" import { css } from "@emotion/react" import InputBase from "@mui/material/InputBase" import type { InputBaseProps } from "@mui/material/InputBase" @@ -244,20 +246,6 @@ const baseInputStyles = (theme: Theme) => ({ paddingRight: "8px", }, }, - /* Override potentially conflicting styles from parent page to reasonable specificity - * - Will override .class1 .class2 input - * - Will override .class1 input[type="text"] - * - Will override .class1 input:focus - * - Will override .class1 input:active - * - May not override .class1 .class2 input:focus (equal specificity) - * - May not override .class1 .class2 input[type="text"] (equal specificity) - * - Will not override .class1 .class2 input:active[type="text"] - */ - "&&& input": { - background: "unset", - border: "unset", - boxShadow: "unset", - }, }) const noForward = Object.keys({ @@ -275,17 +263,19 @@ const noForward = Object.keys({ * - [Smoot Design Input Documentation](https://mitodl.github.io/smoot-design/?path=/docs/smoot-design-input--docs) * - [InputBase Documentation](https://mui.com/api/input-base/) */ -const Input: React.FC = styled(InputBase, { +const Input: React.FC = emotionStyled(InputBase, { shouldForwardProp: (prop) => !noForward.includes(prop), })(({ theme, size = defaultProps.size, multiline, responsive }) => [ - baseInputStyles(theme), - sizeStyles({ size, theme, multiline }), + useStyleIsolation(baseInputStyles(theme)), + useStyleIsolation(sizeStyles({ size, theme, multiline })), responsive && { - [theme.breakpoints.down("sm")]: sizeStyles({ - size: responsiveSize[size], - theme, - multiline, - }), + [theme.breakpoints.down("sm")]: useStyleIsolation( + sizeStyles({ + size: responsiveSize[size], + theme, + multiline, + }), + ), }, ]) diff --git a/src/components/LinkAdapter/LinkAdapter.tsx b/src/components/LinkAdapter/LinkAdapter.tsx index 2378cf18..c87b886b 100644 --- a/src/components/LinkAdapter/LinkAdapter.tsx +++ b/src/components/LinkAdapter/LinkAdapter.tsx @@ -1,6 +1,6 @@ import * as React from "react" import { useTheme } from "@emotion/react" -import styled from "@emotion/styled" +import { styled } from "../StyleIsolation/StyleIsolation" const PlainLink = styled.a({ color: "inherit", diff --git a/src/components/MenuItem/MenuItem.tsx b/src/components/MenuItem/MenuItem.tsx index 41a3db4e..bb7f9a1a 100644 --- a/src/components/MenuItem/MenuItem.tsx +++ b/src/components/MenuItem/MenuItem.tsx @@ -1,6 +1,6 @@ import MuiMenuItem from "@mui/material/MenuItem" import type { MenuItemProps as MuiMenuItemProps } from "@mui/material/MenuItem" -import styled from "@emotion/styled" +import { styled } from "../StyleIsolation/StyleIsolation" type MenuItemProps = MuiMenuItemProps & { size?: "small" | "medium" | "large" diff --git a/src/components/RadioChoiceField/RadioChoiceField.tsx b/src/components/RadioChoiceField/RadioChoiceField.tsx index 590175f7..c285703d 100644 --- a/src/components/RadioChoiceField/RadioChoiceField.tsx +++ b/src/components/RadioChoiceField/RadioChoiceField.tsx @@ -5,7 +5,7 @@ import FormControlLabel from "@mui/material/FormControlLabel" import Radio from "@mui/material/Radio" import RadioGroup from "@mui/material/RadioGroup" import type { RadioGroupProps } from "@mui/material/RadioGroup" -import styled from "@emotion/styled" +import { styled } from "../StyleIsolation/StyleIsolation" const RadioGroupStyled = styled(RadioGroup)(({ theme }) => ({ display: "flex", diff --git a/src/components/ScrollSnap/ScrollSnap.stories.tsx b/src/components/ScrollSnap/ScrollSnap.stories.tsx index 51adc7dd..571fd300 100644 --- a/src/components/ScrollSnap/ScrollSnap.stories.tsx +++ b/src/components/ScrollSnap/ScrollSnap.stories.tsx @@ -1,7 +1,7 @@ import * as React from "react" import type { Meta, StoryObj } from "@storybook/nextjs" import { ScrollSnap } from "./ScrollSnap" -import styled from "@emotion/styled" +import { styled } from "../StyleIsolation/StyleIsolation" import { faker } from "@faker-js/faker/locale/en" import { useInterval } from "../../utils/useInterval" import Slider from "@mui/material/Slider" diff --git a/src/components/ScrollSnap/ScrollSnap.tsx b/src/components/ScrollSnap/ScrollSnap.tsx index 6b0a725c..d9760322 100644 --- a/src/components/ScrollSnap/ScrollSnap.tsx +++ b/src/components/ScrollSnap/ScrollSnap.tsx @@ -1,5 +1,5 @@ import { composeRefs } from "../../utils/composeRefs" -import styled from "@emotion/styled" +import { styled } from "../StyleIsolation/StyleIsolation" import * as React from "react" /** diff --git a/src/components/SelectField/SelectField.tsx b/src/components/SelectField/SelectField.tsx index 7e21d5f7..7f81a30b 100644 --- a/src/components/SelectField/SelectField.tsx +++ b/src/components/SelectField/SelectField.tsx @@ -8,7 +8,7 @@ import InputBase from "@mui/material/InputBase" import type { InputBaseProps } from "@mui/material/InputBase" import { FormFieldWrapper } from "../FormHelpers/FormHelpers" import type { FormFieldWrapperProps } from "../FormHelpers/FormHelpers" -import styled from "@emotion/styled" +import { styled } from "../StyleIsolation/StyleIsolation" import { RiArrowDownSLine } from "@remixicon/react" import { baseInputStyles } from "../Input/Input" diff --git a/src/components/SrAnnouncer/SrAnnouncer.stories.tsx b/src/components/SrAnnouncer/SrAnnouncer.stories.tsx index e439efbe..37d97808 100644 --- a/src/components/SrAnnouncer/SrAnnouncer.stories.tsx +++ b/src/components/SrAnnouncer/SrAnnouncer.stories.tsx @@ -1,7 +1,7 @@ import * as React from "react" import type { Meta, StoryObj } from "@storybook/nextjs" import { SrAnnouncer } from "./SrAnnouncer" -import styled from "@emotion/styled" +import { styled } from "../StyleIsolation/StyleIsolation" const Container = styled.div<{ forceVisible?: boolean }>(({ forceVisible }) => [ forceVisible && { diff --git a/src/components/StyleIsolation/StyleIsolation.stories.tsx b/src/components/StyleIsolation/StyleIsolation.stories.tsx new file mode 100644 index 00000000..fad323ef --- /dev/null +++ b/src/components/StyleIsolation/StyleIsolation.stories.tsx @@ -0,0 +1,467 @@ +import * as React from "react" +import styled from "@emotion/styled" +import type { Meta, StoryObj } from "@storybook/nextjs" +import { Button } from "../Button/Button" +import { Input } from "../Input/Input" +import { StyleIsolation, styled as styledWithIsolation } from "./StyleIsolation" +import { ActionButton } from "../Button/ActionButton" +import { RiArrowRightLine } from "@remixicon/react" +import Grid from "@mui/material/Grid2" + +const meta: Meta = { + title: "smoot-design/StyleIsolation", + component: StyleIsolation, + parameters: { + layout: "padded", + }, +} + +export default meta +type Story = StoryObj + +/** + * Conflicting page styles that would normally override component styles. + */ +const ConflictingPageStyles = styled.div(` + button { + border: 1px solid red; + border-radius: 3px; + box-shadow: inset 0 1px 0 0 #fff; + color: aqua; + display: inline-block; + font-size: inherit; + font-weight: bold; + background-color: blue; + background-image: linear-gradient(red, blue); + padding: 7px 18px; + text-decoration: none; + text-shadow: 0 1px 0 green; + background-clip: padding-box; + font-size: 0.8125em; + } + + input { + background-color: red; + border: 2px solid blue; + } + + input[type="text"] { + background: red; + } + + input:disabled { + background-image: linear-gradient(135deg, #2196F3 0%, #21CBF3 100%); + } +`) + +/** + * StyleIsolation protects child components from conflicting parent page styles. + */ +export const Default: Story = { + render: () => ( + + + + + + + + + + + + + ), +} + +/** + * StyleIsolation can wrap multiple components. + */ +export const MultipleComponents: Story = { + render: () => ( + + + + + + + + + + + + + + + + + + + + + ), +} + +/** + * StyleIsolation protects components from conflicting parent styles. + * Notice how the buttons maintain their intended styling despite + * the conflicting page styles. + */ +export const PageStyleResistance: Story = { + render: () => ( + + + +

Without StyleIsolation:

+ + + + + + + + + + + + + + + + + + + + + +
+ +

With StyleIsolation:

+ + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ ), +} + +/** + * StyleIsolation can be styled itself using the sx prop. + */ +export const StyledContainer: Story = { + render: () => ( + + + + + + + + + + + + + + + + ), +} + +/** + * StyleIsolation protects components while still allowing intentional overrides. + * + * When inside a StyleIsolation context, you must use `styledWithIsolation`, the `styled` + * wrapper from StyleIsolation (not Emotion's styled directly) to override styles. + * This automatically applies useStyleIsolation, wrapping styles with + * higher specificity (0,4,0) to override StyleIsolation's resets (0,2,1). + * + * For components that need shouldForwardProp, use `import { default as emotionStyled }` + * from "@emotion/styled" and manually call useStyleIsolation in the style callback. + */ +export const IntentionalOverrides: Story = { + render: () => { + // Create styled versions of Button with intentional overrides using useStyleIsolation + const StyledPrimaryButton = styledWithIsolation(Button)(({ theme }) => ({ + backgroundColor: "aqua", + color: theme.custom.colors.white, + borderRadius: "8px", + padding: "16px 32px", + fontSize: "18px", + ...theme.typography.subtitle1, + "&:hover:not(:disabled)": { + backgroundColor: theme.custom.colors.darkGray2, + transform: "scale(1.05)", + transition: "transform 0.2s ease", + }, + })) + + const StyledSecondaryButton = styledWithIsolation(Button)(({ theme }) => ({ + border: "2px solid aqua", + borderRadius: "20px", + padding: "12px 24px", + ...theme.typography.body1, + textTransform: "uppercase", + letterSpacing: "1px", + })) + + return ( + + + +

Default Components (Protected):

+ + + + + + + + + + + + + + + + + + + + +
+ + +

Styled Components with Intentional Overrides:

+ + + + + + Custom Styled Primary + + + + + Custom Styled Secondary + + + + + + + + + + + + + +
+ + +

Mixed: Default + Styled (Both Protected):

+ + + + + + + + + Styled Override + + + + + + + + + + + +
+
+
+ ) + }, +} + +/** + * Demonstrates that styled() overrides work even with StyleIsolation's + * customResets, showing the override API is fully functional. + */ +/** + * Tests specificity against complex parent selectors like form button[type="button"]. + * StyleIsolation uses && button (0,2,1) which will override form button[type="button"] (0,1,2) + * due to higher specificity. + * Component styles use .css-abc123 &&& (0,4,0) to override StyleIsolation's resets. + */ +export const ComplexParentSelectors: Story = { + render: () => { + const ComplexParentStyles = styled.div(` + /* These selectors have various specificity levels */ + button { + background-color: red; + border: 3px solid orange; + padding: 30px; + font-size: 30px; + } + + button[type="button"] { + background-color: purple; + border: 3px solid pink; + } + + form button[type="button"] { + background-color: yellow; + border: 3px solid green; + } + `) + + return ( + +
+ + +

Without StyleIsolation (affected by parent styles):

+ +
+ +

+ With StyleIsolation (&& button = 0,2,1 overrides form + button[type="button"] = 0,1,2 via higher specificity; component + uses .css-abc123 &&& = 0,4,0 to override resets): +

+ + + +
+
+
+
+ ) + }, +} + +/** + * Demonstrates styled component overrides with custom resets. + * + * When using StyleIsolation with customResets, styled components need sufficient + * specificity to override both the custom reset and the component's own styles. + * + * - Custom reset uses: `& button = .css-abc123 button` (0,1,1 specificity) + * - Component's useStyleIsolation uses: `.css-abc123 &&&&` (0,4,0 specificity) + * - Using `&&&&&` (5 ampersands) gives us 0,5,0 (5 classes) which is higher + * specificity than both 0,1,1 and 0,4,0 + * + * **Recommendation**: When using the StyleIsolation styled wrapper + * (`import { styled } from StyleIsolation`), styles are automatically wrapped + * with the correct specificity (.css-abc123 &&&&), so you typically don't need + * to use &&&&& manually unless you need even higher specificity. + */ +export const OverridesWithCustomResets: Story = { + render: () => { + // Styled button WITHOUT &&&& - won't override custom reset + // Normal styled component styles have (0,0,0) specificity, which is lower than both + const WeakOverrideButton = styled(Button)({ + backgroundColor: "#10b981", + color: "white", + border: "3px solid #059669", + borderRadius: "16px", + padding: "20px 40px", + }) + + // Styled button WITH "&&&&&": (5 ampersands) - will override custom reset + const StrongOverrideButton = styled(Button)(({ theme }) => ({ + "&&&&&": { + backgroundColor: "#10b981", + color: "white", + border: "3px solid #059669", + borderRadius: "16px", + padding: "20px 40px", + fontSize: "20px", + ...theme.typography.subtitle1, + boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1)", + }, + "&&&&&:hover:not(:disabled)": { + backgroundColor: "#059669", + transform: "translateY(-2px)", + boxShadow: "0 6px 12px rgba(0, 0, 0, 0.15)", + }, + })) + + return ( + + + + +

+ Custom reset applies to default Button (transparent bg, small + padding): +

+ +
+ +

+ Styled WITHOUT &&&& - custom reset wins (transparent bg, small + padding): +

+ + Weak Override (No &&&&) + +
+ +

+ Styled WITH &&&& (4 ampersands) - override takes precedence + (green bg, large padding): +

+ + Strong Override (With &&&&) + +
+
+
+
+ ) + }, +} diff --git a/src/components/StyleIsolation/StyleIsolation.tsx b/src/components/StyleIsolation/StyleIsolation.tsx new file mode 100644 index 00000000..392393e3 --- /dev/null +++ b/src/components/StyleIsolation/StyleIsolation.tsx @@ -0,0 +1,508 @@ +import * as React from "react" +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { default as emotionStyled } from "@emotion/styled" +import type { CreateStyled } from "@emotion/styled" +import type { CSSObject } from "@emotion/react" + +/** + * Context for providing the StyleIsolation container className to child components. + * Components can use this className in their CSS selectors to target elements + * within the StyleIsolation container without relying on specificity levels. + */ +const StyleIsolationContext = React.createContext(null) + +/** + * Hook to wrap styles with StyleIsolation className for correct specificity. + * When a component is inside StyleIsolation, this wraps styles with the isolation + * className selector to override StyleIsolation's reset specificity (0,2,1). + * + * When isolationClassName is available, styles are wrapped as: + * `.css-abc123 &&&& { ...styles }` (specificity: 0,4,0) + * + * This ensures component styles (0,4,0) override StyleIsolation's resets (0,2,1). + * When isolationClassName is null/undefined, styles are returned as-is. + * + * This is used internally by components like Button to ensure their styles + * properly override StyleIsolation's resets when inside a StyleIsolation container. + * + * @example + * ```tsx + * const MyComponent = () => { + * const Button = styled("button")((props) => + * useStyleIsolation({ + * backgroundColor: "red", + * padding: "8px 16px", + * }) + * ) + * return + * } + * ``` + */ +export const useStyleIsolation = ( + styles: CSSObject | Array, +) => { + const isolationClassName = React.useContext(StyleIsolationContext) + if (!isolationClassName) { + return styles + } + + // If styles is an array, merge all valid CSS objects into a single object + if (Array.isArray(styles)) { + const mergedStyles: CSSObject = {} + for (const style of styles) { + // Skip falsy values (false, null, undefined) - these are used for conditional styles + if (!style || typeof style !== "object") { + continue + } + Object.assign(mergedStyles, style as CSSObject) + } + return { + [`.${isolationClassName} &&&&`]: mergedStyles, + } + } + + // If styles is a single CSSObject, wrap it directly + return { + [`.${isolationClassName} &&&&`]: styles, + } +} + +/** + * A wrapper around Emotion's styled() that automatically applies useStyleIsolation + * to style callbacks when inside a StyleIsolation context. This ensures styles + * automatically override StyleIsolation's resets without manual intervention. + * + * Usage is identical to Emotion's styled(). Styles are automatically wrapped + * with the correct specificity when inside a StyleIsolation container. + * + * @example + * ```tsx + * import { styled } from "./StyleIsolation" + * + * const MyButton = styled("button")(({ theme }) => ({ + * backgroundColor: theme.custom.colors.mitRed, + * padding: "8px 16px", + * })) + * ``` + */ +export const styled: CreateStyled = new Proxy( + emotionStyled as unknown as Record, + { + get(target, prop) { + const originalStyled = target[prop as string] + if (typeof originalStyled !== "function") { + return originalStyled + } + + const wrapFunction = (fn: unknown): unknown => { + if (typeof fn !== "function") { + return fn + } + return (...args: unknown[]) => { + // Check if the last argument is a style callback function + if (args.length > 0 && typeof args[args.length - 1] === "function") { + const originalCallback = args[args.length - 1] as ( + props: unknown, + ) => unknown + + // Wrap the callback to automatically apply useStyleIsolation + const wrappedCallback = ((props: unknown) => { + const styles = originalCallback(props) + + // Apply useStyleIsolation if we're in a StyleIsolation context + // This hook call happens during render, so context is available + // eslint-disable-next-line react-hooks/rules-of-hooks + const wrapped = useStyleIsolation(styles as CSSObject) + + return wrapped + }) as (props: unknown) => unknown + + // Copy function properties to maintain compatibility + Object.setPrototypeOf( + wrappedCallback, + Object.getPrototypeOf(originalCallback), + ) + Object.defineProperty(wrappedCallback, "length", { + value: originalCallback.length, + writable: false, + }) + Object.defineProperty(wrappedCallback, "name", { + value: originalCallback.name || "wrappedCallback", + writable: false, + }) + + // Replace the callback in args and call the original function + const newArgs = [...args] + newArgs[newArgs.length - 1] = wrappedCallback + return (fn as (...args: unknown[]) => unknown)(...newArgs) + } + + // No style callback detected - this might be a component being wrapped + // (e.g., styled(Button)) or a chained call (e.g., styled("button", config)) + const result = (fn as (...args: unknown[]) => unknown)(...args) + + // If result is a function (chained call or component wrapper), wrap it recursively + // This handles cases like: + // - styled("button", config) -> returns function that takes style callback + // - styled(Button) -> returns function that takes style callback + if (typeof result === "function") { + return wrapFunction(result) + } + return result + } + } + + return wrapFunction(originalStyled as (...args: unknown[]) => unknown) + }, + // Handle direct function calls like styled(Button) + apply(target, thisArg, args) { + // When styled is called directly (e.g., styled(Button)), wrap the result + const result = ( + target as unknown as (...args: unknown[]) => unknown + ).apply(thisArg, args) + // If result is a function (component wrapper), wrap it recursively + const wrapFunction = (fn: unknown): unknown => { + if (typeof fn !== "function") { + return fn + } + return (...innerArgs: unknown[]) => { + // Check if the last argument is a style callback function + if ( + innerArgs.length > 0 && + typeof innerArgs[innerArgs.length - 1] === "function" + ) { + const originalCallback = innerArgs[innerArgs.length - 1] as ( + props: unknown, + ) => unknown + + // Wrap the callback to automatically apply useStyleIsolation + const wrappedCallback = ((props: unknown) => { + const styles = originalCallback(props) + // eslint-disable-next-line react-hooks/rules-of-hooks + return useStyleIsolation(styles as CSSObject) + }) as (props: unknown) => unknown + + Object.setPrototypeOf( + wrappedCallback, + Object.getPrototypeOf(originalCallback), + ) + Object.defineProperty(wrappedCallback, "length", { + value: originalCallback.length, + writable: false, + }) + Object.defineProperty(wrappedCallback, "name", { + value: originalCallback.name || "wrappedCallback", + writable: false, + }) + + const newArgs = [...innerArgs] + newArgs[newArgs.length - 1] = wrappedCallback + return (fn as (...args: unknown[]) => unknown)(...newArgs) + } + + const innerResult = (fn as (...args: unknown[]) => unknown)( + ...innerArgs, + ) + if (typeof innerResult === "function") { + return wrapFunction(innerResult) + } + return innerResult + } + } + if (typeof result === "function") { + return wrapFunction(result) + } + return result + }, + }, +) as unknown as CreateStyled + +type StyleIsolationProps = { + /** + * Child components to protect from parent CSS conflicts + */ + children: React.ReactNode + /** + * CSS class name to apply to the isolation container + */ + className?: string + /** + * Style overrides for the isolation container itself + */ + sx?: CSSObject + /** + * Custom CSS resets to apply to specific selectors within the container. + * Keys are CSS selectors, values are style objects. + * + * Example: + * ```tsx + * + * + * + * ``` + */ + customResets?: Record +} + +/** + * StyleIsolation provides a wrapper that protects child components from + * conflicting parent page styles using high-specificity CSS overrides. + * + * This is useful when embedding components in pages with existing CSS that + * might conflict with component styles. + * + * @example + * ```tsx + * + * + * + * + * ``` + * + * @example With custom resets + * ```tsx + * + * + * + * ``` + */ +const StyleIsolationRoot = emotionStyled.div<{ + customResets?: Record + sx?: CSSObject +}>(({ customResets, sx }) => { + const baseStyles: CSSObject = { + /* CSS Containment: contain: "style" + * + * This tells the browser that styles defined inside this element should not + * affect elements outside of it, and styles from outside should not affect + * elements inside (except through inheritance, which we handle with &&&&). + * + * Benefits: + * - Creates a style boundary - prevents style leakage in/out + * - Browser can optimize rendering by isolating style calculations + * - Helps prevent accidental style conflicts + * + * Note: This doesn't prevent parent selectors like ".parent button" from + * matching children, but it does prevent style inheritance issues and + * helps the browser optimize. We still need &&&& for specificity overrides. + */ + contain: "style", + + /* CSS Isolation: isolation: "isolate" + * + * Creates a new stacking context and isolates the element from its siblings + * in terms of z-index and positioning. More importantly for our use case, + * it creates a new containing block for positioned descendants. + * + * Benefits: + * - Creates a new stacking context (useful for z-index isolation) + * - Helps with positioning context isolation + * - Works together with contain: "style" for better isolation + * + * Note: This is primarily for layout/positioning isolation, but complements + * contain: "style" for comprehensive isolation. The real protection against + * parent CSS comes from the && high-specificity selectors below. + */ + isolation: "isolate", + } + + // Build high-specificity resets for common elements + // Based on common conflicts from MITx Online and similar LMS CSS files + const commonResets: CSSObject = { + // Use && to create selectors with higher specificity + // This generates .css-abc123.css-abc123 button + // Specificity: 0,2,1 (2 classes + 1 element) + // This will override: + // - form input[type="button"] (0,1,2) - higher specificity wins + // - Most common parent page selectors + // Components use .css-abc123 &&&& (0,4,0) via useStyleIsolation to override these resets + // Consumers can use &&&&& (0,5,0) to override component styles + "&&": { + "button, input[type='button']": { + backgroundImage: "unset", + textTransform: "unset", + letterSpacing: "unset", + textDecoration: "unset", + textShadow: "unset", + boxShadow: "unset", + backgroundClip: "unset", + verticalAlign: "unset", + background: "unset", + border: "unset", + }, + + "input, input[type='text'], input[type='email'], input[type='password'], input[type='number'], input[type='search'], input[type='tel'], input[type='url']": + { + background: "unset", + backgroundImage: "unset", + border: "unset", + boxShadow: "unset", + verticalAlign: "unset", + }, + + "input[type='submit'], input[type='reset']": { + background: "unset", + backgroundImage: "unset", + border: "unset", + boxShadow: "unset", + verticalAlign: "unset", + }, + + "input:disabled": { + background: "unset", + backgroundImage: "unset", + border: "unset", + boxShadow: "unset", + }, + + "input:focus": { + background: "unset", + backgroundImage: "unset", + border: "unset", + boxShadow: "unset", + }, + + textarea: { + background: "unset", + backgroundImage: "unset", + border: "unset", + boxShadow: "unset", + verticalAlign: "unset", + }, + + "textarea:disabled": { + background: "unset", + backgroundImage: "unset", + border: "unset", + boxShadow: "unset", + }, + + "textarea:focus": { + background: "unset", + backgroundImage: "unset", + border: "unset", + boxShadow: "unset", + }, + + "button:hover:not(:disabled), input[type='button']:hover:not(:disabled), input[type='submit']:hover:not(:disabled), input[type='reset']:hover:not(:disabled)": + { + background: "unset", + border: "unset", + boxShadow: "unset", + backgroundImage: "unset", + textTransform: "unset", + textDecoration: "unset", + textShadow: "unset", + }, + + "button:active:not(:disabled), button:focus:not(:disabled), input[type='button']:active:not(:disabled), input[type='button']:focus:not(:disabled), input[type='submit']:active:not(:disabled), input[type='submit']:focus:not(:disabled), input[type='reset']:active:not(:disabled), input[type='reset']:focus:not(:disabled)": + { + background: "unset", + border: "unset", + boxShadow: "unset", + backgroundImage: "unset", + textTransform: "unset", + textDecoration: "unset", + textShadow: "unset", + }, + + a: { + textDecoration: "unset", + textShadow: "unset", + }, + + "h1, h2, h3, h4, h5, h6": { + textDecoration: "unset", + verticalAlign: "unset", + }, + + p: { + textDecoration: "unset", + verticalAlign: "unset", + }, + }, + } + + const customResetStyles: CSSObject = customResets + ? Object.entries(customResets).reduce( + (acc, [selector, styles]) => { + // Apply moderate specificity to custom selectors + // Note: Custom resets use & (0,1,1) while common resets use && (0,2,1) + // Components use .css-abc123 &&&& (0,4,0) via useStyleIsolation to override + acc[`& ${selector}`] = styles + return acc + }, + {} as Record, + ) + : {} + + return { + ...baseStyles, + ...commonResets, + ...customResetStyles, + ...sx, + } +}) + +const StyleIsolation: React.FC = ({ + children, + className, + sx, + customResets, +}) => { + const [isolationClassName, setIsolationClassName] = React.useState< + string | null + >(null) + + // Callback ref to capture the Emotion-generated className + const handleRef = React.useCallback( + (element: HTMLDivElement | null) => { + if (element) { + const classList = Array.from(element.classList) + + // Emotion generates classNames that start with 'css-' + // Find the first Emotion-generated className (not user-provided) + const emotionClassName = classList.find((cls) => cls.startsWith("css-")) + if (emotionClassName) { + setIsolationClassName(emotionClassName) + } else if (classList.length > 0) { + // Fallback: use the first className if no css- prefix found + // This adds tolerance in case Emotion uses a different format + const firstClassName = classList[0] + if (firstClassName !== className) { + setIsolationClassName(firstClassName) + } + } + } + }, + [className], + ) + + return ( + + + {children} + + + ) +} + +StyleIsolation.displayName = "StyleIsolation" + +export { StyleIsolation } +export type { StyleIsolationProps } diff --git a/src/components/TabButtons/TabButtonList.tsx b/src/components/TabButtons/TabButtonList.tsx index 578002ab..bcd27050 100644 --- a/src/components/TabButtons/TabButtonList.tsx +++ b/src/components/TabButtons/TabButtonList.tsx @@ -3,7 +3,7 @@ import MuiTab from "@mui/material/Tab" import type { TabProps } from "@mui/material/Tab" import MuiTabList from "@mui/lab/TabList" import type { TabListProps } from "@mui/lab/TabList" -import styled from "@emotion/styled" +import { styled } from "../StyleIsolation/StyleIsolation" import { Button, ButtonLink } from "../Button/Button" import type { ButtonLinkProps, ButtonProps } from "../Button/Button" import { css } from "@emotion/react" diff --git a/src/components/Tooltip/Tooltip.tsx b/src/components/Tooltip/Tooltip.tsx index 9db3e0ed..edc85349 100644 --- a/src/components/Tooltip/Tooltip.tsx +++ b/src/components/Tooltip/Tooltip.tsx @@ -1,5 +1,5 @@ import * as React from "react" -import styled from "@emotion/styled" +import { styled } from "../StyleIsolation/StyleIsolation" import { default as MuiTooltip } from "@mui/material/Tooltip" import type { TooltipProps } from "@mui/material/Tooltip" diff --git a/src/components/VisuallyHidden/VisuallyHidden.tsx b/src/components/VisuallyHidden/VisuallyHidden.tsx index cecd5026..549df536 100644 --- a/src/components/VisuallyHidden/VisuallyHidden.tsx +++ b/src/components/VisuallyHidden/VisuallyHidden.tsx @@ -1,4 +1,4 @@ -import styled from "@emotion/styled" +import { styled } from "../StyleIsolation/StyleIsolation" /** * VisuallyHidden is a utility component that hides its children from sighted diff --git a/src/components/internal/FormHelpers/FormHelpers.tsx b/src/components/internal/FormHelpers/FormHelpers.tsx index 0e741927..454be727 100644 --- a/src/components/internal/FormHelpers/FormHelpers.tsx +++ b/src/components/internal/FormHelpers/FormHelpers.tsx @@ -1,5 +1,5 @@ import * as React from "react" -import styled from "@emotion/styled" +import { styled } from "../../StyleIsolation/StyleIsolation" import { RiErrorWarningLine } from "@remixicon/react" import Typography from "@mui/material/Typography" diff --git a/src/index.ts b/src/index.ts index 419e3460..c7ef15b5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,23 +1,12 @@ "use client" -export { default as styled } from "@emotion/styled" export { css, Global } from "@emotion/react" - -export { - ThemeProvider, - createTheme, -} from "./components/ThemeProvider/ThemeProvider" +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +export { default as styled } from "@emotion/styled" export { Alert } from "./components/Alert/Alert" export type { AlertProps } from "./components/Alert/Alert" -export { - Button, - ButtonLoadingIcon, - ButtonLink, -} from "./components/Button/Button" -export type { ButtonProps, ButtonLinkProps } from "./components/Button/Button" - export { ActionButton, ActionButtonLink, @@ -27,12 +16,12 @@ export type { ActionButtonLinkProps, } from "./components/Button/ActionButton" -export type { LinkAdapterPropsOverrides } from "./components/LinkAdapter/LinkAdapter" - -export { Input, AdornmentButton } from "./components/Input/Input" -export type { InputProps, AdornmentButtonProps } from "./components/Input/Input" -export { TextField } from "./components/TextField/TextField" -export type { TextFieldProps } from "./components/TextField/TextField" +export { + Button, + ButtonLoadingIcon, + ButtonLink, +} from "./components/Button/Button" +export type { ButtonProps, ButtonLinkProps } from "./components/Button/Button" export { Checkbox, childCheckboxStyles } from "./components/Checkbox/Checkbox" export type { CheckboxProps } from "./components/Checkbox/Checkbox" @@ -49,6 +38,11 @@ export type { ControlLabelProps, } from "./components/FormHelpers/FormHelpers" +export { Input, AdornmentButton } from "./components/Input/Input" +export type { InputProps, AdornmentButtonProps } from "./components/Input/Input" + +export type { LinkAdapterPropsOverrides } from "./components/LinkAdapter/LinkAdapter" + export { RadioChoiceField, BooleanRadioChoiceField, @@ -68,15 +62,30 @@ export type { export { SrAnnouncer } from "./components/SrAnnouncer/SrAnnouncer" export type { SrAnnouncerProps } from "./components/SrAnnouncer/SrAnnouncer" +export { + StyleIsolation, + useStyleIsolation, + styled as styledWithIsolation, +} from "./components/StyleIsolation/StyleIsolation" +export type { StyleIsolationProps } from "./components/StyleIsolation/StyleIsolation" + export { TabButton, TabButtonLink, TabButtonList, } from "./components/TabButtons/TabButtonList" +export { + ThemeProvider, + createTheme, +} from "./components/ThemeProvider/ThemeProvider" + +export { TextField } from "./components/TextField/TextField" +export type { TextFieldProps } from "./components/TextField/TextField" + export { Tooltip } from "./components/Tooltip/Tooltip" export type { TooltipProps } from "./components/Tooltip/Tooltip" -export { VERSION } from "./VERSION" - export { VisuallyHidden } from "./components/VisuallyHidden/VisuallyHidden" + +export { VERSION } from "./VERSION"