diff --git a/packages/orbit-components/src/OrbitProvider/README.md b/packages/orbit-components/src/OrbitProvider/README.md index a1e1abac6f..5b0594d744 100644 --- a/packages/orbit-components/src/OrbitProvider/README.md +++ b/packages/orbit-components/src/OrbitProvider/README.md @@ -9,7 +9,7 @@ import OrbitProvider from "@kiwicom/orbit-components/lib/OrbitProvider"; After adding import please wrap your application into `OrbitProvider` and you can provide your own [`theme`](https://github.com/kiwicom/orbit/blob/master/.github/theming.md). ```jsx - + ``` @@ -18,8 +18,9 @@ After adding import please wrap your application into `OrbitProvider` and you ca Table below contains all types of the props available in the OrbitProvider component. -| Name | Type | Default | Description | -| :----------- | :----------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------ | -| **children** | `React.Node` | | Your app | -| theme | `[Object]` | | See [`theming`](https://github.com/kiwicom/orbit/blob/master/.github/theming.md) | -| useId | `[Object]` | | If using React 18 or above, use `React.useId`. If not, use `useRandomId` from [`react-uid`](https://www.npmjs.com/package/react-uid). | +| Name | Type | Default | Description | +| :----------- | :------------------------------ | :-------- | :------------------------------------------------------------------------------------------------------------------------------------ | +| **children** | `React.Node` | | Your app | +| theme | `[Object]` | | See [`theming`](https://github.com/kiwicom/orbit/blob/master/.github/theming.md) | +| useId | `[Object]` | | If using React 18 or above, use `React.useId`. If not, use `useRandomId` from [`react-uid`](https://www.npmjs.com/package/react-uid). | +| colorScheme | `"light" \| "dark" \| "system"` | `"light"` | Controls the color scheme of the application. Use "system" to follow the user's system preferences. | diff --git a/packages/orbit-components/src/OrbitProvider/index.tsx b/packages/orbit-components/src/OrbitProvider/index.tsx index 8364c27ec5..49ed7e3244 100644 --- a/packages/orbit-components/src/OrbitProvider/index.tsx +++ b/packages/orbit-components/src/OrbitProvider/index.tsx @@ -44,13 +44,18 @@ const getCssVarsForWL = (theme: typeof defaultTokens) => * */ -const OrbitProvider = ({ theme, children, useId }: Props) => { +const OrbitProvider = ({ theme, children, useId, colorScheme = "light" }: Props) => { + const effectiveTheme = { + ...theme, + colorScheme: colorScheme || theme.colorScheme || "light", + }; + return ( - + {children} diff --git a/packages/orbit-components/src/OrbitProvider/types.d.ts b/packages/orbit-components/src/OrbitProvider/types.d.ts index be7814936e..4ef9100d6b 100644 --- a/packages/orbit-components/src/OrbitProvider/types.d.ts +++ b/packages/orbit-components/src/OrbitProvider/types.d.ts @@ -9,4 +9,5 @@ export interface Props { readonly theme: Theme; readonly children: React.ReactNode; readonly useId: () => string; + readonly colorScheme?: "light" | "dark" | "system"; } diff --git a/packages/orbit-components/src/defaultTheme.ts b/packages/orbit-components/src/defaultTheme.ts index 2f0845f802..eee57eadd8 100644 --- a/packages/orbit-components/src/defaultTheme.ts +++ b/packages/orbit-components/src/defaultTheme.ts @@ -6,6 +6,7 @@ export interface Theme { readonly lockScrolling?: boolean; readonly lockScrollingBarGap?: boolean; readonly rtl?: boolean; + readonly colorScheme?: "light" | "dark" | "system"; } const defaultTheme: Theme = { @@ -14,6 +15,7 @@ const defaultTheme: Theme = { lockScrolling: true, lockScrollingBarGap: false, rtl: false, + colorScheme: "light", }; export interface ThemeProps { diff --git a/packages/orbit-components/src/hooks/useColorScheme/__tests__/index.test.tsx b/packages/orbit-components/src/hooks/useColorScheme/__tests__/index.test.tsx new file mode 100644 index 0000000000..755d3285d2 --- /dev/null +++ b/packages/orbit-components/src/hooks/useColorScheme/__tests__/index.test.tsx @@ -0,0 +1,49 @@ +import * as React from "react"; +import { render } from "@testing-library/react"; + +import { defaultTheme, OrbitProvider } from "../../.."; +import { useColorScheme } from ".."; + +const mockMatchMedia = jest.fn(); +window.matchMedia = mockMatchMedia; + +const TestComponent = () => { + const isDark = useColorScheme(); + return
{isDark ? "dark" : "light"}
; +}; + +describe("useColorScheme", () => { + beforeEach(() => { + mockMatchMedia.mockReset(); + }); + + it("should return false for light theme", () => { + mockMatchMedia.mockReturnValue({ matches: false }); + const { getByTestId } = render( + + + , + ); + expect(getByTestId("dark-mode")).toHaveTextContent("light"); + }); + + it("should return true for dark theme", () => { + mockMatchMedia.mockReturnValue({ matches: false }); + const { getByTestId } = render( + + + , + ); + expect(getByTestId("dark-mode")).toHaveTextContent("dark"); + }); + + it("should follow system preference when set to system", () => { + mockMatchMedia.mockReturnValue({ matches: true }); + const { getByTestId } = render( + + + , + ); + expect(getByTestId("dark-mode")).toHaveTextContent("dark"); + }); +}); diff --git a/packages/orbit-components/src/hooks/useColorScheme/index.ts b/packages/orbit-components/src/hooks/useColorScheme/index.ts new file mode 100644 index 0000000000..1125afd5bc --- /dev/null +++ b/packages/orbit-components/src/hooks/useColorScheme/index.ts @@ -0,0 +1,15 @@ +import { useTheme } from "../.."; + +export const useColorScheme = (): boolean => { + const theme = useTheme(); + const darkQuery = window.matchMedia("(prefers-color-scheme: dark)"); + const prefersDark = darkQuery.matches; + + if (theme.colorScheme === "system") { + return prefersDark; + } + + return theme.colorScheme === "dark"; +}; + +export default useColorScheme; diff --git a/packages/orbit-components/src/index.ts b/packages/orbit-components/src/index.ts index 3a69367454..1d6a069dc0 100644 --- a/packages/orbit-components/src/index.ts +++ b/packages/orbit-components/src/index.ts @@ -124,6 +124,7 @@ export { default as useLockScrolling } from "./hooks/useLockScrolling"; export { default as useRandomId, useRandomIdSeed } from "./hooks/useRandomId"; export { default as useFocusTrap } from "./hooks/useFocusTrap"; export { default as useInterval } from "./hooks/useInterval"; +export { default as useColorScheme } from "./hooks/useColorScheme"; // primitives export { default as BadgePrimitive } from "./primitives/BadgePrimitive"; diff --git a/packages/orbit-components/src/test-utils/DarkModeWrapper.tsx b/packages/orbit-components/src/test-utils/DarkModeWrapper.tsx new file mode 100644 index 0000000000..462aba4cb0 --- /dev/null +++ b/packages/orbit-components/src/test-utils/DarkModeWrapper.tsx @@ -0,0 +1,18 @@ +import * as React from "react"; + +import { OrbitProvider } from ".."; +import defaultTheme from "../defaultTheme"; + +interface Props { + children: React.ReactNode; +} + +export const DarkModeWrapper = ({ children }: Props) => { + return ( + + {children} + + ); +}; + +export default DarkModeWrapper; diff --git a/packages/orbit-design-tokens/src/dictionary/definitions/foundation/palette/darkMode.json b/packages/orbit-design-tokens/src/dictionary/definitions/foundation/palette/darkMode.json new file mode 100644 index 0000000000..b049435c11 --- /dev/null +++ b/packages/orbit-design-tokens/src/dictionary/definitions/foundation/palette/darkMode.json @@ -0,0 +1,63 @@ +{ + "foundation": { + "palette": { + "darkMode": { + "background": { + "value": "#252A31", + "internal": true, + "type": "color" + }, + "background-hover": { + "value": "#181B20", + "internal": true, + "type": "color" + }, + "background-active": { + "value": "#0B0C0F", + "internal": true, + "type": "color" + }, + "surface": { + "value": "#4F5E71", + "internal": true, + "type": "color" + }, + "surface-hover": { + "value": "#3E4E63", + "internal": true, + "type": "color" + }, + "surface-active": { + "value": "#324256", + "internal": true, + "type": "color" + }, + "text": { + "value": "#FFFFFF", + "internal": true, + "type": "color" + }, + "text-secondary": { + "value": "#BAC7D5", + "internal": true, + "type": "color" + }, + "border": { + "value": "#4F5E71", + "internal": true, + "type": "color" + }, + "border-hover": { + "value": "#3E4E63", + "internal": true, + "type": "color" + }, + "border-active": { + "value": "#324256", + "internal": true, + "type": "color" + } + } + } + } +} diff --git a/packages/orbit-design-tokens/src/js/defaultFoundation.ts b/packages/orbit-design-tokens/src/js/defaultFoundation.ts index 70aa5a4897..2d30e074d1 100644 --- a/packages/orbit-design-tokens/src/js/defaultFoundation.ts +++ b/packages/orbit-design-tokens/src/js/defaultFoundation.ts @@ -181,6 +181,19 @@ export interface Palette { blue: Blue; bundle: Bundle; cloud: Cloud; + darkMode: { + background: string; + backgroundHover: string; + backgroundActive: string; + surface: string; + surfaceHover: string; + surfaceActive: string; + text: string; + textSecondary: string; + border: string; + borderHover: string; + borderActive: string; + }; green: Green; ink: Ink; orange: Orange; @@ -307,6 +320,19 @@ const red = { }; const social = { facebook: "#3B5998", facebookHover: "#385490", facebookActive: "#354F88" }; const white = { normal: "#FFFFFF", normalActive: "#E7ECF1", normalHover: "#F1F4F7" }; +const darkMode = { + background: "#252A31", + backgroundHover: "#181B20", + backgroundActive: "#0B0C0F", + surface: "#4F5E71", + surfaceHover: "#3E4E63", + surfaceActive: "#324256", + text: "#FFFFFF", + textSecondary: "#BAC7D5", + border: "#4F5E71", + borderHover: "#3E4E63", + borderActive: "#324256", +}; const borderRadius = { 50: "2px", 100: "4px", @@ -365,7 +391,7 @@ const fontFamily = { const fontSize = { small: "13px", normal: "15px", large: "16px", extraLarge: "18px" }; const lineHeight = { small: "16px", normal: "20px", large: "24px", extraLarge: "24px" }; const fontWeight = { normal: "400", medium: "500", bold: "700" }; -const palette = { blue, bundle, cloud, green, ink, orange, product, red, social, white }; +const palette = { blue, bundle, cloud, darkMode, green, ink, orange, product, red, social, white }; const foundation = { palette, borderRadius,