diff --git a/.gitignore b/.gitignore index 3a90445..e160440 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ # pnpm pack outputs /diamondlightsource-sci-react-ui-*.tgz +/storybook-static/ \ No newline at end of file diff --git a/.storybook/main.ts b/.storybook/main.ts index d60af0e..6426ab2 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -8,7 +8,6 @@ const config: StorybookConfig = { "@chromatic-com/storybook", "@storybook/addon-interactions", '@storybook/addon-links', - '@storybook/addon-toolbars', 'storybook-dark-mode' ], framework: { diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index e2ec5f7..38cdfd0 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -3,11 +3,11 @@ import {CssBaseline} from "@mui/material"; import type { Preview } from "@storybook/react"; import {ThemeProvider} from '../src' -import {BaseTheme, DiamondTheme} from '../src' +import {GenericTheme, DiamondTheme} from '../src' import {ThemeSwapper, TextLight, TextDark} from "./ThemeSwapper"; -const TextThemeBase = 'Theme: Base' +const TextThemeBase = 'Theme: Generic' const TextThemeDiamond = 'Theme: Diamond' export const decorators = [ @@ -26,7 +26,7 @@ export const decorators = [ const selectedThemeMode = context.globals.themeMode || TextLight; return diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5a0119a..c34b425 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,15 @@ importers: '@mui/icons-material': specifier: ^6.1.7 version: 6.1.7(@mui/material@6.1.7(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.12)(react@18.3.1) + jest-transform-stub: + specifier: ^2.0.0 + version: 2.0.0 + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) react-icons: specifier: ^5.3.0 version: 5.3.0(react@18.3.1) @@ -90,6 +99,9 @@ importers: '@storybook/test': specifier: ^8.4.4 version: 8.4.4(storybook@8.4.4) + '@testing-library/jest-dom': + specifier: ^6.6.3 + version: 6.6.3 '@testing-library/react': specifier: ^16.0.1 version: 16.0.1(@testing-library/dom@10.4.0)(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -120,12 +132,6 @@ importers: jest-environment-jsdom: specifier: ^29.7.0 version: 29.7.0 - react: - specifier: ^18.3.1 - version: 18.3.1 - react-dom: - specifier: ^18.3.1 - version: 18.3.1(react@18.3.1) rollup: specifier: ^4.27.3 version: 4.27.3 @@ -3177,6 +3183,9 @@ packages: resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-transform-stub@2.0.0: + resolution: {integrity: sha512-lspHaCRx/mBbnm3h4uMMS3R5aZzMwyNpNIJLXj4cEsV0mIUtS4IjYJLSoyjRCtnxb6RIGJ4NL2quZzfIeNhbkg==} + jest-util@29.7.0: resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -8127,6 +8136,8 @@ snapshots: transitivePeerDependencies: - supports-color + jest-transform-stub@2.0.0: {} + jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 diff --git a/readme.md b/readme.md index 5cf3249..aa72ec3 100644 --- a/readme.md +++ b/readme.md @@ -20,41 +20,41 @@ First use the ThemeProvider and supply a theme. ```js import { - ThemeProvider, + ThemeProvider, DiamondTheme } from "@diamondlightsource/sci-react-ui"; root.render( - - - + + + ) ``` -There are currently two themes, `BaseTheme` or `DiamondTheme`, but you can adapt your own. +There are currently two themes, `GenericTheme` or `DiamondTheme`, but you can - and should - adapt your own. There are various components, here's an example of how to use the NavBar: ```js import {Container, Typography} from "@mui/material"; import { - Navbar, - NavLink, - NavLinks + Navbar, + NavLink, + NavLinks } from "@diamondlightsource/sci-react-ui"; function App() { - return <> - - - A link - - - - Scientific UI Collection - A collection of science based React components. - - + return <> + + + A link + + + + Scientific UI Collection + A collection of science based React components. + + } export default App; ``` diff --git a/src/components/ImageColorSchemeSwitch.stories.tsx b/src/components/ImageColorSchemeSwitch.stories.tsx new file mode 100644 index 0000000..c4b32da --- /dev/null +++ b/src/components/ImageColorSchemeSwitch.stories.tsx @@ -0,0 +1,68 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { ImageColorSchemeSwitch } from "./ImageColorSchemeSwitch"; + +import imageDark from "../public/generic/logo-dark.svg" +import imageLight from "../public/generic/logo-light.svg" + +const meta: Meta = { + title: "SciReactUI/Control/ImageColorSchemeSwitch", + component: ImageColorSchemeSwitch, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: 'Switch between an image depending on the color scheme mode, light or dark versions' + }, + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const SwitchingImage: Story = { + args: { + image: { + src: imageDark, + srcDark: imageLight, + alt: "Testing Switching Image", + width: "100" + } + }, + parameters: { + docs: { + description: { + story: 'This image changes depending on the color scheme mode selected.' + }, + }, + }, +}; + + +export const LargeSwitchingImage: Story = { + args: { + image: { + src: imageDark, + srcDark: imageLight, + alt: "Testing Switching Image", + width: "300" + } + }, +}; + +export const NonSwitchingImage: Story = { + args: { + image: { + src: imageLight, + alt: "Testing Non-Switching Image", + width: "300" + } + }, + parameters: { + docs: { + description: { + story: 'This image only has a single src so will NOT switch when the color scheme mode switches.' + }, + }, + }, +}; diff --git a/src/components/ImageColorSchemeSwitch.test.tsx b/src/components/ImageColorSchemeSwitch.test.tsx new file mode 100644 index 0000000..3d62ad7 --- /dev/null +++ b/src/components/ImageColorSchemeSwitch.test.tsx @@ -0,0 +1,94 @@ +import "@testing-library/jest-dom"; +import { render } from "@testing-library/react"; + +import { ImageColorSchemeSwitch, getLogoSrc } from "./ImageColorSchemeSwitch"; + +jest.mock("@mui/material", () => { + return { + useColorScheme: jest.fn().mockReturnValue({mode:"dark"}) + }; +}) + +describe("ImageColorSchemeSwitch", () => { + const testVals = { + src: "src/light", + alt: "test-alt" + }; + + function getRenderImg( image: any) { + const {getByAltText} = render(); + + const img = getByAltText(testVals.alt) + expect(img).toBeInTheDocument() + return img + } + + it("should render without errors", () => { + render(); + }); + + it("should have src and alt by default", () => { + const img = getRenderImg({}) + + expect(img).toHaveAttribute("alt", testVals.alt) + expect(img).toHaveAttribute("src", testVals.src) + + expect(img).not.toHaveAttribute("width") + expect(img).not.toHaveAttribute("height") + }); + + it("should have width 123", () => { + const width = "123"; + + const img = getRenderImg({width}) + expect(img).toHaveAttribute("width", width) + expect(img).not.toHaveAttribute("height") + }); + + it("should have width 123 and height 124", () => { + const width = "123", + height = "124"; + + const img = getRenderImg({width,height}) + + expect(img).toHaveAttribute("width", width) + expect(img).toHaveAttribute("height", height) + }); + + it("should have alternate src", () => { + const srcDark = "src/dark"; + + const img = getRenderImg({srcDark}) + + expect(img).toHaveAttribute("src", srcDark) + }); +}) + +describe("getLogoSrc", ()=>{ + const srcLight = "src/light", + srcDark = "src/dark"; + + it("should be null if no image", () => { + // @ts-ignore: invalid input + expect(getLogoSrc(null,"")).toStrictEqual(undefined); + // @ts-ignore: invalid input, calm down ts + expect(getLogoSrc()).toStrictEqual(undefined); + }); + + it("should be srcLight if no srcDark", () => { + expect(getLogoSrc({src:srcLight, alt:""},"light")).toStrictEqual(srcLight); + }); + + it("should be srcLight if mode is dark but no srcDark", () => { + expect(getLogoSrc({src:srcLight, alt:""},"dark")).toStrictEqual(srcLight); + }); + + it("should be srcLight if srcDark but mode light", () => { + expect(getLogoSrc( {src: srcLight, srcDark: srcDark, alt:""},"light")).toStrictEqual(srcLight); + }); + + it("should be srcDark if mode dark", () => { + expect( getLogoSrc({src:"src/light", srcDark:srcDark, alt:""},"dark")).toStrictEqual(srcDark); + }); + +}) \ No newline at end of file diff --git a/src/components/ImageColorSchemeSwitch.tsx b/src/components/ImageColorSchemeSwitch.tsx new file mode 100644 index 0000000..8d72a90 --- /dev/null +++ b/src/components/ImageColorSchemeSwitch.tsx @@ -0,0 +1,42 @@ +import {useColorScheme} from "@mui/material"; + +type ImageColorSchemeSwitchType = { + src: string, + srcDark?: string, + alt: string + width?: string, + height?: string, +} + +interface ImageColorSchemeSwitchProps { + image: ImageColorSchemeSwitchType; +} + +export function getLogoSrc(image:ImageColorSchemeSwitchType, mode: string) { + if( !image ) { + return undefined; + } + + if( image.srcDark === undefined ) { + return image.src; + } + + return mode === "dark" ? image.srcDark : image.src; +} + +const ImageColorSchemeSwitch = ({image}: ImageColorSchemeSwitchProps ) => { + const {mode} = useColorScheme(); + if( !mode ) return <> + + const src: string | undefined = getLogoSrc(image, mode) + + return {image.alt} +} + +export {ImageColorSchemeSwitch} +export type {ImageColorSchemeSwitchProps, ImageColorSchemeSwitchType} \ No newline at end of file diff --git a/src/components/Navbar.stories.tsx b/src/components/Navbar.stories.tsx index 38dc7e0..a4857a4 100644 --- a/src/components/Navbar.stories.tsx +++ b/src/components/Navbar.stories.tsx @@ -3,6 +3,9 @@ import { Meta, StoryObj } from "@storybook/react"; import { User } from "./User"; import { NavLink, NavLinks, Navbar } from "./Navbar"; +import logoImageDark from "../public/generic/logo-dark.svg" +import logoImageLight from "../public/generic/logo-light.svg" + const meta: Meta = { title: "SciReactUI/Navigation/Navbar", component: Navbar, @@ -73,7 +76,7 @@ export const LinksAndUser: Story = { }, }; -export const NoLogo: Story = { +export const WithThemeLogo: Story = { args: { children: ( @@ -85,12 +88,48 @@ export const NoLogo: Story = { ), - logo: null, + logo: "theme" + }, + parameters: { + docs: { + description: { + story: 'The logo is pulled in from the theme when `logo` set to "theme".' + }, + }, + }, +}; + +export const WithNonThemeLogo: Story = { + + args: { + children: ( + + + First + + + Second + + + ), + logo: { + src: logoImageLight, + srcDark: logoImageDark, + alt: "Home", + width: "100" + } + }, + parameters: { + docs: { + description: { + story: 'A separate image can also be referenced.' + }, + }, }, }; export const CustomChildElement: Story = { args: { - children: , + children: , }, }; diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 297377c..fcb724b 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -11,22 +11,23 @@ import { useTheme, } from "@mui/material"; import { MdMenu, MdClose } from "react-icons/md"; -import logoImage from "../public/logo-dark.svg"; import React, { useState } from "react"; +import {ImageColorSchemeSwitch, ImageColorSchemeSwitchType} from "./ImageColorSchemeSwitch"; + interface NavLinksProps { children: React.ReactElement | React.ReactElement[]; } interface NavbarProps extends BoxProps { /** Location/content of the logo */ - logo?: string | null; + logo?: ImageColorSchemeSwitchType | "theme" | null; children?: React.ReactElement | React.ReactElement[]; } const NavLink = ({ children, ...props }: LinkProps) => { const theme = useTheme(); - + return ( { */ const Navbar = ({ children, - logo = logoImage as string, + logo, ...props }: NavbarProps) => { const theme = useTheme(); - + + if( logo === "theme" ) { + logo = theme.logos?.normal + } + return ( - Home + ) : null} diff --git a/src/index.ts b/src/index.ts index 540288e..5cdd1f0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,8 +4,10 @@ export * from "./components/Navbar"; export * from "./components/Footer"; export * from "./components/User"; export * from "./components/VisitInput"; +export * from "./components/ImageColorSchemeSwitch"; // themes -export * from "./themes/BaseTheme"; +export * from "./themes/BaseTheme" +export * from "./themes/GenericTheme"; export * from "./themes/DiamondTheme"; export * from "./themes/ThemeProvider"; diff --git a/src/public/logo-light.svg b/src/public/diamond/logo-dark.svg similarity index 100% rename from src/public/logo-light.svg rename to src/public/diamond/logo-dark.svg diff --git a/src/public/logo-dark.svg b/src/public/diamond/logo-light.svg similarity index 100% rename from src/public/logo-dark.svg rename to src/public/diamond/logo-light.svg diff --git a/src/public/generic/logo-dark.svg b/src/public/generic/logo-dark.svg new file mode 100644 index 0000000..f235316 --- /dev/null +++ b/src/public/generic/logo-dark.svg @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + LOGO + + dark + + diff --git a/src/public/generic/logo-light.svg b/src/public/generic/logo-light.svg new file mode 100644 index 0000000..ae82794 --- /dev/null +++ b/src/public/generic/logo-light.svg @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + LOGO + + light + + diff --git a/src/storybook/Introduction.mdx b/src/storybook/Introduction.mdx index b0f6c5e..67758ee 100644 --- a/src/storybook/Introduction.mdx +++ b/src/storybook/Introduction.mdx @@ -18,7 +18,7 @@ import { Meta } from "@storybook/blocks"; ## Add Theme - First select the theme, BaseTheme, or DiamondTheme: + First select a theme: GenericTheme or DiamondTheme (or create your own): ```js import { diff --git a/src/storybook/theme/Colours.mdx b/src/storybook/theme/Colours.mdx index 06df691..8e07ca1 100644 --- a/src/storybook/theme/Colours.mdx +++ b/src/storybook/theme/Colours.mdx @@ -1,7 +1,8 @@ import { Meta, ColorPalette, ColorItem } from '@storybook/blocks'; -import {BaseTheme} from "../../themes/BaseTheme"; -import {DiamondTheme} from "../../themes/DiamondTheme"; +import {GenericTheme} from "../../../src"; +import {DiamondTheme} from "../../../src"; + export function ThemeColorItem({title, theme, colourSet, mode}) { return
-
- ## Base Theme +
+ ## Generic Theme
### Light Mode - - + + ### Dark Mode - - + +
diff --git a/src/storybook/theme/Logos.mdx b/src/storybook/theme/Logos.mdx new file mode 100644 index 0000000..c72c8b3 --- /dev/null +++ b/src/storybook/theme/Logos.mdx @@ -0,0 +1,67 @@ +import { Meta, ColorPalette, ColorItem } from '@storybook/blocks'; +import {ImageColorSchemeSwitch} from "../../components/ImageColorSchemeSwitch"; + + + +
+
+ # Logos + + Logos can be added to new themes by adding to the `createTheme` call: + + ```js + { + ... + logos: { + normal: { + src: logo, + alt: "My Logo", + width: "100", + height: "50" + }, + }, + ... + } + ``` + + + + ## Sizes + There are two image sizes. I regular `normal` and a smaller version `short`, these are used in the Navbar and + the Footer, respectively. + + ## Switch with Color Scheme + You can also switch them based on the current ColorScheme by adding an additional `srcDark` option. + + ```js + import logo from "my-images/logo.svg" + import logoDark from "my-images/logo-dark.svg" + + logos: { + normal: { + src: logo, + srcDark: logoDark, + alt: "My Logo" + }, + }, + ``` + + ## Theme + To create a new theme with your logos: + ```js + import { createTheme } from "@mui/material/styles"; + import { BaseThemeOptions } from "./BaseTheme"; + import logo "my-images/logo.svg" + + const MyTheme = createTheme({ + ...BaseThemeOptions, + logos: { + normal: { + src: logo, + alt: "My Logo" + } + }, + }, + ``` +
+
diff --git a/src/themes/BaseTheme.ts b/src/themes/BaseTheme.ts index 3f07c63..9501d26 100644 --- a/src/themes/BaseTheme.ts +++ b/src/themes/BaseTheme.ts @@ -1,6 +1,23 @@ -import { createTheme } from "@mui/material/styles"; +// import {ThemeOptions} from "@mui/material/styles"; +import {ImageColorSchemeSwitchType} from "../components/ImageColorSchemeSwitch"; -const BaseThemeOptions = { +// Make additions to theme, so that anything can be available throughout the app +declare module '@mui/material/styles' { + interface Theme { + logos?: { + normal: ImageColorSchemeSwitchType; + short?: ImageColorSchemeSwitchType; + }; + } + interface ThemeOptions { + logos?: { + normal: ImageColorSchemeSwitchType; + short?: ImageColorSchemeSwitchType; + }; + } +} + +const BaseThemeOptions /* : ThemeOptions */ = { typography: { fontSize: 14, }, @@ -16,8 +33,16 @@ const BaseThemeOptions = { }, }, }, + components: {}, + breakpoints: { + values: { + xs: 0, + sm: 480, + md: 768, + lg: 992, + xl: 1280, + }, + }, }; -const BaseTheme = createTheme(BaseThemeOptions); - -export { BaseThemeOptions, BaseTheme }; +export { BaseThemeOptions } \ No newline at end of file diff --git a/src/themes/DiamondTheme.ts b/src/themes/DiamondTheme.ts index 85b141f..ec4705c 100644 --- a/src/themes/DiamondTheme.ts +++ b/src/themes/DiamondTheme.ts @@ -2,11 +2,22 @@ import { createTheme, Theme } from "@mui/material/styles"; import { BaseThemeOptions } from "./BaseTheme"; +import logoImageDark from "../public/diamond/logo-dark.svg" +import logoImageLight from "../public/diamond/logo-light.svg" + const dlsLogoBlue = "#202740"; const dlsLogoYellow = "#facf07"; const DiamondTheme: Theme = createTheme({ ...BaseThemeOptions, + logos: { + normal: { + src: logoImageLight, + srcDark: logoImageDark, + alt: "Diamond Source Logo", + width: "100" + }, + }, colorSchemes: { // https://zenoo.github.io/mui-theme-creator/ light: { @@ -72,15 +83,6 @@ const DiamondTheme: Theme = createTheme({ }, }, }, - breakpoints: { - values: { - xs: 0, - sm: 480, - md: 768, - lg: 992, - xl: 1280, - }, - }, }); export { DiamondTheme }; diff --git a/src/themes/GenericTheme.ts b/src/themes/GenericTheme.ts new file mode 100644 index 0000000..1ba46e6 --- /dev/null +++ b/src/themes/GenericTheme.ts @@ -0,0 +1,20 @@ +import { createTheme, Theme } from "@mui/material/styles"; + +import { BaseThemeOptions } from "./BaseTheme"; + +import logoImageDark from "../public/generic/logo-dark.svg" +import logoImageLight from "../public/generic/logo-light.svg" + +const GenericTheme: Theme = createTheme({ + ...BaseThemeOptions, + logos: { + normal: { + src: logoImageLight, + srcDark: logoImageDark, + alt: "Diamond Source Logo", + width: "100" + }, + }, +}); + +export { GenericTheme }; diff --git a/src/themes/ThemeProvider.test.tsx b/src/themes/ThemeProvider.test.tsx index 03dc56c..9ada13a 100644 --- a/src/themes/ThemeProvider.test.tsx +++ b/src/themes/ThemeProvider.test.tsx @@ -1,39 +1,66 @@ -import {render} from "@testing-library/react"; import "@testing-library/jest-dom"; +import {render} from "@testing-library/react"; + +import {createTheme, Theme} from "@mui/material/styles"; import {ThemeProvider} from "./ThemeProvider"; -import {BaseTheme} from "./BaseTheme"; +import {BaseThemeOptions} from "./BaseTheme"; +import {GenericTheme} from "./GenericTheme"; import {DiamondTheme} from "./DiamondTheme"; -import Button from "@mui/material/Button"; + jest.mock("@mui/material/CssBaseline", () => () => { return
; }); +const buttonText = "a test button", + testApp =
+

