├── .devcontainer └── devcontainer.json ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── ci.yml │ ├── codeql-analysis.yml │ ├── npm-prepare-release.yml │ ├── npm-publish.yml │ └── stale.yml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .storybook ├── decorators │ ├── withBoundingBox.tsx │ ├── withColorMode.tsx │ └── withThemeProvider.tsx ├── main.ts └── preview.tsx ├── README.md ├── babel.config.js ├── docs ├── ARCHITECTURE.md ├── CONTRIBUTING.md ├── RELEASING.md ├── SETUP.md └── TESTING.md ├── package-lock.json ├── package.json ├── src ├── declaration.d.ts └── system │ ├── Accordion │ ├── Accordion.stories.tsx │ ├── Accordion.test.tsx │ ├── Accordion.tsx │ └── index.ts │ ├── Avatar │ ├── Avatar.stories.tsx │ ├── Avatar.test.tsx │ ├── Avatar.tsx │ └── index.ts │ ├── Badge │ ├── Badge.stories.tsx │ ├── Badge.test.tsx │ ├── Badge.tsx │ └── index.ts │ ├── Box │ ├── Box.stories.tsx │ ├── Box.tsx │ └── index.js │ ├── Breadcrumbs │ ├── Breadcrumbs.stories.tsx │ ├── Breadcrumbs.test.tsx │ ├── Breadcrumbs.tsx │ └── styles.ts │ ├── Button │ ├── Button.stories.tsx │ ├── Button.test.tsx │ ├── Button.tsx │ ├── ButtonSubmit.stories.tsx │ ├── ButtonSubmit.test.tsx │ ├── ButtonSubmit.tsx │ └── index.ts │ ├── Card │ ├── Card.stories.tsx │ ├── Card.test.tsx │ ├── Card.tsx │ └── index.ts │ ├── Code │ ├── Code.stories.tsx │ ├── Code.test.tsx │ ├── Code.tsx │ └── index.ts │ ├── ConfirmationDialog │ ├── ConfirmationDialog.js │ ├── ConfirmationDialog.stories.jsx │ └── index.js │ ├── Dialog │ ├── Dialog.js │ ├── Dialog.stories.jsx │ ├── DialogButton.js │ ├── DialogContent.js │ ├── DialogDivider.js │ ├── DialogMenu.js │ ├── DialogMenuItem.js │ ├── DialogTrigger.js │ └── index.js │ ├── Drawer │ ├── Drawer.stories.tsx │ ├── Drawer.test.tsx │ ├── Drawer.tsx │ └── styles.ts │ ├── Dropdown │ ├── Dropdown.stories.jsx │ ├── Dropdown.test.tsx │ ├── Dropdown.tsx │ ├── DropdownContent.tsx │ ├── DropdownItem.tsx │ ├── DropdownLabel.tsx │ ├── DropdownSeparator.tsx │ └── index.ts │ ├── FilterDropdown │ ├── FilterDropdown.stories.tsx │ ├── FilterDropdown.test.tsx │ └── FilterDropdown.tsx │ ├── Flex │ ├── Flex.stories.tsx │ ├── Flex.tsx │ └── index.ts │ ├── Footer │ ├── Footer.stories.tsx │ ├── Footer.test.tsx │ └── Footer.tsx │ ├── Form │ ├── Checkbox │ │ ├── Checkbox.stories.tsx │ │ ├── Checkbox.test.tsx │ │ ├── Checkbox.tsx │ │ └── styles.ts │ ├── Input.stories.tsx │ ├── Input.styles.ts │ ├── Input.tsx │ ├── InputWithCopyButton.js │ ├── InputWithCopyButton.stories.jsx │ ├── Label.stories.tsx │ ├── Label.tsx │ ├── Radio │ │ ├── Radio.stories.tsx │ │ ├── Radio.test.tsx │ │ ├── Radio.tsx │ │ ├── RadioOption.tsx │ │ └── styles.ts │ ├── RadioBoxGroup.js │ ├── RadioBoxGroup.stories.jsx │ ├── RadioBoxGroup.test.jsx │ ├── RadioGroupChip.stories.tsx │ ├── RadioGroupChip.test.tsx │ ├── RadioGroupChip.tsx │ ├── RequiredLabel.tsx │ ├── Textarea.js │ ├── Textarea.stories.jsx │ ├── Toggle.stories.tsx │ ├── Toggle.test.tsx │ ├── Toggle.tsx │ ├── ToggleRow.js │ ├── Validation.tsx │ └── index.js │ ├── Grid │ ├── Grid.stories.tsx │ ├── Grid.tsx │ └── index.ts │ ├── Heading │ ├── Heading.stories.tsx │ ├── Heading.tsx │ └── index.js │ ├── Hr │ ├── Hr.stories.tsx │ ├── Hr.test.tsx │ └── Hr.tsx │ ├── Link │ ├── Link.stories.tsx │ ├── Link.tsx │ └── index.ts │ ├── LinkExternal │ ├── LinkExternal.stories.tsx │ ├── LinkExternal.test.tsx │ └── LinkExternal.tsx │ ├── MobileMenu │ ├── MobileMenu.stories.tsx │ ├── MobileMenu.test.tsx │ └── MobileMenu.tsx │ ├── Nav │ ├── Nav.stories.tsx │ ├── Nav.test.tsx │ ├── Nav.tsx │ ├── NavItem.tsx │ ├── NavItemGroup.test.tsx │ ├── NavItemGroup.tsx │ ├── styles.ts │ └── styles │ │ └── variants │ │ ├── breadcrumbs.ts │ │ ├── menu.ts │ │ ├── menugroup.ts │ │ ├── primary.ts │ │ ├── tabs.ts │ │ └── toolbar.ts │ ├── NewConfirmationDialog │ ├── NewConfirmationDialog.js │ ├── NewConfirmationDialog.stories.jsx │ ├── NewConfirmationDialog.test.js │ └── index.js │ ├── NewDialog │ ├── DialogClose.test.tsx │ ├── DialogClose.tsx │ ├── DialogDescription.test.js │ ├── DialogDescription.tsx │ ├── DialogOverlay.test.js │ ├── DialogOverlay.tsx │ ├── DialogTitle.test.js │ ├── DialogTitle.tsx │ ├── NewDialog.stories.jsx │ ├── NewDialog.tsx │ ├── index.ts │ └── styles.ts │ ├── NewForm │ ├── Fieldset.tsx │ ├── Form.tsx │ ├── FormAutocomplete.css │ ├── FormAutocomplete.js │ ├── FormAutocomplete.stories.jsx │ ├── FormAutocomplete.test.js │ ├── FormAutocompleteMultiselect.js │ ├── FormAutocompleteMultiselect.stories.jsx │ ├── FormAutocompleteMultiselect.test.js │ ├── FormSelect.js │ ├── FormSelect.stories.jsx │ ├── FormSelect.test.js │ ├── FormSelectArrow.js │ ├── FormSelectContent.js │ ├── FormSelectInline.js │ ├── FormSelectLoading.js │ ├── FormSelectSearch.js │ ├── Legend.tsx │ └── index.js │ ├── Notice │ ├── Notice.stories.tsx │ ├── Notice.tsx │ └── index.ts │ ├── OptionRow │ ├── OptionRow.js │ ├── OptionRow.stories.jsx │ ├── OptionRow.test.js │ └── index.js │ ├── Page │ ├── Page.test.tsx │ └── Page.tsx │ ├── Progress │ ├── Progress.stories.tsx │ ├── Progress.test.tsx │ ├── Progress.tsx │ └── index.ts │ ├── ScreenReaderText │ ├── ScreenReader.test.js │ ├── ScreenReaderText.tsx │ └── index.js │ ├── Skeleton │ ├── Skeleton.stories.tsx │ ├── Skeleton.tsx │ └── index.ts │ ├── Snackbar │ ├── Snackbar.stories.tsx │ ├── Snackbar.test.tsx │ ├── Snackbar.tsx │ └── index.ts │ ├── Spinner │ ├── Spinner.stories.tsx │ ├── Spinner.test.tsx │ ├── Spinner.tsx │ └── index.ts │ ├── Table │ ├── Table.stories.tsx │ ├── Table.tsx │ ├── TableCell.tsx │ ├── TableRow.tsx │ └── index.ts │ ├── Tabs │ ├── Tabs.js │ ├── Tabs.stories.jsx │ ├── TabsContent.js │ ├── TabsList.js │ ├── TabsTrigger.js │ └── index.js │ ├── Text │ ├── Text.stories.tsx │ ├── Text.tsx │ └── index.ts │ ├── Toolbar │ ├── Logo.tsx │ ├── Toolbar.stories.tsx │ ├── Toolbar.test.tsx │ ├── Toolbar.tsx │ ├── ToolbarUtilNav.tsx │ └── index.tsx │ ├── Tooltip │ ├── Tooltip.css │ ├── Tooltip.stories.tsx │ ├── Tooltip.tsx │ └── index.ts │ ├── Wizard │ ├── Wizard.stories.tsx │ ├── Wizard.tsx │ ├── WizardStep.tsx │ └── index.ts │ ├── assets │ └── a8cLogo.tsx │ ├── index.js │ ├── theme │ ├── breakpoints.ts │ ├── colors.js │ ├── generated │ │ ├── valet-theme-dark.json │ │ └── valet-theme-light.json │ ├── getPropValue.js │ └── index.js │ └── utils │ ├── random.js │ └── stories │ └── CustomLink.tsx ├── test ├── fileMock.ts └── setupAfterEnv.ts ├── tokens ├── utilities │ ├── .DS_Store │ ├── colors │ │ ├── color output │ │ │ ├── blue.json │ │ │ ├── gold.json │ │ │ ├── gray.json │ │ │ ├── green.json │ │ │ ├── orange.json │ │ │ ├── pink.json │ │ │ ├── red.json │ │ │ ├── salmon.json │ │ │ └── yellow.json │ │ ├── colorOutput.json │ │ ├── color_3d_plot.js │ │ ├── color_graph.js │ │ ├── colors.json │ │ ├── index.js │ │ ├── package-lock.json │ │ └── package.json │ └── figma-type-calculator │ │ └── responsive-type.js └── valet-core │ ├── $metadata.json │ ├── $themes.json │ ├── valet-core.json │ ├── wpvip-product-core.json │ └── wpvip-product-dark.json ├── tsconfig.definition.json └── tsconfig.json /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "mcr.microsoft.com/vscode/devcontainers/javascript-node:0-18", 3 | "customizations": { 4 | "vscode": { 5 | "settings": { 6 | "extensions.ignoreRecommendations": true, 7 | "git.autofetch": true, 8 | "editor.defaultFormatter": "esbenp.prettier-vscode", 9 | "[svg]": { 10 | "editor.defaultFormatter": "jock.svg" 11 | }, 12 | "prettier.prettierPath": "./node_modules/.bin/prettier", 13 | "editor.formatOnSave": true 14 | }, 15 | "extensions": [ 16 | "dbaeumer.vscode-eslint", 17 | "deque-systems.vscode-axe-linter", 18 | "eamodio.gitlens", 19 | "editorconfig.editorconfig", 20 | "esbenp.prettier-vscode", 21 | "github.vscode-pull-request-github", 22 | "ionutvmi.path-autocomplete", 23 | "jock.svg", 24 | "styled-components.vscode-styled-components", 25 | "unifiedjs.vscode-mdx", 26 | "usernamehw.errorlens" 27 | ] 28 | } 29 | }, 30 | "features": { 31 | "git": "latest", 32 | "github-cli": "latest" 33 | }, 34 | "forwardPorts": [ 6006 ], 35 | "onCreateCommand": "npm install", 36 | "portsAttributes": { 37 | "6006": { 38 | "label": "Storybook", 39 | "onAutoForward": "openPreview" 40 | } 41 | }, 42 | "postAttachCommand": "npm run dev" 43 | } 44 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = tab 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.{json,yaml,yml}] 11 | indent_style = space 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .git 2 | build 3 | coverage 4 | flow-typed 5 | jest_0 6 | node_modules 7 | storybook-static 8 | tokens/ 9 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | require( '@automattic/eslint-plugin-wpvip/init' ); 2 | 3 | module.exports = { 4 | extends: [ 5 | 'plugin:@automattic/wpvip/recommended', 6 | 'plugin:@automattic/wpvip/weak-javascript', 7 | 'plugin:@automattic/wpvip/weak-testing', 8 | 'plugin:storybook/recommended', 9 | ], 10 | globals: { 11 | alert: true, 12 | window: true, 13 | }, 14 | rules: { 15 | complexity: 'off', 16 | 'id-length': 'off', 17 | 'import/no-extraneous-dependencies': 'off', 18 | 'jsx-a11y/click-events-have-key-events': 'off', 19 | '@typescript-eslint/no-unsafe-call': 'off', 20 | '@typescript-eslint/no-unsafe-member-access': 'off', 21 | '@typescript-eslint/no-unsafe-assignment': 'off', 22 | 'jsx-a11y/no-autofocus': 'off', 23 | 'jsx-a11y/no-static-element-interactions': 'off', 24 | 'no-prototype-builtins': 'off', 25 | 'prettier/prettier': 'error', 26 | 'react/no-unknown-property': 'off', 27 | 'react-hooks/exhaustive-deps': 'off', 28 | 'security/detect-object-injection': 'off', 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | A few sentences describing the overall goals of the pull request. Any special considerations and decisions. 4 | 5 | Also include any references to relevant discussions and documentation. 6 | 7 | ## Checklist 8 | 9 | - [ ] This PR has good automated test coverage 10 | - [ ] The storybook for the component has been updated 11 | 12 | ## Steps to Test 13 | 14 | Outline the steps to test and verify the PR here. 15 | 16 | Example: 17 | 18 | 1. Pull down PR. 19 | 1. `npm run dev`. 20 | 1. Open Storybook. 21 | 1. Eat cookies. 22 | 1. Verify cookies are delicious. 23 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - trunk 8 | workflow_dispatch: 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | lint: 19 | name: Lint and check types 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Setup and install 23 | uses: Automattic/vip-actions/nodejs-setup@trunk 24 | with: 25 | node-version-file: .nvmrc 26 | 27 | - name: Run linter 28 | run: npm run lint 29 | 30 | - name: Check formatting 31 | run: npm run format:check 32 | 33 | - name: Check types 34 | run: npm run check-types 35 | 36 | dependaban: 37 | name: Dependaban 38 | runs-on: ubuntu-latest 39 | steps: 40 | - uses: Automattic/vip-actions/dependaban@trunk 41 | 42 | jest: 43 | name: Jest tests 44 | needs: [lint] 45 | runs-on: ubuntu-latest 46 | steps: 47 | - name: Setup and install 48 | uses: Automattic/vip-actions/nodejs-setup@trunk 49 | with: 50 | node-version-file: .nvmrc 51 | 52 | - name: Run Tests 53 | run: npm run jest 54 | 55 | build: 56 | name: Build 57 | needs: [lint] 58 | runs-on: ubuntu-latest 59 | steps: 60 | - name: Setup and install 61 | uses: Automattic/vip-actions/nodejs-setup@trunk 62 | with: 63 | node-version-file: .nvmrc 64 | 65 | - name: Build 66 | run: npm run build 67 | -------------------------------------------------------------------------------- /.github/workflows/npm-prepare-release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Prepare new npm release 3 | 4 | on: 5 | workflow_dispatch: 6 | inputs: 7 | npm-version-type: 8 | description: 'The npm version type we are publishing.' 9 | required: true 10 | type: choice 11 | default: 'patch' 12 | options: 13 | - patch 14 | - minor 15 | - major 16 | 17 | jobs: 18 | prepare: 19 | name: Prepare a new npm release 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Check out the source code 23 | uses: actions/checkout@v3 24 | 25 | - name: Run npm-prepare-release 26 | uses: Automattic/vip-actions/npm-prepare-release@v0.1.2 27 | with: 28 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | npm-version-type: ${{ inputs.npm-version-type }} 30 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to npm (if applicable) 2 | 3 | on: 4 | pull_request: 5 | types: [closed] 6 | 7 | jobs: 8 | publish: 9 | name: Publish to npm 10 | runs-on: ubuntu-latest 11 | if: contains( github.event.pull_request.labels.*.name, '[ Type ] NPM version update' ) && startsWith( github.head_ref, 'release/') && github.event.pull_request.merged == true 12 | steps: 13 | - uses: Automattic/vip-actions/npm-publish@v0.1.2 14 | with: 15 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Stale monitor 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | 7 | jobs: 8 | stale: 9 | name: Stale 10 | runs-on: ubuntu-latest 11 | permissions: 12 | issues: write 13 | pull-requests: write 14 | steps: 15 | - uses: Automattic/vip-actions/stale@trunk 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | storybook-static 4 | coverage -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18.16.1 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | coverage 3 | node_modules 4 | storybook-static 5 | src/system/theme/generated 6 | tokens/ 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | "@automattic/eslint-plugin-wpvip/prettierrc" 2 | -------------------------------------------------------------------------------- /.storybook/decorators/withBoundingBox.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { makeDecorator } from '@storybook/addons'; 3 | import { Box } from '../../src/system'; 4 | import { Global, css } from '@emotion/react'; 5 | 6 | export default makeDecorator( { 7 | name: 'withBoundingBox', 8 | parameterName: 'colorMode2', 9 | wrapper: storyFn => ( 10 | <> 11 | 34 | { storyFn() } 35 | 36 | ), 37 | } ); 38 | -------------------------------------------------------------------------------- /.storybook/decorators/withColorMode.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | 5 | import React, { useEffect } from 'react'; 6 | import { makeDecorator } from '@storybook/addons'; 7 | import { useColorMode } from 'theme-ui'; 8 | 9 | /** 10 | * Internal dependencies 11 | */ 12 | import ThemeBuilder from '../../src/system/theme/getPropValue'; 13 | 14 | import Valet from '../../src/system/theme/generated/valet-theme-light.json'; 15 | import ValetDark from '../../src/system/theme/generated/valet-theme-dark.json'; 16 | const { getPropValue } = ThemeBuilder( Valet ); 17 | const { getPropValue: getPropValueDark } = ThemeBuilder( ValetDark ); 18 | 19 | // These need to be updated to import VIP design tokens; 20 | const lightBackground = getPropValue( 'background', 'primary' ); 21 | const darkBackground = getPropValueDark( 'background', 'primary' ); 22 | 23 | export const backgrounds = { 24 | default: 'Light', 25 | values: [ 26 | { 27 | name: 'Light', 28 | value: lightBackground, 29 | }, 30 | { 31 | name: 'Dark', 32 | value: darkBackground, 33 | }, 34 | ], 35 | }; 36 | 37 | function ThemeChanger( { background } ) { 38 | const [ _, setColorMode ] = useColorMode(); 39 | const newColorMode = darkBackground === background ? 'dark' : 'default'; 40 | 41 | useEffect( () => { 42 | setColorMode( newColorMode ); 43 | }, [ newColorMode ] ); 44 | 45 | return null; 46 | } 47 | 48 | export default makeDecorator( { 49 | name: 'withColorMode', 50 | parameterName: 'colorMode', 51 | wrapper: ( storyFn, context ) => { 52 | return ( 53 | <> 54 | 55 | { storyFn() } 56 | 57 | ); 58 | }, 59 | } ); 60 | -------------------------------------------------------------------------------- /.storybook/decorators/withThemeProvider.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { makeDecorator } from '@storybook/addons'; 3 | import { ThemeUIProvider } from 'theme-ui'; 4 | import { Box, theme } from '../../src/system'; 5 | 6 | export default makeDecorator( { 7 | name: 'withThemeProvider', 8 | parameterName: 'themeUi', 9 | wrapper: ( storyFn, context ) => { 10 | return { storyFn( context ) }; 11 | }, 12 | } ); 13 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from '@storybook/react-webpack5'; 2 | 3 | const config: StorybookConfig = { 4 | stories: [ '../src/**/*.stories.[jt]sx' ], 5 | addons: [ 6 | '@storybook/addon-a11y', 7 | '@storybook/addon-docs', 8 | '@storybook/addon-essentials', 9 | '@storybook/addon-links', 10 | '@storybook/addon-controls', 11 | '@storybook/addon-storysource', 12 | ], 13 | docs: { 14 | autodocs: true, 15 | }, 16 | framework: { 17 | name: '@storybook/react-webpack5', 18 | options: {}, 19 | }, 20 | }; 21 | 22 | export default config; 23 | -------------------------------------------------------------------------------- /.storybook/preview.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import withBoundingBox from './decorators/withBoundingBox'; 3 | import withColorMode, { backgrounds } from './decorators/withColorMode'; 4 | import withThemeProvider from './decorators/withThemeProvider'; 5 | 6 | import { Title, Subtitle, Description, Controls, Stories } from '@storybook/blocks'; 7 | 8 | export const decorators = [ withBoundingBox, withColorMode, withThemeProvider ]; 9 | 10 | export const parameters = { 11 | actions: { argTypesRegex: '^on[A-Z].*' }, 12 | controls: { expanded: true }, 13 | backgrounds, 14 | docs: { 15 | page: () => ( 16 | <> 17 | 18 | <Subtitle /> 19 | <Description /> 20 | <Controls /> 21 | <Stories /> 22 | </> 23 | ), 24 | canvas: { 25 | sourceState: 'shown', 26 | }, 27 | }, 28 | options: { 29 | storySort: { 30 | method: 'alphabetical', 31 | order: [ '*', 'Deprecated' ], 32 | }, 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VIP Design System 2 | 3 | ![GitHub Actions](https://github.com/Automattic/vip-design-system/actions/workflows/ci.yml/badge.svg) 4 | 5 | This is the main repository to store Design system tokens and components used throughout VIP projects. 6 | 7 | ## Further documentation 8 | 9 | - [SETUP.md](https://github.com/Automattic/vip-design-system/blob/trunk/docs/SETUP.md) for installation and setup instructions, basic usage and environmental variables. 10 | - [ARCHITECTURE.md](https://github.com/Automattic/vip-design-system/blob/trunk/docs/ARCHITECTURE.md) for information on architecture, code structure, data storage and security. 11 | - [CONTRIBUTING.md](https://github.com/Automattic/vip-design-system/blob/trunk/docs/CONTRIBUTING.md) for information on how to contribute patches and features, also issue and pull request labels. 12 | - [TESTING.md](https://github.com/Automattic/vip-design-system/blob/trunk/docs/TESTING.md) for details on testing the software and individual tasks. 13 | - [RELEASING.md](https://github.com/Automattic/vip-design-system/blob/trunk/docs/RELEASING.md) for details on deploying a new release. 14 | 15 | ## Health, logs and monitoring 16 | 17 | - [React Components Website](https://vip-design-system-components.netlify.app/) 18 | - [Figma Productive Components](https://www.figma.com/file/jcGe2KIAlh2PxaAZ5liYWi/Productive-Components?type=design&node-id=7378-4230&mode=design&t=QUgpLoxTpJvAfTiN-0) 19 | 20 | ## Links to historical documents 21 | 22 | For VIP folks: Please see internal documentation on vip-design-system regarding historical documents (search for vip-design-system). 23 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function ( api ) { 2 | const isTest = api.env( 'test' ); 3 | return { 4 | ignore: [], 5 | plugins: [], 6 | presets: [ 7 | [ 8 | '@babel/preset-env', 9 | { 10 | loose: true, 11 | modules: isTest ? 'auto' : false, 12 | }, 13 | ], 14 | [ 15 | '@babel/preset-react', 16 | { 17 | importSource: 'theme-ui', 18 | runtime: 'automatic', 19 | throwIfNamespace: false, 20 | }, 21 | ], 22 | '@babel/preset-typescript', 23 | ], 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /src/declaration.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css'; 2 | -------------------------------------------------------------------------------- /src/system/Accordion/Accordion.stories.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | /** 4 | * External dependencies 5 | */ 6 | import { BiBookContent } from 'react-icons/bi'; 7 | import { RiUserAddLine, RiCodeSSlashFill } from 'react-icons/ri'; 8 | 9 | /** 10 | * Internal dependencies 11 | */ 12 | import { Box, Accordion } from '..'; 13 | 14 | export default { 15 | title: 'Accordion', 16 | component: Accordion, 17 | }; 18 | 19 | const ExampleContent = () => ( 20 | <Box> 21 | <p sx={ { mt: 0 } }>Add your key team members to the VIP Dashboard.</p> 22 | <p>Add developers to GitHub.</p> 23 | <p sx={ { mb: 0 } }>Add content editors and developers to WordPress admin.</p> 24 | </Box> 25 | ); 26 | 27 | const ExampleAccordion = () => ( 28 | <Accordion.Root defaultValue="teamPermissions" sx={ { width: '250px' } }> 29 | <Accordion.Item value="teamPermissions"> 30 | <Accordion.TriggerWithIcon 31 | icon={ <RiUserAddLine sx={ { color: 'support.accent.success' } } /> } 32 | > 33 | Team & Permissions 34 | </Accordion.TriggerWithIcon> 35 | <Accordion.Content> 36 | <ExampleContent /> 37 | </Accordion.Content> 38 | </Accordion.Item> 39 | <Accordion.Item value="addContentMedia"> 40 | <Accordion.TriggerWithIcon 41 | icon={ <BiBookContent sx={ { color: 'support.accent.success' } } /> } 42 | > 43 | Add Content & Media 44 | </Accordion.TriggerWithIcon> 45 | <Accordion.Content> 46 | <ExampleContent /> 47 | </Accordion.Content> 48 | </Accordion.Item> 49 | <Accordion.Item value="addCode"> 50 | <Accordion.TriggerWithIcon 51 | icon={ <RiCodeSSlashFill sx={ { color: 'support.accent.success' } } /> } 52 | > 53 | Add Code 54 | </Accordion.TriggerWithIcon> 55 | <Accordion.Content> 56 | <ExampleContent /> 57 | </Accordion.Content> 58 | </Accordion.Item> 59 | </Accordion.Root> 60 | ); 61 | 62 | export const Default = () => <ExampleAccordion />; 63 | 64 | export const WithLargeText = () => ( 65 | <Box sx={ { '.vip-heading-component > button': { fontSize: 4 } } }> 66 | <ExampleAccordion /> 67 | </Box> 68 | ); 69 | -------------------------------------------------------------------------------- /src/system/Accordion/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import * as Accordion from './Accordion'; 5 | 6 | export { Accordion }; 7 | -------------------------------------------------------------------------------- /src/system/Avatar/Avatar.stories.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { Avatar } from '..'; 5 | 6 | import type { StoryObj } from '@storybook/react'; 7 | 8 | /** 9 | * Internal dependencies 10 | */ 11 | 12 | export default { 13 | title: 'Avatar', 14 | component: Avatar, 15 | }; 16 | 17 | type Story = StoryObj< typeof Avatar >; 18 | 19 | const COMMON_SIZES = [ 128, 64, 48, 32, 24, 16 ]; 20 | 21 | export const Default = () => ( 22 | <> 23 | { COMMON_SIZES.map( size => ( 24 | <Avatar key={ size } size={ size } src="https://i.pravatar.cc/100" /> 25 | ) ) } 26 | </> 27 | ); 28 | export const WithName: Story = { 29 | args: { 30 | name: 'Kitty', 31 | size: 30, 32 | sx: { 33 | backgroundColor: '#D8A45F', 34 | }, 35 | }, 36 | }; 37 | export const WithAbbreviation: Story = { 38 | args: { 39 | name: 'Taylor Swift', 40 | abbr: 'TS', 41 | size: 64, 42 | sx: { 43 | backgroundColor: '#D8A45F', 44 | }, 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /src/system/Avatar/Avatar.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { render, screen } from '@testing-library/react'; 5 | import { axe } from 'jest-axe'; 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import { Avatar } from './Avatar'; 11 | 12 | describe( '<Avatar />', () => { 13 | it( 'renders the Avatar without an image', async () => { 14 | const { container } = render( <Avatar name="John Doe" /> ); 15 | 16 | expect( screen.getByText( 'J' ) ).toBeInTheDocument(); 17 | 18 | // Check for accessibility issues 19 | expect( await axe( container ) ).toHaveNoViolations(); 20 | } ); 21 | 22 | it( 'renders the Avatar with image', async () => { 23 | const { container } = render( <Avatar name="John Doe" src="path/to/image" /> ); 24 | 25 | expect( screen.getByAltText( 'Avatar image from John Doe' ) ).toBeInTheDocument(); 26 | 27 | // Check for accessibility issues 28 | expect( await axe( container ) ).toHaveNoViolations(); 29 | } ); 30 | } ); 31 | -------------------------------------------------------------------------------- /src/system/Avatar/Avatar.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import classNames, { Argument } from 'classnames'; 5 | import { forwardRef, Ref } from 'react'; 6 | import { Image, ImageProps, ThemeUIStyleObject } from 'theme-ui'; 7 | 8 | /** 9 | * Internal dependencies 10 | */ 11 | import { Box, Text } from '..'; 12 | 13 | export interface AvatarProps { 14 | size?: number; 15 | src?: string; 16 | name?: string; 17 | abbr?: string; 18 | className?: Argument; 19 | sx?: ThemeUIStyleObject; 20 | } 21 | 22 | type AvatarImageProps = AvatarProps & ImageProps; 23 | 24 | export const Avatar = forwardRef< HTMLElement, AvatarImageProps >( 25 | ( 26 | { name, size = 32, src, className, sx = {}, abbr, ...props }: AvatarImageProps, 27 | ref: Ref< HTMLElement > 28 | ) => { 29 | const displayName = name && ! abbr ? name.charAt( 0 ) : abbr; 30 | 31 | return ( 32 | <Box 33 | sx={ { 34 | borderRadius: '100%', 35 | height: size, 36 | width: size, 37 | overflow: 'hidden', 38 | border: 'none', 39 | display: 'inline-flex', 40 | alignItems: 'center', 41 | justifyContent: 'center', 42 | color: 'inverse', 43 | textAlign: 'center', 44 | ...sx, 45 | } } 46 | className={ classNames( 'vip-avatar-component', className ) } 47 | aria-hidden="true" 48 | ref={ ref } 49 | { ...props } 50 | > 51 | { src ? ( 52 | <Image 53 | src={ src } 54 | alt={ `Avatar image from ${ name }` } 55 | sx={ { 56 | borderRadius: '100%', 57 | width: '100%', 58 | display: 'block', 59 | } } 60 | /> 61 | ) : ( 62 | <Text 63 | as="span" 64 | sx={ { 65 | color: 'icon.inverse', 66 | mb: 0, 67 | fontWeight: 'bold', 68 | fontSize: 2, 69 | textTransform: 'uppercase', 70 | } } 71 | > 72 | { displayName } 73 | </Text> 74 | ) } 75 | </Box> 76 | ); 77 | } 78 | ); 79 | 80 | Avatar.displayName = 'Avatar'; 81 | -------------------------------------------------------------------------------- /src/system/Avatar/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | export { Avatar } from './Avatar'; 5 | -------------------------------------------------------------------------------- /src/system/Badge/Badge.stories.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { Badge, Link } from '..'; 5 | 6 | import type { StoryObj } from '@storybook/react'; 7 | 8 | /** 9 | * Internal dependencies 10 | */ 11 | 12 | export default { 13 | component: Badge, 14 | title: 'Badge', 15 | }; 16 | 17 | type Story = StoryObj< typeof Badge >; 18 | 19 | export const Default: Story = { 20 | args: { 21 | children: 'Badge', 22 | sx: undefined, 23 | }, 24 | }; 25 | 26 | export const Variants = () => ( 27 | <> 28 | <Badge variant="blue" sx={ { m: 2 } }> 29 | Blue 30 | </Badge> 31 | <Badge variant="gold" sx={ { m: 2 } }> 32 | Gold 33 | </Badge> 34 | <Badge variant="gray" sx={ { m: 2 } }> 35 | Gray 36 | </Badge> 37 | <Badge variant="green" sx={ { m: 2 } }> 38 | Green 39 | </Badge> 40 | <Badge variant="orange" sx={ { m: 2 } }> 41 | Orange 42 | </Badge> 43 | <Badge variant="red" sx={ { m: 2 } }> 44 | Red 45 | </Badge> 46 | <Badge variant="salmon" sx={ { m: 2 } }> 47 | Salmon 48 | </Badge> 49 | <Badge variant="yellow" sx={ { m: 2 } }> 50 | Yellow 51 | </Badge> 52 | </> 53 | ); 54 | 55 | export const WithLink: Story = { 56 | args: {}, 57 | render: args => <Badge children={ <Link href="https://google.com">Google</Link> } { ...args } />, 58 | }; 59 | -------------------------------------------------------------------------------- /src/system/Badge/Badge.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { render, screen } from '@testing-library/react'; 5 | import { axe } from 'jest-axe'; 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import { Badge } from './Badge'; 11 | 12 | describe( '<Badge />', () => { 13 | it( 'renders the Badge component', async () => { 14 | const { container } = render( <Badge>Badge text</Badge> ); 15 | 16 | expect( screen.getByText( 'Badge text' ) ).toBeInTheDocument(); 17 | 18 | // Check for accessibility issues 19 | expect( await axe( container ) ).toHaveNoViolations(); 20 | } ); 21 | 22 | it( 'renders the Badge component with a different variant', async () => { 23 | const { container } = render( <Badge variant="red">Badge text</Badge> ); 24 | 25 | expect( screen.getByText( 'Badge text' ) ).toBeInTheDocument(); 26 | 27 | // Check for accessibility issues 28 | expect( await axe( container ) ).toHaveNoViolations(); 29 | } ); 30 | } ); 31 | -------------------------------------------------------------------------------- /src/system/Badge/Badge.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import classNames from 'classnames'; 5 | import { forwardRef, Ref } from 'react'; 6 | import { TextProps as ThemeTextProps } from 'theme-ui'; 7 | 8 | /** 9 | * Internal dependencies 10 | */ 11 | import { Text } from '..'; 12 | 13 | export interface BadgeProps extends ThemeTextProps { 14 | variant?: 'blue' | 'gold' | 'gray' | 'green' | 'orange' | 'red' | 'salmon' | 'yellow'; 15 | } 16 | 17 | export const Badge = forwardRef< HTMLDivElement, BadgeProps >( 18 | ( { variant = 'blue', sx, className, ...props }: BadgeProps, ref: Ref< HTMLDivElement > ) => ( 19 | <Text 20 | as="span" 21 | sx={ { 22 | fontSize: 0, 23 | padding: 0, // do we need padding declared twice here? 24 | bg: `tag.${ variant }.background`, 25 | color: `tag.${ variant }.text`, 26 | py: 1, 27 | verticalAlign: 'middle', 28 | px: 2, 29 | display: 'inline-block', 30 | borderRadius: 1, 31 | fontWeight: 'heading', 32 | a: { 33 | color: `tag.${ variant }.text`, 34 | '&:hover, &:focus, &:active': { 35 | textDecoration: 'none', 36 | }, 37 | '&:active, &:visited': { 38 | color: `tag.${ variant }.text`, 39 | }, 40 | }, 41 | ...sx, 42 | } } 43 | className={ classNames( 'vip-badge-component', className ) } 44 | ref={ ref } 45 | { ...props } 46 | /> 47 | ) 48 | ); 49 | 50 | Badge.displayName = 'Badge'; 51 | -------------------------------------------------------------------------------- /src/system/Badge/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | export { Badge } from './Badge'; 5 | -------------------------------------------------------------------------------- /src/system/Box/Box.stories.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { Box } from '..'; 5 | 6 | import type { StoryObj } from '@storybook/react'; 7 | 8 | /** 9 | * Internal dependencies 10 | */ 11 | 12 | export default { 13 | title: 'Box', 14 | component: Box, 15 | }; 16 | 17 | type Story = StoryObj< typeof Box >; 18 | 19 | export const Default: Story = { 20 | args: { 21 | children: 'Hello', 22 | sx: undefined, 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /src/system/Box/Box.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import classNames from 'classnames'; 5 | import { forwardRef, Ref } from 'react'; 6 | import { Box as ThemeBox, BoxProps as ThemeBoxProps } from 'theme-ui'; 7 | 8 | export const Box = forwardRef< HTMLElement, ThemeBoxProps >( 9 | ( props: ThemeBoxProps, ref: Ref< HTMLElement > ) => ( 10 | <ThemeBox 11 | className={ classNames( 'vip-box-component', props.className ) } 12 | ref={ ref } 13 | { ...props } 14 | /> 15 | ) 16 | ); 17 | 18 | Box.displayName = 'Box'; 19 | -------------------------------------------------------------------------------- /src/system/Box/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { Box } from './Box'; 5 | 6 | export { Box }; 7 | -------------------------------------------------------------------------------- /src/system/Breadcrumbs/styles.ts: -------------------------------------------------------------------------------- 1 | import { ThemeUIStyleObject } from 'theme-ui'; 2 | 3 | import { linkUnderlineProperties } from '../Link/Link'; 4 | import { breadcrumbsLinkStyles } from '../Nav/styles/variants/breadcrumbs'; 5 | 6 | export const smallestScreenItemStyles = { 7 | '&::before': { 8 | display: 'inline-block', 9 | margin: 0, 10 | mr: 1, 11 | transform: 'rotate(0deg)', 12 | border: 'none', 13 | color: 'link', 14 | height: '0.8em', 15 | content: "'←'", 16 | }, 17 | }; 18 | 19 | export const collapsibleSeparatorStyles: ThemeUIStyleObject = { 20 | all: 'unset', 21 | ...breadcrumbsLinkStyles, 22 | ...linkUnderlineProperties, 23 | }; 24 | -------------------------------------------------------------------------------- /src/system/Button/Button.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { fireEvent, render, screen } from '@testing-library/react'; 5 | import { axe } from 'jest-axe'; 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import { Button } from './Button'; 11 | 12 | const BUTTON_TEXT = 'Button Text'; 13 | 14 | describe( '<Button />', () => { 15 | it( 'renders the Button component', async () => { 16 | const onClick = jest.fn( () => {} ); 17 | const { container } = render( <Button onClick={ onClick }>{ BUTTON_TEXT }</Button> ); 18 | const component = screen.getByText( BUTTON_TEXT ); 19 | 20 | expect( component ).toBeInTheDocument(); 21 | 22 | fireEvent.click( component ); 23 | expect( onClick ).toHaveBeenCalledTimes( 1 ); 24 | 25 | // Check for accessibility issues 26 | expect( await axe( container ) ).toHaveNoViolations(); 27 | } ); 28 | 29 | it( 'renders the Button with disabled prop', async () => { 30 | const onClick = jest.fn( () => {} ); 31 | const { container } = render( 32 | <Button disabled onClick={ onClick }> 33 | { BUTTON_TEXT } 34 | </Button> 35 | ); 36 | 37 | const component = screen.getByText( BUTTON_TEXT ); 38 | 39 | expect( component ).toBeInTheDocument(); 40 | expect( component ).toHaveAttribute( 'disabled', '' ); 41 | expect( component ).not.toHaveAttribute( 'aria-disabled' ); 42 | 43 | fireEvent.click( component ); 44 | expect( onClick ).toHaveBeenCalledTimes( 0 ); 45 | 46 | // Check for accessibility issues 47 | expect( await axe( container ) ).toHaveNoViolations(); 48 | } ); 49 | 50 | it( 'renders the Button with aria-disabled prop', async () => { 51 | const onClick = jest.fn( () => {} ); 52 | const { container } = render( 53 | <Button disabled preferAriaDisabled onClick={ onClick }> 54 | { BUTTON_TEXT } 55 | </Button> 56 | ); 57 | 58 | const component = screen.getByText( BUTTON_TEXT ); 59 | 60 | expect( component ).toBeInTheDocument(); 61 | expect( component ).toHaveAttribute( 'aria-disabled', 'true' ); 62 | expect( component ).not.toHaveAttribute( 'disabled' ); 63 | 64 | fireEvent.click( component ); 65 | expect( onClick ).toHaveBeenCalledTimes( 0 ); 66 | 67 | // Check for accessibility issues 68 | expect( await axe( container ) ).toHaveNoViolations(); 69 | } ); 70 | } ); 71 | -------------------------------------------------------------------------------- /src/system/Button/Button.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import React, { useCallback, forwardRef } from 'react'; 3 | import { Theme, Button as ThemeButton, ButtonProps as ThemeButtonProps } from 'theme-ui'; 4 | 5 | type ButtonClickType = React.MouseEvent< HTMLButtonElement, MouseEvent >; 6 | 7 | interface ButtonTheme extends Theme { 8 | outline?: Record< string, string >; 9 | } 10 | 11 | export enum ButtonVariant { 12 | 'danger', 13 | 'display', 14 | 'ghost', 15 | 'icon', 16 | 'primary', 17 | 'secondary', 18 | 'tertiary', 19 | 'text', 20 | } 21 | 22 | export interface ButtonProps extends ThemeButtonProps { 23 | disabled?: boolean; 24 | preferAriaDisabled?: boolean; 25 | onClick?: ( event: ButtonClickType ) => void; 26 | full?: boolean; 27 | grow?: boolean; 28 | variant?: keyof typeof ButtonVariant; // converts the enum to a string union type 29 | } 30 | 31 | const Button = forwardRef< HTMLButtonElement, ButtonProps >( 32 | ( { className, disabled, preferAriaDisabled, onClick, sx, full, grow, ...rest }, ref ) => { 33 | const disabledAttributes = 34 | disabled && preferAriaDisabled === true ? { 'aria-disabled': true } : { disabled }; 35 | 36 | const handleOnClick = useCallback( 37 | ( event: ButtonClickType ) => { 38 | if ( preferAriaDisabled && disabled ) { 39 | return event.preventDefault(); 40 | } 41 | 42 | if ( onClick ) { 43 | return onClick( event ); 44 | } 45 | }, 46 | [ disabled, onClick ] 47 | ); 48 | 49 | return ( 50 | <ThemeButton 51 | sx={ { 52 | '&:focus': 'none', 53 | '&:focus-visible': ( theme: ButtonTheme ) => theme.outline, 54 | '&[disabled], &[aria-disabled="true"]': { 55 | opacity: 0.7, 56 | backgroundColor: 'input.border.disabled', 57 | color: 'texts.secondary', 58 | cursor: 'not-allowed', 59 | pointerEvents: 'none', 60 | }, 61 | '&:hover, &:focus': { 62 | textDecoration: 'none', 63 | }, 64 | flexGrow: Boolean( grow ) === true ? '1' : undefined, 65 | width: Boolean( full ) === true ? '100%' : undefined, 66 | ...sx, 67 | } } 68 | { ...rest } 69 | { ...disabledAttributes } 70 | onClick={ handleOnClick } 71 | className={ classNames( 'vip-button-component', className ) } 72 | ref={ ref } 73 | /> 74 | ); 75 | } 76 | ); 77 | 78 | Button.displayName = 'Button'; 79 | 80 | export { Button }; 81 | -------------------------------------------------------------------------------- /src/system/Button/ButtonSubmit.stories.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import React from 'react'; 5 | 6 | import { ButtonSubmit } from '..'; 7 | 8 | import type { StoryObj } from '@storybook/react'; 9 | 10 | /** 11 | * Internal dependencies 12 | */ 13 | 14 | export default { 15 | title: 'ButtonSubmit', 16 | component: ButtonSubmit, 17 | }; 18 | 19 | type Story = StoryObj< typeof ButtonSubmit >; 20 | 21 | export const Primary: Story = { 22 | render: () => <ButtonSubmit label="Primary" variant="primary" sx={ { ml: 2 } } />, 23 | }; 24 | 25 | export const Secondary: Story = { 26 | render: () => <ButtonSubmit label="Secondary" variant="secondary" sx={ { ml: 2 } } />, 27 | }; 28 | 29 | export const Loading: Story = { 30 | render: () => ( 31 | <ButtonSubmit label="Loading" loading={ true } variant="primary" sx={ { ml: 2 } } /> 32 | ), 33 | }; 34 | -------------------------------------------------------------------------------- /src/system/Button/ButtonSubmit.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { render, screen } from '@testing-library/react'; 5 | import { axe } from 'jest-axe'; 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import { ButtonSubmit } from './ButtonSubmit'; 11 | 12 | const defaultProps = { 13 | label: 'Load more items', 14 | }; 15 | 16 | describe( '<ButtonSubmit />', () => { 17 | it( 'renders the ButtonSubmit component', async () => { 18 | const { container } = render( <ButtonSubmit { ...defaultProps } /> ); 19 | 20 | expect( screen.getByRole( 'button', { name: 'Load more items' } ) ).toBeInTheDocument(); 21 | 22 | // Check for accessibility issues 23 | expect( await axe( container ) ).toHaveNoViolations(); 24 | } ); 25 | 26 | it( 'renders the ButtonSubmit loading', () => { 27 | render( <ButtonSubmit { ...defaultProps } loading={ true } /> ); 28 | const button = screen.getByRole( 'button', { name: 'Load more items Loading' } ); 29 | 30 | // Button 31 | expect( button ).toHaveAttribute( 'aria-disabled', 'true' ); 32 | 33 | // Spinner 34 | expect( screen.getByTitle( 'Loading' ) ).toBeInTheDocument(); 35 | } ); 36 | } ); 37 | -------------------------------------------------------------------------------- /src/system/Button/ButtonSubmit.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import classNames from 'classnames'; 5 | import React from 'react'; 6 | 7 | import { Button, ButtonProps } from './Button'; 8 | import { Spinner } from '../Spinner'; 9 | 10 | interface DefaultSpinnerProps { 11 | color?: string; 12 | size: number; 13 | } 14 | 15 | function DefaultSpinner( { size, color = 'link' }: DefaultSpinnerProps ) { 16 | return <Spinner size={ size } sx={ { ml: 2, color } } className="vip-button-submit-spinner" />; 17 | } 18 | 19 | DefaultSpinner.displayName = 'DefaultSpinner'; 20 | 21 | export interface ButtonSubmitProps extends ButtonProps { 22 | label: React.ReactNode; 23 | loading?: boolean; 24 | loadingIcon?: ( props: DefaultSpinnerProps ) => JSX.Element; 25 | loadingIconSize?: number; 26 | show?: boolean; 27 | } 28 | 29 | export const ButtonSubmit = React.forwardRef< HTMLButtonElement, ButtonSubmitProps >( 30 | ( 31 | { 32 | show = true, 33 | variant = 'secondary', 34 | label, 35 | loading = false, 36 | disabled = false, 37 | loadingIcon = DefaultSpinner, 38 | loadingIconSize = 20, 39 | ...rest 40 | }: ButtonSubmitProps, 41 | ref: React.Ref< HTMLButtonElement > 42 | ) => { 43 | if ( ! show ) { 44 | return null; 45 | } 46 | 47 | return ( 48 | <Button 49 | ref={ ref } 50 | className={ classNames( 'vip-button-submit-component', `vip-button-submit-${ variant }` ) } 51 | disabled={ disabled || loading } 52 | preferAriaDisabled={ true } 53 | variant={ variant } 54 | aria-busy={ loading } 55 | { ...rest } 56 | > 57 | { label }{ ' ' } 58 | { Boolean( loading ) && 59 | loadingIcon( { size: loadingIconSize, color: `button.${ variant }.label.default` } ) } 60 | </Button> 61 | ); 62 | } 63 | ); 64 | 65 | ButtonSubmit.displayName = 'ButtonSubmit'; 66 | -------------------------------------------------------------------------------- /src/system/Button/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | 5 | export { Button, ButtonVariant } from './Button'; 6 | export type { ButtonProps } from './Button'; 7 | export { ButtonSubmit } from './ButtonSubmit'; 8 | export type { ButtonSubmitProps } from './ButtonSubmit'; 9 | -------------------------------------------------------------------------------- /src/system/Card/Card.stories.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | import { Box, Card } from '..'; 4 | 5 | export default { 6 | title: 'Card', 7 | component: Card, 8 | }; 9 | 10 | export const Default = () => <Card>Hello</Card>; 11 | 12 | export const WithHeader = () => <Card title="Header">This is a card with a header.</Card>; 13 | 14 | export const WithCustomHeader = () => ( 15 | <Box sx={ { maxWidth: 500 } }> 16 | <Card 17 | title="Screenshot of a website" 18 | renderHeader={ title => ( 19 | <img 20 | src={ `https://s0.wp.com/mshots/v1/https://google.com/` } 21 | sx={ { width: '100%' } } 22 | alt={ title } 23 | /> 24 | ) } 25 | > 26 | This is a card with a customized header content. 27 | </Card> 28 | </Box> 29 | ); 30 | 31 | export const DefaultSecondary = () => <Card variant="secondary">Hello</Card>; 32 | 33 | export const WithHeaderSecondary = () => ( 34 | <Card title="Header" variant="secondary"> 35 | This is a card with a header. 36 | </Card> 37 | ); 38 | 39 | export const DefaultIndent = () => <Card variant="indent">Hello</Card>; 40 | export const StyledBody = () => ( 41 | <Card variant="indent" title="Hello world" bodyStyles={ { p: 6, backgroundColor: 'layer.2' } }> 42 | Hello styled body. 43 | </Card> 44 | ); 45 | -------------------------------------------------------------------------------- /src/system/Card/Card.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { render, screen } from '@testing-library/react'; 5 | import { axe } from 'jest-axe'; 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import { Card } from './Card'; 11 | 12 | const defaultProps = {}; 13 | 14 | describe( '<Card />', () => { 15 | it( 'renders the Card component', async () => { 16 | const { container } = render( <Card { ...defaultProps }>This is a Card</Card> ); 17 | 18 | expect( screen.getByText( 'This is a Card' ) ).toBeInTheDocument(); 19 | 20 | // Check for accessibility issues 21 | expect( await axe( container ) ).toHaveNoViolations(); 22 | } ); 23 | 24 | it( 'renders the Card component with a different variant', async () => { 25 | const { container } = render( <Card variant="primary">Card text</Card> ); 26 | 27 | expect( screen.getByText( 'Card text' ) ).toBeInTheDocument(); 28 | 29 | // Check for accessibility issues 30 | expect( await axe( container ) ).toHaveNoViolations(); 31 | } ); 32 | 33 | it( 'renders the Card component with a title', async () => { 34 | const { container } = render( <Card title="Card Header">Card text</Card> ); 35 | 36 | expect( screen.getByText( 'Card Header' ) ).toBeInTheDocument(); 37 | 38 | expect( screen.getByText( 'Card text' ) ).toBeInTheDocument(); 39 | 40 | // Check for accessibility issues 41 | expect( await axe( container ) ).toHaveNoViolations(); 42 | } ); 43 | } ); 44 | -------------------------------------------------------------------------------- /src/system/Card/Card.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | /** 4 | * External dependencies 5 | */ 6 | import { forwardRef, Ref } from 'react'; 7 | import { BoxProps, ThemeUIStyleObject } from 'theme-ui'; 8 | 9 | /** 10 | * Internal dependencies 11 | */ 12 | import { Box } from '..'; 13 | 14 | export enum CardVariant { 15 | 'primary', 16 | 'secondary', 17 | 'notice', 18 | 'indent', 19 | } 20 | 21 | export interface CardProps { 22 | variant?: keyof typeof CardVariant; 23 | title?: string; 24 | children?: React.ReactNode; 25 | renderHeader?: ( title?: string ) => React.ReactNode; 26 | bodyStyles?: ThemeUIStyleObject; 27 | headerStyles?: ThemeUIStyleObject; 28 | hideBody?: boolean; 29 | } 30 | 31 | type CardBoxProps = CardProps & BoxProps; 32 | 33 | export const Card = forwardRef< HTMLElement, CardBoxProps >( 34 | ( 35 | { 36 | variant = 'primary', 37 | title, 38 | renderHeader, 39 | bodyStyles, 40 | headerStyles, 41 | children, 42 | hideBody = false, 43 | ...rest 44 | }: CardProps, 45 | ref: Ref< HTMLElement > 46 | ) => { 47 | return ( 48 | <Box 49 | ref={ ref } 50 | sx={ { 51 | variant: `cards.${ variant }`, 52 | } } 53 | className="vip-card-component" 54 | { ...rest } 55 | > 56 | { renderHeader ? renderHeader( title ) : '' } 57 | { title && ! renderHeader && ( 58 | <Box 59 | className="vip-card-header-component" 60 | sx={ { 61 | variant: `cards.${ variant }.header`, 62 | ...headerStyles, 63 | } } 64 | > 65 | { title } 66 | </Box> 67 | ) } 68 | 69 | { ! hideBody && ( 70 | <Box 71 | className="vip-card-body-component" 72 | sx={ { 73 | variant: `cards.${ variant }.children`, 74 | ...bodyStyles, 75 | } } 76 | > 77 | { children } 78 | </Box> 79 | ) } 80 | </Box> 81 | ); 82 | } 83 | ); 84 | 85 | Card.displayName = 'Card'; 86 | -------------------------------------------------------------------------------- /src/system/Card/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | export { Card } from './Card'; 5 | -------------------------------------------------------------------------------- /src/system/Code/Code.stories.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import React from 'react'; 5 | 6 | import { Code } from '..'; 7 | 8 | import type { StoryObj } from '@storybook/react'; 9 | 10 | /** 11 | * Internal dependencies 12 | */ 13 | 14 | export default { 15 | title: 'Code', 16 | component: Code, 17 | }; 18 | 19 | type Story = StoryObj< typeof Code >; 20 | 21 | export const DefaultWithTime: Story = { 22 | render: () => ( 23 | <Code 24 | children={ 25 | <> 26 | <time sx={ { color: 'logs.text.secondary' } } dateTime="2022-01-01 15:15:15"> 27 | 15:16 28 | </time>{ ' ' } 29 | Code 30 | </> 31 | } 32 | /> 33 | ), 34 | }; 35 | 36 | export const DefaultWithIcon: Story = { 37 | render: () => <Code showCopy={ true }>Code with Icon</Code>, 38 | }; 39 | 40 | export const DefaultWithConsoleInfo: Story = { 41 | render: () => ( 42 | <Code showCopy={ true } onCopy={ () => global.alert( 'Hello world' ) }> 43 | Code with Icon and Click callback — console.info 44 | </Code> 45 | ), 46 | }; 47 | -------------------------------------------------------------------------------- /src/system/Code/Code.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { render, screen, fireEvent, waitFor } from '@testing-library/react'; 5 | import { axe } from 'jest-axe'; 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import { Code } from './Code'; 11 | 12 | const defaultProps = { 13 | showCopy: false, 14 | }; 15 | 16 | // Mock navigator.clipboard because jsdom doesn't support it 17 | Object.defineProperty( window.navigator, 'clipboard', { 18 | value: { 19 | writeText: () => { 20 | return Promise.resolve(); 21 | }, 22 | }, 23 | } ); 24 | 25 | describe( '<Code />', () => { 26 | it( 'renders the Code component', async () => { 27 | const { container } = render( <Code { ...defaultProps }>This is a code</Code> ); 28 | 29 | expect( screen.getByText( 'This is a code' ) ).toBeInTheDocument(); 30 | 31 | // Check for accessibility issues 32 | expect( await axe( container ) ).toHaveNoViolations(); 33 | } ); 34 | 35 | it( 'renders the Code component with a copy button', async () => { 36 | const props = { ...defaultProps, showCopy: true }; 37 | const { container } = render( <Code { ...props }>This is a code</Code> ); 38 | 39 | expect( screen.getByRole( 'button', { name: 'Copy code' } ) ).toBeInTheDocument(); 40 | 41 | // Check for accessibility issues 42 | expect( await axe( container ) ).toHaveNoViolations(); 43 | } ); 44 | 45 | it( 'updates the the UI after clicking on "Copy"', async () => { 46 | const props = { ...defaultProps, showCopy: true }; 47 | const { container } = render( <Code { ...props }>This is a code</Code> ); 48 | 49 | fireEvent.click( screen.getByRole( 'button', { name: 'Copy code' } ) ); 50 | 51 | await waitFor( () => new Promise( resolve => setTimeout( resolve, 0 ) ) ); 52 | 53 | expect( screen.getByText( 'Code copied to clipboard' ) ).toBeInTheDocument(); 54 | 55 | // Check for accessibility issues 56 | expect( await axe( container ) ).toHaveNoViolations(); 57 | } ); 58 | } ); 59 | -------------------------------------------------------------------------------- /src/system/Code/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | export { Code } from './Code'; 5 | -------------------------------------------------------------------------------- /src/system/ConfirmationDialog/ConfirmationDialog.js: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | /** 4 | * External dependencies 5 | */ 6 | import classNames from 'classnames'; 7 | import PropTypes from 'prop-types'; 8 | import React from 'react'; 9 | 10 | /** 11 | * Internal dependencies 12 | */ 13 | import { Dialog, Box, Heading, Text, Flex, Button } from '../'; 14 | 15 | const ConfirmationDialogContent = ( { 16 | title, 17 | body, 18 | onClose, 19 | label = 'Confirm', 20 | onConfirm, 21 | className = null, 22 | } ) => ( 23 | <Box p={ 4 } className={ classNames( 'vip-confirmation-dialog-component', className ) }> 24 | <Heading variant="h3" sx={ { mb: 2 } }> 25 | { title } 26 | </Heading> 27 | <Text>{ body }</Text> 28 | <Flex sx={ { justifyContent: 'flex-end', mt: 4 } }> 29 | <Button variant="text" sx={ { mr: 2 } } onClick={ onClose }> 30 | Cancel 31 | </Button> 32 | <Button 33 | variant="danger" 34 | onClick={ () => { 35 | onConfirm(); 36 | onClose(); 37 | } } 38 | > 39 | { label } 40 | </Button> 41 | </Flex> 42 | </Box> 43 | ); 44 | 45 | ConfirmationDialogContent.propTypes = { 46 | title: PropTypes.node, 47 | body: PropTypes.node, 48 | label: PropTypes.string, 49 | onClose: PropTypes.func, 50 | onConfirm: PropTypes.func, 51 | className: PropTypes.any, 52 | }; 53 | 54 | const ConfirmationDialog = ( { trigger, onConfirm, needsConfirm = true, ...props } ) => { 55 | const directTrigger = React.cloneElement( trigger, { onClick: onConfirm } ); 56 | 57 | if ( ! needsConfirm ) { 58 | return directTrigger; 59 | } 60 | 61 | return ( 62 | <Dialog 63 | variant="modal" 64 | sx={ { maxWidth: 680 } } 65 | content={ ( { onClose } ) => ( 66 | <ConfirmationDialogContent onClose={ onClose } onConfirm={ onConfirm } { ...props } /> 67 | ) } 68 | trigger={ trigger } 69 | /> 70 | ); 71 | }; 72 | 73 | ConfirmationDialog.propTypes = { 74 | needsConfirm: PropTypes.bool, 75 | trigger: PropTypes.node, 76 | onConfirm: PropTypes.func, 77 | }; 78 | 79 | export { ConfirmationDialog, ConfirmationDialogContent }; 80 | -------------------------------------------------------------------------------- /src/system/ConfirmationDialog/ConfirmationDialog.stories.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { Box, ConfirmationDialog, Button, Heading, Text, Flex } from '..'; 5 | 6 | export default { 7 | title: 'Deprecated/ConfirmationDialog', 8 | component: ConfirmationDialog, 9 | }; 10 | 11 | const ConfirmationTrigger = <Button sx={ { mr: 3 } }>Dangerous Action</Button>; 12 | 13 | const ConfirmationContent = ( 14 | <Box p={ 5 }> 15 | <Heading>This is a Modal</Heading> 16 | <Text sx={ { fontSize: 3 } }> 17 | A modal is used to perform more detailed actions that don‘t necessarily need the context 18 | behind. 19 | </Text> 20 | </Box> 21 | ); 22 | 23 | export const Default = () => ( 24 | <Flex> 25 | <Box> 26 | <ConfirmationDialog trigger={ ConfirmationTrigger } content={ ConfirmationContent } /> 27 | </Box> 28 | </Flex> 29 | ); 30 | -------------------------------------------------------------------------------- /src/system/ConfirmationDialog/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { ConfirmationDialog, ConfirmationDialogContent } from './ConfirmationDialog'; 5 | 6 | export { ConfirmationDialog, ConfirmationDialogContent }; 7 | -------------------------------------------------------------------------------- /src/system/Dialog/Dialog.js: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | /** 4 | * External dependencies 5 | */ 6 | import { AnimatePresence } from 'framer-motion'; 7 | import PropTypes from 'prop-types'; 8 | import { useEffect, useRef, useState } from 'react'; 9 | 10 | /** 11 | * Internal dependencies 12 | */ 13 | import { DialogContent, DialogTrigger } from '.'; 14 | 15 | const Dialog = ( { 16 | trigger, 17 | position = 'left', 18 | startOpen = false, 19 | content, 20 | disabled = false, 21 | ...props 22 | } ) => { 23 | const [ isOpen, setIsOpen ] = useState( startOpen ); 24 | const dialogRef = useRef( null ); 25 | 26 | const closeDialog = e => { 27 | if ( ! dialogRef.current.contains( e.target ) ) { 28 | setIsOpen( false ); 29 | } 30 | }; 31 | 32 | useEffect( () => { 33 | window.document.addEventListener( 'click', closeDialog, true ); 34 | 35 | return () => window.document.removeEventListener( 'click', closeDialog, true ); 36 | }, [] ); 37 | 38 | // if content is a function, pass in onClose 39 | const isFunction = typeof content === 'function'; 40 | 41 | const handleOpen = ( event = null ) => { 42 | const open = ! isOpen; 43 | 44 | if ( disabled ) { 45 | return; 46 | } 47 | 48 | if ( event?.key && event?.key !== 13 && event?.key !== 'Enter' ) { 49 | return; 50 | } 51 | 52 | setIsOpen( open ); 53 | }; 54 | 55 | return ( 56 | <div 57 | onClick={ e => e.stopPropagation() } 58 | sx={ { position: 'relative' } } 59 | ref={ dialogRef } 60 | className="vip-dialog-component" 61 | > 62 | <DialogTrigger 63 | tabIndex="0" 64 | sx={ { display: 'inline' } } 65 | onKeyPress={ handleOpen } 66 | onClick={ handleOpen } 67 | aria-haspopup="true" 68 | aria-expanded={ isOpen } 69 | > 70 | { trigger } 71 | </DialogTrigger> 72 | <AnimatePresence> 73 | { isOpen && ( 74 | <DialogContent { ...props } position={ position } onClose={ () => setIsOpen( false ) }> 75 | { isFunction ? content( { onClose: () => setIsOpen( false ) } ) : content } 76 | </DialogContent> 77 | ) } 78 | </AnimatePresence> 79 | </div> 80 | ); 81 | }; 82 | 83 | Dialog.propTypes = { 84 | trigger: PropTypes.node, 85 | disabled: PropTypes.bool, 86 | position: PropTypes.string, 87 | startOpen: PropTypes.bool, 88 | content: PropTypes.oneOfType( [ PropTypes.node, PropTypes.func ] ), 89 | }; 90 | 91 | export { Dialog }; 92 | -------------------------------------------------------------------------------- /src/system/Dialog/Dialog.stories.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { 5 | Box, 6 | Dialog, 7 | DialogMenu, 8 | DialogMenuItem, 9 | DialogDivider, 10 | Button, 11 | Heading, 12 | Text, 13 | Flex, 14 | } from '..'; 15 | 16 | export default { 17 | title: 'Deprecated/Dialog', 18 | component: Dialog, 19 | }; 20 | 21 | const DropdownTrigger = <Button>Trigger Dropdown</Button>; 22 | const ModalTrigger = <Button sx={ { mr: 3 } }>Trigger Modal</Button>; 23 | 24 | const DropdownContent = ( 25 | <div> 26 | <DialogMenu> 27 | <DialogMenuItem>Profile</DialogMenuItem> 28 | <DialogMenuItem>Account</DialogMenuItem> 29 | <DialogMenuItem>Dark Mode</DialogMenuItem> 30 | </DialogMenu> 31 | <DialogDivider /> 32 | <DialogMenu> 33 | <DialogMenuItem>Logout</DialogMenuItem> 34 | </DialogMenu> 35 | </div> 36 | ); 37 | 38 | const ModalContent = ( 39 | <Box p={ 5 }> 40 | <Heading>This is a Modal</Heading> 41 | <Text sx={ { fontSize: 3 } }> 42 | A modal is used to perform more detailed actions that don‘t necessarily need the context 43 | behind. 44 | </Text> 45 | </Box> 46 | ); 47 | 48 | export const Default = () => ( 49 | <Flex> 50 | <Box> 51 | <Dialog 52 | trigger={ ModalTrigger } 53 | content={ ModalContent } 54 | sx={ { width: 480 } } 55 | variant="modal" 56 | /> 57 | </Box> 58 | <Dialog trigger={ DropdownTrigger } content={ DropdownContent } sx={ { width: 200 } } /> 59 | </Flex> 60 | ); 61 | -------------------------------------------------------------------------------- /src/system/Dialog/DialogButton.js: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | /** 4 | * External dependencies 5 | */ 6 | import PropTypes from 'prop-types'; 7 | import { MdExpandMore } from 'react-icons/md'; 8 | 9 | /** 10 | * Internal dependencies 11 | */ 12 | import { Button, Text } from '../'; 13 | 14 | const DialogButton = ( { label, variant = 'secondary', value, children, ...props } ) => ( 15 | <Button 16 | variant={ variant } 17 | sx={ { 18 | textAlign: 'left', 19 | display: 'inline-flex', 20 | py: 2, 21 | pl: 3, 22 | pr: 2, 23 | alignItems: 'center', 24 | } } 25 | { ...props } 26 | > 27 | { children } 28 | { label && ( 29 | <Text as="span" sx={ { mb: 0, color: 'heading', mr: 2, flex: '0 0 auto' } }> 30 | { label }: 31 | </Text> 32 | ) } 33 | { value && ( 34 | <Text 35 | as="span" 36 | sx={ { 37 | mb: 0, 38 | flex: '1 1 auto', 39 | whiteSpace: 'nowrap', 40 | overflow: 'hidden', 41 | color: 'input.text.default', 42 | textOverflow: 'ellipsis', 43 | } } 44 | > 45 | { value } 46 | </Text> 47 | ) } 48 | <MdExpandMore sx={ { ml: 2, top: 0, display: 'block', flex: '0 0 auto' } } /> 49 | </Button> 50 | ); 51 | 52 | DialogButton.propTypes = { 53 | children: PropTypes.node, 54 | label: PropTypes.string, 55 | value: PropTypes.string, 56 | variant: PropTypes.string, 57 | }; 58 | 59 | export { DialogButton }; 60 | -------------------------------------------------------------------------------- /src/system/Dialog/DialogDivider.js: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | /** 4 | * External dependencies 5 | */ 6 | 7 | const DialogDivider = props => ( 8 | <div sx={ { height: 1, backgroundColor: 'border', my: 1 } } { ...props } /> 9 | ); 10 | 11 | export { DialogDivider }; 12 | -------------------------------------------------------------------------------- /src/system/Dialog/DialogMenu.js: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | /** 4 | * External dependencies 5 | */ 6 | 7 | const DialogMenu = props => ( 8 | <ul role="menu" sx={ { listStyleType: 'none', m: 0, px: 0, py: 1 } } { ...props } /> 9 | ); 10 | 11 | export { DialogMenu }; 12 | -------------------------------------------------------------------------------- /src/system/Dialog/DialogMenuItem.js: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | /** 4 | * External dependencies 5 | */ 6 | import PropTypes from 'prop-types'; 7 | import { useEffect, useRef } from 'react'; 8 | 9 | /** 10 | * Internal dependencies 11 | */ 12 | import { Box, Spinner } from '../'; 13 | 14 | const DialogMenuItem = ( { loading = false, children, ...props } ) => { 15 | const itemRef = useRef( null ); 16 | 17 | const triggerClick = e => { 18 | if ( 19 | itemRef.current === window.document.activeElement && 20 | ( e.key === 13 || e.key === 'Enter' ) 21 | ) { 22 | props.onClick(); 23 | } 24 | }; 25 | 26 | useEffect( () => { 27 | if ( props.onClick ) { 28 | window.document.addEventListener( 'keydown', triggerClick, true ); 29 | } 30 | 31 | return () => { 32 | window.document.removeEventListener( 'keydown', triggerClick, true ); 33 | }; 34 | }, [] ); 35 | 36 | return ( 37 | <li role="none"> 38 | <Box 39 | ref={ itemRef } 40 | role="menuitem" 41 | tabIndex="0" 42 | sx={ { 43 | listStyleType: 'none', 44 | display: 'flex', 45 | alignItems: 'center', 46 | textAlign: 'left', 47 | m: 0, 48 | color: 'heading', 49 | px: 2, 50 | py: 1, 51 | cursor: 'pointer', 52 | textDecoration: 'none', 53 | '&:hover, &:focus': { 54 | backgroundColor: 'hover', 55 | outline: 'none', 56 | }, 57 | } } 58 | { ...props } 59 | > 60 | <Box sx={ { flex: '1 1 auto' } }>{ children }</Box> 61 | { loading && <Spinner sx={ { width: 12 } } /> } 62 | </Box> 63 | </li> 64 | ); 65 | }; 66 | 67 | DialogMenuItem.propTypes = { 68 | onClick: PropTypes.func, 69 | loading: PropTypes.bool, 70 | children: PropTypes.node, 71 | }; 72 | 73 | export { DialogMenuItem }; 74 | -------------------------------------------------------------------------------- /src/system/Dialog/DialogTrigger.js: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | /** 4 | * Internal dependencies 5 | */ 6 | 7 | import { Box } from '../'; 8 | 9 | const DialogTrigger = props => <Box sx={ { display: 'inline' } } { ...props } />; 10 | 11 | export { DialogTrigger }; 12 | -------------------------------------------------------------------------------- /src/system/Dialog/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { Dialog } from './Dialog'; 5 | import { DialogButton } from './DialogButton'; 6 | import { DialogContent } from './DialogContent'; 7 | import { DialogDivider } from './DialogDivider'; 8 | import { DialogMenu } from './DialogMenu'; 9 | import { DialogMenuItem } from './DialogMenuItem'; 10 | import { DialogTrigger } from './DialogTrigger'; 11 | 12 | export { 13 | Dialog, 14 | DialogButton, 15 | DialogDivider, 16 | DialogTrigger, 17 | DialogContent, 18 | DialogMenuItem, 19 | DialogMenu, 20 | }; 21 | -------------------------------------------------------------------------------- /src/system/Drawer/Drawer.test.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 3 | // @ts-nocheck 4 | import { fireEvent, render, screen } from '@testing-library/react'; 5 | import { axe } from 'jest-axe'; 6 | import { ThemeUIProvider } from 'theme-ui'; 7 | 8 | import { Drawer } from './Drawer'; 9 | import { Button, theme } from '../'; 10 | 11 | const renderWithTheme = children => 12 | render( <ThemeUIProvider theme={ theme }>{ children }</ThemeUIProvider> ); 13 | 14 | const renderComponent = () => 15 | renderWithTheme( 16 | <Drawer label="Dialog example" sx={ { width: 320 } } trigger={ <Button>Open Drawer</Button> }> 17 | <p sx={ { ml: 3 } }>Hello from default</p> 18 | </Drawer> 19 | ); 20 | 21 | describe( '<Drawer />', () => { 22 | beforeAll( () => { 23 | Object.defineProperty( window, 'matchMedia', { 24 | value: jest.fn( () => { 25 | return { 26 | matches: true, 27 | addListener: jest.fn(), 28 | removeListener: jest.fn(), 29 | }; 30 | } ), 31 | } ); 32 | } ); 33 | 34 | it( 'renders the Drawer component', async () => { 35 | const { container } = renderComponent(); 36 | 37 | const trigger = screen.getByRole( 'button', { name: 'Open Drawer' } ); 38 | 39 | expect( trigger ).toBeInTheDocument(); 40 | 41 | fireEvent.click( trigger ); 42 | 43 | expect( screen.getByText( 'Hello from default' ) ).toBeInTheDocument(); 44 | 45 | expect( await axe( container ) ).toHaveNoViolations(); 46 | } ); 47 | } ); 48 | -------------------------------------------------------------------------------- /src/system/Dropdown/Dropdown.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { fireEvent, render, screen } from '@testing-library/react'; 5 | import { axe } from 'jest-axe'; 6 | import React from 'react'; 7 | 8 | /** 9 | * Internal dependencies 10 | */ 11 | import * as Dropdown from '.'; 12 | 13 | const defaultProps = { 14 | trigger: <button>Trigger</button>, 15 | }; 16 | 17 | const getButton = () => screen.getByRole( 'button', { name: 'Trigger' } ); 18 | 19 | describe( '<Dropdown />', () => { 20 | it( 'renders the Dropdown component', async () => { 21 | const { container } = render( 22 | <Dropdown.Root { ...defaultProps }> 23 | <Dropdown.Item>My Item</Dropdown.Item> 24 | </Dropdown.Root> 25 | ); 26 | 27 | expect( getButton() ).toBeInTheDocument(); 28 | 29 | fireEvent.click( getButton() ); 30 | 31 | // Check for accessibility issues 32 | expect( await axe( container ) ).toHaveNoViolations(); 33 | } ); 34 | } ); 35 | -------------------------------------------------------------------------------- /src/system/Dropdown/Dropdown.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; 4 | import React, { ReactNode } from 'react'; 5 | 6 | import { DropdownContent, DropdownContentProps } from './DropdownContent'; 7 | 8 | const DropdownMenu = DropdownMenuPrimitive.Root; 9 | const DropdownTrigger = DropdownMenuPrimitive.Trigger; 10 | const DropdownRadioGroup = DropdownMenuPrimitive.RadioGroup; 11 | const DropdownItemIndicator = DropdownMenuPrimitive.DropdownMenuItemIndicator; 12 | const DropdownLabel = DropdownMenuPrimitive.DropdownMenuLabel; 13 | const DropdownSeparator = DropdownMenuPrimitive.DropdownMenuSeparator; 14 | const DropdownSub = DropdownMenuPrimitive.DropdownMenuSub; 15 | const DropdownSubTrigger = DropdownMenuPrimitive.DropdownMenuSubTrigger; 16 | const DropdownSubContent = DropdownMenuPrimitive.DropdownMenuSubContent; 17 | 18 | export interface DropdownProps { 19 | trigger: ReactNode; 20 | children: ReactNode; 21 | open?: boolean; 22 | defaultOpen?: boolean; 23 | onOpenChange?: ( open: boolean ) => void; 24 | modal?: boolean; 25 | dir?: 'ltr' | 'rtl'; 26 | contentProps?: DropdownContentProps; 27 | portalProps?: object; 28 | className?: string; 29 | } 30 | 31 | export const Dropdown: React.FC< DropdownProps > = ( { 32 | trigger, 33 | children, 34 | open = undefined, 35 | defaultOpen = false, 36 | onOpenChange = undefined, 37 | modal = true, 38 | dir = 'ltr', 39 | contentProps = {}, 40 | portalProps = {}, 41 | } ) => ( 42 | <DropdownMenu 43 | open={ open } 44 | defaultOpen={ defaultOpen } 45 | onOpenChange={ onOpenChange } 46 | modal={ modal } 47 | dir={ dir } 48 | > 49 | <DropdownTrigger className="vip-dropdown-trigger" asChild> 50 | { trigger } 51 | </DropdownTrigger> 52 | 53 | <DropdownMenuPrimitive.Portal { ...portalProps }> 54 | <DropdownContent { ...contentProps }> 55 | { children } 56 | <DropdownMenuPrimitive.Arrow sx={ { fill: 'background', boxShadow: 'high' } } /> 57 | </DropdownContent> 58 | </DropdownMenuPrimitive.Portal> 59 | </DropdownMenu> 60 | ); 61 | 62 | // Exports 63 | export { 64 | DropdownTrigger, 65 | DropdownRadioGroup, 66 | DropdownItemIndicator, 67 | DropdownLabel, 68 | DropdownSeparator, 69 | DropdownSub, 70 | DropdownSubTrigger, 71 | DropdownSubContent, 72 | }; 73 | -------------------------------------------------------------------------------- /src/system/Dropdown/DropdownContent.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; 4 | import classNames from 'classnames'; 5 | import React from 'react'; 6 | 7 | export interface DropdownContentProps { 8 | className?: string; 9 | align?: 'start' | 'center' | 'end'; 10 | } 11 | 12 | export const styles = { 13 | minWidth: 220, 14 | borderRadius: 2, 15 | backgroundColor: 'background', 16 | boxShadow: 'high', 17 | px: 2, 18 | py: 1, 19 | }; 20 | 21 | export const DropdownContent = React.forwardRef< HTMLDivElement, DropdownContentProps >( 22 | ( { className, align = 'center', ...props }, forwardRef ) => ( 23 | <DropdownMenuPrimitive.DropdownMenuContent 24 | className={ classNames( 'vip-dropdown-menu-content', className ) } 25 | ref={ forwardRef } 26 | sx={ styles } 27 | align={ align } 28 | { ...props } 29 | /> 30 | ) 31 | ); 32 | 33 | DropdownContent.displayName = 'DropdownContent'; 34 | 35 | export const DropdownSubContent = React.forwardRef< HTMLDivElement, DropdownContentProps >( 36 | ( { className, ...props }, forwardRef ) => ( 37 | <DropdownMenuPrimitive.Portal> 38 | <DropdownMenuPrimitive.DropdownMenuSubContent 39 | className={ classNames( 'vip-dropdown-menu-sub-content', className ) } 40 | ref={ forwardRef } 41 | sx={ styles } 42 | { ...props } 43 | /> 44 | </DropdownMenuPrimitive.Portal> 45 | ) 46 | ); 47 | 48 | DropdownSubContent.displayName = 'DropdownSubContent'; 49 | -------------------------------------------------------------------------------- /src/system/Dropdown/DropdownLabel.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; 4 | import classNames from 'classnames'; 5 | import React from 'react'; 6 | 7 | export interface DropdownLabelProps { 8 | className?: string; 9 | } 10 | 11 | export const styles = { 12 | paddingLeft: 3, 13 | fontSize: 12, 14 | lineHeight: '25px', 15 | color: 'muted', 16 | }; 17 | 18 | export const DropdownLabel = React.forwardRef< HTMLDivElement, DropdownLabelProps >( 19 | ( { className, ...props }, forwardRef ) => ( 20 | <DropdownMenuPrimitive.DropdownMenuLabel 21 | className={ classNames( 'vip-dropdown-menu-label', className ) } 22 | ref={ forwardRef } 23 | sx={ styles } 24 | { ...props } 25 | /> 26 | ) 27 | ); 28 | 29 | DropdownLabel.displayName = 'DropdownLabel'; 30 | -------------------------------------------------------------------------------- /src/system/Dropdown/DropdownSeparator.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; 4 | import classNames from 'classnames'; 5 | import React from 'react'; 6 | 7 | export interface DropdownSeparatorProps { 8 | className?: string; 9 | } 10 | 11 | export const styles = { 12 | height: '1px', 13 | backgroundColor: 'borders.2', 14 | my: '5px', 15 | }; 16 | 17 | export const DropdownSeparator = React.forwardRef< HTMLDivElement, DropdownSeparatorProps >( 18 | ( { className, ...props }, forwardRef ) => ( 19 | <DropdownMenuPrimitive.DropdownMenuSeparator 20 | className={ classNames( 'vip-dropdown-menu-separator', className ) } 21 | ref={ forwardRef } 22 | sx={ styles } 23 | { ...props } 24 | /> 25 | ) 26 | ); 27 | 28 | DropdownSeparator.displayName = 'DropdownSeparator'; 29 | -------------------------------------------------------------------------------- /src/system/Dropdown/index.ts: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | import { 4 | Dropdown, 5 | DropdownTrigger, 6 | DropdownRadioGroup, 7 | DropdownItemIndicator, 8 | DropdownSub, 9 | } from './Dropdown'; 10 | import { DropdownSubContent, DropdownContent } from './DropdownContent'; 11 | import { 12 | DropdownItem, 13 | DropdownCheckboxItem, 14 | DropdownRadioItem, 15 | DropdownSubTrigger, 16 | } from './DropdownItem'; 17 | import { DropdownLabel } from './DropdownLabel'; 18 | import { DropdownSeparator } from './DropdownSeparator'; 19 | 20 | const Root = Dropdown; 21 | const Content = DropdownContent; 22 | const Trigger = DropdownTrigger; 23 | const Item = DropdownItem; 24 | const CheckboxItem = DropdownCheckboxItem; 25 | const RadioGroup = DropdownRadioGroup; 26 | const RadioItem = DropdownRadioItem; 27 | const ItemIndicator = DropdownItemIndicator; 28 | const Label = DropdownLabel; 29 | const Separator = DropdownSeparator; 30 | const Sub = DropdownSub; 31 | const SubTrigger = DropdownSubTrigger; 32 | const SubContent = DropdownSubContent; 33 | 34 | export { 35 | Root, 36 | Trigger, 37 | Content, 38 | Item, 39 | CheckboxItem, 40 | RadioGroup, 41 | RadioItem, 42 | ItemIndicator, 43 | Label, 44 | Separator, 45 | Sub, 46 | SubTrigger, 47 | SubContent, 48 | }; 49 | 50 | export default Dropdown; 51 | -------------------------------------------------------------------------------- /src/system/FilterDropdown/FilterDropdown.stories.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | import { FilterDropdown } from './FilterDropdown'; 4 | 5 | import type { StoryObj } from '@storybook/react'; 6 | 7 | export default { 8 | title: 'FilterDropdown', 9 | component: FilterDropdown, 10 | parameters: { 11 | docs: { 12 | description: { 13 | component: ` 14 | 15 | A Dropdown component that acts as a filter for a list of items. 16 | 17 | ## Guidance 18 | 19 | ### When to use the FilterDropdown component 20 | 21 | - When you want a Dropdown option that sticks with the selected value on the trigger button; 22 | 23 | ### When to consider something else 24 | 25 | - When you want to display a list of options that does not require to stick with the selected value on the trigger button; 26 | 27 | ## Component Properties 28 | `, 29 | }, 30 | }, 31 | }, 32 | }; 33 | 34 | type Story = StoryObj< typeof FilterDropdown >; 35 | 36 | const filterTypes = [ 'all', 'hasUpdate', 'isVulnerable' ] as const; 37 | type FilterType = ( typeof filterTypes )[ number ]; 38 | 39 | const FILTER_OPTIONS: Record< FilterType, { value: FilterType; label: string } > = { 40 | all: { value: 'all', label: 'All' }, 41 | hasUpdate: { value: 'hasUpdate', label: 'Update Available' }, 42 | isVulnerable: { value: 'isVulnerable', label: 'Known Vulnerabilities' }, 43 | }; 44 | 45 | export const Default: Story = { 46 | render: () => ( 47 | <> 48 | <FilterDropdown 49 | className="vip-plugins-filter-dropdown" 50 | label="Filter:" 51 | filters={ FILTER_OPTIONS } 52 | onSelect={ () => {} } 53 | defaultValue={ FILTER_OPTIONS.all.value } 54 | /> 55 | </> 56 | ), 57 | }; 58 | -------------------------------------------------------------------------------- /src/system/FilterDropdown/FilterDropdown.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import React from 'react'; 3 | import '@testing-library/jest-dom'; 4 | 5 | import { FilterDropdown } from './FilterDropdown'; 6 | 7 | function getMenu() { 8 | return screen.getByRole( 'button', { name: 'Filter: Auth Method All checked' } ); 9 | } 10 | 11 | const props = { 12 | filters: { 13 | ALL_USERS: { 14 | label: 'All', 15 | authMethod: '', 16 | }, 17 | WP_USERS: { 18 | label: 'WP.com', 19 | authMethod: 'wpcom', 20 | }, 21 | GH_USERS: { 22 | label: 'GitHub', 23 | authMethod: 'github', 24 | }, 25 | SSO_USERS: { 26 | label: 'SSO', 27 | authMethod: 'organization_sso', 28 | }, 29 | SSO_OTHER_USERS: { 30 | label: 'SSO (third-party)', 31 | authMethod: 'other_sso', 32 | }, 33 | BLOCKED_USERS: { 34 | label: 'Blocked Auth Methods', 35 | authMethod: 'restricted', 36 | }, 37 | }, 38 | label: 'Auth Method', 39 | onSelect: jest.fn(), 40 | }; 41 | 42 | describe( '<FilterDropdown />', () => { 43 | it( 'render with all props passed', () => { 44 | render( <FilterDropdown { ...props } /> ); 45 | 46 | const menu = getMenu(); 47 | 48 | expect( menu ).toBeInTheDocument(); 49 | expect( menu ).toHaveTextContent( /Filter:/ ); 50 | expect( menu ).toHaveTextContent( /Auth Method/ ); 51 | } ); 52 | } ); 53 | -------------------------------------------------------------------------------- /src/system/Flex/Flex.stories.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { Flex } from '..'; 5 | 6 | export default { 7 | title: 'Flex', 8 | component: Flex, 9 | }; 10 | 11 | export const Default = () => <Flex>Hello</Flex>; 12 | -------------------------------------------------------------------------------- /src/system/Flex/Flex.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { forwardRef, Ref } from 'react'; 5 | import { Flex as ThemeFlex, FlexProps as ThemeFlexProps } from 'theme-ui'; 6 | 7 | export const Flex = forwardRef< HTMLElement, ThemeFlexProps >( 8 | ( props: ThemeFlexProps, ref: Ref< HTMLElement > ) => <ThemeFlex ref={ ref } { ...props } /> 9 | ); 10 | 11 | Flex.displayName = 'Flex'; 12 | -------------------------------------------------------------------------------- /src/system/Flex/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | export { Flex } from './Flex'; 5 | -------------------------------------------------------------------------------- /src/system/Footer/Footer.stories.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { StoryObj } from '@storybook/react'; 5 | 6 | import { Footer } from '../Footer/Footer'; 7 | 8 | export default { 9 | title: 'Navigation/Footer', 10 | component: Footer, 11 | argTypes: { 12 | hasTrailingSeparator: { control: { type: 'boolean' } }, 13 | links: { control: { type: 'array' } }, 14 | customLogo: { control: { type: null } }, 15 | maxWidth: { control: { type: 'text' } }, 16 | }, 17 | }; 18 | 19 | type Story = StoryObj< typeof Footer >; 20 | 21 | export const Default: Story = { 22 | render: () => ( 23 | <Footer 24 | links={ [ 25 | { 26 | children: 'About', 27 | href: 'https://wpvip.com/', 28 | screenReaderText: 'WordPress VIP. Learn more about us', 29 | showExternalIcon: false, 30 | }, 31 | { 32 | children: 'Docs', 33 | href: 'https://docs.wpvip.com/', 34 | screenReaderText: 'our public documentation on our platform and tools', 35 | }, 36 | { 37 | children: 'Status', 38 | href: 'https://wpvipstatus.com', 39 | screenReaderText: 40 | ". See real-time availability and performance monitoring for WordPress VIP's services", 41 | newTab: true, 42 | }, 43 | ] } 44 | /> 45 | ), 46 | }; 47 | -------------------------------------------------------------------------------- /src/system/Footer/Footer.test.tsx: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | import { render, screen } from '@testing-library/react'; 3 | 4 | /** 5 | * Internal dependencies 6 | */ 7 | import { Footer } from './Footer'; 8 | 9 | const links = [ 10 | { 11 | children: 'Link1', 12 | href: 'https://wpvip.com/', 13 | }, 14 | { 15 | children: 'Link2', 16 | href: 'https://docs.wpvip.com/', 17 | }, 18 | ]; 19 | 20 | describe( '<Footer />', () => { 21 | it( 'should accept LinkExternal props for Footer links', () => { 22 | const moreLinks = [ 23 | { 24 | children: 'Link3', 25 | href: 'https://wpvipstatus.com', 26 | newTab: true, 27 | }, 28 | ]; 29 | 30 | const combinedLinks = [ ...links, ...moreLinks ]; 31 | 32 | render( <Footer links={ combinedLinks } /> ); 33 | 34 | const link1 = screen.getByRole( 'link', { name: /link1/i } ); 35 | const link3 = screen.getByRole( 'link', { name: /link3/i } ); 36 | 37 | expect( link1 ).toHaveAttribute( 'target', '_self' ); 38 | expect( link3 ).toHaveAttribute( 'target', '_blank' ); 39 | } ); 40 | } ); 41 | -------------------------------------------------------------------------------- /src/system/Form/Checkbox/Checkbox.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { render, screen } from '@testing-library/react'; 5 | import { axe } from 'jest-axe'; 6 | import { Flex } from 'theme-ui'; 7 | 8 | /** 9 | * Internal dependencies 10 | */ 11 | import { Checkbox } from './Checkbox'; 12 | import { Label } from '..'; 13 | 14 | describe( '<Checkbox />', () => { 15 | it( 'renders', async () => { 16 | const { container } = render( 17 | <Flex sx={ { alignItems: 'center' } }> 18 | <Checkbox 19 | id={ `check1` } 20 | checked 21 | aria-labelledby={ `label-check1` } 22 | onCheckedChange={ () => {} } 23 | /> 24 | <Label clickable htmlFor={ `check1` } id={ `label-check1` }> 25 | This option 26 | </Label> 27 | </Flex> 28 | ); 29 | 30 | expect( screen.getByLabelText( 'This option' ) ).toBeInTheDocument(); 31 | 32 | const checkbox = screen.getByRole( 'checkbox' ); 33 | expect( checkbox ).toBeChecked(); 34 | 35 | // Check for accessibility issues 36 | expect( await axe( container ) ).toHaveNoViolations(); 37 | } ); 38 | 39 | it( 'renders disabled', async () => { 40 | const { container } = render( 41 | <Flex sx={ { alignItems: 'center' } }> 42 | <Checkbox 43 | id={ `check1` } 44 | disabled 45 | aria-labelledby={ `label-check1` } 46 | onCheckedChange={ () => {} } 47 | /> 48 | <Label clickable htmlFor={ `check1` } id={ `label-check1` }> 49 | This option 50 | </Label> 51 | </Flex> 52 | ); 53 | 54 | const button = screen.getByLabelText( 'This option' ); 55 | 56 | expect( button ).toHaveAttribute( 'aria-disabled', 'true' ); 57 | 58 | // Check for accessibility issues 59 | expect( await axe( container ) ).toHaveNoViolations(); 60 | } ); 61 | } ); 62 | -------------------------------------------------------------------------------- /src/system/Form/Checkbox/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | import * as CheckboxPrimitive from '@radix-ui/react-checkbox'; 4 | 5 | import { checkboxIndicatorStyle, checkboxStyle } from './styles'; 6 | import { RadioOptionProps } from '../Radio/RadioOption'; 7 | 8 | export interface CheckboxProps extends CheckboxPrimitive.CheckboxProps { 9 | disabled?: boolean; 10 | variant?: 'primary' | 'success' | 'brand' | 'disabled'; 11 | } 12 | 13 | const StyledCheckbox = ( { variant = 'primary', ...rest }: CheckboxProps ) => ( 14 | <CheckboxPrimitive.Root sx={ checkboxStyle( variant ) } { ...rest } /> 15 | ); 16 | 17 | interface StyledIndicatorProps extends CheckboxPrimitive.CheckboxIndicatorProps { 18 | variant: RadioOptionProps[ 'variant' ]; 19 | } 20 | 21 | const StyledIndicator = ( { variant, ...rest }: StyledIndicatorProps ) => ( 22 | <CheckboxPrimitive.Indicator sx={ checkboxIndicatorStyle( variant ) } { ...rest } /> 23 | ); 24 | 25 | const Checkbox = ( { 26 | disabled = false, 27 | onCheckedChange, 28 | variant = 'primary', 29 | ...props 30 | }: CheckboxProps ) => { 31 | if ( disabled === true || disabled === undefined ) { 32 | variant = 'disabled'; 33 | } 34 | 35 | return ( 36 | <StyledCheckbox 37 | onCheckedChange={ disabled ? undefined : onCheckedChange } 38 | aria-disabled={ disabled } 39 | variant={ variant } 40 | { ...props } 41 | > 42 | <StyledIndicator variant={ variant } /> 43 | </StyledCheckbox> 44 | ); 45 | }; 46 | 47 | export { Checkbox }; 48 | -------------------------------------------------------------------------------- /src/system/Form/Checkbox/styles.ts: -------------------------------------------------------------------------------- 1 | import { ThemeUIStyleObject } from 'theme-ui'; 2 | 3 | import { 4 | baseControlBorderStyle, 5 | baseControlFocusStyle, 6 | inputBaseBackground, 7 | inputBaseText, 8 | } from '../Input.styles'; 9 | 10 | // The output willl be 16px because of the 1px border. 11 | const CHECKBOX_SIZE = 14; 12 | 13 | export const checkboxStyle = ( variant: string ): ThemeUIStyleObject => { 14 | const variantColor = variant === 'disabled' ? 'input.background.disabled' : variant; 15 | 16 | return { 17 | all: 'unset', 18 | position: 'relative', 19 | backgroundColor: inputBaseBackground, 20 | ...baseControlBorderStyle, 21 | ...baseControlFocusStyle, 22 | width: CHECKBOX_SIZE, 23 | height: CHECKBOX_SIZE, 24 | borderRadius: 0, 25 | display: 'flex', 26 | justifyContent: 'center', 27 | '&[aria-disabled="true"]': { 28 | opacity: 0.7, 29 | cursor: 'not-allowed', 30 | pointerEvents: 'none', 31 | }, 32 | '&[data-state=checked], &[data-state=indeterminate]': { 33 | backgroundColor: variantColor, 34 | color: variantColor, 35 | borderColor: variantColor, 36 | }, 37 | '& ~ label': { 38 | fontWeight: 'regular', 39 | color: inputBaseText, 40 | m: 0, 41 | ml: 2, 42 | }, 43 | svg: { 44 | position: 'absolute', 45 | fill: 'currentColor', 46 | top: 0, 47 | left: 0, 48 | }, 49 | }; 50 | }; 51 | 52 | export const checkboxIndicatorStyle = ( variant: string ): ThemeUIStyleObject => { 53 | const backgroundColor = variant === 'disabled' ? 'icon.inverse-disabled' : 'icon.inverse'; 54 | 55 | return { 56 | width: 14, 57 | height: 14, 58 | backgroundColor, 59 | maskImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='1 1 14 14' fill='none'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M13.9259 4.9995L6.15142 12.4008L2.92603 9.33023L4.2591 7.92994L6.15142 9.73143L12.5928 3.59921L13.9259 4.9995Z' fill='currentcolor'/%3E%3C/svg%3E")`, 60 | 61 | '&[data-state=indeterminate]': { 62 | maskImage: 'none', 63 | backgroundColor, 64 | position: 'absolute', 65 | top: '50%', 66 | width: 8, 67 | height: 2, 68 | marginTop: '-1px', 69 | }, 70 | }; 71 | }; 72 | -------------------------------------------------------------------------------- /src/system/Form/Input.stories.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | /** 4 | * Internal dependencies 5 | */ 6 | import { Form } from '..'; 7 | 8 | export default { 9 | title: 'Form/Input', 10 | }; 11 | 12 | export const Default = () => ( 13 | <Form.Root> 14 | <Form.Input 15 | placeholder="Your input here..." 16 | label="Always add a label to inputs" 17 | forLabel="input-simple" 18 | /> 19 | 20 | <hr sx={ { my: 4 } } /> 21 | 22 | <Form.Input 23 | forLabel="input-with-error" 24 | label="Error Input" 25 | errorMessage="Please type numeric characters only" 26 | hasError 27 | /> 28 | 29 | <hr sx={ { my: 4 } } /> 30 | 31 | <Form.Input forLabel="input-with-required" label="Required" required /> 32 | 33 | <hr sx={ { my: 4 } } /> 34 | 35 | <Form.Label htmlFor="input-with-custom-label">Custom Label outside the Input</Form.Label> 36 | <Form.Input forLabel="input-with-custom-label" required /> 37 | <Form.Input forLabel="input-readonly" readOnly value="This is a readonly input" /> 38 | </Form.Root> 39 | ); 40 | -------------------------------------------------------------------------------- /src/system/Form/Input.styles.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from 'theme-ui'; 2 | 3 | export const baseControlBorderStyle = { 4 | borderWidth: '1px', 5 | borderStyle: 'solid', 6 | borderColor: 'input.border.default', 7 | }; 8 | // Temporary interface until we add types to the theme definition. 9 | interface InputTheme extends Theme { 10 | outline?: Record< string, string >; 11 | } 12 | 13 | export const inputBaseText = 'input.text.default'; 14 | export const inputBaseBackground = 'input.background.default'; 15 | export const baseControlFocusStyle = { 16 | '&:focus-visible': ( theme: InputTheme ) => theme.outline, 17 | '&:focus-within': ( theme: InputTheme ) => theme.outline, 18 | }; 19 | 20 | export const baseControlStyle = { 21 | ...baseControlBorderStyle, 22 | backgroundColor: inputBaseBackground, 23 | color: inputBaseText, 24 | borderRadius: 1, 25 | display: 'block', 26 | width: '100%', 27 | 28 | ...baseControlFocusStyle, 29 | '&:disabled': { 30 | borderColor: 'input.border.disabled', 31 | }, 32 | '&[readonly]': { 33 | borderColor: 'input.border.disabled', 34 | backgroundColor: 'input.background.read-only', 35 | }, 36 | '&::placeholder': { 37 | color: 'input.text.placeholder', 38 | opacity: 1, 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /src/system/Form/Input.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | /** 4 | * External dependencies 5 | */ 6 | import React from 'react'; 7 | import { Input as ThemeInput, InputProps as ThemeInputProps } from 'theme-ui'; 8 | 9 | /** 10 | * Internal dependencies 11 | */ 12 | import { baseControlStyle } from './Input.styles'; 13 | import { Validation, Label } from '../'; 14 | 15 | const inputStyles = { 16 | unset: 'all', 17 | ...baseControlStyle, 18 | lineHeight: 'inherit', 19 | minHeight: '36px', 20 | px: 3, 21 | py: 2, 22 | fontSize: 2, 23 | mb: 2, 24 | variant: 'inputs.default', 25 | }; 26 | 27 | interface InputProps extends ThemeInputProps { 28 | label?: string; 29 | hasError?: boolean; 30 | required?: boolean; 31 | forLabel?: string; 32 | errorMessage?: string; 33 | } 34 | export const Input = React.forwardRef< HTMLInputElement, InputProps >( 35 | ( { label, forLabel, hasError = false, required, sx = {}, errorMessage, ...props }, ref ) => ( 36 | <React.Fragment> 37 | { label && ( 38 | <Label required={ required } htmlFor={ forLabel }> 39 | { label } 40 | </Label> 41 | ) } 42 | <ThemeInput 43 | ref={ ref } 44 | id={ forLabel } 45 | required={ required } 46 | aria-required={ required } 47 | aria-describedby={ hasError ? `describe-${ forLabel }-validation` : undefined } 48 | sx={ { 49 | ...inputStyles, 50 | ...sx, 51 | ...( hasError ? { borderColor: 'input.border.error' } : {} ), 52 | } } 53 | { ...props } 54 | /> 55 | { hasError && errorMessage && ( 56 | <Validation isValid={ false } describedId={ forLabel }> 57 | { errorMessage } 58 | </Validation> 59 | ) } 60 | </React.Fragment> 61 | ) 62 | ); 63 | 64 | Input.displayName = 'Input'; 65 | -------------------------------------------------------------------------------- /src/system/Form/InputWithCopyButton.stories.jsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | /** 4 | * Internal dependencies 5 | */ 6 | import { useState } from 'react'; 7 | 8 | import { Form, Notice } from '..'; 9 | 10 | export default { 11 | title: 'Form/InputWithCopyButton', 12 | }; 13 | 14 | export const Default = () => { 15 | const [ copiedText, setCopiedText ] = useState( '' ); 16 | return ( 17 | <Form.Root> 18 | { copiedText && ( 19 | <Notice variant="success" sx={ { mb: 4 } }> 20 | Input successfully copied value! <strong>{ copiedText }</strong> 21 | </Notice> 22 | ) } 23 | <Form.InputWithCopyButton 24 | placeholder="Your input here..." 25 | label="Always add a label to inputs" 26 | forLabel="input-simple" 27 | copyHandler={ value => setCopiedText( value ) } 28 | /> 29 | <Form.InputWithCopyButton 30 | value="Copy me!" 31 | label="This is a readonly input" 32 | forLabel="input-simple" 33 | readOnly 34 | copyHandler={ value => setCopiedText( value ) } 35 | /> 36 | </Form.Root> 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /src/system/Form/Label.stories.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | /** 4 | * Internal dependencies 5 | */ 6 | import { Form, Label } from '..'; 7 | 8 | export default { 9 | title: 'Form/Label', 10 | }; 11 | 12 | const DefaultComponent = props => ( 13 | <Form.Root> 14 | <Label { ...props }>Label</Label> 15 | </Form.Root> 16 | ); 17 | 18 | export const Default = () => { 19 | return ( 20 | <> 21 | <DefaultComponent /> 22 | </> 23 | ); 24 | }; 25 | 26 | export const Required = () => { 27 | const args = { 28 | required: true, 29 | }; 30 | 31 | return ( 32 | <> 33 | <DefaultComponent { ...args } /> 34 | </> 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /src/system/Form/Label.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | /** 4 | * External dependencies 5 | */ 6 | import React from 'react'; 7 | import { Box, BoxProps } from 'theme-ui'; 8 | 9 | /** 10 | * Internal dependencies 11 | */ 12 | import { RequiredLabel } from './RequiredLabel'; 13 | 14 | export const baseLabelColor = 'input.label.default'; 15 | export const baseLabelStyle = { 16 | fontWeight: 'heading', 17 | fontSize: 2, 18 | lineHeight: 1.5, 19 | color: baseLabelColor, 20 | }; 21 | 22 | export interface LabelProps extends BoxProps { 23 | children?: React.ReactNode; 24 | clickable?: boolean; 25 | required?: boolean; 26 | htmlFor?: string; 27 | } 28 | 29 | export const Label = React.forwardRef< HTMLLabelElement, LabelProps >( 30 | ( { sx, children, required, clickable, as = 'label', ...rest }, forwardRef ) => ( 31 | <Box 32 | as={ as } 33 | sx={ { 34 | all: 'unset', 35 | ...baseLabelStyle, 36 | display: 'block', 37 | mb: 2, 38 | cursor: clickable ? 'pointer' : 'default', 39 | ...sx, 40 | } } 41 | ref={ forwardRef } 42 | { ...rest } 43 | > 44 | { children } 45 | { required && <RequiredLabel /> } 46 | </Box> 47 | ) 48 | ); 49 | 50 | Label.displayName = 'Label'; 51 | -------------------------------------------------------------------------------- /src/system/Form/Radio/Radio.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { render, screen } from '@testing-library/react'; 5 | import { axe } from 'jest-axe'; 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import { Radio } from './Radio'; 11 | 12 | describe( '<Radio />', () => { 13 | it( 'renders', async () => { 14 | const { container } = render( 15 | <Radio 16 | disabled 17 | name={ `the_option_` } 18 | defaultValue={ `disabled-option-a` } 19 | options={ [ 20 | { 21 | id: `disabled-option-a`, 22 | value: `disabled-option-a`, 23 | label: `I am the option A`, 24 | }, 25 | { 26 | id: `disabled-option-b`, 27 | value: `disabled-option-b`, 28 | label: `I am the option B`, 29 | }, 30 | ] } 31 | /> 32 | ); 33 | 34 | expect( screen.getByLabelText( 'I am the option A' ) ).toBeInTheDocument(); 35 | expect( screen.getByLabelText( 'I am the option B' ) ).toBeInTheDocument(); 36 | 37 | // Check for accessibility issues 38 | expect( await axe( container ) ).toHaveNoViolations(); 39 | } ); 40 | 41 | it( 'renders with a custom label', async () => { 42 | const { container } = render( 43 | <Radio 44 | disabled 45 | name={ `the_option_` } 46 | defaultValue={ `disabled-option-a` } 47 | options={ [ 48 | { 49 | id: `disabled-option-a`, 50 | value: `disabled-option-a`, 51 | label: 'I am ignored', 52 | renderLabel: ( props, labelStyles ) => ( 53 | <label htmlFor="disabled-option-a" { ...props } sx={ labelStyles }> 54 | <span>I am the custom option A</span> 55 | </label> 56 | ), 57 | }, 58 | { 59 | id: `disabled-option-b`, 60 | value: `disabled-option-b`, 61 | label: `I am the option B`, 62 | }, 63 | ] } 64 | /> 65 | ); 66 | 67 | expect( screen.getByLabelText( 'I am the custom option A' ) ).toBeInTheDocument(); 68 | expect( screen.getByLabelText( 'I am the option B' ) ).toBeInTheDocument(); 69 | 70 | // Check for accessibility issues 71 | expect( await axe( container ) ).toHaveNoViolations(); 72 | } ); 73 | } ); 74 | -------------------------------------------------------------------------------- /src/system/Form/Radio/Radio.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | import classNames from 'classnames'; 4 | import React, { Ref, forwardRef } from 'react'; 5 | 6 | import { RadioOption, RadioOptionOptionProps } from './RadioOption'; 7 | 8 | export const VIP_RADIO = 'vip-radio-component'; 9 | 10 | export type RadioProps = { 11 | variant?: 'primary' | 'success' | 'brand' | 'disabled'; 12 | disabled?: boolean | undefined; 13 | defaultValue?: string | number; 14 | onChange?: ( e: React.ChangeEvent< HTMLInputElement >, option?: RadioOptionOptionProps ) => void; 15 | name?: string; 16 | options?: RadioOptionOptionProps[]; 17 | className?: string; 18 | }; 19 | 20 | const Radio = forwardRef< HTMLDivElement, RadioProps >( 21 | ( 22 | { 23 | variant = 'primary', 24 | disabled = false, 25 | defaultValue, 26 | onChange, 27 | name = '', 28 | options = [], 29 | className, 30 | ...props 31 | }: RadioProps, 32 | ref: Ref< HTMLDivElement > 33 | ) => { 34 | // If disabled is pass globally, it will overwrite the variant 35 | if ( disabled === true || disabled === undefined ) { 36 | variant = 'disabled'; 37 | } 38 | 39 | const onChangeHandler = ( e: React.ChangeEvent< HTMLInputElement > ) => { 40 | const optionTriggered = options.find( option => { 41 | const optionValue = `${ option.value }`; 42 | const selectedOptionValue = `${ e.target.value }`; 43 | 44 | return optionValue === selectedOptionValue; 45 | } ); 46 | 47 | if ( onChange ) { 48 | onChange( e, optionTriggered ); 49 | } 50 | }; 51 | 52 | const renderedOptions = options.map( option => ( 53 | <RadioOption 54 | variant={ option?.disabled ? 'disabled' : variant } 55 | key={ option?.id } 56 | name={ name } 57 | option={ option } 58 | disabled={ disabled || option?.disabled } 59 | onChangeHandler={ onChangeHandler } 60 | checked={ `${ defaultValue }` === `${ option?.value }` } 61 | /> 62 | ) ); 63 | 64 | return ( 65 | <div 66 | ref={ ref } 67 | className={ classNames( VIP_RADIO, `${ VIP_RADIO }-${ name }`, className ) } 68 | { ...props } 69 | > 70 | { renderedOptions } 71 | </div> 72 | ); 73 | } 74 | ); 75 | 76 | export { Radio }; 77 | -------------------------------------------------------------------------------- /src/system/Form/Radio/RadioOption.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | import classNames from 'classnames'; 4 | import React from 'react'; 5 | import { Box, ThemeUIStyleObject } from 'theme-ui'; 6 | 7 | import { VIP_RADIO } from './Radio'; 8 | import { inputStyle, itemStyle, labelStyle } from './styles'; 9 | import { Label } from '../Label'; 10 | 11 | export type RadioOptionOptionProps = { 12 | id: string; 13 | value: string; 14 | disabled?: boolean; 15 | className?: string; 16 | label?: string; 17 | renderLabel?: ( props, labelStyles: ThemeUIStyleObject ) => JSX.Element; 18 | labelProps?: object; 19 | inputProps?: object; 20 | }; 21 | 22 | export interface RadioOptionProps { 23 | option: RadioOptionOptionProps; 24 | name: string; 25 | variant: string; 26 | disabled: boolean | undefined; 27 | onChangeHandler: ( e: React.ChangeEvent< HTMLInputElement > ) => void; 28 | checked: boolean; 29 | } 30 | 31 | const RadioOption = ( { 32 | option: { id, value, className, label, renderLabel, labelProps = {}, inputProps = {} }, 33 | name, 34 | variant, 35 | disabled, 36 | onChangeHandler, 37 | checked, 38 | }: RadioOptionProps ) => { 39 | const commonLabelProps = { 40 | className: `${ VIP_RADIO }item-label`, 41 | htmlFor: id, 42 | ...labelProps, 43 | }; 44 | 45 | return ( 46 | <Box 47 | as="div" 48 | sx={ itemStyle } 49 | className={ classNames( 50 | `${ VIP_RADIO }item`, 51 | `${ VIP_RADIO }item-${ id }`, 52 | checked ? `${ VIP_RADIO }item-checked` : '', 53 | className 54 | ) } 55 | > 56 | <input 57 | type="radio" 58 | id={ id } 59 | aria-disabled={ disabled } 60 | name={ name } 61 | value={ `${ value }` } 62 | sx={ inputStyle( variant ) } 63 | onChange={ onChangeHandler } 64 | className={ `${ VIP_RADIO }item-input` } 65 | checked={ checked } 66 | { ...inputProps } 67 | /> 68 | 69 | { renderLabel ? ( 70 | renderLabel( commonLabelProps, labelStyle( variant ) ) 71 | ) : ( 72 | <Label { ...commonLabelProps } sx={ labelStyle( variant ) }> 73 | { label } 74 | </Label> 75 | ) } 76 | </Box> 77 | ); 78 | }; 79 | 80 | export { RadioOption }; 81 | -------------------------------------------------------------------------------- /src/system/Form/Radio/styles.ts: -------------------------------------------------------------------------------- 1 | import { ThemeUIStyleObject } from 'theme-ui'; 2 | 3 | import { baseControlBorderStyle, inputBaseText } from '../Input.styles'; 4 | 5 | // The output willl be 18px because of the 1px border. 6 | const RADIO_SIZE = 16; 7 | 8 | export const inputStyle = ( variant: string ): ThemeUIStyleObject => ( { 9 | position: 'absolute', 10 | top: 0, 11 | left: 0, 12 | clip: 'rect(1px, 1px, 1px, 1px)', 13 | clipPath: 'inset(50%)', 14 | width: RADIO_SIZE, 15 | height: RADIO_SIZE, 16 | '&:focus ~ label:before': { 17 | variant: 'outline', 18 | content: '""', 19 | border: '1px solid', 20 | borderColor: baseControlBorderStyle.borderColor, 21 | left: 0, 22 | }, 23 | '&:checked ~ label::after': { 24 | borderColor: variant, 25 | opacity: 1, 26 | }, 27 | '&[aria-disabled="true"] ~ label::before': { 28 | backgroundColor: variant, 29 | borderColor: variant, 30 | }, 31 | } ); 32 | 33 | export const labelStyle = ( variant: string ): ThemeUIStyleObject => ( { 34 | display: 'flex', 35 | cursor: 'pointer', 36 | position: 'relative', 37 | marginBottom: 0, 38 | userSelect: 'none', 39 | color: inputBaseText, 40 | fontWeight: 'regular', 41 | lineHeight: 'normal', 42 | '&:before, &:after': { 43 | borderRadius: '100%', 44 | transition: 'all .3s ease-out', 45 | width: RADIO_SIZE, 46 | height: RADIO_SIZE, 47 | }, 48 | '&::before': { 49 | content: '""', 50 | border: '1px solid', 51 | borderColor: baseControlBorderStyle.borderColor, 52 | marginRight: 2, 53 | }, 54 | '&::after': { 55 | position: 'absolute', 56 | top: 0, 57 | left: 0, 58 | content: '""', 59 | backgroundColor: variant, 60 | backgroundSize: '100%', 61 | backgroundRepeat: 'no-repeat', 62 | backgroundImage: `url("data:image/svg+xml,%3Csvg width='6' height='6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='3' cy='3' r='1.25' fill='%23fff'/%3E%3C/svg%3E")`, 63 | border: '1px solid', 64 | color: 'white', 65 | opacity: 0, 66 | }, 67 | } ); 68 | 69 | export const itemStyle: ThemeUIStyleObject = { 70 | display: 'flex', 71 | alignItems: 'center', 72 | my: 2, 73 | position: 'relative', 74 | }; 75 | -------------------------------------------------------------------------------- /src/system/Form/RadioBoxGroup.stories.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { useState } from 'react'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import { RadioBoxGroup } from '..'; 10 | 11 | export default { 12 | title: 'RadioBoxGroup', 13 | component: RadioBoxGroup, 14 | parameters: { 15 | docs: { 16 | description: { 17 | component: ` 18 | A radio-box-group is a group of radio buttons that are styled as boxes. This component is used 19 | to allow users to select one option from a list of options. 20 | 21 | ## Guidance 22 | 23 | ### When to use the component 24 | 25 | - <strong>Select an option in a form.</strong> Use a radio-box-group when you want users to select 26 | a single option from a list of options. 27 | - <strong>Use as a toggle-group.</strong> Use a radio-box-group with the chip variant when you want 28 | to allow users to toggle between different options. 29 | 30 | ------- 31 | 32 | This documentation is heavily inspired by the [U.S Web Design System (USWDS)](https://designsystem.digital.gov/components/tooltip/#package). We use USWDS as trusted source of truth for accessibility and usability best practices. 33 | 34 | ## Component Properties 35 | `, 36 | }, 37 | }, 38 | }, 39 | }; 40 | 41 | const options = [ 42 | { 43 | label: 'One', 44 | value: 'one', 45 | description: 'This is a description', 46 | }, 47 | { 48 | label: 'Two', 49 | value: 'two', 50 | description: 'This is a description', 51 | }, 52 | { 53 | label: 'Three', 54 | value: 'three', 55 | description: 'This is a description', 56 | }, 57 | ]; 58 | 59 | export const Default = () => { 60 | const [ value, setValue ] = useState( 'one' ); 61 | return ( 62 | <RadioBoxGroup 63 | defaultValue={ value } 64 | onChange={ e => setValue( e.target.value ) } 65 | options={ options } 66 | optionWidth="300px" 67 | /> 68 | ); 69 | }; 70 | 71 | export const Errors = () => { 72 | const [ value, setValue ] = useState( null ); 73 | 74 | return ( 75 | <RadioBoxGroup 76 | defaultValue={ value } 77 | onChange={ e => setValue( e.target.value ) } 78 | options={ options } 79 | required 80 | groupLabel="Radio Box Group" 81 | hasError={ true } 82 | errorMessage="This is an error message" 83 | /> 84 | ); 85 | }; 86 | -------------------------------------------------------------------------------- /src/system/Form/RadioBoxGroup.test.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { render, screen } from '@testing-library/react'; 5 | import { axe } from 'jest-axe'; 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import { RadioBoxGroup } from './RadioBoxGroup'; 11 | 12 | const defaultProps = { 13 | options: [ 14 | { 15 | label: 'One', 16 | value: 'one', 17 | description: 'This is desc 1', 18 | }, 19 | { 20 | label: 'Two', 21 | value: 'two', 22 | description: 'This is desc 2', 23 | }, 24 | { 25 | label: 'Three', 26 | value: 'three', 27 | description: 'This is desc 3', 28 | }, 29 | ], 30 | onChange: jest.fn(), 31 | }; 32 | 33 | describe( '<RadioBoxGroup />', () => { 34 | it( 'renders the component', async () => { 35 | const { container } = render( <RadioBoxGroup { ...defaultProps } /> ); 36 | 37 | const dom = await screen.findAllByRole( 'radio' ); 38 | 39 | expect( dom ).toHaveLength( 3 ); 40 | expect( dom[ 0 ] ).toHaveAttribute( 'value', 'one' ); 41 | expect( dom[ 1 ] ).toHaveAttribute( 'value', 'two' ); 42 | expect( dom[ 2 ] ).toHaveAttribute( 'value', 'three' ); 43 | 44 | // Check for accessibility issues 45 | expect( await axe( container ) ).toHaveNoViolations(); 46 | } ); 47 | } ); 48 | -------------------------------------------------------------------------------- /src/system/Form/RadioGroupChip.stories.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { useState } from 'react'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import { RadioGroupChip } from './RadioGroupChip'; 10 | 11 | export default { 12 | title: 'RadioGroupChip', 13 | component: RadioGroupChip, 14 | parameters: { 15 | docs: { 16 | description: { 17 | component: ` 18 | A radio-group-chip is a group of radio buttons that are styled as boxes. This component is used 19 | to allow users to toggle between different options 20 | 21 | ------- 22 | 23 | This documentation is heavily inspired by the [U.S Web Design System (USWDS)](https://designsystem.digital.gov/components/tooltip/#package). We use USWDS as trusted source of truth for accessibility and usability best practices. 24 | 25 | ## Component Properties 26 | `, 27 | }, 28 | }, 29 | }, 30 | }; 31 | 32 | export const MediumSize = () => { 33 | const [ value, setValue ] = useState( 'table' ); 34 | 35 | return ( 36 | <RadioGroupChip 37 | defaultValue={ value } 38 | onChange={ e => setValue( e.target.value ) } 39 | options={ [ 40 | { 41 | label: 'Table', 42 | value: 'table', 43 | }, 44 | { 45 | label: 'Grid', 46 | value: 'grid', 47 | }, 48 | ] } 49 | /> 50 | ); 51 | }; 52 | 53 | export const SmallSize = () => { 54 | const [ value, setValue ] = useState( 'table' ); 55 | 56 | return ( 57 | <RadioGroupChip 58 | defaultValue={ value } 59 | onChange={ e => setValue( e.target.value ) } 60 | options={ [ 61 | { 62 | label: 'Table', 63 | value: 'table', 64 | }, 65 | { 66 | label: 'Grid', 67 | value: 'grid', 68 | }, 69 | ] } 70 | size="small" 71 | /> 72 | ); 73 | }; 74 | -------------------------------------------------------------------------------- /src/system/Form/RadioGroupChip.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { render, screen } from '@testing-library/react'; 5 | import { axe } from 'jest-axe'; 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import { RadioGroupChip } from './RadioGroupChip'; 11 | 12 | const defaultProps = { 13 | options: [ 14 | { 15 | label: 'One', 16 | value: 'one', 17 | }, 18 | { 19 | label: 'Two', 20 | value: 'two', 21 | }, 22 | { 23 | label: 'Three', 24 | value: 'three', 25 | }, 26 | ], 27 | onChange: jest.fn(), 28 | }; 29 | 30 | describe( '<RadioGroupChip />', () => { 31 | it( 'renders the default variant', async () => { 32 | const { container } = render( <RadioGroupChip { ...defaultProps } /> ); 33 | 34 | const dom = await screen.findAllByRole( 'radio' ); 35 | 36 | expect( dom ).toHaveLength( 3 ); 37 | expect( dom[ 0 ] ).toHaveAttribute( 'value', 'one' ); 38 | expect( dom[ 1 ] ).toHaveAttribute( 'value', 'two' ); 39 | expect( dom[ 2 ] ).toHaveAttribute( 'value', 'three' ); 40 | 41 | // Check for accessibility issues 42 | expect( await axe( container ) ).toHaveNoViolations(); 43 | } ); 44 | } ); 45 | -------------------------------------------------------------------------------- /src/system/Form/RequiredLabel.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | /** 4 | * External dependencies 5 | */ 6 | 7 | export const RequiredLabel = () => ( 8 | <span sx={ { color: 'texts.helper', display: 'inline-block', ml: 2, fontSize: 1 } }> 9 | (Required) 10 | </span> 11 | ); 12 | -------------------------------------------------------------------------------- /src/system/Form/Textarea.js: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | /** 4 | * External dependencies 5 | */ 6 | import React from 'react'; 7 | 8 | /** 9 | * Internal dependencies 10 | */ 11 | import { Input } from './Input'; 12 | 13 | const Textarea = React.forwardRef( ( props, ref ) => ( 14 | <Input ref={ ref } as="textarea" { ...props } /> 15 | ) ); 16 | 17 | Textarea.displayName = 'Textarea'; 18 | 19 | export { Textarea }; 20 | -------------------------------------------------------------------------------- /src/system/Form/Textarea.stories.jsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | /** 4 | * Internal dependencies 5 | */ 6 | import * as Form from '../NewForm'; 7 | 8 | export default { 9 | title: 'Form/Textarea', 10 | argTypes: { 11 | placeholder: { 12 | type: { name: 'string', required: false }, 13 | control: { type: 'text' }, 14 | }, 15 | label: { 16 | type: { name: 'string', required: false }, 17 | control: { type: 'text' }, 18 | }, 19 | }, 20 | }; 21 | 22 | const DefaultComponent = () => ( 23 | <Form.Root> 24 | <Form.Textarea forLabel="my-text-area" rows="5" label="Regular textarea" /> 25 | 26 | <hr sx={ { my: 4 } } /> 27 | 28 | <Form.Textarea 29 | forLabel="my-text-area-error" 30 | rows="5" 31 | label="Error textarea" 32 | errorMessage="Please type numeric characters only" 33 | required 34 | hasError 35 | /> 36 | </Form.Root> 37 | ); 38 | 39 | export const Default = DefaultComponent.bind( {} ); 40 | Default.args = {}; 41 | -------------------------------------------------------------------------------- /src/system/Form/Toggle.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { render, screen } from '@testing-library/react'; 5 | import { axe } from 'jest-axe'; 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import { Toggle } from './Toggle'; 11 | 12 | describe( '<Toggle />', () => { 13 | it( 'renders the Toggle component', async () => { 14 | const { container } = render( 15 | <Toggle aria-label="Dinner room Light" defaultChecked name="my-toggle" /> 16 | ); 17 | 18 | expect( screen.getByRole( 'switch' ) ).toBeInTheDocument(); 19 | 20 | // Check for accessibility issues 21 | expect( await axe( container ) ).toHaveNoViolations(); 22 | } ); 23 | } ); 24 | -------------------------------------------------------------------------------- /src/system/Form/ToggleRow.js: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | /** 4 | * External dependencies 5 | */ 6 | import PropTypes from 'prop-types'; 7 | 8 | /** 9 | * Internal dependencies 10 | */ 11 | import { Badge, Box, Card, Flex, Heading, Text, Toggle } from '..'; 12 | 13 | const ToggleRow = ( { image, badge, title, subTitle, body, meta, sx, ...props } ) => ( 14 | <Flex 15 | sx={ { 16 | alignItems: 'center', 17 | py: 3, 18 | borderBottom: '1px solid', 19 | textDecoration: 'none', 20 | color: 'inherit', 21 | '&:first-of-type': { 22 | borderTop: '1px solid', 23 | borderColor: 'border', 24 | }, 25 | borderColor: 'border', 26 | ...sx, 27 | } } 28 | > 29 | { image && ( 30 | <Box sx={ { mr: 3 } }> 31 | <Card 32 | sx={ { 33 | p: 3, 34 | m: 0, 35 | boxShadow: 'low', 36 | flex: '0 0 auto', 37 | } } 38 | > 39 | <img 40 | src={ image } 41 | width={ 32 } 42 | sx={ { display: 'block' } } 43 | alt="Icon representing a toggle" 44 | /> 45 | </Card> 46 | </Box> 47 | ) } 48 | 49 | <Box sx={ { flex: '1 1 auto', mr: 3 } }> 50 | <Heading variant="h4" sx={ { mb: subTitle || body ? 1 : 0 } }> 51 | { title } 52 | { badge && <Badge sx={ { marginLeft: 2 } }>{ badge }</Badge> } 53 | </Heading> 54 | { subTitle && <Text sx={ { mb: 1, color: 'muted' } }>{ subTitle }</Text> } 55 | { body && <Text sx={ { mb: 0 } }>{ body }</Text> } 56 | { meta && meta } 57 | </Box> 58 | <Box> 59 | <Toggle { ...props } /> 60 | </Box> 61 | </Flex> 62 | ); 63 | 64 | ToggleRow.propTypes = { 65 | image: PropTypes.oneOfType( [ PropTypes.object, PropTypes.string ] ), 66 | badge: PropTypes.string, 67 | title: PropTypes.node, 68 | subTitle: PropTypes.node, 69 | body: PropTypes.node, 70 | meta: PropTypes.node, 71 | sx: PropTypes.object, 72 | }; 73 | 74 | export { ToggleRow }; 75 | -------------------------------------------------------------------------------- /src/system/Form/Validation.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | /** 4 | * External dependencies 5 | */ 6 | import { MdErrorOutline, MdCheckCircle } from 'react-icons/md'; 7 | 8 | const errorColor = 'texts.error'; 9 | const helperColor = 'texts.helper'; 10 | 11 | interface ValidationProps { 12 | children?: React.ReactNode; 13 | isValid?: boolean; 14 | describedId?: string; 15 | } 16 | 17 | export const Validation = ( { children, isValid, describedId, ...props }: ValidationProps ) => { 18 | return ( 19 | <p 20 | sx={ { 21 | color: isValid ? helperColor : errorColor, 22 | display: 'flex', 23 | alignItems: 'center', 24 | m: 0, 25 | fontSize: 1, 26 | } } 27 | id={ describedId ? `describe-${ describedId }-validation` : undefined } 28 | { ...props } 29 | > 30 | { isValid ? ( 31 | <MdCheckCircle sx={ { mr: 1 } } aria-hidden="true" /> 32 | ) : ( 33 | <MdErrorOutline sx={ { mr: 1 } } aria-hidden="true" /> 34 | ) } 35 | { children } 36 | </p> 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /src/system/Form/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { Checkbox } from './Checkbox/Checkbox'; 5 | import { Input } from './Input'; 6 | import { InputWithCopyButton } from './InputWithCopyButton'; 7 | import { Label } from './Label'; 8 | import { Radio } from './Radio/Radio'; 9 | import { RadioBoxGroup } from './RadioBoxGroup'; 10 | import { RadioGroupChip } from './RadioGroupChip'; 11 | import { Textarea } from './Textarea'; 12 | import { Toggle } from './Toggle'; 13 | import { ToggleRow } from './ToggleRow'; 14 | import { Validation } from './Validation'; 15 | 16 | export { 17 | Input, 18 | InputWithCopyButton, 19 | Label, 20 | Radio, 21 | RadioBoxGroup, 22 | RadioGroupChip, 23 | Textarea, 24 | Toggle, 25 | ToggleRow, 26 | Validation, 27 | Checkbox, 28 | }; 29 | -------------------------------------------------------------------------------- /src/system/Grid/Grid.stories.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { Grid } from '..'; 5 | 6 | export default { 7 | title: 'Grid', 8 | component: Grid, 9 | }; 10 | 11 | export const Default = () => <Grid>Hello</Grid>; 12 | -------------------------------------------------------------------------------- /src/system/Grid/Grid.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { forwardRef, Ref } from 'react'; 5 | import { Grid as ThemeGrid, GridProps as ThemeGridProps } from 'theme-ui'; 6 | 7 | export const Grid = forwardRef< HTMLDivElement, ThemeGridProps >( 8 | ( props: ThemeGridProps, ref: Ref< HTMLDivElement > ) => <ThemeGrid { ...props } ref={ ref } /> 9 | ); 10 | 11 | Grid.displayName = 'Grid'; 12 | -------------------------------------------------------------------------------- /src/system/Grid/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | export { Grid } from './Grid'; 5 | -------------------------------------------------------------------------------- /src/system/Heading/Heading.stories.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { Box, Heading } from '..'; 5 | 6 | export default { 7 | title: 'Heading', 8 | component: Heading, 9 | }; 10 | 11 | export const Default = () => ( 12 | <Box> 13 | <Heading variant="h1">Your Applications</Heading> 14 | <Heading variant="h2">Heading Two</Heading> 15 | <Heading variant="h3">Heading Three</Heading> 16 | <Heading variant="h4">Heading Four</Heading> 17 | <Heading variant="h5">Heading Five</Heading> 18 | <Heading variant="h6">Heading Six</Heading> 19 | </Box> 20 | ); 21 | -------------------------------------------------------------------------------- /src/system/Heading/Heading.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import classNames from 'classnames'; 5 | import { forwardRef, Ref } from 'react'; 6 | import { Heading as ThemeHeading, HeadingProps as ThemeHeadingProps } from 'theme-ui'; 7 | 8 | export interface HeadingProps extends ThemeHeadingProps { 9 | variant?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; 10 | } 11 | 12 | export const Heading = forwardRef< HTMLHeadingElement, HeadingProps >( 13 | ( { variant = 'h3', sx, className, ...rest }: HeadingProps, ref: Ref< HTMLHeadingElement > ) => ( 14 | <ThemeHeading 15 | as={ variant } 16 | sx={ { 17 | color: 'heading', 18 | // pass variant prop to sx 19 | variant: `text.${ variant.toString() }`, 20 | ...sx, 21 | } } 22 | className={ classNames( 'vip-heading-component', className ) } 23 | ref={ ref } 24 | { ...rest } 25 | /> 26 | ) 27 | ); 28 | 29 | Heading.displayName = 'Heading'; 30 | -------------------------------------------------------------------------------- /src/system/Heading/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { Heading } from './Heading'; 5 | 6 | export { Heading }; 7 | -------------------------------------------------------------------------------- /src/system/Hr/Hr.stories.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { Hr } from './Hr'; 5 | 6 | import type { StoryObj } from '@storybook/react'; 7 | 8 | /** 9 | * Internal dependencies 10 | */ 11 | 12 | export default { 13 | title: 'Hr', 14 | component: Hr, 15 | parameters: { 16 | docs: { 17 | description: { 18 | component: ` 19 | 20 | Horizontal Line. 21 | 22 | ## Guidance 23 | 24 | ### When to use the link component 25 | 26 | - When you want to separate sections with a horizontal line. 27 | 28 | ### When to consider something else 29 | 30 | - When you want to display a vertical line; 31 | 32 | ## Component Properties 33 | `, 34 | }, 35 | }, 36 | }, 37 | }; 38 | 39 | type Story = StoryObj< typeof Hr >; 40 | 41 | export const Default: Story = { 42 | render: () => ( 43 | <> 44 | Horizontal Line: 45 | <Hr /> 46 | </> 47 | ), 48 | }; 49 | -------------------------------------------------------------------------------- /src/system/Hr/Hr.test.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 3 | // @ts-nocheck 4 | import { render } from '@testing-library/react'; 5 | import { axe } from 'jest-axe'; 6 | import { ThemeUIProvider } from 'theme-ui'; 7 | 8 | import { Hr } from './Hr'; 9 | import theme from '../theme'; 10 | 11 | const renderWithTheme = children => 12 | render( <ThemeUIProvider theme={ theme }>{ children }</ThemeUIProvider> ); 13 | 14 | const renderComponent = () => renderWithTheme( <Hr /> ); 15 | 16 | describe( '<Hr />', () => { 17 | it( 'renders the Hr component', async () => { 18 | const { container } = renderComponent(); 19 | 20 | expect( await axe( container ) ).toHaveNoViolations(); 21 | } ); 22 | } ); 23 | -------------------------------------------------------------------------------- /src/system/Hr/Hr.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | import { ThemeUIStyleObject } from 'theme-ui'; 4 | 5 | export type HrProps = { 6 | sx?: ThemeUIStyleObject; 7 | }; 8 | 9 | export const Hr = ( { sx, ...rest }: HrProps ) => ( 10 | <hr sx={ { my: 4, border: 0, height: '1px', backgroundColor: 'borders.2', ...sx } } { ...rest } /> 11 | ); 12 | -------------------------------------------------------------------------------- /src/system/Link/Link.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { forwardRef, Ref } from 'react'; 5 | import { 6 | Link as ThemeLink, 7 | LinkProps as ThemeLinkProps, 8 | Theme, 9 | ThemeUIStyleObject, 10 | } from 'theme-ui'; 11 | 12 | // Temporary interface until we add types to the theme definition. 13 | interface LinkTheme extends Theme { 14 | outline?: Record< string, string >; 15 | } 16 | 17 | export enum LinkVariant { 18 | 'primary', 19 | 'button-primary', 20 | 'button-secondary', 21 | 'button-tertiary', 22 | 'button-ghost', 23 | 'button-display', 24 | 'button-danger', 25 | } 26 | 27 | export interface LinkProps extends ThemeLinkProps { 28 | variant?: keyof typeof LinkVariant; 29 | } 30 | 31 | export const linkUnderlineProperties: ThemeUIStyleObject = { 32 | textDecorationLine: 'underline', 33 | textDecorationThickness: '0.07rem', 34 | textUnderlineOffset: '0.250rem', 35 | }; 36 | 37 | export const defaultLinkComponentStyle: ThemeUIStyleObject = { 38 | '&:focus-visible': ( theme: LinkTheme ) => theme.outline, 39 | }; 40 | 41 | export const Link = forwardRef< HTMLAnchorElement, LinkProps >( 42 | ( { variant = 'primary', sx, ...props }: LinkProps, ref: Ref< HTMLAnchorElement > ) => ( 43 | <ThemeLink 44 | variant={ variant } 45 | sx={ { 46 | ...defaultLinkComponentStyle, 47 | ...sx, 48 | } } 49 | ref={ ref } 50 | { ...props } 51 | /> 52 | ) 53 | ); 54 | 55 | Link.displayName = 'Link'; 56 | -------------------------------------------------------------------------------- /src/system/Link/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | export { Link } from './Link'; 5 | -------------------------------------------------------------------------------- /src/system/LinkExternal/LinkExternal.stories.tsx: -------------------------------------------------------------------------------- 1 | import LinkExternal from './LinkExternal'; 2 | 3 | import type { StoryObj } from '@storybook/react'; 4 | 5 | export default { 6 | title: 'Navigation/LinkExternal', 7 | component: LinkExternal, 8 | }; 9 | 10 | type Story = StoryObj< typeof LinkExternal >; 11 | 12 | export const Default: Story = { 13 | args: { 14 | children: 'View on GitHub', 15 | href: 'https://github.com/Automattic/vip-design-system', 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /src/system/LinkExternal/LinkExternal.test.tsx: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | import { render, screen } from '@testing-library/react'; 3 | 4 | /** 5 | * Internal dependencies 6 | */ 7 | import LinkExternal from './LinkExternal'; 8 | 9 | const props = { 10 | children: 'View on Github', 11 | href: 'https://github.com/Automattic/vip-design-system', 12 | }; 13 | 14 | describe( '<LinkExternal />', () => { 15 | it( 'should render correctly', () => { 16 | render( <LinkExternal { ...props }>View on Github</LinkExternal> ); 17 | 18 | const link = screen.getByRole( 'link' ); 19 | 20 | expect( link ).toHaveTextContent( /view on github/i ); 21 | expect( link ).toHaveTextContent( /external link/i ); 22 | expect( link ).toHaveAttribute( 'target', '_self' ); 23 | expect( link ).toHaveAttribute( 'rel', '' ); 24 | } ); 25 | 26 | it( 'should open in new tab when newTab is true', () => { 27 | render( <LinkExternal { ...props } newTab /> ); 28 | 29 | const link = screen.getByRole( 'link' ); 30 | 31 | expect( link ).toHaveTextContent( /opens in a new tab/i ); 32 | expect( link ).toHaveAttribute( 'target', '_blank' ); 33 | expect( link ).toHaveAttribute( 'rel', 'noopener noreferrer' ); 34 | } ); 35 | 36 | it( 'should contain additional screenreader text when added', () => { 37 | render( <LinkExternal { ...props } screenReaderText="VIP Design System" /> ); 38 | 39 | expect( screen.getByRole( 'link', { name: /vip design system/i } ) ).toBeInTheDocument(); 40 | } ); 41 | 42 | it( 'should hide icon when showExternalIcon is false', () => { 43 | render( <LinkExternal { ...props } showExternalIcon={ false } /> ); 44 | 45 | expect( screen.queryByText( '↗' ) ).not.toBeInTheDocument(); 46 | } ); 47 | } ); 48 | -------------------------------------------------------------------------------- /src/system/LinkExternal/LinkExternal.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { translate } from 'i18n-calypso'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import { Link } from '../Link'; 10 | import ScreenReaderText from '../ScreenReaderText'; 11 | 12 | import type { LinkProps } from '../Link/Link'; 13 | 14 | // Screen reader announcements 15 | const DEFAULT_EXTERNAL_LINK_TEXT = translate( ', external link' ); 16 | const NEW_TAB_TEXT = translate( ', opens in a new tab' ); 17 | 18 | export type LinkExternalProps = LinkProps & { 19 | /** 20 | * Element to be linked. 21 | **/ 22 | children?: React.ReactNode; 23 | /** 24 | * Additional text to include after `defaultScreenReaderText` if enabled. 25 | **/ 26 | screenReaderText?: string | number; 27 | /** 28 | * Optional arrow icon. 29 | * 30 | * @default true 31 | **/ 32 | showExternalIcon?: boolean; 33 | /** 34 | * Include default text which reads as: `link, <link text>, external link` 35 | * or if `newTab` is `true`, reads as: `link, <link text>, external link, opens in a new tab` 36 | **/ 37 | defaultScreenReaderText?: boolean; 38 | /** 39 | * If link should open in a new tab. 40 | * 41 | * @default false 42 | **/ 43 | newTab?: boolean; 44 | }; 45 | 46 | export const LinkExternal = ( { 47 | children = null, 48 | screenReaderText = '', 49 | showExternalIcon = true, 50 | newTab = false, 51 | ...rest 52 | }: LinkExternalProps ) => ( 53 | <Link 54 | as="a" 55 | target={ newTab ? '_blank' : '_self' } 56 | rel={ newTab ? 'noopener noreferrer' : '' } 57 | { ...rest } 58 | > 59 | { children } 60 | <ScreenReaderText> 61 | { screenReaderText } 62 | { DEFAULT_EXTERNAL_LINK_TEXT } 63 | { newTab ? NEW_TAB_TEXT : '' } 64 | </ScreenReaderText> 65 | { showExternalIcon && <span aria-hidden="true"> ↗</span> } 66 | </Link> 67 | ); 68 | 69 | export default LinkExternal; 70 | -------------------------------------------------------------------------------- /src/system/MobileMenu/MobileMenu.test.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | // @ts-nocheck 3 | /** @jsxImportSource theme-ui */ 4 | import { render, screen } from '@testing-library/react'; 5 | import userEvent from '@testing-library/user-event'; 6 | import { axe } from 'jest-axe'; 7 | import { ThemeUIProvider } from 'theme-ui'; 8 | 9 | import { MobileMenuExample } from './MobileMenu.stories'; 10 | import { theme } from '../'; 11 | 12 | const renderWithTheme = children => 13 | render( <ThemeUIProvider theme={ theme }>{ children }</ThemeUIProvider> ); 14 | 15 | const renderComponent = () => renderWithTheme( <MobileMenuExample /> ); 16 | 17 | function getMenuTrigger() { 18 | return screen.getByRole( 'button', { name: 'Menu' } ); 19 | } 20 | 21 | describe( '<MobileMenu />', () => { 22 | it( 'renders the MobileMenu trigger', async () => { 23 | const { container } = renderComponent(); 24 | 25 | // Should find the trigger 26 | expect( getMenuTrigger() ).toBeInTheDocument(); 27 | 28 | expect( getMenuTrigger() ).toHaveAttribute( 'data-state', 'closed' ); 29 | 30 | // Check for accessibility issues 31 | expect( await axe( container ) ).toHaveNoViolations(); 32 | } ); 33 | 34 | it( 'opens MobileMenu and check for items', async () => { 35 | const user = userEvent.setup(); 36 | 37 | const { container } = renderComponent(); 38 | 39 | await user.click( getMenuTrigger() ); 40 | 41 | // Should find the open content 42 | expect( screen.getByText( 'My Applications' ) ).toBeVisible(); // First menu 43 | expect( screen.getByText( 'My Organizations' ) ).toBeVisible(); 44 | expect( screen.getByText( 'Performance' ) ).toBeVisible(); 45 | expect( screen.getByText( 'Features' ) ).toBeVisible(); // Last Menu 46 | 47 | // Check for accessibility issues 48 | expect( await axe( container ) ).toHaveNoViolations(); 49 | } ); 50 | } ); 51 | -------------------------------------------------------------------------------- /src/system/Nav/Nav.test.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 3 | // @ts-nocheck 4 | /** 5 | * External dependencies 6 | */ 7 | import { render, screen } from '@testing-library/react'; 8 | import { axe } from 'jest-axe'; 9 | import { ThemeUIProvider } from 'theme-ui'; 10 | 11 | /** 12 | * Internal dependencies 13 | */ 14 | import { Nav } from './Nav'; 15 | import { NavItem } from './NavItem'; 16 | import { Link, theme } from '../'; 17 | 18 | const renderWithTheme = children => 19 | render( <ThemeUIProvider theme={ theme }>{ children }</ThemeUIProvider> ); 20 | 21 | const renderComponent = () => 22 | renderWithTheme( 23 | <Nav.Primary variant="primary" label="Main"> 24 | <NavItem.Primary as={ Link } href="#"> 25 | PHP 26 | </NavItem.Primary> 27 | <NavItem.Primary as={ Link } href="https://wordpress.com"> 28 | WordPress 29 | </NavItem.Primary> 30 | <NavItem.Primary as={ Link } active href="https://newrelic.com/"> 31 | New Relic 32 | </NavItem.Primary> 33 | <NavItem.Primary as={ Link } disabled href="https://google.com/"> 34 | Not accessible 35 | </NavItem.Primary> 36 | </Nav.Primary> 37 | ); 38 | 39 | describe( '<Nav />', () => { 40 | it( 'renders the Nav component with default value visible', async () => { 41 | const { container } = renderComponent(); 42 | 43 | // Should find the nav label 44 | expect( screen.getByLabelText( 'Main' ) ).toBeInTheDocument(); 45 | 46 | // Should find all links 47 | expect( screen.queryByText( 'PHP' ) ).toBeInTheDocument(); 48 | expect( screen.queryByText( 'WordPress' ) ).toBeInTheDocument(); 49 | expect( screen.queryByText( 'New Relic' ) ).toBeInTheDocument(); 50 | expect( screen.queryByText( 'Not accessible' ) ).toHaveAttribute( 'aria-disabled', 'true' ); 51 | 52 | // Check for accessibility issues 53 | expect( await axe( container ) ).toHaveNoViolations(); 54 | } ); 55 | } ); 56 | -------------------------------------------------------------------------------- /src/system/Nav/Nav.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | import * as NavigationMenu from '@radix-ui/react-navigation-menu'; 3 | import classNames from 'classnames'; 4 | import { Ref, forwardRef } from 'react'; 5 | 6 | import { navMenuListStyles, navStyles } from './styles'; 7 | 8 | export const VIP_NAV = 'vip-nav-component'; 9 | export type NavVariant = 10 | | 'primary' 11 | | 'tabs' 12 | | 'toolbar' 13 | | 'menu' 14 | | 'menu-inverse' 15 | | 'breadcrumbs' 16 | | 'primary-inverse'; 17 | 18 | export interface NavProps extends NavigationMenu.NavigationMenuProps { 19 | className?: string; 20 | variant?: NavVariant; 21 | label: string; 22 | orientation?: 'horizontal' | 'vertical'; 23 | } 24 | 25 | const NavBase = forwardRef< HTMLElement, NavProps >( 26 | ( 27 | { className, children, orientation = 'horizontal', variant = 'primary', label }: NavProps, 28 | ref: Ref< HTMLElement > 29 | ) => ( 30 | <NavigationMenu.Root 31 | aria-label={ label } 32 | ref={ ref } 33 | className={ classNames( VIP_NAV, className ) } 34 | sx={ navStyles( variant, orientation ) } 35 | orientation={ orientation } 36 | > 37 | <NavigationMenu.List 38 | className={ classNames( `${ VIP_NAV }-list` ) } 39 | sx={ navMenuListStyles( orientation ) } 40 | > 41 | { children } 42 | </NavigationMenu.List> 43 | </NavigationMenu.Root> 44 | ) 45 | ); 46 | 47 | const NavPrimary = forwardRef< HTMLElement, NavProps >( 48 | ( props: NavProps, ref: Ref< HTMLElement > ) => ( 49 | <NavBase { ...props } variant="primary" ref={ ref } /> 50 | ) 51 | ); 52 | 53 | const NavPrimaryInverse = forwardRef< HTMLElement, NavProps >( 54 | ( props: NavProps, ref: Ref< HTMLElement > ) => ( 55 | <NavBase { ...props } variant="primary-inverse" ref={ ref } /> 56 | ) 57 | ); 58 | 59 | const NavTab = forwardRef< HTMLElement, NavProps >( 60 | ( props: NavProps, ref: Ref< HTMLElement > ) => ( 61 | <NavBase { ...props } variant="tabs" ref={ ref } /> 62 | ) 63 | ); 64 | 65 | const NavToolbar = forwardRef< HTMLElement, NavProps >( 66 | ( props: NavProps, ref: Ref< HTMLElement > ) => ( 67 | <NavBase { ...props } variant="toolbar" ref={ ref } /> 68 | ) 69 | ); 70 | 71 | const NavMenu = forwardRef< HTMLElement, NavProps >( 72 | ( props: NavProps, ref: Ref< HTMLElement > ) => ( 73 | <NavBase { ...props } variant="menu" orientation="vertical" ref={ ref } /> 74 | ) 75 | ); 76 | 77 | export type NavItemRenderIconProp = ( size: number ) => JSX.Element | null; 78 | 79 | export const Nav = { 80 | Primary: NavPrimary, 81 | PrimaryInverse: NavPrimaryInverse, 82 | Tab: NavTab, 83 | Toolbar: NavToolbar, 84 | Menu: NavMenu, 85 | }; 86 | -------------------------------------------------------------------------------- /src/system/Nav/NavItemGroup.test.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 3 | // @ts-nocheck 4 | 5 | import { render, screen } from '@testing-library/react'; 6 | import { axe } from 'jest-axe'; 7 | import { ThemeUIProvider } from 'theme-ui'; 8 | 9 | import { Nav } from './Nav'; 10 | import { NavItem } from './NavItem'; 11 | import { theme } from '../'; 12 | import { CustomLink } from '../utils/stories/CustomLink'; 13 | 14 | const renderWithTheme = children => 15 | render( <ThemeUIProvider theme={ theme }>{ children }</ThemeUIProvider> ); 16 | 17 | const renderComponent = () => 18 | renderWithTheme( 19 | <Nav.Menu label="Nav Menu"> 20 | <NavItem.MenuGroup active activeChildren label="Logs"> 21 | <NavItem.Menu active as={ CustomLink } href="https://google.com/"> 22 | Audit 23 | </NavItem.Menu> 24 | <NavItem.Menu as={ CustomLink } href="https://wpvip.com/"> 25 | Runtime 26 | </NavItem.Menu> 27 | <NavItem.Menu as={ CustomLink } href="https://dashboard.wpvip.com/"> 28 | Slow Query 29 | </NavItem.Menu> 30 | </NavItem.MenuGroup> 31 | </Nav.Menu> 32 | ); 33 | 34 | describe( '<NavItemGroup />', () => { 35 | it( 'renders the NavItemGroup component a data-active-children', async () => { 36 | const { container } = renderComponent(); 37 | 38 | // Should find the button label 39 | const button = screen.getByRole( 'button', { label: /Logs/ } ); 40 | 41 | expect( button ).toBeInTheDocument(); 42 | 43 | // Expect to have another attribute 44 | expect( button ).toHaveAttribute( 'data-active', 'true' ); 45 | expect( button ).toHaveAttribute( 'data-active-children', 'true' ); 46 | 47 | // Check for accessibility issues 48 | expect( await axe( container ) ).toHaveNoViolations(); 49 | } ); 50 | } ); 51 | -------------------------------------------------------------------------------- /src/system/Nav/styles/variants/breadcrumbs.ts: -------------------------------------------------------------------------------- 1 | import { ThemeUIStyleObject } from 'theme-ui'; 2 | 3 | import { defaultLinkComponentStyle } from '../../../Link/Link'; 4 | 5 | // Breadcrumbs Nav Item Style <li> 6 | export const breadcrumbsItemStyles: ThemeUIStyleObject = { 7 | // This code will generate the breadcrumb separator: /. We create the separator via CSS to hide it from screen readers. 8 | '&::before': { 9 | display: 'inline-block', 10 | margin: '0 0.45em', 11 | transform: 'rotate(15deg)', 12 | borderRightColor: 'text', 13 | borderRightStyle: 'solid', 14 | borderRightWidth: '0.1em', 15 | height: '0.8em', 16 | content: '""', 17 | }, 18 | 19 | '&:not(&[data-active]):first-of-type::before': { 20 | display: 'none', 21 | }, 22 | }; 23 | 24 | // Breadcrumbs Link <a> 25 | export const breadcrumbsLinkStyles: ThemeUIStyleObject = { 26 | ...defaultLinkComponentStyle, 27 | m: 0, 28 | }; 29 | -------------------------------------------------------------------------------- /src/system/Nav/styles/variants/menugroup.ts: -------------------------------------------------------------------------------- 1 | import { keyframes } from '@emotion/react'; 2 | import { ThemeUIStyleObject } from 'theme-ui'; 3 | 4 | import { NavProps, NavVariant } from '../../Nav'; 5 | 6 | export const navItemGroupStyles = ( 7 | orientation: NavProps[ 'orientation' ], 8 | variant?: NavVariant 9 | ): ThemeUIStyleObject => { 10 | const defaultStyle = { 11 | li: { 12 | mb: 1, 13 | }, 14 | 'li:last-of-type': { 15 | mr: orientation === 'horizontal' ? 0 : undefined, 16 | mb: orientation === 'vertical' ? 0 : undefined, 17 | }, 18 | }; 19 | 20 | switch ( variant ) { 21 | case 'menu': { 22 | return { 23 | ...defaultStyle, 24 | mr: 0, 25 | width: '100%', 26 | }; 27 | } 28 | 29 | default: { 30 | return defaultStyle; 31 | } 32 | } 33 | }; 34 | 35 | export const navItemGroupTriggerStyles: ThemeUIStyleObject = { 36 | 'svg[data-arrow-indicator]': { 37 | position: 'absolute', 38 | right: 3, 39 | top: 2, 40 | transform: 'rotate(0deg)', 41 | }, 42 | '&[data-open]': { 43 | 'svg[data-arrow-indicator]': { 44 | transform: 'rotate(180deg)', 45 | transition: 'transform 200ms', 46 | }, 47 | }, 48 | '&:focus:not(&[data-active]), &:hover:not(&[data-active])': { 49 | // This will make the trigger button look like a link 50 | cursor: 'pointer', 51 | textDecorationLine: 'underline', 52 | textDecorationThickness: '2px', 53 | }, 54 | }; 55 | 56 | export const navItemGroupContentUlStyles: ThemeUIStyleObject = { 57 | m: 0, 58 | p: 0, 59 | pl: 5, 60 | listStyle: 'none', 61 | pt: 1, 62 | }; 63 | 64 | const slideDown = keyframes( { 65 | from: { height: 0 }, 66 | to: { height: 'var(--radix-collapsible-content-height)' }, 67 | } ); 68 | 69 | const slideUp = keyframes( { 70 | from: { height: 'var(--radix-collapsible-content-height)' }, 71 | to: { height: 0 }, 72 | } ); 73 | 74 | export const navItemGroupContentStyles: ThemeUIStyleObject = { 75 | overflow: 'hidden', 76 | '&[data-state="open"]': { 77 | animation: `${ slideDown } 300ms cubic-bezier(0.87, 0, 0.13, 1)`, 78 | }, 79 | '&[data-state="closed"]': { 80 | animation: `${ slideUp } 300ms cubic-bezier(0.87, 0, 0.13, 1)`, 81 | }, 82 | }; 83 | -------------------------------------------------------------------------------- /src/system/Nav/styles/variants/primary.ts: -------------------------------------------------------------------------------- 1 | import { ThemeUIStyleObject } from 'theme-ui'; 2 | 3 | import { NavProps, NavVariant } from '../../Nav'; 4 | 5 | // Default Root Styles <nav> 6 | export const defaultNavRootStyles: ThemeUIStyleObject = { 7 | width: '100%', 8 | borderColor: 'transparent', 9 | }; 10 | 11 | // Default List Item style <li> 12 | 13 | export const defaultNavItemStyles = ( 14 | orientation: NavProps[ 'orientation' ] 15 | ): ThemeUIStyleObject => ( { 16 | mr: 2, 17 | '&:last-of-type': { 18 | mr: orientation === 'horizontal' ? 0 : undefined, 19 | }, 20 | } ); 21 | 22 | // Default Link <a> 23 | export const defaultItemLinkStyles: ThemeUIStyleObject = { 24 | alignItems: 'center', 25 | display: 'inline-flex', 26 | fontSize: 2, 27 | justifyContent: 'center', 28 | lineHeight: 'inherit', 29 | minHeight: '36px', 30 | px: 3, 31 | py: 0, 32 | textDecoration: 'none', 33 | verticalAlign: 'middle', 34 | }; 35 | 36 | // Primary Link <a> 37 | export const primaryItemLinkStyles = ( variant: NavVariant ): ThemeUIStyleObject => ( { 38 | ...defaultItemLinkStyles, 39 | variant: `buttons.tertiary`, 40 | borderRadius: 1, 41 | '&[data-active]': { 42 | variant: `buttons.${ variant }`, 43 | }, 44 | '&[aria-disabled="true"]': { 45 | opacity: 0.7, 46 | color: 'texts.secondary', 47 | cursor: 'not-allowed', 48 | }, 49 | ':hover': { 50 | backgroundColor: `button.${ variant }.background.hover`, 51 | textDecoration: 'none', 52 | }, 53 | } ); 54 | -------------------------------------------------------------------------------- /src/system/Nav/styles/variants/tabs.ts: -------------------------------------------------------------------------------- 1 | import { ThemeUIStyleObject } from 'theme-ui'; 2 | 3 | import { defaultItemLinkStyles } from './primary'; 4 | import { NavProps } from '../../Nav'; 5 | 6 | // Tab Root Styles <nav> 7 | const getTabPropsByOrientation = ( orientation: NavProps[ 'orientation' ] ): ThemeUIStyleObject => { 8 | if ( orientation === 'vertical' ) { 9 | return { 10 | '> div:first-of-type': { 11 | height: '100%', 12 | overflowY: 'auto', 13 | }, 14 | ul: { 15 | minHeight: 'max-content', 16 | }, 17 | }; 18 | } 19 | return { 20 | '> div:first-of-type': { 21 | width: '100%', 22 | overflowX: 'auto', 23 | }, 24 | ul: { 25 | minWidth: 'max-content', 26 | }, 27 | }; 28 | }; 29 | 30 | export const tabRootStyles = ( orientation: NavProps[ 'orientation' ] ): ThemeUIStyleObject => ( { 31 | width: '100%', 32 | borderColor: 'borders.2', 33 | gap: 2, 34 | 35 | // Responsive in case the content is bigger than the viewport 36 | ...getTabPropsByOrientation( orientation ), 37 | } ); 38 | 39 | // Tab Link <a> 40 | export const tabItemLinkStyles: ThemeUIStyleObject = { 41 | ...defaultItemLinkStyles, 42 | px: 2, 43 | height: '100%', 44 | backgroundColor: 'red', 45 | color: 'heading', 46 | '&[data-active]': { 47 | color: 'link', 48 | fontWeight: 'normal', 49 | position: 'relative', 50 | '&::after': { 51 | position: 'absolute', 52 | bottom: 0, 53 | display: 'block', 54 | width: '100%', 55 | content: '""', 56 | height: '0.125rem', 57 | backgroundColor: 'link', 58 | }, 59 | }, 60 | '&[aria-disabled="true"]': { 61 | color: 'muted', 62 | }, 63 | ':hover': { fontWeight: 'regular', color: 'link' }, 64 | }; 65 | -------------------------------------------------------------------------------- /src/system/Nav/styles/variants/toolbar.ts: -------------------------------------------------------------------------------- 1 | import { ThemeUIStyleObject } from 'theme-ui'; 2 | 3 | import { defaultItemLinkStyles, defaultNavRootStyles } from './primary'; 4 | 5 | // Toolbar Root Styles <nav> 6 | export const toolbarRootStyles: ThemeUIStyleObject = { 7 | ...defaultNavRootStyles, 8 | display: [ 'none', 'none', 'flex' ], 9 | height: '100%', 10 | ml: 0, 11 | gap: 6, 12 | width: 'max-content', 13 | }; 14 | 15 | // Toolbar Default Link Styles <a> 16 | export const defaultToolbarLinkStyle: ThemeUIStyleObject = { 17 | color: 'toolbar.text.default', 18 | textDecoration: 'none', 19 | borderBottom: 'none', 20 | display: 'inline-flex', 21 | alignItems: 'center', 22 | fontWeight: 500, 23 | '&:hover': { 24 | color: 'toolbar.text.hover', 25 | }, 26 | }; 27 | 28 | // Toolbar Link <a> 29 | export const toolbarItemLinkStyles: ThemeUIStyleObject = { 30 | ...defaultItemLinkStyles, 31 | position: 'relative', 32 | height: '100%', 33 | ...defaultToolbarLinkStyle, 34 | 35 | '&[data-active], &[aria-current="page"]': { 36 | color: 'toolbar.text.default', 37 | }, 38 | '&[data-active]:before, &[aria-current="page"]:before': { 39 | display: 'block', 40 | content: '""', 41 | width: '100%', 42 | height: 3, 43 | overflow: 'hidden', 44 | backgroundColor: 'borders.accent', 45 | position: 'absolute', 46 | top: 0, 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /src/system/NewConfirmationDialog/NewConfirmationDialog.stories.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import React from 'react'; 5 | 6 | import { Box, NewConfirmationDialog, Button } from '..'; 7 | 8 | export default { 9 | title: 'Dialog/NewConfirmationDialog', 10 | component: NewConfirmationDialog, 11 | }; 12 | 13 | const ConfirmationTrigger = <Button sx={ { mr: 3 } }>Click to answer</Button>; 14 | 15 | export const Default = () => { 16 | const [ answer, setAnswer ] = React.useState( '🤔' ); 17 | return ( 18 | <Box> 19 | <p>Confirm that your name is John doe?</p> 20 | <NewConfirmationDialog 21 | className="storybook-confirmation-dialog" 22 | title={ 'Are you John Doe?' } 23 | buttonVariant="danger" 24 | description={ 'Please confirm that your name is John Doe.' } 25 | trigger={ ConfirmationTrigger } 26 | body="A modal is used to perform more detailed actions that don‘t necessarily need the context 27 | behind." 28 | onConfirm={ () => setAnswer( '👍' ) } 29 | needsConfirm={ true } 30 | /> 31 | 32 | <p>Answer: { answer }</p> 33 | </Box> 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /src/system/NewConfirmationDialog/NewConfirmationDialog.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { fireEvent, render, screen } from '@testing-library/react'; 5 | import { axe } from 'jest-axe'; 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import { NewConfirmationDialog } from './NewConfirmationDialog'; 11 | 12 | const defaultProps = { 13 | className: 'my-custom-class', 14 | needsConfirm: true, 15 | title: 'My Custom Title', 16 | body: 'My Custom Text', 17 | label: 'Submit this!', 18 | trigger: <button>Trigger</button>, 19 | }; 20 | 21 | const getDialog = () => screen.getByRole( 'dialog' ); 22 | const getButton = () => screen.getByText( 'Trigger' ); 23 | const getConfirmButton = () => screen.getByText( defaultProps.label ); 24 | const getTitle = () => screen.getByRole( 'heading', { level: 2 } ); 25 | 26 | describe( '<NewConfirmationDialog />', () => { 27 | it( 'renders the NewConfirmationDialog component', async () => { 28 | const { container } = render( <NewConfirmationDialog { ...defaultProps } /> ); 29 | 30 | expect( getButton() ).toBeInTheDocument(); 31 | 32 | fireEvent.click( getButton() ); 33 | 34 | const dialog = getDialog(); 35 | expect( dialog ).toBeInTheDocument(); 36 | expect( dialog ).toHaveClass( 'vip-dialog-component' ); 37 | expect( dialog ).toHaveClass( defaultProps.className ); 38 | 39 | expect( getTitle() ).toHaveTextContent( defaultProps.title ); 40 | 41 | expect( getConfirmButton() ).toBeInTheDocument(); 42 | 43 | // Check for accessibility issues 44 | await expect( await axe( container ) ).toHaveNoViolations(); 45 | } ); 46 | } ); 47 | -------------------------------------------------------------------------------- /src/system/NewConfirmationDialog/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { NewConfirmationDialog } from './NewConfirmationDialog'; 5 | 6 | export { NewConfirmationDialog }; 7 | -------------------------------------------------------------------------------- /src/system/NewDialog/DialogClose.test.tsx: -------------------------------------------------------------------------------- 1 | import { Dialog } from '@radix-ui/react-dialog'; 2 | import { render, screen } from '@testing-library/react'; 3 | import { axe } from 'jest-axe'; 4 | import React from 'react'; 5 | 6 | import { DialogCloseDefault as DialogClose } from './DialogClose'; 7 | 8 | const Wrapper = props => <Dialog open={ true } { ...props } />; 9 | 10 | describe( '<DialogClose />', () => { 11 | it( 'renders the DialogClose component', async () => { 12 | const { container } = render( 13 | <Wrapper> 14 | <DialogClose /> 15 | </Wrapper> 16 | ); 17 | 18 | expect( screen.getByLabelText( 'Close' ) ).toBeInTheDocument(); 19 | 20 | // Check for accessibility issues 21 | expect( await axe( container ) ).toHaveNoViolations(); 22 | } ); 23 | } ); 24 | -------------------------------------------------------------------------------- /src/system/NewDialog/DialogClose.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | import * as DialogPrimitive from '@radix-ui/react-dialog'; 4 | import React, { forwardRef } from 'react'; 5 | import { IoClose } from 'react-icons/io5'; 6 | import { ThemeUIStyleObject } from 'theme-ui'; 7 | 8 | import { Button } from '..'; 9 | 10 | export interface DialogCloseProps { 11 | children?: React.ReactNode; 12 | } 13 | 14 | export const DialogClose = forwardRef< HTMLButtonElement, DialogCloseProps >( 15 | ( props, forwardedRef ) => ( 16 | <DialogPrimitive.Close asChild { ...props } ref={ forwardedRef }> 17 | { props.children } 18 | </DialogPrimitive.Close> 19 | ) 20 | ); 21 | 22 | DialogClose.displayName = 'DialogClose'; 23 | 24 | export interface DialogCloseDefaultProps { 25 | variant?: 'primary' | 'inverse'; 26 | } 27 | 28 | export const defaultCloseStyles = ( variant = 'primary' ): ThemeUIStyleObject => ( { 29 | position: 'absolute', 30 | top: 3, 31 | right: 3, 32 | width: 38, 33 | height: 38, 34 | p: 0, 35 | color: variant === 'primary' ? 'icon.primary' : 'icon.inverse', 36 | svg: { 37 | '&:hover': { 38 | fill: 'inherit', 39 | }, 40 | }, 41 | } ); 42 | 43 | export const DialogCloseDefault = forwardRef< HTMLButtonElement, DialogCloseDefaultProps >( 44 | ( { variant = 'primary' }, forwardedRef ) => { 45 | return ( 46 | <DialogClose> 47 | <Button 48 | ref={ forwardedRef } 49 | aria-label="Close" 50 | variant="tertiary" 51 | sx={ defaultCloseStyles( variant ) } 52 | > 53 | <IoClose aria-hidden="true" width={ 20 } height={ 20 } /> 54 | </Button> 55 | </DialogClose> 56 | ); 57 | } 58 | ); 59 | 60 | DialogCloseDefault.displayName = 'DialogCloseDefault'; 61 | -------------------------------------------------------------------------------- /src/system/NewDialog/DialogDescription.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { Dialog } from '@radix-ui/react-dialog'; 5 | import { render, screen } from '@testing-library/react'; 6 | import { axe } from 'jest-axe'; 7 | 8 | /** 9 | * Internal dependencies 10 | */ 11 | import { DialogDescription } from './DialogDescription'; 12 | 13 | // If you render any Dialog child without the `<Dialog />` parent, it will throw an error. 14 | const Wrapper = props => <Dialog open={ true } { ...props } />; 15 | const defaultProps = { 16 | description: 'My Custom Text', 17 | }; 18 | 19 | const getParagraph = () => screen.getByText( defaultProps.description ); 20 | 21 | describe( '<DialogDescription />', () => { 22 | it( 'renders the DialogDescription component', async () => { 23 | const { container } = render( 24 | <Wrapper> 25 | <DialogDescription { ...defaultProps } /> 26 | </Wrapper> 27 | ); 28 | 29 | expect( getParagraph() ).toHaveTextContent( defaultProps.description ); 30 | 31 | // Check for accessibility issues 32 | await expect( await axe( container ) ).toHaveNoViolations(); 33 | } ); 34 | 35 | it( 'renders text visually hidden for a11y purposes', async () => { 36 | const { container } = render( 37 | <Wrapper> 38 | <DialogDescription { ...defaultProps } hidden={ true } /> 39 | </Wrapper> 40 | ); 41 | 42 | // Small check to make sure we are hiding with the css class 43 | expect( container.innerHTML ).toContain( 'screen-reader-text' ); 44 | 45 | expect( getParagraph() ).toHaveTextContent( defaultProps.description ); 46 | } ); 47 | } ); 48 | -------------------------------------------------------------------------------- /src/system/NewDialog/DialogDescription.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | import * as DialogPrimitive from '@radix-ui/react-dialog'; 4 | import React, { ReactNode, forwardRef } from 'react'; 5 | 6 | import ScreenReaderText from '../ScreenReaderText'; 7 | 8 | export interface DialogDescriptionProps { 9 | description?: ReactNode; 10 | hidden?: boolean; 11 | } 12 | 13 | export const DialogDescription = forwardRef< HTMLDivElement, DialogDescriptionProps >( 14 | ( { description, hidden, ...rest }, forwardedRef ) => { 15 | let text = description; 16 | 17 | if ( hidden ) { 18 | text = <ScreenReaderText>{ text }</ScreenReaderText>; 19 | } 20 | 21 | return ( 22 | <DialogPrimitive.Description 23 | { ...rest } 24 | ref={ forwardedRef } 25 | sx={ { margin: 0, color: 'text' } } 26 | > 27 | { text } 28 | </DialogPrimitive.Description> 29 | ); 30 | } 31 | ); 32 | 33 | DialogDescription.displayName = 'DialogDescription'; 34 | -------------------------------------------------------------------------------- /src/system/NewDialog/DialogOverlay.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { Dialog } from '@radix-ui/react-dialog'; 5 | import { render } from '@testing-library/react'; 6 | import { axe } from 'jest-axe'; 7 | 8 | /** 9 | * Internal dependencies 10 | */ 11 | import { DialogOverlay } from './DialogOverlay'; 12 | 13 | // If you render any Dialog child without the `<Dialog />` parent, it will throw an error. 14 | const Wrapper = props => <Dialog open={ true } { ...props } />; 15 | 16 | describe( '<DialogOverlay />', () => { 17 | it( 'renders the DialogOverlay component', async () => { 18 | const { container } = render( 19 | <Wrapper> 20 | <DialogOverlay /> 21 | </Wrapper> 22 | ); 23 | 24 | // Check for accessibility issues 25 | await expect( await axe( container ) ).toHaveNoViolations(); 26 | } ); 27 | } ); 28 | -------------------------------------------------------------------------------- /src/system/NewDialog/DialogOverlay.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | /** 3 | * External dependencies 4 | */ 5 | import * as Dialog from '@radix-ui/react-dialog'; 6 | import React, { forwardRef } from 'react'; 7 | 8 | /** 9 | * Internal dependencies 10 | */ 11 | 12 | export interface DialogOverlayProps 13 | extends React.ComponentPropsWithoutRef< typeof Dialog.Overlay > {} 14 | 15 | export const DialogOverlay = forwardRef< HTMLDivElement, DialogOverlayProps >( 16 | ( props, forwardedRef ) => ( 17 | <Dialog.Overlay 18 | sx={ { 19 | position: 'fixed', 20 | top: 0, 21 | left: 0, 22 | right: 0, 23 | bottom: 0, 24 | inset: 0, 25 | opacity: 0.7, 26 | backgroundColor: 'backgrounds.overlay', 27 | } } 28 | { ...props } 29 | ref={ forwardedRef } 30 | /> 31 | ) 32 | ); 33 | 34 | DialogOverlay.displayName = 'DialogOverlay'; 35 | -------------------------------------------------------------------------------- /src/system/NewDialog/DialogTitle.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { Dialog } from '@radix-ui/react-dialog'; 5 | import { render, screen } from '@testing-library/react'; 6 | import { axe } from 'jest-axe'; 7 | 8 | /** 9 | * Internal dependencies 10 | */ 11 | import { DialogTitle } from './DialogTitle'; 12 | 13 | // If you render any Dialog child without the `<Dialog />` parent, it will throw an error. 14 | const Wrapper = props => <Dialog open={ true } { ...props } />; 15 | const defaultProps = { 16 | title: 'This is a DialogTitle', 17 | }; 18 | 19 | const getTitle = () => screen.getByRole( 'heading' ); 20 | 21 | describe( '<DialogTitle />', () => { 22 | it( 'renders the DialogTitle component', async () => { 23 | const { container } = render( 24 | <Wrapper> 25 | <DialogTitle { ...defaultProps } /> 26 | </Wrapper> 27 | ); 28 | 29 | expect( getTitle() ).toHaveTextContent( defaultProps.title ); 30 | 31 | // Check for accessibility issues 32 | await expect( await axe( container ) ).toHaveNoViolations(); 33 | } ); 34 | 35 | it( 'renders text visually hidden for a11y purposes', async () => { 36 | const { container } = render( 37 | <Wrapper> 38 | <DialogTitle { ...defaultProps } hidden={ true } /> 39 | </Wrapper> 40 | ); 41 | 42 | // Small check to make sure we are hiding with the css class 43 | expect( container.innerHTML ).toContain( 'screen-reader-text' ); 44 | 45 | expect( getTitle() ).toHaveTextContent( defaultProps.title ); 46 | } ); 47 | } ); 48 | -------------------------------------------------------------------------------- /src/system/NewDialog/DialogTitle.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | /** 4 | * External dependencies 5 | */ 6 | import * as DialogPrimitive from '@radix-ui/react-dialog'; 7 | import React, { ReactNode } from 'react'; 8 | 9 | import ScreenReaderText from '../ScreenReaderText'; 10 | 11 | /** 12 | * Internal dependencies 13 | */ 14 | 15 | export interface DialogTitleProps { 16 | title?: ReactNode; 17 | hidden?: boolean; 18 | } 19 | 20 | export const DialogTitle: React.FC< DialogTitleProps > = ( { title, hidden = false } ) => { 21 | let titleNode = title; 22 | 23 | if ( hidden ) { 24 | titleNode = <ScreenReaderText>{ titleNode }</ScreenReaderText>; 25 | } 26 | 27 | return ( 28 | <DialogPrimitive.Title 29 | sx={ { margin: 0, fontSize: 3, fontWeight: 'medium', color: 'heading' } } 30 | > 31 | { titleNode } 32 | </DialogPrimitive.Title> 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /src/system/NewDialog/NewDialog.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | import * as DialogPrimitive from '@radix-ui/react-dialog'; 4 | import classNames from 'classnames'; 5 | import React, { ReactNode } from 'react'; 6 | import { ThemeUIStyleObject } from 'theme-ui'; 7 | 8 | import { DialogCloseDefault as DialogClose } from './DialogClose'; 9 | import { DialogDescription } from './DialogDescription'; 10 | import { DialogOverlay } from './DialogOverlay'; 11 | import { DialogTitle } from './DialogTitle'; 12 | import { contentStyles } from './styles'; 13 | 14 | export interface DialogContentProps extends DialogPrimitive.DialogContentProps { 15 | sx?: ThemeUIStyleObject; 16 | className?: string; 17 | } 18 | 19 | export interface NewDialogProps extends DialogPrimitive.DialogProps { 20 | trigger?: ReactNode; 21 | title: ReactNode; 22 | description: ReactNode; 23 | content?: ReactNode | ( ( { onClose }: { onClose: () => void } ) => ReactNode ); 24 | showHeading?: boolean; 25 | disabled?: boolean; 26 | style?: ThemeUIStyleObject; 27 | className?: string; 28 | contentProps?: DialogContentProps; 29 | } 30 | 31 | export const NewDialog: React.FC< NewDialogProps > = ( { 32 | trigger = null, 33 | description, 34 | title, 35 | content = null, 36 | showHeading = true, 37 | disabled = false, 38 | style: extraStyles, 39 | contentProps = {}, 40 | className = null, 41 | ...props 42 | } ) => { 43 | const closeRef = React.useRef< HTMLButtonElement >( null ); 44 | 45 | if ( disabled ) { 46 | return null; 47 | } 48 | 49 | // if content is a function, pass in onClose 50 | const isContentFunction = typeof content === 'function'; 51 | 52 | const onClose = () => { 53 | closeRef?.current?.click(); 54 | }; 55 | 56 | return ( 57 | <DialogPrimitive.Root { ...props }> 58 | { trigger && <DialogPrimitive.Trigger asChild>{ trigger }</DialogPrimitive.Trigger> } 59 | 60 | <DialogPrimitive.Portal> 61 | <DialogOverlay /> 62 | 63 | <DialogPrimitive.Content 64 | className={ classNames( 'vip-dialog-component', className ) } 65 | sx={ { ...contentStyles, ...extraStyles } } 66 | { ...contentProps } 67 | > 68 | <DialogClose ref={ closeRef } /> 69 | <DialogTitle title={ title } hidden={ ! showHeading } /> 70 | <DialogDescription description={ description } hidden={ ! showHeading } /> 71 | 72 | <div role="document">{ isContentFunction ? content( { onClose } ) : content }</div> 73 | </DialogPrimitive.Content> 74 | </DialogPrimitive.Portal> 75 | </DialogPrimitive.Root> 76 | ); 77 | }; 78 | -------------------------------------------------------------------------------- /src/system/NewDialog/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | 5 | import { DialogClose, DialogCloseDefault } from './DialogClose'; 6 | import { NewDialog } from './NewDialog'; 7 | 8 | const Root = NewDialog; 9 | const Close = DialogClose; 10 | const CloseDefault = DialogCloseDefault; 11 | 12 | export { NewDialog, Root, Close, CloseDefault }; 13 | 14 | export default NewDialog; 15 | -------------------------------------------------------------------------------- /src/system/NewDialog/styles.ts: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | import { ThemeUIStyleObject } from 'theme-ui'; 4 | 5 | export const contentStyles: ThemeUIStyleObject = { 6 | background: 'dialog', 7 | variant: 'dialog.modal', 8 | borderRadius: 2, 9 | boxShadow: 'medium', 10 | position: 'fixed', 11 | top: '50%', 12 | left: '50%', 13 | transform: 'translate(-50%, -50%)', 14 | width: '90vw', 15 | maxWidth: '640px', 16 | maxHeight: '85vh', 17 | padding: 6, 18 | overflowY: 'auto', 19 | '> h1, > h2': { marginTop: '0 !important' }, 20 | }; 21 | -------------------------------------------------------------------------------- /src/system/NewForm/Fieldset.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | /** 4 | * External dependencies 5 | */ 6 | import classNames from 'classnames'; 7 | import React from 'react'; 8 | import { ThemeUIStyleObject } from 'theme-ui'; 9 | 10 | /** 11 | * Internal dependencies 12 | */ 13 | import { baseControlBorderStyle, inputBaseBackground, inputBaseText } from '../Form/Input.styles'; 14 | 15 | interface FieldsetProps { 16 | children?: React.ReactNode; 17 | sx?: ThemeUIStyleObject; 18 | className?: string; 19 | } 20 | export const Fieldset = React.forwardRef< HTMLFieldSetElement, FieldsetProps >( 21 | ( { children, className, sx = {}, ...props }, forwardRef ) => ( 22 | <fieldset 23 | ref={ forwardRef } 24 | className={ classNames( 'vip-form-fieldset-component', className ) } 25 | sx={ { 26 | all: 'unset', 27 | ...baseControlBorderStyle, 28 | backgroundColor: inputBaseBackground, 29 | color: inputBaseText, 30 | borderRadius: 1, 31 | display: 'block', 32 | pt: 1, 33 | pb: 2, 34 | px: 3, 35 | mb: 3, 36 | ...sx, 37 | } } 38 | { ...props } 39 | > 40 | { children } 41 | </fieldset> 42 | ) 43 | ); 44 | 45 | Fieldset.displayName = 'Fieldset'; 46 | -------------------------------------------------------------------------------- /src/system/NewForm/Form.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import classNames from 'classnames'; 5 | import React from 'react'; 6 | export type FormProps = React.ComponentPropsWithoutRef< 'form' > & { 7 | children?: React.ReactNode; 8 | className?: string; 9 | }; 10 | export const Form = React.forwardRef< HTMLFormElement, FormProps >( 11 | ( { children, className, ...props }, forwardRef ) => ( 12 | <form 13 | ref={ forwardRef } 14 | className={ classNames( 'vip-form-component', className ) } 15 | noValidate 16 | { ...props } 17 | > 18 | { children } 19 | </form> 20 | ) 21 | ); 22 | 23 | Form.displayName = 'Form'; 24 | -------------------------------------------------------------------------------- /src/system/NewForm/FormAutocomplete.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { render, screen } from '@testing-library/react'; 5 | import { axe } from 'jest-axe'; 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import { FormAutocomplete } from './FormAutocomplete'; 11 | 12 | const options = [ 13 | { value: 'chocolate', label: 'Chocolate' }, 14 | { value: 'strawberry', label: 'Strawberry Chocolate Vanilla Chocolate Vanilla' }, 15 | { value: 'vanilla', label: 'Vanilla' }, 16 | ]; 17 | 18 | const defaultProps = { 19 | label: 'This is a label', 20 | options, 21 | }; 22 | 23 | describe( '<FormAutocomplete />', () => { 24 | it( 'renders the FormAutocomplete component', async () => { 25 | const { container } = render( 26 | <FormAutocomplete forLabel="my_desert_list" label="This is a label" /> 27 | ); 28 | 29 | // Check for accessibility issues 30 | await expect( await axe( container ) ).toHaveNoViolations(); 31 | } ); 32 | 33 | it( 'renders the FormAutocomplete component with options', async () => { 34 | const { container } = render( 35 | <FormAutocomplete forLabel="my_desert_list" { ...defaultProps } /> 36 | ); 37 | 38 | expect( screen.getByLabelText( defaultProps.label ) ).toBeInTheDocument(); 39 | 40 | // Check for accessibility issues 41 | await expect( await axe( container ) ).toHaveNoViolations(); 42 | } ); 43 | } ); 44 | -------------------------------------------------------------------------------- /src/system/NewForm/FormAutocompleteMultiselect.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { render, screen } from '@testing-library/react'; 5 | import { axe } from 'jest-axe'; 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import { FormAutocompleteMultiselect } from './FormAutocompleteMultiselect'; 11 | 12 | const options = [ 13 | { value: 'chocolate', label: 'Chocolate' }, 14 | { value: 'strawberry', label: 'Strawberry Chocolate Vanilla Chocolate Vanilla' }, 15 | { value: 'vanilla', label: 'Vanilla' }, 16 | ]; 17 | 18 | const defaultProps = { 19 | label: 'This is a label', 20 | options, 21 | }; 22 | 23 | describe( '<FormAutocompleteMultiselect />', () => { 24 | it( 'renders the FormAutocompleteMultiselect component', async () => { 25 | const { container } = render( 26 | <FormAutocompleteMultiselect forLabel="my_desert_list" label="This is a label" /> 27 | ); 28 | // Check for accessibility issues 29 | await expect( await axe( container ) ).toHaveNoViolations(); 30 | } ); 31 | it( 'renders the FormAutocompleteMultiselect component with options', async () => { 32 | const { container } = render( 33 | <FormAutocompleteMultiselect forLabel="my_desert_list" { ...defaultProps } /> 34 | ); 35 | expect( screen.getByLabelText( defaultProps.label ) ).toBeInTheDocument(); 36 | // Check for accessibility issues 37 | await expect( await axe( container ) ).toHaveNoViolations(); 38 | } ); 39 | } ); 40 | -------------------------------------------------------------------------------- /src/system/NewForm/FormSelectArrow.js: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | /** 4 | * External dependencies 5 | */ 6 | import React from 'react'; 7 | import { MdExpandMore } from 'react-icons/md'; 8 | 9 | /** 10 | * Internal dependencies 11 | */ 12 | import { baseControlBorderStyle as borderStyle } from '../Form/Input.styles'; 13 | 14 | export const FormSelectArrow = React.forwardRef( ( { iconSize = 24, ...props }, forwardRef ) => ( 15 | <MdExpandMore 16 | ref={ forwardRef } 17 | aria-hidden="true" 18 | size={ iconSize } 19 | sx={ { 20 | position: 'absolute', 21 | paddingLeft: 2, 22 | borderLeftWidth: borderStyle.borderWidth, 23 | borderLeftStyle: borderStyle.borderStyle, 24 | borderLeftColor: borderStyle.borderColor, 25 | right: 3, 26 | top: '7px', 27 | pointerEvents: 'none', 28 | svg: { 29 | fill: borderStyle.borderColor, 30 | }, 31 | } } 32 | { ...props } 33 | /> 34 | ) ); 35 | 36 | FormSelectArrow.displayName = 'FormSelectArrow'; 37 | -------------------------------------------------------------------------------- /src/system/NewForm/FormSelectContent.js: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | /** 4 | * External dependencies 5 | */ 6 | import PropTypes from 'prop-types'; 7 | import React from 'react'; 8 | 9 | import { inlineStyles } from './FormSelectInline'; 10 | 11 | /** 12 | * Internal dependencies 13 | */ 14 | 15 | const defaultStyles = { 16 | position: 'relative', 17 | width: '100%', 18 | display: 'inline-flex', 19 | flexDirection: 'row', 20 | alignItems: 'center', 21 | }; 22 | 23 | const FormSelectContent = React.forwardRef( ( { isInline, label, children }, forwardRef ) => ( 24 | <div sx={ isInline ? inlineStyles : {} } className="vip-select-component" ref={ forwardRef }> 25 | { isInline && label } 26 | 27 | <div sx={ defaultStyles }>{ children }</div> 28 | </div> 29 | ) ); 30 | 31 | FormSelectContent.propTypes = { 32 | isInline: PropTypes.bool, 33 | label: PropTypes.any, 34 | children: PropTypes.any, 35 | }; 36 | 37 | FormSelectContent.displayName = 'FormSelectContent'; 38 | 39 | export { FormSelectContent }; 40 | -------------------------------------------------------------------------------- /src/system/NewForm/FormSelectInline.js: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | /** 4 | * External dependencies 5 | */ 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import { baseControlBorderStyle } from '../Form/Input.styles'; 11 | 12 | export const inlineStyles = { 13 | display: 'grid', 14 | gridTemplateColumns: 'auto 1fr', 15 | position: 'relative', 16 | alignItems: 'center', 17 | backgroundColor: 'input.background.default', 18 | borderRadius: 1, 19 | ...baseControlBorderStyle, 20 | paddingRight: 0, 21 | paddingLeft: 3, 22 | 23 | label: { 24 | margin: 0, 25 | paddingRight: 2, 26 | borderRightWidth: baseControlBorderStyle.borderWidth, 27 | borderRightStyle: baseControlBorderStyle.borderStyle, 28 | borderRightColor: baseControlBorderStyle.borderColor, 29 | }, 30 | 31 | select: { 32 | width: '100%', 33 | border: 'none', 34 | margin: 0, 35 | paddingLeft: 2, 36 | }, 37 | 38 | svg: { 39 | right: 2, 40 | position: 'absolute', 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /src/system/NewForm/FormSelectLoading.js: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | /** 4 | * External dependencies 5 | */ 6 | import { keyframes } from '@emotion/react'; 7 | import PropTypes from 'prop-types'; 8 | import React from 'react'; 9 | import { AiOutlineLoading3Quarters } from 'react-icons/ai'; 10 | 11 | /** 12 | * Internal dependencies 13 | */ 14 | import { inputBaseText } from '../Form/Input.styles'; 15 | 16 | const kf = keyframes( { 17 | from: { transform: 'rotate(0deg)' }, 18 | to: { transform: 'rotate(360deg) ' }, 19 | } ); 20 | 21 | export const FormSelectLoading = React.forwardRef( ( { sx = {}, ...rest }, forwardRef ) => ( 22 | <AiOutlineLoading3Quarters 23 | ref={ forwardRef } 24 | aria-hidden="true" 25 | size={ 18 } 26 | sx={ { 27 | position: 'absolute', 28 | right: 3, 29 | pointerEvents: 'none', 30 | animation: `${ kf } 1s infinite linear`, 31 | opacity: 0.5, 32 | svg: { 33 | fill: inputBaseText, 34 | }, 35 | ...sx, 36 | } } 37 | { ...rest } 38 | /> 39 | ) ); 40 | 41 | FormSelectLoading.propTypes = { 42 | sx: PropTypes.object, 43 | }; 44 | 45 | FormSelectLoading.displayName = 'FormSelectLoading'; 46 | -------------------------------------------------------------------------------- /src/system/NewForm/FormSelectSearch.js: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | /** 4 | * External dependencies 5 | */ 6 | import React from 'react'; 7 | import { MdSearch } from 'react-icons/md'; 8 | 9 | export const FormSelectSearch = React.forwardRef( ( props, forwardRef ) => ( 10 | <MdSearch 11 | ref={ forwardRef } 12 | aria-hidden="true" 13 | size={ 24 } 14 | sx={ { 15 | position: 'absolute', 16 | pr: 2, 17 | left: 3, 18 | pointerEvents: 'none', 19 | } } 20 | { ...props } 21 | /> 22 | ) ); 23 | 24 | FormSelectSearch.displayName = 'FormSelectSearch'; 25 | -------------------------------------------------------------------------------- /src/system/NewForm/Legend.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | /** 4 | * External dependencies 5 | */ 6 | import classNames from 'classnames'; 7 | import React from 'react'; 8 | import { ThemeUIStyleObject } from 'theme-ui'; 9 | 10 | /** 11 | * Internal dependencies 12 | */ 13 | import { baseLabelStyle } from '../Form/Label'; 14 | 15 | interface LegendProps { 16 | children?: React.ReactNode; 17 | sx?: ThemeUIStyleObject; 18 | className?: string; 19 | } 20 | export const Legend = React.forwardRef< HTMLLegendElement, LegendProps >( 21 | ( { children, className, sx = {}, ...props }, forwardRef ) => ( 22 | <legend 23 | ref={ forwardRef } 24 | className={ classNames( 'vip-form-legend-component', className ) } 25 | sx={ { 26 | all: 'unset', 27 | ...baseLabelStyle, 28 | px: 1, 29 | mb: 0, 30 | ...sx, 31 | } } 32 | { ...props } 33 | > 34 | { children } 35 | </legend> 36 | ) 37 | ); 38 | 39 | Legend.displayName = 'Legend'; 40 | -------------------------------------------------------------------------------- /src/system/NewForm/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | 5 | import { Fieldset } from './Fieldset'; 6 | import { Form } from './Form'; 7 | import { FormAutocomplete } from './FormAutocomplete'; 8 | import { FormAutocompleteMultiselect } from './FormAutocompleteMultiselect'; 9 | import { FormSelect } from './FormSelect'; 10 | import { Legend } from './Legend'; 11 | import { Input } from '../Form/Input'; 12 | import { InputWithCopyButton } from '../Form/InputWithCopyButton'; 13 | import { Label } from '../Form/Label'; 14 | import { Textarea } from '../Form/Textarea'; 15 | 16 | const Select = FormSelect; 17 | const Autocomplete = FormAutocomplete; 18 | const AutocompleteMulti = FormAutocompleteMultiselect; 19 | const Root = Form; 20 | 21 | export { 22 | Root, 23 | Select, 24 | Autocomplete, 25 | AutocompleteMulti, 26 | Textarea, 27 | Input, 28 | InputWithCopyButton, 29 | Label, 30 | Fieldset, 31 | Legend, 32 | }; 33 | 34 | export default Root; 35 | -------------------------------------------------------------------------------- /src/system/Notice/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { Notice } from './Notice'; 5 | 6 | export { Notice }; 7 | -------------------------------------------------------------------------------- /src/system/OptionRow/OptionRow.stories.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | 5 | import { BiAddToQueue, BiCalendarHeart, BiBellMinus } from 'react-icons/bi'; 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import { Box, OptionRow } from '..'; 11 | 12 | export default { 13 | title: 'OptionRow', 14 | component: OptionRow, 15 | }; 16 | 17 | // eslint-disable-next-line react/prop-types 18 | const Base = ( { variant } ) => ( 19 | <Box> 20 | <OptionRow 21 | image={ <BiAddToQueue size={ 24 } /> } 22 | label="Option Row 1" 23 | subTitle="Mostly used to link off to other pages." 24 | as="a" 25 | href="http://google.com/" 26 | variant={ variant } 27 | /> 28 | <OptionRow 29 | image={ <BiCalendarHeart size={ 24 } /> } 30 | label="Option Row 2" 31 | subTitle="Mostly used to link off to other pages." 32 | as="a" 33 | href="http://google.com/" 34 | order={ 2 } 35 | variant={ variant } 36 | /> 37 | 38 | <OptionRow 39 | image={ <BiBellMinus size={ 24 } /> } 40 | label="Custom heading HTML h2" 41 | subTitle="Use the variant prop to adjust the heading structure" 42 | as="a" 43 | href="http://google.com/" 44 | titleVariant="h2" 45 | meta="" 46 | variant={ variant } 47 | /> 48 | </Box> 49 | ); 50 | 51 | export const Default = () => <Base />; 52 | 53 | export const Alternative = () => <Base variant="alt" />; 54 | 55 | export const WithMeta = () => ( 56 | <Box> 57 | <OptionRow 58 | image={ <BiAddToQueue size={ 24 } /> } 59 | label="Option Row 1" 60 | subTitle="Build changes from def5fee229ecda72382e7d881305b572417a53b8 https://github.com/wpcomvip/my-repo/actions/runs/6883309086" 61 | as="div" 62 | href="http://google.com/" 63 | meta="Meta text" 64 | /> 65 | <OptionRow 66 | image={ <BiCalendarHeart size={ 24 } /> } 67 | label="Option Row 2" 68 | subTitle="Build changes from def5fee229ecda72382e7d881305b572417a53b8 https://github.com/wpcomvip/my-repo/actions/runs/6883309086" 69 | as="div" 70 | href="http://google.com/" 71 | order={ 2 } 72 | meta="Meta text" 73 | /> 74 | </Box> 75 | ); 76 | -------------------------------------------------------------------------------- /src/system/OptionRow/OptionRow.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { render, screen } from '@testing-library/react'; 5 | import { axe } from 'jest-axe'; 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import { OptionRow } from './OptionRow'; 11 | 12 | describe( '<OptionRow />', () => { 13 | it( 'renders the OptionRow', async () => { 14 | const { container } = render( 15 | <OptionRow label="Option Row" subTitle="Mostly used to link off to other pages." as="a" /> 16 | ); 17 | 18 | expect( screen.getByText( 'Mostly used to link off to other pages.' ) ).toBeInTheDocument(); 19 | 20 | // Check for accessibility issues 21 | await expect( await axe( container ) ).toHaveNoViolations(); 22 | } ); 23 | 24 | it( 'renders a disabled OptionRow', async () => { 25 | const { container } = render( 26 | <OptionRow 27 | label="Option Row" 28 | subTitle="Mostly used to link off to other pages." 29 | as="a" 30 | disabled 31 | variant="default" 32 | meta="" 33 | /> 34 | ); 35 | 36 | expect( screen.queryByTestId( 'meta' ) ).not.toBeInTheDocument(); 37 | 38 | // Check for accessibility issues 39 | await expect( await axe( container ) ).toHaveNoViolations(); 40 | } ); 41 | } ); 42 | -------------------------------------------------------------------------------- /src/system/OptionRow/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { OptionRow } from './OptionRow'; 5 | 6 | export { OptionRow }; 7 | -------------------------------------------------------------------------------- /src/system/Page/Page.test.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 3 | // @ts-nocheck 4 | import { render } from '@testing-library/react'; 5 | import { axe } from 'jest-axe'; 6 | import { ThemeUIProvider } from 'theme-ui'; 7 | 8 | import { Page } from './Page'; 9 | import theme from '../theme'; 10 | 11 | const renderWithTheme = children => 12 | render( <ThemeUIProvider theme={ theme }>{ children }</ThemeUIProvider> ); 13 | 14 | const renderComponent = () => renderWithTheme( <Page /> ); 15 | 16 | describe( '<Page />', () => { 17 | it( 'renders the Page component', async () => { 18 | const { container } = renderComponent(); 19 | 20 | expect( await axe( container ) ).toHaveNoViolations(); 21 | } ); 22 | } ); 23 | -------------------------------------------------------------------------------- /src/system/Page/Page.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from '../Box'; 2 | 3 | export const Page = props => <Box { ...props } />; 4 | -------------------------------------------------------------------------------- /src/system/Progress/Progress.stories.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import React, { useEffect } from 'react'; 5 | 6 | import { Progress } from '..'; 7 | 8 | export default { 9 | title: 'Progress', 10 | component: Progress, 11 | }; 12 | 13 | export const Default = () => { 14 | const [ counter, setCounter ] = React.useState( 0 ); 15 | const steps = [ 'Downloading Data', 'Importing Data...', 'Finalizing', 'Done' ]; 16 | 17 | useEffect( () => { 18 | setTimeout( () => { 19 | if ( counter < steps.length - 1 ) { 20 | setCounter( counter + 1 ); 21 | } 22 | }, 2000 ); 23 | }, [ counter, setCounter ] ); 24 | 25 | return <Progress forLabel="Update site progress" steps={ steps } activeStep={ counter } />; 26 | }; 27 | -------------------------------------------------------------------------------- /src/system/Progress/Progress.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { render, screen } from '@testing-library/react'; 5 | import { axe } from 'jest-axe'; 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import { Progress } from './Progress'; 11 | 12 | const steps = [ 'Downloading Data', 'Importing Data...', 'Finalizing', 'Done' ]; 13 | describe( '<Progress />', () => { 14 | it( 'renders the progress component', async () => { 15 | const { container } = render( <Progress steps={ steps } activeStep={ 1 } /> ); 16 | 17 | expect( container.getElementsByClassName( 'vip-progress-component' ) ).toBeDefined(); 18 | 19 | // Check for accessibility issues 20 | expect( await axe( container ) ).toHaveNoViolations(); 21 | } ); 22 | 23 | it( 'renders the progress component with different label text', async () => { 24 | const { container } = render( 25 | <Progress forLabel="My progress bar" steps={ steps } activeStep={ 1 } /> 26 | ); 27 | 28 | expect( screen.getByLabelText( 'My progress bar' ) ).toBeInTheDocument(); 29 | 30 | // Check for accessibility issues 31 | expect( await axe( container ) ).toHaveNoViolations(); 32 | } ); 33 | } ); 34 | -------------------------------------------------------------------------------- /src/system/Progress/Progress.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import classNames from 'classnames'; 5 | import { forwardRef, Ref } from 'react'; 6 | import { MdCheck } from 'react-icons/md'; 7 | import { ProgressProps, Progress as ThemeProgress } from 'theme-ui'; 8 | 9 | /** 10 | * Internal dependencies 11 | */ 12 | import { Box, Text, Flex } from '..'; 13 | import { Spinner } from '../Spinner'; 14 | 15 | const prefix = 'vip-progress-component'; 16 | const uniqueID = () => Math.random().toString( 36 ).substring( 7 ); 17 | 18 | export interface ThemeProgressProps extends ProgressProps { 19 | steps: string[]; 20 | activeStep: number; 21 | forLabel?: string; 22 | className?: string; 23 | } 24 | 25 | export const Progress = forwardRef< HTMLProgressElement, ThemeProgressProps >( 26 | ( 27 | { steps, activeStep, sx, forLabel = '', className, ...props }: ThemeProgressProps, 28 | ref: Ref< HTMLProgressElement > 29 | ) => { 30 | const stepsTotal = steps.length; 31 | const isDone = activeStep === stepsTotal - 1; 32 | const instance = uniqueID(); 33 | const htmlFor = `${ prefix }-${ instance }`; 34 | const currentValue = activeStep + 1; 35 | 36 | return ( 37 | <Box className={ classNames( prefix, className ) }> 38 | <ThemeProgress 39 | sx={ { 40 | color: 'primary', 41 | backgroundColor: 'background', 42 | ...sx, 43 | } } 44 | max={ stepsTotal } 45 | value={ currentValue } 46 | id={ htmlFor } 47 | aria-label={ forLabel } 48 | ref={ ref } 49 | { ...props } 50 | /> 51 | 52 | { steps && ( 53 | <Flex 54 | sx={ { alignItems: 'center', mt: 2 } } 55 | aria-live="polite" 56 | aria-atomic="true" 57 | aria-describedby={ htmlFor } 58 | > 59 | { ! isDone && <Spinner size={ 24 } aria-hidden="true" /> } 60 | { isDone && <MdCheck size={ 24 } aria-hidden="true" /> } 61 | 62 | <Text sx={ { ml: 2, mb: 0 } }> 63 | <strong>{ `${ currentValue } of ${ stepsTotal }` }: </strong> 64 | <Text as="span" sx={ { color: 'muted' } }> 65 | { steps[ activeStep ] } 66 | </Text> 67 | </Text> 68 | </Flex> 69 | ) } 70 | </Box> 71 | ); 72 | } 73 | ); 74 | 75 | Progress.displayName = 'Progress'; 76 | -------------------------------------------------------------------------------- /src/system/Progress/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | export { Progress } from './Progress'; 5 | -------------------------------------------------------------------------------- /src/system/ScreenReaderText/ScreenReader.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { render, screen } from '@testing-library/react'; 5 | 6 | import '@testing-library/jest-dom'; 7 | /** 8 | * Internal dependencies 9 | */ 10 | import ScreenReaderText from '.'; 11 | 12 | describe( '<ScreenReaderText />', () => { 13 | it( 'should render correctly', () => { 14 | const props = {}; 15 | const text = 'Hello there'; 16 | const { container } = render( <ScreenReaderText { ...props }>{ text }</ScreenReaderText> ); 17 | // we're using the querySelector to ensure the class is rendered since it affects the A11Y 18 | // in case it's removed it could compromise the A11Y of the components using it. 19 | expect( container.querySelector( '.screen-reader-text' ) ).toBeInTheDocument(); 20 | 21 | expect( screen.queryByText( text ) ).toBeInTheDocument(); 22 | } ); 23 | } ); 24 | -------------------------------------------------------------------------------- /src/system/ScreenReaderText/ScreenReaderText.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | // we'll need jsxImportSource for the sx prop when used with html elements 3 | 4 | /** 5 | * External dependencies 6 | */ 7 | 8 | import { forwardRef, ReactNode, Ref } from 'react'; 9 | import { ThemeUIStyleObject } from 'theme-ui'; 10 | 11 | /** 12 | * Internal dependencies 13 | */ 14 | 15 | export const screenReaderTextClass: ThemeUIStyleObject = { 16 | border: 'none', 17 | clip: 'rect(1px, 1px, 1px, 1px)', 18 | clipPath: 'inset(50%)', 19 | height: '1px', 20 | margin: '-1px', 21 | overflow: 'hidden', 22 | padding: '0', 23 | position: 'absolute', 24 | width: '1px', 25 | wordWrap: 'normal !important' as 'normal', 26 | }; 27 | 28 | export interface ScreenReaderTextProps { 29 | children: ReactNode; 30 | } 31 | 32 | export const ScreenReaderText = forwardRef< HTMLSpanElement, ScreenReaderTextProps >( 33 | ( props: ScreenReaderTextProps, ref: Ref< HTMLSpanElement > ) => ( 34 | <span className="screen-reader-text" sx={ screenReaderTextClass } { ...props } ref={ ref }> 35 | { props.children } 36 | </span> 37 | ) 38 | ); 39 | -------------------------------------------------------------------------------- /src/system/ScreenReaderText/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | 5 | import { ScreenReaderText } from './ScreenReaderText'; 6 | 7 | export { ScreenReaderText }; 8 | 9 | export default ScreenReaderText; 10 | -------------------------------------------------------------------------------- /src/system/Skeleton/Skeleton.stories.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { Skeleton } from '..'; 5 | 6 | export default { 7 | title: 'Skeleton', 8 | component: Skeleton, 9 | }; 10 | 11 | export const Default = () => <Skeleton />; 12 | 13 | export const Grouped = () => <Skeleton times={ 3 } />; 14 | 15 | export const Circle = () => <Skeleton variant="circle" width="50px" height="50px" />; 16 | 17 | export const Text = () => <Skeleton variant="text" />; 18 | -------------------------------------------------------------------------------- /src/system/Skeleton/Skeleton.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | 5 | /** 6 | * Internal dependencies 7 | */ 8 | import { Box } from '../Box'; 9 | 10 | export interface SkeletonProps { 11 | variant?: string; 12 | width?: number | string; 13 | height?: number | string; 14 | borderRadius?: number; 15 | } 16 | 17 | export const Skeleton = ( { 18 | variant = 'text', 19 | width = '100%', 20 | height = '30px', 21 | borderRadius = 1, 22 | times = 1, 23 | ...props 24 | } ) => ( 25 | <> 26 | { Array.from( { length: times } ).map( ( i, index ) => ( 27 | <Box 28 | key={ index } 29 | sx={ { 30 | borderRadius: variant === 'circle' ? '50%' : borderRadius, 31 | width, 32 | height, 33 | backgroundColor: 'skeleton.background', 34 | animation: 'pulse 1.5s ease-in-out 3', 35 | opacity: 0.125, 36 | '@keyframes pulse': { 37 | '0%': { 38 | opacity: 0.125, 39 | }, 40 | '50%': { 41 | opacity: 0.2, 42 | }, 43 | '100%': { 44 | opacity: 0.125, 45 | }, 46 | }, 47 | mb: index === times - 1 ? 0 : 4, 48 | } } 49 | aria-hidden 50 | { ...props } 51 | ></Box> 52 | ) ) } 53 | </> 54 | ); 55 | 56 | Skeleton.displayName = 'Skeleton'; 57 | 58 | export default Skeleton; 59 | -------------------------------------------------------------------------------- /src/system/Skeleton/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | 5 | import { Skeleton } from './Skeleton'; 6 | 7 | export { Skeleton }; 8 | 9 | export default Skeleton; 10 | -------------------------------------------------------------------------------- /src/system/Snackbar/Snackbar.stories.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | /** 3 | * External dependencies 4 | */ 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import React, { useState } from 'react'; 10 | 11 | import { Snackbar } from '..'; 12 | 13 | export default { 14 | title: 'Snackbar', 15 | component: Snackbar, 16 | }; 17 | 18 | export const Default = () => { 19 | const [ visible, setVisible ] = useState( true ); 20 | return ( 21 | <React.Fragment> 22 | { visible && ( 23 | <Snackbar 24 | variant="error" 25 | sx={ { mb: 4 } } 26 | ctaText="Resolve" 27 | ctaOnClick={ () => { 28 | setVisible( false ); 29 | } } 30 | > 31 | Error message. 32 | </Snackbar> 33 | ) } 34 | 35 | <Snackbar 36 | variant="warning" 37 | sx={ { mb: 4 } } 38 | ctaText="View" 39 | ctaOnClick={ () => { 40 | setVisible( false ); 41 | } } 42 | > 43 | Warning message. 44 | </Snackbar> 45 | 46 | <Snackbar 47 | variant="info" 48 | sx={ { mb: 4 } } 49 | ctaText="View" 50 | ctaOnClick={ () => { 51 | setVisible( false ); 52 | } } 53 | > 54 | Tip or information. 55 | </Snackbar> 56 | 57 | <Snackbar 58 | variant="success" 59 | sx={ { mb: 4 } } 60 | ctaText="Preview" 61 | ctaOnClick={ () => { 62 | setVisible( false ); 63 | } } 64 | > 65 | Success message. 66 | </Snackbar> 67 | 68 | <Snackbar 69 | loading 70 | variant="warning" 71 | sx={ { mb: 4 } } 72 | title="Operation in progress..." 73 | ctaText="Pause" 74 | ctaOnClick={ () => { 75 | setVisible( false ); 76 | } } 77 | > 78 | Check back again in a few seconds. 79 | </Snackbar> 80 | 81 | <Snackbar variant="system" sx={ { mb: 4 } }> 82 | System message. 83 | </Snackbar> 84 | </React.Fragment> 85 | ); 86 | }; 87 | -------------------------------------------------------------------------------- /src/system/Snackbar/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { Snackbar } from './Snackbar'; 5 | 6 | export { Snackbar }; 7 | -------------------------------------------------------------------------------- /src/system/Spinner/Spinner.stories.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { Spinner } from '..'; 5 | 6 | export default { 7 | title: 'Spinner', 8 | component: Spinner, 9 | }; 10 | 11 | export const Default = () => <Spinner />; 12 | -------------------------------------------------------------------------------- /src/system/Spinner/Spinner.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { render, screen } from '@testing-library/react'; 5 | import { axe } from 'jest-axe'; 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import { Spinner } from './Spinner'; 11 | 12 | describe( '<Spinner />', () => { 13 | it( 'renders the Spinner component', async () => { 14 | const { container } = render( <Spinner /> ); 15 | 16 | expect( container.getElementsByClassName( 'vip-spinner-component' ) ).toBeDefined(); 17 | 18 | // Check for accessibility issues 19 | expect( await axe( container ) ).toHaveNoViolations(); 20 | } ); 21 | 22 | it( 'renders the Spinner component with a different title', async () => { 23 | const { container } = render( <Spinner title="Please Wait" /> ); 24 | 25 | expect( screen.getByTitle( 'Please Wait' ) ).toBeInTheDocument(); 26 | 27 | // Check for accessibility issues 28 | expect( await axe( container ) ).toHaveNoViolations(); 29 | } ); 30 | } ); 31 | -------------------------------------------------------------------------------- /src/system/Spinner/Spinner.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import classNames from 'classnames'; 5 | import { forwardRef, Ref } from 'react'; 6 | import { Spinner as ThemeSpinner, SpinnerProps, ThemeUIStyleObject } from 'theme-ui'; 7 | 8 | export interface ThemeSpinnerProps extends SpinnerProps { 9 | sx?: ThemeUIStyleObject; 10 | className?: string; 11 | color?: string; 12 | strokeWidth?: number; 13 | } 14 | 15 | export const Spinner = forwardRef< SVGSVGElement, ThemeSpinnerProps >( 16 | ( 17 | { sx, color = 'icon.helper', strokeWidth = 2, className, ...props }: ThemeSpinnerProps, 18 | ref: Ref< SVGSVGElement > 19 | ) => ( 20 | <ThemeSpinner 21 | as="svg" 22 | sx={ { 23 | ...sx, 24 | color, 25 | } } 26 | strokeWidth={ strokeWidth } 27 | className={ classNames( 'vip-spinner-component', className ) } 28 | ref={ ref } 29 | { ...props } 30 | /> 31 | ) 32 | ); 33 | 34 | Spinner.displayName = 'Spinner'; 35 | -------------------------------------------------------------------------------- /src/system/Spinner/index.ts: -------------------------------------------------------------------------------- 1 | export { Spinner } from './Spinner'; 2 | -------------------------------------------------------------------------------- /src/system/Table/Table.stories.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | // we'll need jsxImportSource for the sx prop when used with html elements 3 | 4 | /** 5 | * External dependencies 6 | */ 7 | 8 | /** 9 | * Internal dependencies 10 | */ 11 | import { Table, TableRow, Flex, Text, TableCell } from '..'; 12 | 13 | export default { 14 | title: 'Table', 15 | component: Table, 16 | }; 17 | 18 | interface ExampleTableProps { 19 | caption: string; 20 | } 21 | 22 | const ExampleTable = ( { caption }: ExampleTableProps ) => ( 23 | <Table caption={ caption }> 24 | <thead> 25 | <TableRow head cells={ [ 'User', 'Command', 'Duration', 'Time' ] } /> 26 | </thead> 27 | <tbody> 28 | <TableRow 29 | cells={ [ 30 | <Flex sx={ { alignItems: 'center' } } key="user"> 31 | kwaves 32 | </Flex>, 33 | <Flex key="command">wp rewrite flush</Flex>, 34 | <Text sx={ { mb: 0 } } key="duration"> 35 | 2s 36 | </Text>, 37 | <Text key="time">11th Mar 2020, 16:49:22</Text>, 38 | ] } 39 | /> 40 | <TableRow> 41 | <TableCell> 42 | <Flex sx={ { alignItems: 'center' } } key="user"> 43 | simon 44 | </Flex> 45 | </TableCell> 46 | <TableCell>wp posts list</TableCell> 47 | <TableCell> 48 | <Text sx={ { mb: 0 } } key="duration"> 49 | 3s 50 | </Text> 51 | </TableCell> 52 | <TableCell> 53 | <Text key="time">3rd May 2021, 13:22:13</Text> 54 | </TableCell> 55 | </TableRow> 56 | </tbody> 57 | </Table> 58 | ); 59 | 60 | export const Default = () => <ExampleTable caption="Example Table" />; 61 | 62 | export const WithHorizontalScroll = () => ( 63 | <div sx={ { maxWidth: '800px' } }> 64 | <ExampleTable caption="Horizontal Scroll Example" /> 65 | </div> 66 | ); 67 | -------------------------------------------------------------------------------- /src/system/Table/Table.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | // we'll need jsxImportSource for the sx prop when used with html elements 3 | 4 | /** 5 | * External dependencies 6 | */ 7 | import classNames, { Argument } from 'classnames'; 8 | import { forwardRef, ReactNode, Ref, useMemo } from 'react'; 9 | 10 | /** 11 | * Internal dependencies 12 | */ 13 | import { Box } from '../'; 14 | import { screenReaderTextClass } from '../ScreenReaderText/ScreenReaderText'; 15 | import { generateId } from '../utils/random'; 16 | 17 | import type { ThemeUIStyleObject } from 'theme-ui'; 18 | 19 | export interface TableProps { 20 | caption?: string; 21 | children?: ReactNode; 22 | className?: Argument; 23 | sx?: ThemeUIStyleObject; 24 | } 25 | 26 | export const Table = forwardRef< HTMLTableElement, TableProps >( 27 | ( { sx, className, children, caption, ...props }: TableProps, ref: Ref< HTMLTableElement > ) => { 28 | if ( ! caption ) { 29 | // eslint-disable-next-line no-console 30 | console.warn( '[A11Y] Please, add a caption to your table.' ); 31 | } 32 | 33 | const captionId = useMemo( () => `table_caption_${ generateId() }`, [] ); 34 | 35 | return ( 36 | <Box 37 | className={ classNames( 'vip-table-component', className ) } 38 | sx={ { width: '100%', maxWidth: '100vw', overflowX: 'auto' } } 39 | role="region" 40 | aria-labelledby={ captionId } 41 | tabIndex={ 0 } 42 | > 43 | <table 44 | sx={ { width: '100%', minWidth: '1024px', borderSpacing: 0, ...sx } } 45 | className={ classNames( 'vip-table-component-element', className ) } 46 | ref={ ref } 47 | { ...props } 48 | > 49 | { caption && ( 50 | <caption id={ captionId } sx={ screenReaderTextClass }> 51 | { caption } 52 | </caption> 53 | ) } 54 | { children } 55 | </table> 56 | </Box> 57 | ); 58 | } 59 | ); 60 | 61 | Table.displayName = 'Table'; 62 | -------------------------------------------------------------------------------- /src/system/Table/TableCell.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | // we'll need jsxImportSource for the sx prop when used with html elements 3 | 4 | /** 5 | * External dependencies 6 | */ 7 | import { ReactNode } from 'react'; 8 | import { ThemeUIStyleObject } from 'theme-ui'; 9 | 10 | /** 11 | * Internal dependencies 12 | */ 13 | import { Box } from '../'; 14 | 15 | export interface TableCellProps extends React.HTMLProps< HTMLTableCellElement > { 16 | children: ReactNode; 17 | head?: boolean; 18 | sx?: ThemeUIStyleObject; 19 | } 20 | 21 | export const TableCell = ( { children, head, sx, ...rest }: TableCellProps ) => { 22 | const style: ThemeUIStyleObject = { 23 | borderBottom: '1px solid', 24 | borderTop: head ? '1px solid' : 'none', 25 | // borderColor should come after borderTop so it can override it 26 | borderColor: 'table.border', 27 | fontWeight: 'body', 28 | px: 3, 29 | py: 2, 30 | textAlign: 'left', 31 | ...sx, 32 | }; 33 | 34 | return ( 35 | <Box { ...rest } as={ head ? 'th' : 'td' } ref={ undefined } sx={ style }> 36 | { head ? ( 37 | <span sx={ { mb: 0, color: 'table.heading', fontSize: 2, fontWeight: 'bold' } }> 38 | { children } 39 | </span> 40 | ) : ( 41 | children 42 | ) } 43 | </Box> 44 | ); 45 | }; 46 | 47 | TableCell.displayName = 'TableCell'; 48 | -------------------------------------------------------------------------------- /src/system/Table/TableRow.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | // we'll need jsxImportSource for the sx prop when used with html elements 3 | 4 | /** 5 | * External dependencies 6 | */ 7 | import { KeyboardEvent, ReactNode } from 'react'; 8 | import { ThemeUIStyleObject } from 'theme-ui'; 9 | 10 | /** 11 | * Internal dependencies 12 | */ 13 | import { TableCell } from './TableCell'; 14 | 15 | export interface TableRowProps extends React.HTMLProps< HTMLTableRowElement > { 16 | cells?: ReactNode[]; 17 | children?: ReactNode; 18 | head?: boolean; 19 | onClick?: () => void; 20 | sx?: ThemeUIStyleObject; 21 | } 22 | 23 | export const TableRow = ( { 24 | onClick, 25 | head = false, 26 | cells = [], 27 | children, 28 | sx, 29 | ...rest 30 | }: TableRowProps ) => { 31 | const hoverStyles: ThemeUIStyleObject = { 32 | cursor: 'pointer', 33 | '&:hover': { 34 | bg: 'hover', 35 | borderRadius: 2, 36 | }, 37 | ...sx, 38 | }; 39 | 40 | function handleKeyPress( evt: KeyboardEvent< HTMLTableRowElement > ) { 41 | if ( onClick && evt.key === 'Enter' ) { 42 | onClick(); 43 | } 44 | } 45 | 46 | return ( 47 | <tr 48 | sx={ onClick ? hoverStyles : sx } 49 | onClick={ onClick } 50 | tabIndex={ onClick ? 0 : undefined } 51 | onKeyDown={ handleKeyPress } 52 | { ...rest } 53 | > 54 | { cells.map( ( cell, index ) => ( 55 | <TableCell key={ index } head={ head }> 56 | { cell } 57 | </TableCell> 58 | ) ) } 59 | 60 | { children } 61 | </tr> 62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /src/system/Table/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | export { Table } from './Table'; 5 | export { TableRow } from './TableRow'; 6 | export { TableCell } from './TableCell'; 7 | -------------------------------------------------------------------------------- /src/system/Tabs/Tabs.js: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | /** 4 | * External dependencies 5 | */ 6 | import * as TabsPrimitive from '@radix-ui/react-tabs'; 7 | import classNames from 'classnames'; 8 | import PropTypes from 'prop-types'; 9 | import React from 'react'; 10 | 11 | /** 12 | * Internal dependencies 13 | */ 14 | 15 | const Tabs = React.forwardRef( 16 | ( 17 | { 18 | children, 19 | onValueChange = undefined, 20 | defaultValue = undefined, 21 | value = undefined, 22 | className = null, 23 | }, 24 | ref 25 | ) => { 26 | return ( 27 | <TabsPrimitive.Root 28 | ref={ ref } 29 | value={ value } 30 | defaultValue={ defaultValue } 31 | onValueChange={ onValueChange } 32 | className={ classNames( 'vip-tabs-component', className ) } 33 | > 34 | { children } 35 | </TabsPrimitive.Root> 36 | ); 37 | } 38 | ); 39 | 40 | Tabs.propTypes = { 41 | className: PropTypes.any, 42 | defaultValue: PropTypes.node, 43 | value: PropTypes.node, 44 | onValueChange: PropTypes.func, 45 | children: PropTypes.node.isRequired, 46 | }; 47 | 48 | Tabs.displayName = 'Tabs'; 49 | 50 | export { Tabs }; 51 | -------------------------------------------------------------------------------- /src/system/Tabs/Tabs.stories.jsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | /** 4 | * External dependencies 5 | */ 6 | import React from 'react'; 7 | 8 | /** 9 | * Internal dependencies 10 | */ 11 | import { Tabs, TabsTrigger, TabsList, TabsContent, Text, Link, Button } from '..'; 12 | 13 | export default { 14 | title: 'Navigation/Tabs', 15 | component: Tabs, 16 | }; 17 | 18 | export const Default = () => ( 19 | <Tabs defaultValue="all"> 20 | <TabsList title="See all the content"> 21 | <TabsTrigger value="all">All (5)</TabsTrigger> 22 | <TabsTrigger value="live">Live (2)</TabsTrigger> 23 | <TabsTrigger value="dev">In Development (3)</TabsTrigger> 24 | <TabsTrigger value="protect" disabled> 25 | Not accessible 26 | </TabsTrigger> 27 | </TabsList> 28 | <TabsContent value="all"> 29 | <Text> 30 | All content <Link href="https://google.com">https://google.com</Link> 31 | </Text> 32 | </TabsContent> 33 | <TabsContent value="live">Live content</TabsContent> 34 | <TabsContent value="dev"> 35 | <Text> 36 | In Development content <Button variant="secondary">Hey I am a button</Button>{ ' ' } 37 | </Text> 38 | </TabsContent> 39 | </Tabs> 40 | ); 41 | 42 | export const SetActiveTab = () => { 43 | const [ activeTab, setActiveTab ] = React.useState( 'all' ); 44 | 45 | return ( 46 | <Tabs value={ activeTab } onValueChange={ val => setActiveTab( val ) }> 47 | <TabsList title="See all the content"> 48 | <TabsTrigger value="all">All (5)</TabsTrigger> 49 | <TabsTrigger value="live">Live (2)</TabsTrigger> 50 | <TabsTrigger value="dev">In Development (3)</TabsTrigger> 51 | <TabsTrigger value="protect" disabled={ true }> 52 | Not accessible 53 | </TabsTrigger> 54 | </TabsList> 55 | <TabsContent value="all"> 56 | <Text> 57 | <button type="button" onClick={ () => setActiveTab( 'live' ) }> 58 | Switch to live tab 59 | </button> 60 | </Text> 61 | </TabsContent> 62 | <TabsContent value="live">Live content</TabsContent> 63 | <TabsContent value="dev"> 64 | <Text> 65 | In Development content <button type="button">Hey I am a button</button>{ ' ' } 66 | </Text> 67 | </TabsContent> 68 | </Tabs> 69 | ); 70 | }; 71 | -------------------------------------------------------------------------------- /src/system/Tabs/TabsContent.js: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | /** 4 | * External dependencies 5 | */ 6 | import * as TabsPrimitive from '@radix-ui/react-tabs'; 7 | import classNames from 'classnames'; 8 | import PropTypes from 'prop-types'; 9 | 10 | /** 11 | * Internal dependencies 12 | */ 13 | 14 | const TabsContent = ( { value, children, className = null } ) => ( 15 | <TabsPrimitive.Content 16 | className={ classNames( 'vip-tabs-content', `vip-tabs-content-${ value }`, className ) } 17 | value={ value } 18 | sx={ { 19 | mt: 4, 20 | } } 21 | > 22 | { children } 23 | </TabsPrimitive.Content> 24 | ); 25 | 26 | TabsContent.propTypes = { 27 | className: PropTypes.string, 28 | value: PropTypes.string, 29 | children: PropTypes.node.isRequired, 30 | }; 31 | 32 | export { TabsContent }; 33 | -------------------------------------------------------------------------------- /src/system/Tabs/TabsList.js: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | /** 4 | * External dependencies 5 | */ 6 | import * as TabsPrimitive from '@radix-ui/react-tabs'; 7 | import PropTypes from 'prop-types'; 8 | 9 | /** 10 | * Internal dependencies 11 | */ 12 | 13 | const TabsList = ( { children, title, ...props } ) => ( 14 | <TabsPrimitive.List 15 | sx={ { 16 | borderBottom: '1px solid', 17 | borderColor: 'borders.2', 18 | display: 'flex', 19 | } } 20 | aria-label={ title } 21 | { ...props } 22 | > 23 | { children } 24 | </TabsPrimitive.List> 25 | ); 26 | 27 | TabsList.propTypes = { 28 | title: PropTypes.string.isRequired, 29 | children: PropTypes.node.isRequired, 30 | }; 31 | 32 | export { TabsList }; 33 | -------------------------------------------------------------------------------- /src/system/Tabs/TabsTrigger.js: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | /** 4 | * External dependencies 5 | */ 6 | import * as TabsPrimitive from '@radix-ui/react-tabs'; 7 | import classNames from 'classnames'; 8 | import PropTypes from 'prop-types'; 9 | import React from 'react'; 10 | 11 | /** 12 | * Internal dependencies 13 | */ 14 | 15 | const styles = { 16 | cursor: 'pointer', 17 | background: 'none', 18 | mr: 3, 19 | fontSize: 2, 20 | px: 0, 21 | pb: 3, 22 | border: 'none', 23 | color: 'heading', 24 | '&[data-state="active"]': { 25 | color: 'link', 26 | fontWeight: 'regular', 27 | position: 'relative', 28 | '&::after': { 29 | position: 'absolute', 30 | bottom: 0, 31 | display: 'block', 32 | width: '100%', 33 | content: '""', 34 | height: '0.125rem', 35 | backgroundColor: 'link', 36 | }, 37 | }, 38 | '&:disabled': { 39 | color: 'muted', 40 | }, 41 | ':hover': { fontWeight: 'regular', color: 'heading' }, 42 | '&:focus-visible': theme => theme.outline, 43 | }; 44 | 45 | const TabsTrigger = React.forwardRef( 46 | ( { value, disabled = false, children, className = null }, forwardRef ) => ( 47 | <TabsPrimitive.TabsTrigger 48 | className={ classNames( 'vip-tabs-trigger', `vip-tabs-trigger-${ value }`, className ) } 49 | value={ value } 50 | disabled={ disabled } 51 | sx={ { 52 | ...styles, 53 | } } 54 | ref={ forwardRef } 55 | > 56 | { children } 57 | </TabsPrimitive.TabsTrigger> 58 | ) 59 | ); 60 | 61 | TabsTrigger.propTypes = { 62 | className: PropTypes.string, 63 | value: PropTypes.string, 64 | disabled: PropTypes.bool, 65 | children: PropTypes.node.isRequired, 66 | }; 67 | 68 | TabsTrigger.displayName = 'TabsTrigger'; 69 | export { TabsTrigger }; 70 | -------------------------------------------------------------------------------- /src/system/Tabs/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { Tabs } from './Tabs'; 5 | import { TabsContent } from './TabsContent'; 6 | import { TabsList } from './TabsList'; 7 | import { TabsTrigger } from './TabsTrigger'; 8 | 9 | export { Tabs, TabsList, TabsTrigger, TabsContent }; 10 | -------------------------------------------------------------------------------- /src/system/Text/Text.stories.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | // we'll need jsxImportSource for the sx prop when used with html elements 3 | 4 | /** 5 | * Internal dependencies 6 | */ 7 | import { Text } from '..'; 8 | 9 | export default { 10 | title: 'Text', 11 | component: Text, 12 | }; 13 | 14 | export const Default = () => ( 15 | <> 16 | <Text> 17 | Apparently we had reached a great height in the atmosphere, for the sky was a dead black, and 18 | the stars had ceased to twinkle. By the same illusion which lifts the horizon of the sea to 19 | the level of the spectator on a hillside, the sable cloud beneath was dished out, and the car 20 | seemed to float in the middle of an immense dark sphere, whose upper half was strewn with 21 | silver.{ ' ' } 22 | </Text> 23 | 24 | <Text sx={ { color: 'texts.accent' } }>Text Accent</Text> 25 | 26 | <Text sx={ { color: 'texts.helper' } }>Text Helper</Text> 27 | 28 | <Text sx={ { color: 'texts.helper', fontWeight: 'body' } }>Text Helper</Text> 29 | <Text sx={ { color: 'texts.helper', fontWeight: 'heading' } }>Text Helper</Text> 30 | <Text sx={ { color: 'texts.helper', fontWeight: 'regular' } }>Text Helper</Text> 31 | <Text sx={ { color: 'texts.helper', fontWeight: 'medium' } }>Text Helper</Text> 32 | <Text sx={ { color: 'texts.helper', fontWeight: 'bold' } }>Text Helper</Text> 33 | <Text sx={ { color: 'texts.helper', fontWeight: 'light' } }>Text Helper</Text> 34 | 35 | <Text sx={ { color: 'texts.secondary' } }>Text Secondary</Text> 36 | 37 | <Text sx={ { color: 'texts.primary' } }>Text Primary</Text> 38 | 39 | <Text sx={ { color: 'texts.placeholder' } }>Text placeholder</Text> 40 | 41 | <Text sx={ { color: 'texts.disabled' } }>Text disabled</Text> 42 | 43 | <div sx={ { bg: 'layer.inverse' } }> 44 | <Text sx={ { color: 'texts.inverse' } }>Text inverse</Text> 45 | </div> 46 | </> 47 | ); 48 | -------------------------------------------------------------------------------- /src/system/Text/Text.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import classNames from 'classnames'; 5 | import { forwardRef, Ref } from 'react'; 6 | import { Text as ThemeText, TextProps as ThemeTextProps } from 'theme-ui'; 7 | 8 | export const Text = forwardRef< HTMLDivElement, ThemeTextProps >( 9 | ( { sx, className, ...props }: ThemeTextProps, ref: Ref< HTMLDivElement > ) => ( 10 | <ThemeText 11 | as="p" 12 | sx={ { 13 | lineHeight: 'body', 14 | marginBottom: 2, 15 | color: 'text', 16 | ...sx, 17 | } } 18 | className={ classNames( 'vip-text-component', className ) } 19 | ref={ ref } 20 | { ...props } 21 | /> 22 | ) 23 | ); 24 | 25 | Text.displayName = 'Text'; 26 | -------------------------------------------------------------------------------- /src/system/Text/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | export { Text } from './Text'; 5 | -------------------------------------------------------------------------------- /src/system/Toolbar/Toolbar.test.tsx: -------------------------------------------------------------------------------- 1 | // TODO: Fix this 2 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 3 | // @ts-nocheck 4 | /** @jsxImportSource theme-ui */ 5 | 6 | import { render, screen } from '@testing-library/react'; 7 | import { axe } from 'jest-axe'; 8 | import { ThemeUIProvider } from 'theme-ui'; 9 | 10 | import { Toolbar, Nav, NavItem, Text, theme } from '../../system'; 11 | 12 | const renderWithTheme = children => 13 | render( <ThemeUIProvider theme={ theme }>{ children }</ThemeUIProvider> ); 14 | 15 | const renderComponent = () => 16 | renderWithTheme( 17 | <Toolbar.Primary> 18 | <Toolbar.Logo href="https://wpvip.com/" /> 19 | <Nav.Toolbar label="Main links"> 20 | <NavItem.Toolbar active href="https://googles.com"> 21 | My Applications 22 | </NavItem.Toolbar> 23 | <NavItem.Toolbar href="https://google.com">My Organization</NavItem.Toolbar> 24 | </Nav.Toolbar> 25 | 26 | <Toolbar.UtilNav> 27 | <Text>Utility content</Text> 28 | </Toolbar.UtilNav> 29 | </Toolbar.Primary> 30 | ); 31 | 32 | describe( '<Toolbar />', () => { 33 | it( 'renders the Toolbar component', async () => { 34 | const { container } = renderComponent(); 35 | 36 | // Should find the toolbar main nav label 37 | expect( screen.getByLabelText( 'Main links' ) ).toBeInTheDocument(); 38 | 39 | // Should find all links 40 | expect( screen.queryByText( 'My Applications' ) ).toBeInTheDocument(); 41 | expect( screen.queryByText( 'My Organization' ) ).toBeInTheDocument(); 42 | 43 | // Renders utility nav 44 | expect( screen.getByLabelText( 'Utility' ) ).toBeInTheDocument(); 45 | 46 | // Renders utility content 47 | expect( screen.queryByText( 'Utility content' ) ).toBeInTheDocument(); 48 | 49 | // Check for accessibility issues 50 | expect( await axe( container ) ).toHaveNoViolations(); 51 | } ); 52 | } ); 53 | -------------------------------------------------------------------------------- /src/system/Toolbar/Toolbar.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | import classNames from 'classnames'; 4 | import React, { Ref, forwardRef } from 'react'; 5 | 6 | import { VIP_TOOLBAR } from './index'; 7 | import { Flex } from '..'; 8 | 9 | export type ToolbarVariant = 'primary'; 10 | 11 | export interface ToolbarProps { 12 | className?: string; 13 | children: React.ReactNode; 14 | } 15 | 16 | const Toolbar = forwardRef< HTMLElement, ToolbarProps >( 17 | ( { className, children }: ToolbarProps, ref: Ref< HTMLElement > ) => ( 18 | <Flex 19 | ref={ ref } 20 | className={ classNames( VIP_TOOLBAR, className ) } 21 | as="header" 22 | role="banner" 23 | sx={ { 24 | display: 'flex', 25 | height: 64, 26 | backgroundColor: 'toolbar.background', 27 | flexDirection: 'row', 28 | alignItems: 'center', 29 | px: [ 4, 4, 5 ], 30 | } } 31 | > 32 | { children } 33 | </Flex> 34 | ) 35 | ); 36 | 37 | // Variant: Primary (TODO) 38 | export const ToolbarPrimary = forwardRef< HTMLElement, ToolbarProps >( 39 | ( props: ToolbarProps, ref: Ref< HTMLElement > ) => <Toolbar { ...props } ref={ ref } /> 40 | ); 41 | -------------------------------------------------------------------------------- /src/system/Toolbar/ToolbarUtilNav.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | import React, { Ref, forwardRef } from 'react'; 3 | 4 | import { Flex } from '..'; 5 | 6 | export type ToolbarUtilNavProps = { 7 | children: React.ReactNode; 8 | label?: string; 9 | }; 10 | 11 | export const ToolbarUtilNav = forwardRef< HTMLElement, ToolbarUtilNavProps >( 12 | ( { label = 'Utility', children }: ToolbarUtilNavProps, ref: Ref< HTMLElement > ) => ( 13 | <nav 14 | aria-label={ label } 15 | ref={ ref } 16 | sx={ { 17 | marginLeft: 'auto', 18 | alignItems: 'center', 19 | flexDirection: 'row', 20 | display: 'flex', 21 | gap: 4, 22 | } } 23 | > 24 | { children } 25 | </nav> 26 | ) 27 | ); 28 | 29 | export const ToolbarIconHolder = ( { children } ) => ( 30 | <Flex 31 | sx={ { 32 | width: 38, 33 | height: 38, 34 | alignItems: 'center', 35 | justifyContent: 'center', 36 | color: 'icon.inverse', 37 | '&:hover': { color: 'icon.primary' }, 38 | } } 39 | > 40 | { children } 41 | </Flex> 42 | ); 43 | 44 | export const ToolbarUtilNavSeparator = () => ( 45 | <span 46 | aria-hidden="true" 47 | sx={ { 48 | display: [ 'block', 'none', 'none', 'block', 'block' ], 49 | '&:after': { 50 | display: 'block', 51 | backgroundColor: 'borders.inverse', 52 | width: 1, 53 | height: 30, 54 | overflow: 'hidden', 55 | content: '""', 56 | }, 57 | position: 'relative', 58 | } } 59 | ></span> 60 | ); 61 | -------------------------------------------------------------------------------- /src/system/Toolbar/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { Logo } from './Logo'; 5 | import { ToolbarPrimary as Primary } from './Toolbar'; 6 | import { 7 | ToolbarUtilNav as UtilNav, 8 | ToolbarUtilNavSeparator as Separator, 9 | ToolbarIconHolder as IconHolder, 10 | } from './ToolbarUtilNav'; 11 | 12 | export const VIP_TOOLBAR = 'vip-toolbar-component'; 13 | 14 | export const Toolbar = { 15 | Logo, 16 | Primary, 17 | Separator, 18 | UtilNav, 19 | IconHolder, 20 | }; 21 | -------------------------------------------------------------------------------- /src/system/Tooltip/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { PropsWithChildren, ReactElement, cloneElement } from 'react'; 5 | 6 | import css from './Tooltip.css'; 7 | 8 | /** 9 | * Internal dependencies 10 | */ 11 | 12 | export interface TooltipProps { 13 | title?: string; 14 | trigger?: ReactElement; 15 | position?: 'top' | 'bottom' | 'left' | 'right'; 16 | } 17 | 18 | const Tooltip = ( { 19 | title, 20 | trigger, 21 | children, 22 | position = 'top', 23 | }: PropsWithChildren< TooltipProps > ) => { 24 | const triggerCloned = trigger 25 | ? cloneElement( trigger, { 26 | 'data-vip-tooltip': title, 27 | 'aria-label': title, 28 | 'data-vip-tooltip-position': position, 29 | } ) 30 | : null; 31 | 32 | return ( 33 | <> 34 | { triggerCloned } 35 | { children } 36 | </> 37 | ); 38 | }; 39 | 40 | export { Tooltip, css }; 41 | -------------------------------------------------------------------------------- /src/system/Tooltip/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { Tooltip } from './Tooltip'; 5 | 6 | export { Tooltip }; 7 | -------------------------------------------------------------------------------- /src/system/Wizard/Wizard.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | /** 4 | * External dependencies 5 | */ 6 | import classNames from 'classnames'; 7 | import React, { useLayoutEffect, useState } from 'react'; 8 | 9 | /** 10 | * Internal dependencies 11 | */ 12 | import { WizardStepProps } from './WizardStep'; 13 | import { Box, WizardStep } from '..'; 14 | 15 | export interface WizardProps { 16 | steps: WizardStepProps[]; 17 | activeStep?: number; 18 | completed?: number[]; 19 | skipped?: number[]; 20 | className?: string; 21 | titleAutofocus?: boolean; 22 | } 23 | export const Wizard = React.forwardRef< HTMLDivElement, WizardProps >( 24 | ( 25 | { steps, activeStep, completed = [], skipped = [], className = null, titleAutofocus = false }, 26 | forwardRef 27 | ) => { 28 | const [ didMount, setDidMount ] = useState( false ); 29 | const [ initialStep ] = useState( activeStep ); 30 | // didMount helps us to track the initial render, so we can focus the title only subsequent renders 31 | // to avoid stealing the focus from the page we're in. 32 | useLayoutEffect( () => { 33 | if ( ! didMount && activeStep !== initialStep ) { 34 | setDidMount( true ); 35 | } 36 | }, [ initialStep, activeStep, didMount, setDidMount ] ); 37 | return ( 38 | <Box className={ classNames( 'vip-wizard-component', className ) } ref={ forwardRef }> 39 | { steps.map( ( { title, subTitle, children, titleVariant, summary, onChange }, index ) => ( 40 | <WizardStep 41 | active={ index === activeStep } 42 | complete={ completed.includes( index ) } 43 | skipped={ skipped.includes( index ) } 44 | key={ index } 45 | order={ index + 1 } 46 | totalSteps={ steps.length } 47 | subTitle={ subTitle } 48 | title={ title } 49 | titleVariant={ titleVariant } 50 | summary={ summary } 51 | onChange={ onChange } 52 | shouldFocusTitle={ titleAutofocus && didMount } 53 | > 54 | { children } 55 | </WizardStep> 56 | ) ) } 57 | </Box> 58 | ); 59 | } 60 | ); 61 | 62 | Wizard.displayName = 'Wizard'; 63 | -------------------------------------------------------------------------------- /src/system/Wizard/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { Wizard } from './Wizard'; 5 | import { WizardStep } from './WizardStep'; 6 | 7 | export { Wizard, WizardStep }; 8 | -------------------------------------------------------------------------------- /src/system/assets/a8cLogo.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { translate } from 'i18n-calypso'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import { LinkExternal } from '../LinkExternal/LinkExternal'; 10 | import { Text } from '../Text'; 11 | 12 | export const a8cLogo = ( 13 | <Text as="span" sx={ { display: 'inline-flex', alignItems: 'center', gap: 2, mb: 0 } }> 14 | { translate( 'An' ) } 15 | <LinkExternal 16 | href="https://automattic.com" 17 | showExternalIcon={ false } 18 | screenReaderText={ translate( 'Automattic' ) } 19 | > 20 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 15.4" style={ { minWidth: 100 } }> 21 | <path 22 | d="M72.5 15.4c-5.1 0-8.4-3.7-8.4-7.5v-.4c0-3.9 3.3-7.5 8.4-7.5 5.1 0 8.4 3.6 8.4 7.5V8c0 3.8-3.3 7.4-8.4 7.4zm5.7-7.9c0-2.8-2-5.3-5.7-5.3s-5.7 2.5-5.7 5.3v.3c0 2.8 2 5.3 5.7 5.3s5.7-2.5 5.7-5.3v-.3z" 23 | fill="#3298CB" 24 | /> 25 | <path d="M15 14.9l-1.9-3.6H4.7l-1.8 3.6H0L7.8.5H10l7.9 14.4H15zM8.8 3.3l-3.1 6h6.4l-3.3-6zm21.4 12.1c-5.2 0-7.6-2.8-7.6-6.5V.5h2.7V9c0 2.7 1.7 4.2 5.1 4.2 3.4 0 4.8-1.6 4.8-4.2V.5h2.7v8.4c0 3.6-2.3 6.5-7.7 6.5zM52.9 2.8v12.1h-2.7V2.8h-6.3V.5h15.3v2.2h-6.3zM105 14.9V3.5l-.7 1.3-6 10.1H97L91 4.8l-.7-1.3v11.4h-2.6V.5h3.7l5.7 9.9.7 1.2.7-1.2 5.6-9.9h3.7v14.4H105zm23.1 0l-1.9-3.6h-8.4l-1.8 3.6h-3L120.8.5h2.2l7.9 14.4h-2.8zm-6.2-11.6l-3.1 6h6.4l-3.3-6zm19.9-.5v12.1h-2.7V2.8h-6.3V.5h15.3v2.2h-6.3zm19.8 0v12.1h-2.7V2.8h-6.3V.5h15.3v2.2h-6.3zm12.9 12.1v-13c1.1 0 1.5-.6 1.5-1.4h1.1v14.4h-2.6zm23.8-10.3c-1.3-1.2-3.2-2.3-5.8-2.3-3.8 0-6 2.6-6 5.4V8c0 2.7 2.2 5.3 6.2 5.3 2.4 0 4.4-1.1 5.6-2.3l1.6 1.7c-1.6 1.6-4.3 2.9-7.4 2.9-5.4 0-8.7-3.5-8.7-7.4v-.6c0-3.9 3.6-7.6 8.9-7.6 3 0 5.8 1.3 7.3 2.9l-1.7 1.7zM74.3 5c.5.3.6 1 .3 1.5l-2.5 3.8c-.3.5-1 .6-1.5.3s-.6-1-.3-1.5l2.5-3.8c.4-.5 1-.6 1.5-.3z" /> 26 | </svg> 27 | </LinkExternal> 28 | { translate( 'Creation' ) } 29 | </Text> 30 | ); 31 | -------------------------------------------------------------------------------- /src/system/theme/breakpoints.ts: -------------------------------------------------------------------------------- 1 | type Breakpoints = { 2 | [ key: string ]: number; 3 | }; 4 | 5 | export const generateBreakpoints = ( breakpoints: Breakpoints ) => { 6 | const values = Object.values( breakpoints ); 7 | 8 | return values.map( ( bp, index ) => { 9 | if ( index === 0 ) { 10 | return `0px`; 11 | } 12 | 13 | return `${ bp }px`; 14 | } ); 15 | }; 16 | -------------------------------------------------------------------------------- /src/system/theme/colors.js: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource theme-ui */ 2 | 3 | /** 4 | * External dependencies 5 | */ 6 | 7 | export default theme => ( { 8 | ...theme.color, 9 | grey: theme.color.gray, 10 | } ); 11 | -------------------------------------------------------------------------------- /src/system/theme/getPropValue.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | 5 | // Valet Theme Productive Theme 6 | // https://www.figma.com/file/sILtW5Cs2tAnPWrSOEVyER/Productive-Color?node-id=1%3A17&t=4kHdpoprxntk5Ilw-0 7 | 8 | export default theme => { 9 | const getPropValue = ( prop, variant = 'default' ) => { 10 | if ( ! theme[ prop ] ) { 11 | return '#000000'; 12 | } 13 | 14 | return theme[ prop ][ variant ].value; 15 | }; 16 | 17 | const resolvePath = ( object, path, defaultValue ) => { 18 | return path.split( '.' ).reduce( ( acc, property ) => { 19 | return acc ? acc[ property ] : defaultValue; 20 | }, object ); 21 | }; 22 | 23 | const getVariants = color => { 24 | const property = resolvePath( theme, color, {} ); 25 | 26 | return Object.keys( property ).reduce( 27 | ( variants, variant ) => ( { ...variants, [ variant ]: property[ variant ].value } ), 28 | {} 29 | ); 30 | }; 31 | 32 | const traverse = root => { 33 | if ( root.hasOwnProperty( 'value' ) && root.hasOwnProperty( 'type' ) ) { 34 | return root.value; 35 | } 36 | 37 | return Object.entries( root ).reduce( 38 | ( acc, [ key, value ] ) => ( { 39 | ...acc, 40 | [ key ]: traverse( value ), 41 | } ), 42 | {} 43 | ); 44 | }; 45 | 46 | // We get the following format: '1', '2', '3', 'caps'. 47 | // We need to build h1: {}, h2: {}, h3: {}, caps: {}. 48 | const getHeadingStyles = () => { 49 | const variantValues = getVariants( 'heading' ); 50 | 51 | const headingStyles = {}; 52 | const baseProps = { 53 | fontWeight: 'heading', 54 | color: 'heading', 55 | }; 56 | 57 | Object.keys( variantValues ).forEach( variant => { 58 | if ( variant === 'caps' ) { 59 | headingStyles.caps = { 60 | ...variantValues[ variant ], 61 | ...baseProps, 62 | }; 63 | } 64 | 65 | if ( parseInt( variant, 10 ) > 0 ) { 66 | headingStyles[ `h${ variant }` ] = { 67 | ...variantValues[ variant ], 68 | ...baseProps, 69 | fontFamily: variant.toString() === '1' ? 'serif' : 'body', 70 | }; 71 | } 72 | } ); 73 | 74 | return headingStyles; 75 | }; 76 | 77 | return { 78 | ValetTheme: traverse( theme ), 79 | getPropValue, 80 | getVariants, 81 | traverse, 82 | resolvePath, 83 | getHeadingStyles, 84 | }; 85 | }; 86 | -------------------------------------------------------------------------------- /src/system/utils/random.js: -------------------------------------------------------------------------------- 1 | export function generateId() { 2 | return Math.random().toString( 36 ).substring( 2, 15 ); 3 | } 4 | -------------------------------------------------------------------------------- /src/system/utils/stories/CustomLink.tsx: -------------------------------------------------------------------------------- 1 | import React, { Ref, forwardRef } from 'react'; 2 | 3 | import { Link } from '../../Link/Link'; 4 | 5 | export const CustomLink = forwardRef< HTMLAnchorElement >( 6 | // eslint-disable-next-line jsx-a11y/anchor-has-content 7 | ( props, ref: Ref< HTMLAnchorElement > ) => <a { ...props } ref={ ref } /> 8 | ); 9 | 10 | export const CustomLinkComponentized = forwardRef< HTMLAnchorElement >( 11 | ( props, ref: Ref< HTMLAnchorElement > ) => <Link { ...props } ref={ ref } /> 12 | ); 13 | -------------------------------------------------------------------------------- /test/fileMock.ts: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /test/setupAfterEnv.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import 'jest-axe/extend-expect'; 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /tokens/utilities/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/vip-design-system/d8c2af0598fade156a8c5ae2b6e96772ee81f078/tokens/utilities/.DS_Store -------------------------------------------------------------------------------- /tokens/utilities/colors/color_3d_plot.js: -------------------------------------------------------------------------------- 1 | const plotly = require('plotly.js-dist-min'); 2 | 3 | function getColor(x) { 4 | const labValues = channels.map((channel) => interpolators[channel](x)); 5 | return labValues; 6 | } 7 | 8 | const xValues = Array.from({ length: 100 }, (_, i) => i); 9 | const lValues = []; 10 | const aValues = []; 11 | const bValues = []; 12 | for (let x of xValues) { 13 | const labColor = getColor(x); 14 | lValues.push(labColor[0]); 15 | aValues.push(labColor[1]); 16 | bValues.push(labColor[2]); 17 | } 18 | const trace = { 19 | x: lValues, 20 | y: aValues, 21 | z: bValues, 22 | mode: 'markers', 23 | type: 'scatter3d', 24 | marker: { 25 | size: 3, 26 | color: xValues, 27 | colorscale: 'Viridis', 28 | opacity: 0.8, 29 | }, 30 | }; 31 | 32 | const layout = { 33 | scene: { 34 | xaxis: { title: 'L' }, 35 | yaxis: { title: 'a' }, 36 | zaxis: { title: 'b' }, 37 | }, 38 | }; 39 | 40 | const data = [trace]; 41 | 42 | plotly.newPlot('plot', data, layout); 43 | -------------------------------------------------------------------------------- /tokens/utilities/colors/color_graph.js: -------------------------------------------------------------------------------- 1 | const plot = require('nodeplotlib'); 2 | const getColor = require('./index'); 3 | 4 | const channels = ['l', 'a', 'b']; 5 | const interpolators = {}; 6 | 7 | const xValues = Array.from({ length: 100 }, (_, i) => i); 8 | const lValues = []; 9 | const aValues = []; 10 | const bValues = []; 11 | 12 | for (let x of xValues) { 13 | const labColor = getColor(x); 14 | lValues.push(labColor[0]); 15 | aValues.push(labColor[1]); 16 | bValues.push(labColor[2]); 17 | } 18 | 19 | const data = [ 20 | { x: xValues, y: lValues, name: 'L' }, 21 | { x: xValues, y: aValues, name: 'a' }, 22 | { x: xValues, y: bValues, name: 'b' }, 23 | ]; 24 | 25 | const layout = { 26 | title: 'Lab Color Space', 27 | xaxis: { title: 'x' }, 28 | yaxis: { title: 'Lab Value' }, 29 | }; 30 | 31 | plot.plot(data, layout); 32 | -------------------------------------------------------------------------------- /tokens/utilities/colors/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "colors", 3 | "version": "1.0.0", 4 | "description": "color calculation utility", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "test" 8 | }, 9 | "author": "David Bowman", 10 | "license": "ISC", 11 | "dependencies": { 12 | "color": "^4.2.3", 13 | "colorjs.io": "^0.4.3", 14 | "csv-writer": "^1.6.0", 15 | "natural-spline-interpolator": "^1.0.2", 16 | "nodeplotlib": "^1.1.2", 17 | "plotly.js": "^2.25.2", 18 | "plotly.js-dist-min": "^2.23.0", 19 | "regression": "^2.0.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tokens/valet-core/$metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "tokenSetOrder": [ 3 | "valet-core", 4 | "wpvip-product-core", 5 | "wpvip-product-dark" 6 | ] 7 | } -------------------------------------------------------------------------------- /tsconfig.definition.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | // Produce .d.ts declaration files. Must be enabled via command-line flags 5 | // since noEmit=true in this file. See `npm run build:types` script. 6 | "declaration": true, 7 | 8 | // Output directory for declaration files. 9 | "outDir": "build/system" 10 | }, 11 | "include": [ "src" ] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node18/tsconfig.json", 3 | "compilerOptions": { 4 | // Override lib since this code runs in a browser. 5 | "lib": [ "es6", "dom" ], 6 | // Process & infer types from .js files. 7 | "allowJs": true, 8 | // Don't emit; allow Babel to transform files. 9 | "noEmit": true, 10 | // Disallow features that require cross-file information for emit. 11 | "isolatedModules": true, 12 | // Allow to import *.json files 13 | "resolveJsonModule": true, 14 | "allowSyntheticDefaultImports": true, 15 | "baseUrl": ".", 16 | "jsx": "react-jsx", 17 | "jsxImportSource": "theme-ui", 18 | "noImplicitAny": false 19 | }, 20 | "include": [ ".storybook", "src", "test" ], 21 | "exclude": [ "node_modules" ] 22 | } 23 | --------------------------------------------------------------------------------