From 9100a796737df835f76d34c862402228caa574ad Mon Sep 17 00:00:00 2001 From: Haider Alshamma Date: Wed, 30 Oct 2024 15:27:47 -0400 Subject: [PATCH] feat: support tablet and phone media queries --- .storybook/nds-theme/index.tsx | 2 +- .storybook/nds-theme/register.tsx | 20 ++++-- src/NDSProvider/GlobalStylesComposer.tsx | 33 +++++++++ src/NDSProvider/NDSProvider.tsx | 56 +++------------ ...js => mountWithNDSProvider.spec-utils.tsx} | 0 ...s => renderWithNDSProvider.spec-utils.tsx} | 0 src/NDSProvider/useNDSTheme.tsx | 50 +++++++++++++ src/theme.ts | 71 ++++++++++++++++++- 8 files changed, 176 insertions(+), 56 deletions(-) create mode 100644 src/NDSProvider/GlobalStylesComposer.tsx rename src/NDSProvider/{mountWithNDSProvider.spec-utils.js => mountWithNDSProvider.spec-utils.tsx} (100%) rename src/NDSProvider/{renderWithNDSProvider.spec-utils.js => renderWithNDSProvider.spec-utils.tsx} (100%) create mode 100644 src/NDSProvider/useNDSTheme.tsx diff --git a/.storybook/nds-theme/index.tsx b/.storybook/nds-theme/index.tsx index 6666b965b..008759614 100644 --- a/.storybook/nds-theme/index.tsx +++ b/.storybook/nds-theme/index.tsx @@ -22,7 +22,7 @@ const StorybookNDSProvider = ({ children }) => { const [themeVariant] = useLocalStorage("nds-sb-theme-variant", "desktop"); return ( - + {children} ); diff --git a/.storybook/nds-theme/register.tsx b/.storybook/nds-theme/register.tsx index d2e777793..7703644f8 100644 --- a/.storybook/nds-theme/register.tsx +++ b/.storybook/nds-theme/register.tsx @@ -1,14 +1,17 @@ // .storybook/my-addon/register.js -import React from "react"; +import React, { useEffect } from "react"; import { addons, types, RenderOptions } from "@storybook/addons"; import { AddonPanel } from "@storybook/components"; -import { Box, Flex, NDSProvider, Heading3, Heading2, QuietButton } from "../../src"; -import { themes } from "../../src/theme"; +import { Box, Flex, NDSProvider, Heading3, Heading2, QuietButton, DefaultNDSThemeType } from "../../src"; +import { desktop, themes } from "../../src/theme"; import ThemeKey from "./ThemeKey"; import { ThemeInput, ThemeOption, ThemeSelect } from "./ThemeInput"; import ThemeColorInput from "./ThemeColorInput"; import { useLocalStorage } from "./useLocalStorage/useLocalStorage"; +import { ComponentVariant } from "../../src/NDSProvider/ComponentVariantContext"; +import useMediaQuery from "../../src/hooks/useMediaQuery"; +import { getThemeByVariant } from "../../src/NDSProvider/useNDSTheme"; const ADDON_ID = "ndsThemeAddon"; const PANEL_ID = `${ADDON_ID}/panel`; @@ -29,11 +32,20 @@ const composeTheme = (data, theme) => { const DEFAULT_THEME_VARIANT = "desktop"; const ThemePanel = () => { - const [themeVariant, setThemeVariant] = useLocalStorage("nds-sb-theme-variant", DEFAULT_THEME_VARIANT); + const [themeVariant, setThemeVariant] = useLocalStorage( + "nds-sb-theme-variant", + DEFAULT_THEME_VARIANT + ); const [theme, setTheme] = useLocalStorage("nds-sb-theme", themes[themeVariant], { serializer: (value) => JSON.stringify(value), deserializer: (value) => JSON.parse(value), }); + const isTabletSize = useMediaQuery(`(min-width: ${desktop.breakpoints.small})`); + + useEffect(() => { + const newTheme = getThemeByVariant(themeVariant, isTabletSize); + setTheme(newTheme); + }, [themeVariant, isTabletSize]); const onChange = (group, prop) => (e) => { const value = e.target.value; diff --git a/src/NDSProvider/GlobalStylesComposer.tsx b/src/NDSProvider/GlobalStylesComposer.tsx new file mode 100644 index 000000000..ec89ce351 --- /dev/null +++ b/src/NDSProvider/GlobalStylesComposer.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import { DefaultNDSThemeType } from "../theme.type"; +import Reset from "./Reset"; +import ModalStyleOverride from "./ModalStyleOverride"; +import GlobalStyles from "./GlobalStyles"; + +type GlobalStylesComposerProps = { + theme?: DefaultNDSThemeType; + locale?: string; + disableGlobalStyles?: boolean; + children?: React.ReactNode; +}; + +export default function GlobalStylesComposer({ + theme, + locale, + disableGlobalStyles, + children, +}: GlobalStylesComposerProps) { + if (disableGlobalStyles) { + return <>{children}; + } + + return ( + <> + + + + {children} + + + ); +} diff --git a/src/NDSProvider/NDSProvider.tsx b/src/NDSProvider/NDSProvider.tsx index 994f16170..903b2f7da 100644 --- a/src/NDSProvider/NDSProvider.tsx +++ b/src/NDSProvider/NDSProvider.tsx @@ -1,24 +1,12 @@ import React, { useEffect } from "react"; import { ThemeProvider } from "styled-components"; import { I18nextProvider } from "react-i18next"; -import { themes } from "../theme"; import i18n from "../i18n"; -import { ThemeType, DefaultNDSThemeType, Breakpoints } from "../theme.type"; +import { ThemeType } from "../theme.type"; import { LocaleContext } from "./LocaleContext"; -import { mergeThemes } from "./mergeThemes.util"; -import GlobalStyles from "./GlobalStyles"; -import ModalStyleOverride from "./ModalStyleOverride"; -import Reset from "./Reset"; import ComponentVariantContextProvider, { ComponentVariant } from "./ComponentVariantContext"; - -export const buildBreakPoints = (breakpointsConfig: Readonly) => ({ - ...breakpointsConfig, - - // We need the map function as a polyfill because the `variant` function - // from `styled-system` expects the breakpoints - // to be an array and not an object - map: (fn) => Object.values(breakpointsConfig).map(fn), -}); +import GlobalStylesComposer from "./GlobalStylesComposer"; +import { useNDSTheme } from "./useNDSTheme"; type NDSProviderProps = { theme?: ThemeType; @@ -28,28 +16,8 @@ type NDSProviderProps = { variant?: ComponentVariant; }; -type AllNDSGlobalStylesProps = { - theme?: DefaultNDSThemeType; - locale?: string; - disableGlobalStyles?: boolean; - children?: any; -}; - -const AllNDSGlobalStyles = ({ theme, locale, disableGlobalStyles, children }: AllNDSGlobalStylesProps) => - !disableGlobalStyles ? ( - <> - - - - {children} - - - ) : ( - children - ); - function NDSProvider({ - theme, + theme: customTheme, children, disableGlobalStyles = false, locale = "en_US", @@ -59,24 +27,16 @@ function NDSProvider({ i18n.changeLanguage(locale); }, [locale]); - if (!(variant in themes)) { - throw new Error( - `Invalid variant "${variant}" provided to NDSProvider. Valid variants are: ${Object.keys(themes).join(", ")}` - ); - } - - const themeVariant = themes[variant]; - const mergedTheme = mergeThemes(themeVariant, theme); - const themeWithBreakpoints = { ...mergedTheme, breakpoints: buildBreakPoints(mergedTheme.breakpoints) }; + const theme = useNDSTheme(variant, customTheme); return ( - + - {children} + {children} - + ); diff --git a/src/NDSProvider/mountWithNDSProvider.spec-utils.js b/src/NDSProvider/mountWithNDSProvider.spec-utils.tsx similarity index 100% rename from src/NDSProvider/mountWithNDSProvider.spec-utils.js rename to src/NDSProvider/mountWithNDSProvider.spec-utils.tsx diff --git a/src/NDSProvider/renderWithNDSProvider.spec-utils.js b/src/NDSProvider/renderWithNDSProvider.spec-utils.tsx similarity index 100% rename from src/NDSProvider/renderWithNDSProvider.spec-utils.js rename to src/NDSProvider/renderWithNDSProvider.spec-utils.tsx diff --git a/src/NDSProvider/useNDSTheme.tsx b/src/NDSProvider/useNDSTheme.tsx new file mode 100644 index 000000000..634738bae --- /dev/null +++ b/src/NDSProvider/useNDSTheme.tsx @@ -0,0 +1,50 @@ +import { useState, useEffect } from "react"; +import { desktop, themes } from "../theme"; +import { ThemeType, DefaultNDSThemeType, Breakpoints } from "../theme.type"; +import useMediaQuery from "../hooks/useMediaQuery"; +import { mergeThemes } from "./mergeThemes.util"; +import { ComponentVariant } from "./ComponentVariantContext"; + +const THEME_VARIANTS: ReadonlySet = new Set(["desktop", "touch"]); + +export const buildBreakPoints = (breakpointsConfig: Readonly) => ({ + ...breakpointsConfig, + map: function (fn: (value: string) => T) { + return Object.values(breakpointsConfig).map(fn); + }, +}); + +const validateVariantOrThrow = (variant: ComponentVariant): void => { + if (!THEME_VARIANTS.has(variant)) { + throw new Error( + `Invalid variant "${variant}" provided to NDSProvider. Valid variants are: ${Array.from(THEME_VARIANTS).join( + ", " + )}.` + ); + } +}; + +export const getThemeByVariant = (variant: ComponentVariant, isTabletSize: boolean): DefaultNDSThemeType => { + if (variant === "touch") { + return isTabletSize ? themes.tablet : themes.phone; + } + return themes.desktop; +}; + +export function useNDSTheme(variant: ComponentVariant = "desktop", customTheme?: ThemeType): DefaultNDSThemeType { + validateVariantOrThrow(variant); + + const [themeVariant, setThemeVariant] = useState(desktop); + const isTabletSize = useMediaQuery(`(min-width: ${desktop.breakpoints.small})`); + + useEffect(() => { + const newTheme = getThemeByVariant(variant, isTabletSize); + setThemeVariant(newTheme); + }, [variant, isTabletSize]); + + const mergedTheme = mergeThemes(themeVariant, customTheme); + return { + ...mergedTheme, + breakpoints: buildBreakPoints(mergedTheme.breakpoints), + }; +} diff --git a/src/theme.ts b/src/theme.ts index 4dcc0944a..5caf655d8 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -1,7 +1,7 @@ import * as tokens from "@nulogy/tokens"; import type { DefaultNDSThemeType } from "./theme.type"; -type ThemeKey = "desktop" | "touch"; +type ThemeKey = "desktop" | "tablet" | "phone"; const BASE_THEME: DefaultNDSThemeType = { colors: { @@ -135,7 +135,7 @@ const themes: Record = { desktop: { ...BASE_THEME, }, - touch: { + tablet: { ...BASE_THEME, fontSizes: { smaller: "18px", @@ -200,7 +200,72 @@ const themes: Record = { rounded: "9999px", }, }, + phone: { + ...BASE_THEME, + fontSizes: { + smaller: "13.5px", + small: "15.75px", + medium: "18px", + large: "20.25px", + larger: "22.5px", + largest: "31.5px", + heading1: "22.5px", + heading2: "18px", + heading3: "15.75px", + heading4: "13.5px", + }, + lineHeights: { + base: "1.33333333", + relaxed: "1.66666667", + smallTextBase: "1.33333333", + smallTextCompressed: "1.33333333", + smallerText: "1.33333333", + heading1: "1.33333333", + heading2: "1.33333333", + heading3: "1.33333333", + heading4: "1.33333333", + title: "1.33333333", + sectionTitle: "1.33333333", + subsectionTitle: "1.33333333", + }, + space: { + none: "0px", + half: "3.6px", + x0_5: "3.6px", + x0: "0px", + x1: "7.2px", + x1_5: "10.8", + x2: "14.4px", + x2_5: "18px", + x3: "21.6px", + x4: "28.8px", + x5: "36px", + x6: "43.2px", + x8: "50.4px", + }, + sizes: { + none: "0px", + half: "3.6px", + x0_5: "3.6px", + x0: "0px", + x1: "7.2px", + x1_5: "10.8", + x2: "14.4px", + x2_5: "18px", + x3: "21.6px", + x4: "28.8px", + x5: "36px", + x6: "43.2px", + x8: "50.4px", + }, + radii: { + small: "1.8px", + medium: "3.6px", + circle: "50%", + rounded: "9999px", + }, + }, }; export { themes }; -export const { desktop, touch } = themes; +export const { desktop, tablet, phone } = themes;