From aa21259b65eb16d5207ee57c1e52950aec2905f2 Mon Sep 17 00:00:00 2001 From: Cheton Wu <447801+cheton@users.noreply.github.com> Date: Sun, 14 May 2023 11:32:31 +0800 Subject: [PATCH] feat: add `InvertedMode` component for color mode inversion (#759) * feat: add `InvertedMode` component for color mode inversion * docs: update icons in sidebar routes * docs: update color mode and color style sections * docs: update tooltip.mdx * feat: add inverted background to color style * docs: update useColorMode.mdx * test: update tests for color mode and color style --- packages/react-docs/config/sidebar-routes.js | 22 ++++- .../pages/components/color-mode/darkmode.mdx | 43 +++++++++ .../pages/components/color-mode/index.mdx | 22 +---- .../components/color-mode/invertedmode.mdx | 61 ++++++++++++ .../pages/components/color-mode/lightmode.mdx | 43 +++++++++ .../pages/components/color-style/index.mdx | 2 +- .../components/color-style/useColorStyle.mdx | 94 ++++++++++++++----- .../react-docs/pages/components/tooltip.mdx | 12 +-- packages/react/__tests__/index.js | 1 + packages/react/src/color-mode/DarkMode.js | 12 ++- packages/react/src/color-mode/InvertedMode.js | 23 +++++ packages/react/src/color-mode/LightMode.js | 12 ++- .../__tests__/ColorModeProvider.test.js | 12 +-- .../src/color-mode/__tests__/DarkMode.test.js | 39 +++----- .../color-mode/__tests__/InvertedMode.test.js | 48 ++++++++++ .../color-mode/__tests__/LightMode.test.js | 39 +++----- packages/react/src/color-mode/index.js | 2 + .../__tests__/ColorStyleProvider.test.js | 57 +++++++++-- packages/react/src/color-style/color-style.js | 6 +- .../react/src/color-style/useColorStyle.js | 5 +- 20 files changed, 418 insertions(+), 137 deletions(-) create mode 100644 packages/react-docs/pages/components/color-mode/darkmode.mdx create mode 100644 packages/react-docs/pages/components/color-mode/invertedmode.mdx create mode 100644 packages/react-docs/pages/components/color-mode/lightmode.mdx create mode 100644 packages/react/src/color-mode/InvertedMode.js create mode 100644 packages/react/src/color-mode/__tests__/InvertedMode.test.js diff --git a/packages/react-docs/config/sidebar-routes.js b/packages/react-docs/config/sidebar-routes.js index 3b8b7b9de7..043a60be48 100644 --- a/packages/react-docs/config/sidebar-routes.js +++ b/packages/react-docs/config/sidebar-routes.js @@ -114,10 +114,14 @@ export const routes = [ ), routes: [ { title: 'Getting Started', path: 'components' }, - { title: 'COLORS', heading: true }, - { title: 'Color Mode', path: 'components/color-mode' }, + { title: 'COLOR MODE', heading: true }, + { title: 'ColorModeProvider', path: 'components/color-mode' }, + { title: 'DarkMode', path: 'components/color-mode/darkmode' }, + { title: 'LightMode', path: 'components/color-mode/lightmode' }, + { title: 'InvertedMode', path: 'components/color-mode/invertedmode' }, { title: 'useColorMode', path: 'components/color-mode/useColorMode' }, - { title: 'Color Style', path: 'components/color-style' }, + { title: 'COLOR STYLE', heading: true }, + { title: 'ColorStyleProvider', path: 'components/color-style' }, { title: 'useColorStyle', path: 'components/color-style/useColorStyle' }, { title: 'LAYOUT', heading: true }, @@ -359,7 +363,17 @@ export const routes = [ { title: 'Zoom', path: 'components/transitions/zoom' }, { title: 'TYPOGRAPHY', heading: true }, - { title: 'Code', path: 'components/code' }, + { + title: 'Code', + path: 'components/code', + render: () => { + return ( + {`tag: code`}}> + + + ); + }, + }, { title: 'Text', path: 'components/text' }, { title: 'TextLabel', diff --git a/packages/react-docs/pages/components/color-mode/darkmode.mdx b/packages/react-docs/pages/components/color-mode/darkmode.mdx new file mode 100644 index 0000000000..c1eb9d22d6 --- /dev/null +++ b/packages/react-docs/pages/components/color-mode/darkmode.mdx @@ -0,0 +1,43 @@ +# DarkMode + +The `DarkMode` component enables you to render its children in dark mode. It will override current color mode and set the color scheme to `dark`. + +## Import + +```js +import { DarkMode } from '@tonic-ui/react'; +``` + +## Usage + +```jsx noInline +function Example() { + const [colorMode] = useColorMode(); + const [colorStyle] = useColorStyle({ colorMode }); + + return ( + + + The color mode is set to {colorMode} + + + ); +} + +render( + + + +); +``` + +## Props + +### DarkMode + +| Name | Type | Default | Description | +| :--- | :--- | :------ | :---------- | +| children | ReactNode | | | diff --git a/packages/react-docs/pages/components/color-mode/index.mdx b/packages/react-docs/pages/components/color-mode/index.mdx index 2e9b0086a8..fe7a7c17ea 100644 --- a/packages/react-docs/pages/components/color-mode/index.mdx +++ b/packages/react-docs/pages/components/color-mode/index.mdx @@ -1,4 +1,4 @@ -# Color Mode +# ColorModeProvider Tonic UI comes with built-in support for managing color modes in your app. @@ -130,23 +130,3 @@ function App({ children }) { ## Managing Color Mode To manage color mode in your application, you can use the [useColorMode](color-mode/useColorMode) hook. - -## Forcing a Specific Color Mode - -To force a specific color mode, wrap your component in `LightMode` or `DarkMode`, it will override the global color mode and set the color scheme to `dark` or `light` respectively. - -```jsx - - - - This is dark mode - - - - - - This is light mode - - - -``` diff --git a/packages/react-docs/pages/components/color-mode/invertedmode.mdx b/packages/react-docs/pages/components/color-mode/invertedmode.mdx new file mode 100644 index 0000000000..a4937133cc --- /dev/null +++ b/packages/react-docs/pages/components/color-mode/invertedmode.mdx @@ -0,0 +1,61 @@ +# InvertedMode + +The `InvertedMode` component is used to invert the color mode of its children. + +## Import + +```js +import { InvertedMode } from '@tonic-ui/react'; +``` + +## Usage + +```jsx noInline +function Example() { + const [colorMode] = useColorMode(); + const [colorStyle] = useColorStyle({ colorMode }); + + return ( + + + The current color mode is inverted to {colorMode} mode + + + ); +} + +render( + + + +); +``` + +### Rendering tooltip label + +The `InvertedMode` component is useful when you want to render a tooltip label in inverted mode. + +```jsx + + Title + + Content + + )} +> + Hover Me + +``` + +## Props + +### InvertedMode + +| Name | Type | Default | Description | +| :--- | :--- | :------ | :---------- | +| children | ReactNode | | | diff --git a/packages/react-docs/pages/components/color-mode/lightmode.mdx b/packages/react-docs/pages/components/color-mode/lightmode.mdx new file mode 100644 index 0000000000..55428d2d97 --- /dev/null +++ b/packages/react-docs/pages/components/color-mode/lightmode.mdx @@ -0,0 +1,43 @@ +# LightMode + +The `LightMode` component enables you to render its children in light mode. It will override current color mode and set the color scheme to `light`. + +## Import + +```js +import { LightMode } from '@tonic-ui/react'; +``` + +## Usage + +```jsx noInline +function Example() { + const [colorMode] = useColorMode(); + const [colorStyle] = useColorStyle({ colorMode }); + + return ( + + + The color mode is set to {colorMode} + + + ); +} + +render( + + + +); +``` + +## Props + +### LightMode + +| Name | Type | Default | Description | +| :--- | :--- | :------ | :---------- | +| children | ReactNode | | | diff --git a/packages/react-docs/pages/components/color-style/index.mdx b/packages/react-docs/pages/components/color-style/index.mdx index 8854a32247..74893786aa 100644 --- a/packages/react-docs/pages/components/color-style/index.mdx +++ b/packages/react-docs/pages/components/color-style/index.mdx @@ -16,7 +16,7 @@ import ColorStyleContent from '../../../components/ColorStyleContent'; import ColorStyleHeader from '../../../components/ColorStyleHeader'; import jsonPrettify from "../../../utils/json-prettify"; -# Color Style +# ColorStyleProvider Tonic UI comes with a color style system that defines functional color values. diff --git a/packages/react-docs/pages/components/color-style/useColorStyle.mdx b/packages/react-docs/pages/components/color-style/useColorStyle.mdx index 8c8705f418..23d813988f 100644 --- a/packages/react-docs/pages/components/color-style/useColorStyle.mdx +++ b/packages/react-docs/pages/components/color-style/useColorStyle.mdx @@ -46,34 +46,78 @@ Returns an array with the color style object and a function to set the color sty function Example() { const [colorMode] = useColorMode(); const [colorStyle, setColorStyle] = useColorStyle({ colorMode }); - const [colorVariant, setColorVariant] = React.useState('primary'); - const backgroundColor = colorStyle.background[colorVariant]; - const color = colorStyle.color[colorVariant]; - const changeColorVariant = (colorVariant) => (event) => { - setColorVariant(colorVariant); - }; + const invertedPrimaryColor = { + dark: 'black:primary', + light: 'white:primary', + }[colorMode]; return ( - <> - - - - - - + + Background + *': { + px: '3x', + py: '2x', + }, + }} + > + + colorStyle.background.primary + + + colorStyle.background.secondary + + + colorStyle.background.tertiary + + + colorStyle.background.inverted + + + colorStyle.background.highlighted + + + colorStyle.background.selected + - - - To change the color mode, click the toggle color mode button at the top right corner. - + + *': { + px: '3x', + }, + '> *:not(:last-child)': { + pb: '2x', + }, + }} + > + + colorStyle.color.primary + + + colorStyle.color.secondary + + + colorStyle.color.tertiary + + + colorStyle.color.disabled + + + colorStyle.color.success + + + colorStyle.color.info + + + colorStyle.color.warning + + + colorStyle.color.error + - + ); -}; +} ``` diff --git a/packages/react-docs/pages/components/tooltip.mdx b/packages/react-docs/pages/components/tooltip.mdx index 55b36c375f..64c505fad0 100644 --- a/packages/react-docs/pages/components/tooltip.mdx +++ b/packages/react-docs/pages/components/tooltip.mdx @@ -243,22 +243,22 @@ function Example() { backgroundColor={backgroundColor} color={color} > - Hover me + Hover Me - Tooltip Title + + Title - This is a tooltip + Content )} backgroundColor={backgroundColor} color={color} > - Hover me + Hover Me ); @@ -385,7 +385,7 @@ To mitigate this issue, you can pass `PopperProps={{ usePortal: true }}` to `Too PopperProps={{ usePortal: true }} label="This is a tooltip" > - Hover me + Hover Me ``` diff --git a/packages/react/__tests__/index.js b/packages/react/__tests__/index.js index 506704ce7d..a6f7546023 100644 --- a/packages/react/__tests__/index.js +++ b/packages/react/__tests__/index.js @@ -52,6 +52,7 @@ test('should match expected exports', () => { // color-mode 'ColorModeProvider', 'DarkMode', + 'InvertedMode', 'LightMode', 'useColorMode', diff --git a/packages/react/src/color-mode/DarkMode.js b/packages/react/src/color-mode/DarkMode.js index 67fa64a22f..5a0fafd043 100644 --- a/packages/react/src/color-mode/DarkMode.js +++ b/packages/react/src/color-mode/DarkMode.js @@ -1,12 +1,16 @@ -import React from 'react'; +import React, { forwardRef } from 'react'; import { Box } from '../box'; import ColorModeProvider from './ColorModeProvider'; -const DarkMode = (props) => ( +const DarkMode = forwardRef((props, ref) => ( - + -); +)); DarkMode.displayName = 'DarkMode'; diff --git a/packages/react/src/color-mode/InvertedMode.js b/packages/react/src/color-mode/InvertedMode.js new file mode 100644 index 0000000000..a27f659d40 --- /dev/null +++ b/packages/react/src/color-mode/InvertedMode.js @@ -0,0 +1,23 @@ +import React, { forwardRef } from 'react'; +import { Box } from '../box'; +import ColorModeProvider from './ColorModeProvider'; +import useColorMode from './useColorMode'; + +const InvertedMode = forwardRef((props, ref) => { + const [colorMode] = useColorMode(); + const invertedColorMode = colorMode === 'light' ? 'dark' : 'light'; + + return ( + + + + ); +}); + +InvertedMode.displayName = 'InvertedMode'; + +export default InvertedMode; diff --git a/packages/react/src/color-mode/LightMode.js b/packages/react/src/color-mode/LightMode.js index 86bdddfb03..5bdb6e4a89 100644 --- a/packages/react/src/color-mode/LightMode.js +++ b/packages/react/src/color-mode/LightMode.js @@ -1,12 +1,16 @@ -import React from 'react'; +import React, { forwardRef } from 'react'; import { Box } from '../box'; import ColorModeProvider from './ColorModeProvider'; -const LightMode = (props) => ( +const LightMode = forwardRef((props, ref) => ( - + -); +)); LightMode.displayName = 'LightMode'; diff --git a/packages/react/src/color-mode/__tests__/ColorModeProvider.test.js b/packages/react/src/color-mode/__tests__/ColorModeProvider.test.js index 76bc9f9ba0..72749acd12 100644 --- a/packages/react/src/color-mode/__tests__/ColorModeProvider.test.js +++ b/packages/react/src/color-mode/__tests__/ColorModeProvider.test.js @@ -18,8 +18,8 @@ Object.defineProperty(window, 'matchMedia', { })), }); -describe('', () => { - test('toggle color mode using a toggle button', async () => { +describe('ColorModeProvider', () => { + it('should toggle the color mode using a toggle button', async () => { const user = userEvent.setup(); const ToggleColorModeApp = () => { @@ -48,7 +48,7 @@ describe('', () => { expect(toggleColorModeButton).toHaveTextContent('dark'); }); - test('toggle color mode using the toggle function', () => { + it('should toggle the color mode using the toggle function', () => { const WrapperComponent = ({ children }) => { return ( ', () => { expect(result.current[0]).toEqual('dark'); }); - test('prefer useSystemColorMode over defaultValue', () => { + it('should prioritize the useSystemColorMode over defaultValue', () => { const getColorSchemeSpy = jest .spyOn(colorModeUtils, 'getColorScheme') .mockReturnValueOnce('dark'); @@ -94,7 +94,7 @@ describe('', () => { expect(result.current[0]).toEqual('dark'); }); - test('controlled color mode cannot be changed', () => { + it('should not change the current color mode when a controlled value is provided', () => { const WrapperComponent = ({ children }) => { return ( ', () => { expect(result.current[0]).toEqual('light'); }); - test('change color mode using the onChange callback', () => { + it('should change the color mode using the onChange callback', () => { const WrapperComponent = ({ children }) => { const [colorMode, setColorMode] = useState('light'); return ( diff --git a/packages/react/src/color-mode/__tests__/DarkMode.test.js b/packages/react/src/color-mode/__tests__/DarkMode.test.js index 09be70e61a..37acb121f1 100644 --- a/packages/react/src/color-mode/__tests__/DarkMode.test.js +++ b/packages/react/src/color-mode/__tests__/DarkMode.test.js @@ -1,38 +1,21 @@ import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { DarkMode, useColorMode } from '@tonic-ui/react/src'; -import React, { useCallback } from 'react'; +import { Box, DarkMode, useColorMode } from '@tonic-ui/react/src'; -const TestApp = () => { - const [colorMode, setColorMode] = useColorMode(); - const toggleColorMode = useCallback(() => { - setColorMode(colorMode === 'light' ? 'dark' : 'light'); - }, [colorMode, setColorMode]); - return ( - - ); -}; - -const getToggleColorModeButton = () => { - return screen.getByRole('button'); -}; - -describe('', () => { - test('always dark mode', async () => { - const user = userEvent.setup(); +describe('DarkMode', () => { + it('should render in dark mode', () => { + const TestComponent = () => { + const [colorMode] = useColorMode(); + return ( + {colorMode} + ); + }; render( - + ); - expect(getToggleColorModeButton()).toHaveTextContent('dark'); - - await user.click(getToggleColorModeButton()); - - expect(getToggleColorModeButton()).toHaveTextContent('dark'); + expect(screen.getByTestId('color-mode')).toHaveTextContent('dark'); }); }); diff --git a/packages/react/src/color-mode/__tests__/InvertedMode.test.js b/packages/react/src/color-mode/__tests__/InvertedMode.test.js new file mode 100644 index 0000000000..948e989cc8 --- /dev/null +++ b/packages/react/src/color-mode/__tests__/InvertedMode.test.js @@ -0,0 +1,48 @@ +import { render, screen } from '@testing-library/react'; +import { + Box, + DarkMode, + InvertedMode, + LightMode, + useColorMode, +} from '@tonic-ui/react/src'; + +describe('InvertedMode', () => { + it('should invert dark mode to light mode', () => { + const TestComponent = () => { + const [colorMode] = useColorMode(); + return ( + {colorMode} + ); + }; + + render( + + + + + + ); + + expect(screen.getByTestId('color-mode')).toHaveTextContent('light'); + }); + + it('should invert light mode to dark mode', () => { + const TestComponent = () => { + const [colorMode] = useColorMode(); + return ( + {colorMode} + ); + }; + + render( + + + + + + ); + + expect(screen.getByTestId('color-mode')).toHaveTextContent('dark'); + }); +}); diff --git a/packages/react/src/color-mode/__tests__/LightMode.test.js b/packages/react/src/color-mode/__tests__/LightMode.test.js index 29358946bf..6961bb61a5 100644 --- a/packages/react/src/color-mode/__tests__/LightMode.test.js +++ b/packages/react/src/color-mode/__tests__/LightMode.test.js @@ -1,38 +1,21 @@ import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { LightMode, useColorMode } from '@tonic-ui/react/src'; -import React, { useCallback } from 'react'; +import { Box, LightMode, useColorMode } from '@tonic-ui/react/src'; -const TestApp = () => { - const [colorMode, setColorMode] = useColorMode(); - const toggleColorMode = useCallback(() => { - setColorMode(colorMode === 'light' ? 'dark' : 'light'); - }, [colorMode, setColorMode]); - return ( - - ); -}; - -const getToggleColorModeButton = () => { - return screen.getByRole('button'); -}; - -describe('', () => { - test('always light mode', async () => { - const user = userEvent.setup(); +describe('LightMode', () => { + it('should render in light mode', () => { + const TestComponent = () => { + const [colorMode] = useColorMode(); + return ( + {colorMode} + ); + }; render( - + ); - expect(getToggleColorModeButton()).toHaveTextContent('light'); - - await user.click(getToggleColorModeButton()); - - expect(getToggleColorModeButton()).toHaveTextContent('light'); + expect(screen.getByTestId('color-mode')).toHaveTextContent('light'); }); }); diff --git a/packages/react/src/color-mode/index.js b/packages/react/src/color-mode/index.js index 962118c199..68b50c93f1 100644 --- a/packages/react/src/color-mode/index.js +++ b/packages/react/src/color-mode/index.js @@ -1,11 +1,13 @@ import ColorModeProvider from './ColorModeProvider'; import DarkMode from './DarkMode'; +import InvertedMode from './InvertedMode'; import LightMode from './LightMode'; import useColorMode from './useColorMode'; export { ColorModeProvider, DarkMode, + InvertedMode, LightMode, useColorMode, }; diff --git a/packages/react/src/color-style/__tests__/ColorStyleProvider.test.js b/packages/react/src/color-style/__tests__/ColorStyleProvider.test.js index ade29b2454..547c32b42f 100644 --- a/packages/react/src/color-style/__tests__/ColorStyleProvider.test.js +++ b/packages/react/src/color-style/__tests__/ColorStyleProvider.test.js @@ -1,16 +1,19 @@ import { renderHook, act } from '@testing-library/react'; import { + ColorModeProvider, ColorStyleProvider, colorStyle as defaultColorStyle, useColorStyle, } from '@tonic-ui/react/src'; import React, { useState } from 'react'; -describe('', () => { - test('color style for dark mode', () => { +describe('ColorStyleProvider', () => { + it('should return the correct color style based on the specified dark mode', () => { const colorMode = 'dark'; const WrapperComponent = ({ children }) => ( - + {children} ); @@ -20,10 +23,12 @@ describe('', () => { expect(colorStyle).toEqual(defaultColorStyle[colorMode]); }); - test('color style for light mode', () => { + it('should return the correct color style based on the specified light mode', () => { const colorMode = 'light'; const WrapperComponent = ({ children }) => ( - + {children} ); @@ -33,7 +38,45 @@ describe('', () => { expect(colorStyle).toEqual(defaultColorStyle[colorMode]); }); - test('controlled color style cannot be changed', () => { + it('should return the correct color style based on the current color mode - dark', () => { + const colorMode = 'dark'; + const WrapperComponent = ({ children }) => ( + + + {children} + + + ); + const { result } = renderHook(() => useColorStyle(), { wrapper: WrapperComponent }); + + const [colorStyle] = result.current; + expect(colorStyle).toEqual(defaultColorStyle[colorMode]); + }); + + it('should return the correct color style based on the current color mode - light', () => { + const colorMode = 'light'; + const WrapperComponent = ({ children }) => ( + + + {children} + + + ); + const { result } = renderHook(() => useColorStyle(), { wrapper: WrapperComponent }); + + const [colorStyle] = result.current; + expect(colorStyle).toEqual(defaultColorStyle[colorMode]); + }); + + it('should not change the current color style when a controlled value is provided', () => { const colorMode = 'dark'; const WrapperComponent = ({ children }) => ( ', () => { expect(result.current[0]).toEqual(defaultColorStyle[colorMode]); }); - test('change color style using the onChange callback', () => { + it('changes color style using the onChange callback', () => { const colorMode = 'dark'; const WrapperComponent = ({ children }) => { const [colorStyle, setColorStyle] = useState(defaultColorStyle); diff --git a/packages/react/src/color-style/color-style.js b/packages/react/src/color-style/color-style.js index fce3af3663..3b108b0906 100644 --- a/packages/react/src/color-style/color-style.js +++ b/packages/react/src/color-style/color-style.js @@ -4,7 +4,8 @@ const colorStyle = { primary: 'gray:100', secondary: 'gray:90', tertiary: 'gray:80', - inverse: 'gray:10', + inverted: 'gray:10', + inverse: 'gray:10', // alias for inverted highlighted: 'rgba(255, 255, 255, 0.12)', selected: 'rgba(255, 255, 255, 0.08)', }, @@ -73,7 +74,8 @@ const colorStyle = { primary: 'white:emphasis', secondary: 'gray:10', tertiary: 'gray:20', - inverse: 'gray:70', + inverted: 'gray:70', + inverse: 'gray:70', // alias for inverted highlighted: 'rgba(0, 0, 0, 0.12)', selected: 'rgba(0, 0, 0, 0.08)', }, diff --git a/packages/react/src/color-style/useColorStyle.js b/packages/react/src/color-style/useColorStyle.js index 1cc67edf27..f981e47829 100644 --- a/packages/react/src/color-style/useColorStyle.js +++ b/packages/react/src/color-style/useColorStyle.js @@ -1,9 +1,12 @@ import { ensurePlainObject } from 'ensure-type'; import { useContext } from 'react'; +import { ColorModeContext } from '../color-mode/context'; import { ColorStyleContext } from './context'; const useColorStyle = (options) => { - const { colorMode } = { ...options }; + const { colorMode: specifiedColorMode } = ensurePlainObject(options); + const { colorMode: currentColorMode } = ensurePlainObject(useContext(ColorModeContext)); + const colorMode = specifiedColorMode ?? currentColorMode; if (!useContext) { throw new Error('The `useContext` hook is not available with your React version.');