H1

+

Paragraph

+ +
Footer
+
+ describe("ThemeProvider Component", () => { + it("should render", () => { - render(); + render(); }); - it("should render with button", async () => { - const buttonText = "A button" - const {findByText} = render( - + it("should render with button", () => { + const {getByText} = render( + {testApp} ); - expect(await findByText(buttonText)) + expect(getByText(buttonText)).toBeInTheDocument() }); - it("should render with base theme", () => { - render( -
+ it("should render with generic theme", () => { + const {getByText} = render( + {testApp} ); + + expect(getByText(buttonText)).toBeInTheDocument() }); - it("should render with base theme", () => { - render( -
+ it("should render with diamond theme", () => { + const {getByText} = render( + {testApp} ); + + expect(getByText(buttonText)).toBeInTheDocument() + }); + + + it("should render with a new theme", () => { + const NewTheme: Theme = createTheme({ + ...BaseThemeOptions, + }) + const {getByText} = render( + {testApp} + ); + + expect(getByText(buttonText)).toBeInTheDocument() }); }) @@ -49,12 +76,11 @@ describe("ThemeProvider CssBaseline Component", () => { expect(queryByTestId("Mock_CssBaseline")).not.toBeInTheDocument() }) - it("should render with button but without CssBaseline", async () => { - const buttonText = "A button" - const {findByText} = render( - + it("should render with app but without CssBaseline", () => { + const {getByText} = render( + {testApp} ); - expect(await findByText(buttonText)) + expect(getByText(buttonText)).toBeInTheDocument() }); }) \ No newline at end of file diff --git a/src/themes/ThemeProvider.tsx b/src/themes/ThemeProvider.tsx index ff68034..5f2545d 100644 --- a/src/themes/ThemeProvider.tsx +++ b/src/themes/ThemeProvider.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import { ThemeProvider as Mui_ThemeProvider } from "@mui/material/styles"; import {CssBaseline} from "@mui/material"; -import { BaseTheme } from "./BaseTheme"; +import { GenericTheme } from "./GenericTheme"; import {ThemeProviderProps as Mui_ThemeProviderProps} from "@mui/material/styles/ThemeProvider"; interface ThemeProviderProps extends Partial { @@ -11,7 +11,7 @@ interface ThemeProviderProps extends Partial { const ThemeProvider = function ({ children, - theme = BaseTheme, + theme = GenericTheme, baseline = true, defaultMode = "system", ...props