├── .babelrc ├── .cspell.json ├── .editorconfig ├── .env ├── .eslintignore ├── .eslintrc.json ├── .github ├── CONTRIBUTING.md ├── FUNDING.yml ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .storybook ├── main.js └── preview.js ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── README.md ├── config ├── tsconfig.build.json └── tsconfig.module.build.json ├── cypress.config.ts ├── cypress.d.ts ├── cypress ├── fixtures │ └── example.json ├── plugins │ └── index.js ├── support │ ├── commands.ts │ ├── component-index.html │ └── component.tsx └── videos │ ├── Badge │ └── Badge.cy.tsx.mp4 │ ├── Button │ └── Button.cy.tsx.mp4 │ ├── Checkbox │ └── Checkbox.cy.tsx.mp4 │ ├── Container │ └── Container.cy.tsx.mp4 │ ├── Grid │ └── Grid.cy.tsx.mp4 │ ├── Input │ └── Input.cy.tsx.mp4 │ ├── Modal │ └── Modal.cy.tsx.mp4 │ ├── Popover │ └── Popover.cy.tsx.mp4 │ ├── Radio │ └── Radio.cy.tsx.mp4 │ ├── Switch │ └── Switch.cy.tsx.mp4 │ └── Text │ └── Text.cy.tsx.mp4 ├── package-lock.json ├── package.json ├── public └── cover.png ├── setupTests.ts ├── src ├── index.ts └── lib │ ├── Badge │ ├── Badge.cy.tsx │ ├── Badge.stories.tsx │ ├── Badge.styles.tsx │ ├── Badge.test.tsx │ ├── Badge.tsx │ ├── __snapshots__ │ │ └── Badge.test.tsx.snap │ └── index.ts │ ├── Box │ ├── Box.stories.tsx │ ├── Box.styles.tsx │ ├── Box.test.tsx │ ├── Box.tsx │ ├── __snapshots__ │ │ └── Box.test.tsx.snap │ └── index.ts │ ├── Button │ ├── Button.cy.tsx │ ├── Button.stories.tsx │ ├── Button.styles.ts │ ├── Button.test.tsx │ ├── Button.tsx │ ├── ButtonIcon.tsx │ ├── __snapshots__ │ │ └── Button.test.tsx.snap │ └── index.ts │ ├── Checkbox │ ├── Checkbox.cy.tsx │ ├── Checkbox.stories.tsx │ ├── Checkbox.styles.tsx │ ├── Checkbox.test.tsx │ ├── Checkbox.tsx │ ├── CheckboxGroup │ │ ├── CheckboxGroup.stories.tsx │ │ ├── CheckboxGroup.styles.tsx │ │ ├── CheckboxGroup.test.tsx │ │ ├── CheckboxGroup.tsx │ │ ├── __snapshots__ │ │ │ └── CheckboxGroup.test.tsx.snap │ │ └── index.ts │ ├── __snapshots__ │ │ └── Checkbox.test.tsx.snap │ └── index.ts │ ├── Container │ ├── Container.cy.tsx │ ├── Container.stories.tsx │ ├── Container.styles.ts │ ├── Container.test.tsx │ ├── Container.tsx │ ├── __snapshots__ │ │ └── Container.test.tsx.snap │ └── index.ts │ ├── Grid │ ├── Grid.cy.tsx │ ├── Grid.stories.tsx │ ├── Grid.styles.tsx │ ├── Grid.test.tsx │ ├── Grid.tsx │ ├── GridContainer.tsx │ ├── GridRuler.tsx │ ├── __snapshots__ │ │ └── Grid.test.tsx.snap │ └── index.ts │ ├── Input │ ├── Input.cy.tsx │ ├── Input.stories.tsx │ ├── Input.styles.ts │ ├── Input.test.tsx │ ├── Input.tsx │ ├── __snapshots__ │ │ └── Input.test.tsx.snap │ └── index.ts │ ├── Modal │ ├── Modal.cy.tsx │ ├── Modal.stories.tsx │ ├── Modal.styles.tsx │ ├── Modal.test.tsx │ ├── Modal.tsx │ ├── ModalBody.tsx │ ├── ModalFooter.tsx │ ├── ModalHeader.tsx │ ├── __snapshots__ │ │ └── Modal.test.tsx.snap │ └── index.ts │ ├── Popover │ ├── Popover.cy.tsx │ ├── Popover.stories.tsx │ ├── Popover.styles.tsx │ ├── Popover.test.tsx │ ├── Popover.tsx │ ├── PopoverContent.tsx │ ├── PopoverTrigger.tsx │ ├── __snapshots__ │ │ └── Popover.test.tsx.snap │ └── index.ts │ ├── Radio │ ├── Radio.cy.tsx │ ├── Radio.stories.tsx │ ├── Radio.styles.tsx │ ├── Radio.test.tsx │ ├── Radio.tsx │ ├── RadioGroup │ │ ├── RadioGroup.stories.tsx │ │ ├── RadioGroup.styles.tsx │ │ ├── RadioGroup.test.tsx │ │ ├── RadioGroup.tsx │ │ ├── __snapshots__ │ │ │ └── RadioGroup.test.tsx.snap │ │ └── index.ts │ ├── __snapshots__ │ │ └── Radio.test.tsx.snap │ └── index.ts │ ├── Switch │ ├── Switch.cy.tsx │ ├── Switch.stories.tsx │ ├── Switch.styles.tsx │ ├── Switch.test.tsx │ ├── Switch.tsx │ ├── __snapshots__ │ │ └── Switch.test.tsx.snap │ └── index.ts │ ├── Text │ ├── Text.cy.tsx │ ├── Text.stories.tsx │ ├── Text.styles.tsx │ ├── Text.test.tsx │ ├── Text.tsx │ ├── __snapshots__ │ │ └── Text.test.tsx.snap │ └── index.ts │ ├── Theme │ ├── DecaUIProvider.test.tsx │ ├── DecaUIProvider.tsx │ ├── __snapshots__ │ │ └── DecaUIProvider.test.tsx.snap │ ├── index.ts │ └── stitches.config.ts │ └── Utils │ ├── color.ts │ ├── env.ts │ ├── hooks.ts │ ├── index.ts │ ├── random.ts │ ├── refs.ts │ ├── test.ts │ ├── types.ts │ └── utils.test.tsx ├── tsconfig.json ├── tsconfig.module.json └── vite.config.ts /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-flow"] 3 | } 4 | -------------------------------------------------------------------------------- /.cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2", 3 | "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/master/cspell.schema.json", 4 | "language": "en", 5 | "words": [ 6 | "bitjson", 7 | "bitauth", 8 | "cimg", 9 | "circleci", 10 | "codecov", 11 | "commitlint", 12 | "dependabot", 13 | "editorconfig", 14 | "esnext", 15 | "execa", 16 | "exponentiate", 17 | "globby", 18 | "libauth", 19 | "mkdir", 20 | "prettierignore", 21 | "sandboxed", 22 | "transpiled", 23 | "typedoc", 24 | "untracked" 25 | ], 26 | "flagWords": [], 27 | "ignorePaths": [ 28 | "package.json", 29 | "package-lock.json", 30 | "yarn.lock", 31 | "tsconfig.json", 32 | "node_modules/**" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | max_line_length = 80 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | max_line_length = 0 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | NODE_ENV="development" 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | package.json 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { "project": ["./tsconfig.json", "./setupTests.ts"], "warnOnUnsupportedTypeScriptVersion": false 5 | }, 6 | "env": { "es6": true }, 7 | "ignorePatterns": ["node_modules", "build", "coverage", "setupTests.ts"], 8 | "plugins": ["import", "eslint-comments", "jest-dom", "testing-library"], 9 | "extends": [ 10 | "eslint:recommended", 11 | "plugin:cypress/recommended", 12 | "plugin:eslint-comments/recommended", 13 | "plugin:@typescript-eslint/recommended", 14 | "plugin:import/typescript", 15 | "plugin:storybook/recommended", 16 | "prettier", 17 | "prettier/@typescript-eslint" 18 | ], 19 | "globals": { "BigInt": true, "console": true, "WebAssembly": true }, 20 | "rules": { 21 | "@typescript-eslint/no-explicit-any": "off", 22 | "@typescript-eslint/explicit-module-boundary-types": "off", 23 | "no-unused-vars": "off", 24 | "@typescript-eslint/no-unused-vars": ["error", { "varsIgnorePattern": "^_", "argsIgnorePattern": "^_" }], 25 | "eslint-comments/disable-enable-pair": [ 26 | "error", 27 | { "allowWholeFile": true } 28 | ], 29 | "eslint-comments/no-unused-disable": "error", 30 | "import/order":"off", 31 | "sort-imports":"off" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Example Contributing Guidelines 2 | 3 | This is an example of GitHub's contributing guidelines file. Check out GitHub's [CONTRIBUTING.md help center article](https://help.github.com/articles/setting-guidelines-for-repository-contributors/) for more information. 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: deca-ui 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - **I'm submitting a ...** 2 | [ ] bug report 3 | [ ] feature request 4 | [ ] question about the decisions made in the repository 5 | [ ] question about how to use this project 6 | 7 | - **Summary** 8 | 9 | - **Other information** (e.g. detailed explanation, stack traces, related issues, suggestions how to fix, links for us to have context, eg. StackOverflow, personal fork, etc.) 10 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - **What kind of change does this PR introduce?** (Bug fix, feature, docs update, ...) 2 | 3 | - **What is the current behavior?** (You can also link to an open issue here) 4 | 5 | - **What is the new behavior (if this is a feature change)?** 6 | 7 | - **Other information**: 8 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI/CD 2 | on: [push] 3 | jobs: 4 | quality: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | matrix: 8 | node-version: [14.x, 16.x] 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v3 12 | - name: Use Node.js ${{ matrix.node-version }} 13 | uses: actions/setup-node@v3 14 | with: 15 | node-version: ${{ matrix.node-version }} 16 | cache: 'npm' 17 | 18 | - name: Install dependencies 19 | run: npm ci 20 | - name: Test 21 | run: npm test 22 | - name: Codecov 23 | uses: codecov/codecov-action@v3.1.0 24 | publish: 25 | runs-on: ubuntu-latest 26 | if: ${{ github.ref == 'refs/heads/main' }} 27 | needs: [quality] 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v3 31 | with: 32 | fetch-depth: 0 33 | - name: Setup Node.js 34 | uses: actions/setup-node@v3 35 | with: 36 | node-version: 16.x 37 | cache: 'npm' 38 | - name: Install Packages 39 | run: npm ci 40 | - name: Build 41 | run: npm run build 42 | - name: Generate Release Body 43 | run: npm run extract-changelog > RELEASE_BODY.md 44 | - name: Publish to NPM 45 | uses: JS-DevTools/npm-publish@v1 46 | with: 47 | access: "public" 48 | token: ${{ secrets.NPM_TOKEN }} 49 | - name: 'Get Latest Tag' 50 | id: latestTag 51 | uses: "WyriHaximus/github-action-get-previous-tag@v1" 52 | - name: 'Get Latest Release' 53 | id: latestRelease 54 | uses: pozetroninc/github-action-get-latest-release@master 55 | with: 56 | repository: ${{ github.repository }} 57 | - name: Create GitHub Release 58 | if: ${{ steps.latestTag.outputs.tag != steps.latestRelease.outputs.release }} 59 | uses: ncipollo/release-action@v1 60 | with: 61 | bodyFile: "RELEASE_BODY.md" 62 | token: ${{ secrets.GITHUB_TOKEN }} 63 | tag: ${{ steps.latestTag.outputs.tag }} 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/* 2 | .nyc_output 3 | build 4 | node_modules 5 | test 6 | src/**.js 7 | coverage 8 | *.log 9 | yarn.lock 10 | storybook-static 11 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # package.json is formatted by package managers, so we ignore it here 2 | package.json -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); 2 | 3 | module.exports = { 4 | core: { 5 | builder: '@storybook/builder-webpack5', 6 | }, 7 | stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], 8 | addons: [ 9 | '@storybook/addon-links', 10 | '@storybook/addon-essentials', 11 | '@storybook/addon-interactions', 12 | '@storybook/addon-docs', 13 | ], 14 | typescript: { 15 | check: false, 16 | checkOptions: {}, 17 | reactDocgen: 'react-docgen-typescript', 18 | }, 19 | framework: '@storybook/react', 20 | webpackFinal: async (config, { configType }) => { 21 | config.resolve.plugins = [new TsconfigPathsPlugin()]; 22 | return config; 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import { DecaUIProvider } from '@lib/Theme'; 2 | const { addDecorator } = require('@storybook/react'); 3 | const { withPropsTable } = require('storybook-addon-react-docgen'); 4 | 5 | export const parameters = { 6 | backgrounds: { 7 | values: [ 8 | { 9 | name: 'dark', 10 | value: '#000000', 11 | }, 12 | ], 13 | }, 14 | actions: { argTypesRegex: '^on[A-Z].*' }, 15 | controls: { 16 | matchers: { 17 | color: /(background|color)$/i, 18 | date: /Date$/, 19 | }, 20 | }, 21 | }; 22 | 23 | export const decorators = [ 24 | (Story) => ( 25 | // adds provider for stories that do not contain it 26 | // (only to see changes made to component by css reset) 27 | 28 | 29 | 30 | ), 31 | ]; 32 | 33 | addDecorator(withPropsTable); 34 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "eamodio.gitlens", 6 | "streetsidesoftware.code-spell-checker", 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | // To debug, make sure a *.spec.ts file is active in the editor, then run a configuration 5 | { 6 | "type": "node", 7 | "request": "launch", 8 | "name": "Debug Active Spec", 9 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/ava", 10 | "runtimeArgs": ["debug", "--break", "--serial", "${file}"], 11 | "port": 9229, 12 | "outputCapture": "std", 13 | "skipFiles": ["/**/*.js"], 14 | "preLaunchTask": "npm: build" 15 | // "smartStep": true 16 | }, 17 | { 18 | // Use this one if you're already running `yarn watch` 19 | "type": "node", 20 | "request": "launch", 21 | "name": "Debug Active Spec (no build)", 22 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/ava", 23 | "runtimeArgs": ["debug", "--break", "--serial", "${file}"], 24 | "port": 9229, 25 | "outputCapture": "std", 26 | "skipFiles": ["/**/*.js"] 27 | // "smartStep": true 28 | }] 29 | } 30 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.userWords": [], // only use words from .cspell.json 3 | "cSpell.enabled": true, 4 | "editor.formatOnSave": true, 5 | "typescript.tsdk": "node_modules/typescript/lib", 6 | "typescript.enablePromptUseWorkspaceTsdk": true 7 | } 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # DecaUI Contributing Guide 2 | 3 | Hello!, I am very excited that you are interested in contributing with DecaUI. However, before submitting your contribution, be sure to take a moment and read the following guidelines. 4 | 5 | - [Code of Conduct](https://github.com/deca-org/deca-ui/blob/main/CODE_OF_CONDUCT.md) 6 | - [Extraction request guidelines](#pull-request-guidelines) 7 | - [Development Setup](#development-setup) 8 | - [Visual Changes](#visual-changes) 9 | - [Documentation](#documentation) 10 | 11 | ## Pull Request Guidelines 12 | 13 | - The `main` branch is basically a snapshot of the latest stable version. All development must be done in dedicated branches. 14 | - Make sure that Github Actions are green 15 | - It is good to have multiple small commits while working on the PR. We'll let GitHub squash it automatically before the merge. 16 | - If you add a new feature: 17 | - Add the test case that accompanies it. 18 | - Provide a compelling reason to add this feature. Ideally, I would first open a suggestion topic and green it before working on it. 19 | - If you correct an error: 20 | - If you are solving a special problem, add (fix #xxxx [, # xxx]) (# xxxx is the problem identification) in your PR title for a better launch record, for example update entities encoding / decoding (fix # 3899). 21 | - Provide a detailed description of the error in the PR. Favorite live demo. 22 | - Add the appropriate test coverage, if applicable. 23 | 24 | ## Development Setup 25 | 26 | After cloning the repository, execute the following commands in the root folder: 27 | 28 | 1. Install dependencies 29 | 30 | ```bash 31 | npm install 32 | ``` 33 | 34 | 2. Edit component / docs code 35 | 36 | 3. Previewing components using storybook 37 | 38 | You can use this command to start up storybook: 39 | 40 | ```bash 41 | npm run storybook 42 | ``` 43 | 44 | 4. Test your code 45 | 46 | ```bash 47 | npm run test 48 | ``` 49 | 50 | 5. Create a branch for your feature or fix: 51 | 52 | ```bash 53 | # Move into a new branch for your feature 54 | git checkout -b feat/thing 55 | ``` 56 | 57 | ```bash 58 | # Move into a new branch for your fix 59 | git checkout -b fix/something 60 | ``` 61 | 62 | 6. If your code passes all the tests, then push your feature/fix branch: 63 | 64 | All commits that fix bugs or add features need a test. 65 | 66 | 7. Be sure the package builds. 67 | 68 | ``` 69 | # Build current code 70 | npm run build 71 | ``` 72 | 73 | > Note: ensure your version of Node is 14 or higher to run scripts 74 | 75 | 8. Send your pull request: 76 | 77 | - Send your pull request to the `main` branch 78 | - Your pull request will be reviewed by the maintainers and the maintainers will decide if it is accepted or not 79 | - Once the pull request is accepted, the maintainers will merge it to the `main` branch 80 | 81 | ## Visual Changes 82 | 83 | When making a visual change, please provide screenshots 84 | and/or screencasts of the proposed change. This will help us to understand the 85 | desired change easier. 86 | 87 | ## Documentation 88 | 89 | If you want to make changes to existing pages on the documentation website (www.deca-ui.com), 90 | please create an issue that includes the URL of the page you want the change to occur on, and a brief 91 | description of what needs to be changed or documented 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | DecaUI logo 3 |

DecaUI

4 |

5 |

6 | 7 | 8 | 9 | 10 | 11 | 12 |

13 | Overview of Components 14 |

DecaUI provides a set of accessible and customizable React components that make it easy to quickly prototype and develop stunning websites.

15 | 16 | ## Getting Started 17 | 18 | ``` 19 | npm install @deca-ui/react 20 | ``` 21 | 22 | ## Using a component 23 | 24 | Here is a simple example of a basic app using DecaUI's `Button` component: 25 | 26 | ```jsx 27 | import { Button } from '@deca-ui/react'; 28 | 29 | function App() { 30 | return ; 31 | } 32 | ``` 33 | 34 | #### [Click here for the full documentation](https://www.deca-ui.com/docs/guide/installation) 35 | 36 | ## What's so different about DecaUI 37 | 38 | With DecaUI, developers can use the centralized theming system anywhere within their application with shorthand names for css properties. 39 | 40 | ### Custom CSS with other UI libraries 41 | 42 | ```jsx 43 | 51 | 52 | 53 | 60 | 61 | ``` 62 | 63 | ### Custom CSS with DecaUI 64 | 65 | ```jsx 66 | 67 | 68 | 69 | 70 | 71 | ``` 72 | 73 | ## Our focus is consistency 74 | 75 | The main problem with other UI libraries is that it's confusing to create consistent webpage layouts with them. DecaUI allows developers to utilize a root theme object which serves properties following the [System UI](https://github.com/system-ui/theme-specification) specification. 76 | 77 | ## Thank you React Status! 78 |
79 | React Status 80 |
81 | Thanks to React Status for showcasing this project on their newsletter (issue #323). 82 | 83 | 84 | -------------------------------------------------------------------------------- /config/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "exclude": [ 4 | "../src/**/*.cy.tsx", 5 | "../src/**/*.test.tsx", 6 | "../src/**/*.stories.tsx" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /config/tsconfig.module.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.module.json", 3 | "exclude": [ 4 | "../src/**/*.cy.tsx", 5 | "../src/**/*.test.tsx", 6 | "../src/**/*.stories.tsx" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress'; 2 | 3 | export default defineConfig({ 4 | projectId: 'udbcua', 5 | component: { 6 | devServer: { 7 | framework: 'react', 8 | bundler: 'vite', 9 | }, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /cypress.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { mount } from 'cypress/react'; 3 | import '@testing-library/cypress/add-commands'; 4 | 5 | export {}; 6 | 7 | declare global { 8 | namespace Cypress { 9 | interface Chainable { 10 | baseMount: typeof mount; 11 | mount: typeof mount; 12 | darkMount: typeof mount; 13 | before(value: string): any; 14 | after(value: string): any; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const vite = require('vite'); 3 | 4 | const cache = {}; 5 | 6 | module.exports = (on, config) => { 7 | on('file:preprocessor', async (file) => { 8 | const { filePath, outputPath, shouldWatch } = file; 9 | if (cache[filePath]) { 10 | return cache[filePath]; 11 | } 12 | 13 | const filename = path.basename(outputPath); 14 | const filenameWithoutExtension = path.basename( 15 | outputPath, 16 | path.extname(outputPath) 17 | ); 18 | 19 | const viteConfig = { 20 | build: { 21 | emptyOutDir: false, 22 | minify: false, 23 | outDir: path.dirname(outputPath), 24 | sourcemap: true, 25 | write: true, 26 | }, 27 | }; 28 | 29 | if (filename.endsWith('.html')) { 30 | viteConfig.build.rollupOptions = { 31 | input: { 32 | [filenameWithoutExtension]: filePath, 33 | }, 34 | }; 35 | } else { 36 | viteConfig.build.lib = { 37 | entry: filePath, 38 | fileName: () => filename, 39 | formats: ['es'], 40 | name: filenameWithoutExtension, 41 | }; 42 | } 43 | 44 | if (shouldWatch) { 45 | viteConfig.build.watch = true; 46 | } 47 | 48 | const watcher = await vite.build(viteConfig); 49 | 50 | if (shouldWatch) { 51 | watcher.on('event', (event) => { 52 | if (event.code === 'END') { 53 | file.emit('rerun'); 54 | } 55 | }); 56 | file.on('close', () => { 57 | delete cache[filePath]; 58 | watcher.close(); 59 | }); 60 | } 61 | 62 | cache[filePath] = outputPath; 63 | return outputPath; 64 | }); 65 | 66 | return config; 67 | }; 68 | -------------------------------------------------------------------------------- /cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import '@testing-library/cypress/add-commands'; 3 | -------------------------------------------------------------------------------- /cypress/support/component-index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Components App 8 | 9 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /cypress/support/component.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Box from '../../src/lib/Box'; 3 | import DecaUIProvider from '../../src/lib/Theme'; 4 | import './commands'; 5 | import { mount } from 'cypress/react18'; 6 | import _cyp from '../../cypress'; 7 | 8 | Cypress.Commands.add('mount', (component: React.ReactElement, options = {}) => { 9 | const wrapped = ( 10 | 11 | 20 | {component} 21 | 22 | 23 | ); 24 | return mount(wrapped, options); 25 | }); 26 | 27 | Cypress.Commands.add( 28 | 'darkMount', 29 | (component: React.ReactElement, options = {}) => { 30 | const wrapped = ( 31 | 32 | 41 | {component} 42 | 43 | 44 | ); 45 | return mount(wrapped, options); 46 | } 47 | ); 48 | 49 | Cypress.Commands.add('baseMount', mount); 50 | 51 | function unquote(str: string) { 52 | return str.replace(/(^")|("$)/g, ''); 53 | } 54 | 55 | Cypress.Commands.add( 56 | 'before', 57 | { 58 | prevSubject: 'element', 59 | }, 60 | (el, property) => { 61 | const win = el[0].ownerDocument.defaultView; 62 | const before = win.getComputedStyle(el[0], 'before'); return unquote(before.getPropertyValue(property)); 63 | } 64 | ); 65 | 66 | Cypress.Commands.add( 67 | 'after', 68 | { 69 | prevSubject: 'element', 70 | }, 71 | (el, property) => { 72 | const win = el[0].ownerDocument.defaultView; 73 | const before = win.getComputedStyle(el[0], 'after'); 74 | return unquote(before.getPropertyValue(property)); 75 | } 76 | ); 77 | -------------------------------------------------------------------------------- /cypress/videos/Badge/Badge.cy.tsx.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deca-org/deca-ui/7ab900dbc64219d9f2ee17db3b6646793aa8247c/cypress/videos/Badge/Badge.cy.tsx.mp4 -------------------------------------------------------------------------------- /cypress/videos/Button/Button.cy.tsx.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deca-org/deca-ui/7ab900dbc64219d9f2ee17db3b6646793aa8247c/cypress/videos/Button/Button.cy.tsx.mp4 -------------------------------------------------------------------------------- /cypress/videos/Checkbox/Checkbox.cy.tsx.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deca-org/deca-ui/7ab900dbc64219d9f2ee17db3b6646793aa8247c/cypress/videos/Checkbox/Checkbox.cy.tsx.mp4 -------------------------------------------------------------------------------- /cypress/videos/Container/Container.cy.tsx.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deca-org/deca-ui/7ab900dbc64219d9f2ee17db3b6646793aa8247c/cypress/videos/Container/Container.cy.tsx.mp4 -------------------------------------------------------------------------------- /cypress/videos/Grid/Grid.cy.tsx.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deca-org/deca-ui/7ab900dbc64219d9f2ee17db3b6646793aa8247c/cypress/videos/Grid/Grid.cy.tsx.mp4 -------------------------------------------------------------------------------- /cypress/videos/Input/Input.cy.tsx.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deca-org/deca-ui/7ab900dbc64219d9f2ee17db3b6646793aa8247c/cypress/videos/Input/Input.cy.tsx.mp4 -------------------------------------------------------------------------------- /cypress/videos/Modal/Modal.cy.tsx.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deca-org/deca-ui/7ab900dbc64219d9f2ee17db3b6646793aa8247c/cypress/videos/Modal/Modal.cy.tsx.mp4 -------------------------------------------------------------------------------- /cypress/videos/Popover/Popover.cy.tsx.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deca-org/deca-ui/7ab900dbc64219d9f2ee17db3b6646793aa8247c/cypress/videos/Popover/Popover.cy.tsx.mp4 -------------------------------------------------------------------------------- /cypress/videos/Radio/Radio.cy.tsx.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deca-org/deca-ui/7ab900dbc64219d9f2ee17db3b6646793aa8247c/cypress/videos/Radio/Radio.cy.tsx.mp4 -------------------------------------------------------------------------------- /cypress/videos/Switch/Switch.cy.tsx.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deca-org/deca-ui/7ab900dbc64219d9f2ee17db3b6646793aa8247c/cypress/videos/Switch/Switch.cy.tsx.mp4 -------------------------------------------------------------------------------- /cypress/videos/Text/Text.cy.tsx.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deca-org/deca-ui/7ab900dbc64219d9f2ee17db3b6646793aa8247c/cypress/videos/Text/Text.cy.tsx.mp4 -------------------------------------------------------------------------------- /public/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deca-org/deca-ui/7ab900dbc64219d9f2ee17db3b6646793aa8247c/public/cover.png -------------------------------------------------------------------------------- /setupTests.ts: -------------------------------------------------------------------------------- 1 | jest.mock('uuid', () => ({ 2 | v4: jest.fn(() => 1), 3 | })); 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@lib/Badge'; 2 | export * from '@lib/Button'; 3 | export * from '@lib/Grid'; 4 | export * from '@lib/Modal'; 5 | export * from '@lib/Radio'; 6 | export * from '@lib/Text'; 7 | export * from '@lib/Utils'; 8 | export * from '@lib/Box'; 9 | export * from '@lib/Checkbox'; 10 | export * from '@lib/Input'; 11 | export * from '@lib/Popover'; 12 | export * from '@lib/Switch'; 13 | export * from '@lib/Theme'; 14 | export * from '@lib/Container'; 15 | -------------------------------------------------------------------------------- /src/lib/Badge/Badge.cy.tsx: -------------------------------------------------------------------------------- 1 | import { Test } from '../Utils'; 2 | import _cyp from '../../../cypress'; 3 | import React from 'react'; 4 | import Badge from './Badge'; 5 | import { standardColors } from '../Theme'; 6 | 7 | describe('components/Badge', () => { 8 | const selector = '[data-testid="test.badge"]'; 9 | it('pill', () => { 10 | cy.mount( 11 | 12 | Label 13 | 14 | ); 15 | cy.get(selector).should( 16 | 'have.css', 17 | 'border-radius', 18 | Test.borderRadius('pill') 19 | ); 20 | }); 21 | describe('sizes', () => { 22 | it('sm', () => { 23 | cy.mount( 24 | 25 | Label 26 | 27 | ); 28 | cy.get(selector).should('have.css', 'fontSize', Test.fontSize('caption')); 29 | cy.get(selector).should('have.css', 'paddingTop', Test.space('1')); 30 | cy.get(selector).should('have.css', 'paddingBottom', Test.space('1')); 31 | cy.get(selector).should('have.css', 'paddingLeft', Test.space('2')); 32 | cy.get(selector).should('have.css', 'paddingRight', Test.space('2')); 33 | }); 34 | it('md', () => { 35 | cy.mount( 36 | 37 | Label 38 | 39 | ); 40 | cy.get(selector).should('have.css', 'fontSize', Test.fontSize('bodySm')); 41 | cy.get(selector).should('have.css', 'paddingTop', Test.space('1')); 42 | cy.get(selector).should('have.css', 'paddingBottom', Test.space('1')); 43 | cy.get(selector).should('have.css', 'paddingLeft', Test.space('3 - 1')); 44 | cy.get(selector).should('have.css', 'paddingRight', Test.space('3 - 1')); 45 | }); 46 | it('lg', () => { 47 | cy.mount( 48 | 49 | Label 50 | 51 | ); 52 | cy.get(selector).should('have.css', 'fontSize', Test.fontSize('body')); 53 | cy.get(selector).should('have.css', 'paddingTop', Test.space('1')); 54 | cy.get(selector).should('have.css', 'paddingBottom', Test.space('1')); 55 | cy.get(selector).should('have.css', 'paddingLeft', Test.space('3')); 56 | cy.get(selector).should('have.css', 'paddingRight', Test.space('3')); 57 | }); 58 | }); 59 | describe('color', () => { 60 | standardColors.map((color) => { 61 | it(color, () => { 62 | cy.mount( 63 | 64 | Label 65 | 66 | ); 67 | cy.get(selector).should( 68 | 'have.css', 69 | 'background-color', 70 | Test.color(color) 71 | ); 72 | cy.get(selector).should( 73 | 'have.css', 74 | 'color', 75 | Test.color(`${color}-readable`) 76 | ); 77 | }); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /src/lib/Badge/Badge.stories.tsx: -------------------------------------------------------------------------------- 1 | import { DecaUIProvider } from '@lib/Theme'; 2 | import { ComponentMeta, ComponentStory } from '@storybook/react'; 3 | import React from 'react'; 4 | 5 | import Badge from './Badge'; 6 | 7 | export default { 8 | title: 'Badge', 9 | component: Badge, 10 | } as ComponentMeta; 11 | 12 | const Template: ComponentStory = (args) => ; 13 | 14 | export const Default = Template.bind({}); 15 | Default.args = { 16 | children: 'Hello', 17 | size: 'md', 18 | color: 'primary', 19 | pill: false, 20 | css: {}, 21 | className: '', 22 | }; 23 | 24 | export const WithTheme = Template.bind({}); 25 | 26 | WithTheme.args = { ...Default.args }; 27 | WithTheme.decorators = [ 28 | (Story) => ( 29 | 39 | 40 | 41 | ), 42 | ]; 43 | -------------------------------------------------------------------------------- /src/lib/Badge/Badge.styles.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from '@lib/Theme/stitches.config'; 2 | 3 | export const StyledBadge = styled('span', { 4 | transition: '$default', 5 | fontFamily: '$normal', 6 | fontWeight: '$bold', 7 | variants: { 8 | pill: { 9 | true: { 10 | br: '$pill', 11 | }, 12 | false: { 13 | br: '$sm', 14 | }, 15 | }, 16 | size: { 17 | sm: { 18 | fontSize: '$caption', 19 | py: '$1', 20 | px: '$2', 21 | }, 22 | md: { 23 | fontSize: '$bodySm', 24 | py: '$1', 25 | paddingRight: 'calc($3 - $1)', 26 | paddingLeft: 'calc($3 - $1)', 27 | }, 28 | lg: { 29 | fontSize: '$body', 30 | py: '$1', 31 | px: '$3', 32 | }, 33 | }, 34 | color: { 35 | primary: { 36 | bg: '$primary', 37 | color: '$primary-readable', 38 | }, 39 | secondary: { 40 | bg: '$secondary', 41 | color: '$secondary-readable', 42 | }, 43 | success: { 44 | bg: '$success', 45 | color: '$success-readable', 46 | }, 47 | warning: { 48 | bg: '$warning', 49 | color: '$warning-readable', 50 | }, 51 | error: { 52 | bg: '$error', 53 | color: '$error-readable', 54 | }, 55 | }, 56 | }, 57 | }); 58 | -------------------------------------------------------------------------------- /src/lib/Badge/Badge.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | import '@testing-library/jest-dom'; 3 | import React from 'react'; 4 | 5 | import Badge from './Badge'; 6 | 7 | describe('components/Badge', () => { 8 | it('renders content', () => { 9 | const { getByText } = render(Label); 10 | expect(getByText('Label')).toBeInTheDocument(); 11 | }); 12 | it('matches snapshot', () => { 13 | const { asFragment } = render(); 14 | expect(asFragment()).toMatchSnapshot(); 15 | }); 16 | it('renders all colors', () => { 17 | const { asFragment } = render( 18 | <> 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | expect(asFragment()).toMatchSnapshot(); 27 | }); 28 | it('renders all sizes', () => { 29 | const { asFragment } = render( 30 | <> 31 | 32 | 33 | 34 | 35 | ); 36 | expect(asFragment()).toMatchSnapshot(); 37 | }); 38 | it('renders pill option', () => { 39 | const { asFragment } = render(); 40 | expect(asFragment()).toMatchSnapshot(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/lib/Badge/Badge.tsx: -------------------------------------------------------------------------------- 1 | import { CSS, StandardColors } from '@lib/Theme/stitches.config'; 2 | import { 3 | __DEV__, 4 | PolymorphicRef, 5 | PolymorphicComponentPropWithRef, 6 | } from '@lib/Utils'; 7 | import clsx from 'clsx'; 8 | import React from 'react'; 9 | 10 | import { StyledBadge } from './Badge.styles'; 11 | 12 | /** 13 | * Badges are used to highlight an item's status for quick recognition. 14 | */ 15 | interface Props { 16 | /** 17 | * The content of the component. 18 | */ 19 | children?: React.ReactNode | undefined; 20 | /** 21 | * ClassName applied to the component. 22 | * @default '' 23 | */ 24 | className?: string; 25 | /** 26 | * Color to use. 27 | * @default primary 28 | */ 29 | color?: StandardColors; 30 | /** 31 | * Size of the component. 32 | * @default md 33 | */ 34 | size?: 'sm' | 'md' | 'lg'; 35 | /** 36 | * Override default CSS style. 37 | */ 38 | css?: CSS; 39 | /** 40 | * Have component be pill shaped 41 | */ 42 | pill?: boolean; 43 | } 44 | 45 | export type BadgeProps = 46 | PolymorphicComponentPropWithRef; 47 | 48 | export type BadgeComponent = (( 49 | props: BadgeProps 50 | ) => React.ReactElement | null) & { displayName?: string }; 51 | 52 | const Badge: BadgeComponent = React.forwardRef( 53 | ( 54 | { 55 | children, 56 | className, 57 | color = 'primary', 58 | size = 'md', 59 | as, 60 | css, 61 | pill = false, 62 | ...props 63 | }: BadgeProps, 64 | ref?: PolymorphicRef 65 | ) => { 66 | const preClass = 'decaBadge'; 67 | 68 | return ( 69 | 79 | {children} 80 | 81 | ); 82 | } 83 | ); 84 | 85 | if (__DEV__) { 86 | Badge.displayName = 'DecaUI.Badge'; 87 | } 88 | 89 | export default Badge; 90 | -------------------------------------------------------------------------------- /src/lib/Badge/__snapshots__/Badge.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`components/Badge matches snapshot 1`] = ` 4 | 5 | 8 | 9 | `; 10 | 11 | exports[`components/Badge renders all colors 1`] = ` 12 | 13 | 16 | 19 | 22 | 25 | 28 | 29 | `; 30 | 31 | exports[`components/Badge renders all sizes 1`] = ` 32 | 33 | 36 | 39 | 42 | 43 | `; 44 | 45 | exports[`components/Badge renders pill option 1`] = ` 46 | 47 | 50 | 51 | `; 52 | -------------------------------------------------------------------------------- /src/lib/Badge/index.ts: -------------------------------------------------------------------------------- 1 | import Badge from './Badge'; 2 | export * from './Badge'; 3 | export { Badge }; 4 | export default Badge; 5 | -------------------------------------------------------------------------------- /src/lib/Box/Box.stories.tsx: -------------------------------------------------------------------------------- 1 | import Text from '@lib/Text'; 2 | import { ComponentMeta, ComponentStory } from '@storybook/react'; 3 | import React from 'react'; 4 | 5 | import Box from './Box'; 6 | 7 | export default { 8 | title: 'Box', 9 | component: Box, 10 | } as ComponentMeta; 11 | 12 | const Template: ComponentStory = (args) => ; 13 | 14 | const showcaseText = 'Almost before we knew it, we had left the ground.'; 15 | 16 | export const Default = Template.bind({}); 17 | 18 | Default.args = { 19 | css: { 20 | br: '$lg', 21 | size: '$25', 22 | linearGradient: '20deg, $pink900, $red500', 23 | }, 24 | }; 25 | 26 | export const WithChildren = () => ( 27 | 38 | {showcaseText} 39 | 40 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed eleifend 41 | rhoncus ligula, eget porta enim aliquam nec. Suspendisse ultrices lorem 42 | lobortis feugiat tempor. 43 | 44 | 45 | ); 46 | -------------------------------------------------------------------------------- /src/lib/Box/Box.styles.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from '@lib/Theme/stitches.config'; 2 | 3 | export const StyledBox = styled('div', { 4 | fontFamily: '$normal', 5 | }); 6 | -------------------------------------------------------------------------------- /src/lib/Box/Box.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | import '@testing-library/jest-dom'; 3 | import React from 'react'; 4 | 5 | import Box from './Box'; 6 | 7 | describe('components/Box', () => { 8 | it('renders content', () => { 9 | const { getByText } = render(Children); 10 | expect(getByText('Children')).toBeInTheDocument(); 11 | }); 12 | it('matches snapshot', () => { 13 | const { asFragment } = render(); 14 | expect(asFragment()).toMatchSnapshot(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/lib/Box/Box.tsx: -------------------------------------------------------------------------------- 1 | import { CSS } from '@lib/Theme/stitches.config'; 2 | import { 3 | __DEV__, 4 | PolymorphicRef, 5 | PolymorphicComponentPropWithRef, 6 | } from '@lib/Utils'; 7 | import clsx from 'clsx'; 8 | import React from 'react'; 9 | 10 | import { StyledBox } from './Box.styles'; 11 | 12 | /** 13 | * The Box component serves as a wrapper component 14 | */ 15 | interface Props { 16 | /** 17 | * Override default CSS style. 18 | */ 19 | css?: CSS; 20 | /** 21 | * The content of the component. 22 | */ 23 | children?: React.ReactNode | undefined; 24 | /** 25 | * ClassName applied to the component. 26 | * @default '' 27 | */ 28 | className?: string; 29 | } 30 | 31 | export type BoxProps = 32 | PolymorphicComponentPropWithRef; 33 | 34 | export type BoxComponent = (( 35 | props: BoxProps 36 | ) => React.ReactElement | null) & { displayName?: string }; 37 | 38 | const Box: BoxComponent = React.forwardRef( 39 | ( 40 | { as, css, children, className = '', ...boxProps }: BoxProps, 41 | ref?: PolymorphicRef 42 | ) => { 43 | const preClass = 'decaBox'; 44 | 45 | return ( 46 | 53 | {children} 54 | 55 | ); 56 | } 57 | ); 58 | 59 | if (__DEV__) { 60 | Box.displayName = 'DecaUI.Box'; 61 | } 62 | 63 | export default Box; 64 | -------------------------------------------------------------------------------- /src/lib/Box/__snapshots__/Box.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`components/Box matches snapshot 1`] = ` 4 | 5 |
8 | 9 | `; 10 | -------------------------------------------------------------------------------- /src/lib/Box/index.ts: -------------------------------------------------------------------------------- 1 | import Box from './Box'; 2 | export * from './Box'; 3 | export { Box }; 4 | export default Box; 5 | -------------------------------------------------------------------------------- /src/lib/Button/Button.stories.tsx: -------------------------------------------------------------------------------- 1 | import { DecaUIProvider } from '@lib/Theme'; 2 | import { ComponentMeta, ComponentStory } from '@storybook/react'; 3 | import { PurchaseTagAlt } from '@styled-icons/boxicons-solid/PurchaseTagAlt'; 4 | import React from 'react'; 5 | 6 | import Button from './Button'; 7 | 8 | export default { 9 | title: 'Button', 10 | component: Button, 11 | } as ComponentMeta; 12 | 13 | const Template: ComponentStory = (args) => ); 11 | expect(getByText('Label')).toBeInTheDocument(); 12 | }); 13 | 14 | it('matches snapshot', () => { 15 | const { asFragment } = render(); 16 | expect(asFragment()).toMatchSnapshot(); 17 | }); 18 | 19 | it('onClick event fires', () => { 20 | const mockFn = jest.fn(); 21 | const { getByText } = render(); 22 | fireEvent.click(getByText('Label')); 23 | expect(mockFn.mock.calls.length).toBe(1); 24 | }); 25 | 26 | it('renders all colors', () => { 27 | const { asFragment } = render( 28 | <> 29 | 30 | 31 | 32 | 33 | 34 | 35 | ); 36 | expect(asFragment()).toMatchSnapshot(); 37 | }); 38 | 39 | it('renders all sizes', () => { 40 | const { asFragment } = render( 41 | <> 42 | 43 | 44 | 45 | 46 | ); 47 | expect(asFragment()).toMatchSnapshot(); 48 | }); 49 | 50 | it('renders all variants', () => { 51 | const { asFragment } = render( 52 | <> 53 | 54 | 55 | 56 | 57 | 58 | ); 59 | expect(asFragment()).toMatchSnapshot(); 60 | }); 61 | 62 | it('should ignore events when disabled', () => { 63 | const mockFn = jest.fn(); 64 | const { getByText } = render( 65 | 68 | ); 69 | fireEvent.click(getByText('Label')); 70 | expect(mockFn.mock.calls.length).toBe(0); 71 | }); 72 | 73 | it('renders icon', () => { 74 | const { container, getByText } = render( 75 | 76 | ); 77 | const iconEl = container.querySelector('svg'); 78 | expect(iconEl).toBeInTheDocument(); 79 | expect(getByText('Label')).toBeInTheDocument(); 80 | }); 81 | 82 | it('renders icon on right side', () => { 83 | const { container, getByText } = render( 84 | 85 | ); 86 | const iconEl = container.querySelector('svg'); 87 | expect(iconEl).toBeInTheDocument(); 88 | expect(getByText('Label')).toBeInTheDocument(); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /src/lib/Button/ButtonIcon.tsx: -------------------------------------------------------------------------------- 1 | import { CSS, styled } from '@lib/Theme/stitches.config'; 2 | import React from 'react'; 3 | 4 | const StyledButtonIcon = styled('span', { 5 | display: 'flex', 6 | justifyContent: 'center', 7 | alignItems: 'center', 8 | height: '100%', 9 | '& svg': { 10 | background: 'transparent', 11 | }, 12 | compoundVariants: [ 13 | { 14 | isSingle: false, 15 | side: 'left', 16 | css: { 17 | mr: '$1', 18 | }, 19 | }, 20 | { 21 | isSingle: false, 22 | side: 'right', 23 | css: { 24 | ml: '$1', 25 | }, 26 | }, 27 | { 28 | isSingle: true, 29 | size: 'sm', 30 | css: { 31 | '& svg': { 32 | height: 'calc(100% - $0)', 33 | width: '100%', 34 | }, 35 | }, 36 | }, 37 | ], 38 | variants: { 39 | size: { 40 | sm: { 41 | '& svg': { 42 | height: 'calc(100% - $1)', 43 | width: '100%', 44 | }, 45 | }, 46 | md: { 47 | '& svg': { 48 | height: 'calc(100% - $2)', 49 | width: '100%', 50 | }, 51 | }, 52 | lg: { 53 | '& svg': { 54 | height: 'calc(100% - $3)', 55 | width: '100%', 56 | }, 57 | }, 58 | }, 59 | isSingle: { 60 | true: { 61 | '& svg': { 62 | height: 'calc(100% - $2)', 63 | width: '100%', 64 | }, 65 | }, 66 | false: {}, 67 | }, 68 | side: { 69 | left: {}, 70 | right: {}, 71 | }, 72 | }, 73 | }); 74 | 75 | export interface ButtonIconProps { 76 | children: React.ReactNode | undefined; 77 | className?: string; 78 | css?: CSS; 79 | size?: 'sm' | 'md' | 'lg'; 80 | isSingle?: boolean; 81 | side?: 'left' | 'right'; 82 | } 83 | 84 | const ButtonIcon = ({ 85 | children, 86 | className, 87 | css, 88 | size, 89 | isSingle, 90 | side, 91 | ...props 92 | }: ButtonIconProps) => ( 93 | 101 | {children} 102 | 103 | ); 104 | 105 | export default ButtonIcon; 106 | -------------------------------------------------------------------------------- /src/lib/Button/__snapshots__/Button.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`components/Button matches snapshot 1`] = ` 4 | 5 | 11 | 12 | `; 13 | 14 | exports[`components/Button renders all colors 1`] = ` 15 | 16 | 22 | 28 | 34 | 40 | 46 | 47 | `; 48 | 49 | exports[`components/Button renders all sizes 1`] = ` 50 | 51 | 57 | 63 | 69 | 70 | `; 71 | 72 | exports[`components/Button renders all variants 1`] = ` 73 | 74 | 80 | 86 | 92 | 98 | 99 | `; 100 | -------------------------------------------------------------------------------- /src/lib/Button/index.ts: -------------------------------------------------------------------------------- 1 | import Button from './Button'; 2 | export * from './Button'; 3 | export { Button }; 4 | export default Button; 5 | -------------------------------------------------------------------------------- /src/lib/Checkbox/Checkbox.cy.tsx: -------------------------------------------------------------------------------- 1 | import { standardColors } from '../Theme'; 2 | import { Test } from '../Utils'; 3 | import _cyp from '../../../cypress'; 4 | import React from 'react'; 5 | import Checkbox from './Checkbox'; 6 | 7 | describe('components/Checkbox', () => { 8 | describe('before click', () => { 9 | it('border-color', () => { 10 | cy.mount(); 11 | 12 | cy.get('label') 13 | .before('border-color') 14 | .should('eq', Test.color('gray600')); 15 | }); 16 | it('disabled', () => { 17 | cy.mount(); 18 | 19 | cy.get('label').should('have.css', 'color', Test.color('gray500')); 20 | 21 | cy.get('label') 22 | .before('border-color') 23 | .should('eq', Test.color('gray400')); 24 | }); 25 | }); 26 | describe('colors', () => { 27 | it('label color', () => { 28 | cy.mount(); 29 | cy.get('label').should('have.css', 'color', Test.color('black')); 30 | }); 31 | standardColors.map((color) => { 32 | describe(color, () => { 33 | it('background-color', () => { 34 | cy.mount(); 35 | 36 | cy.get('label').click(); 37 | 38 | // wait for css transition to finish 39 | cy.wait(250); 40 | cy.get('label') 41 | .before('background-color') 42 | .should('eq', Test.color(color)); 43 | }); 44 | it('border-color', () => { 45 | cy.mount(); 46 | 47 | cy.get('label').click(); 48 | 49 | // wait for css transition to finish 50 | cy.wait(250); 51 | cy.get('label') 52 | .before('border-color') 53 | .should('eq', Test.color(color)); 54 | }); 55 | it('disabled', () => { 56 | cy.mount( 57 | 58 | ); 59 | 60 | cy.get('label').before('opacity').should('eq', '0.55'); 61 | cy.get('svg').should('have.css', 'opacity', '0.9'); 62 | }); 63 | }); 64 | }); 65 | }); 66 | describe('sizes', () => { 67 | it('sm', () => { 68 | cy.mount(); 69 | cy.get('label').before('width').should('eq', Test.size('2')); 70 | cy.get('label').before('height').should('eq', Test.size('2')); 71 | cy.get('label').before('margin-right').should('eq', Test.space('1')); 72 | cy.get('label').should('have.css', 'font-size', Test.fontSize('caption')); 73 | cy.get('svg').should('have.css', 'width', Test.size('1')); 74 | }); 75 | it('md', () => { 76 | cy.mount(); 77 | cy.get('label').before('width').should('eq', Test.size('3')); 78 | cy.get('label').before('height').should('eq', Test.size('3')); 79 | cy.get('label').before('margin-right').should('eq', Test.space('2')); 80 | cy.get('label').should('have.css', 'font-size', Test.fontSize('bodySm')); 81 | cy.get('svg').should('have.css', 'width', Test.size('2')); 82 | }); 83 | it('lg', () => { 84 | cy.mount(); 85 | cy.get('label').before('width').should('eq', Test.size('4')); 86 | cy.get('label').before('height').should('eq', Test.size('4')); 87 | cy.get('label').before('margin-right').should('eq', Test.space('2')); 88 | cy.get('label').should('have.css', 'font-size', Test.fontSize('body')); 89 | cy.get('svg').should('have.css', 'width', Test.size('3')); 90 | }); 91 | }); 92 | 93 | describe('no label', () => { 94 | describe('should have no margin', () => { 95 | it('sm', () => { 96 | cy.mount(); 97 | cy.get('label').before('margin-right').should('eq', '0px'); 98 | }); 99 | it('md', () => { 100 | cy.mount(); 101 | cy.get('label').before('margin-right').should('eq', '0px'); 102 | }); 103 | it('sm', () => { 104 | cy.mount(); 105 | cy.get('label').before('margin-right').should('eq', '0px'); 106 | }); 107 | }); 108 | }); 109 | 110 | describe('dark mode', () => { 111 | it('label color', () => { 112 | cy.darkMount(); 113 | cy.get('label').should('have.css', 'color', Test.color('white')); 114 | }); 115 | 116 | it('disabled state', () => { 117 | cy.darkMount(); 118 | cy.get('label').before('opacity').should('eq', '0.5'); 119 | cy.get('svg').should('have.css', 'opacity', '0.3'); 120 | }); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /src/lib/Checkbox/Checkbox.stories.tsx: -------------------------------------------------------------------------------- 1 | import { DecaUIProvider } from '@lib/Theme'; 2 | import { ComponentMeta, ComponentStory } from '@storybook/react'; 3 | import React from 'react'; 4 | 5 | import Checkbox from './Checkbox'; 6 | 7 | export default { 8 | title: 'Checkbox', 9 | component: Checkbox, 10 | } as ComponentMeta; 11 | 12 | const Template: ComponentStory = (args) => ( 13 | 14 | ); 15 | 16 | export const Default = Template.bind({}); 17 | Default.args = { 18 | label: 'Label', 19 | size: 'md', 20 | color: 'primary', 21 | disabled: false, 22 | css: {}, 23 | className: '', 24 | initialCheck: true, 25 | required: false, 26 | }; 27 | 28 | export const NoLabel = Template.bind({}); 29 | NoLabel.args = { 30 | ...Default.args, 31 | label: '', 32 | }; 33 | 34 | export const WithTheme = Template.bind({}); 35 | 36 | WithTheme.args = { ...Default.args }; 37 | WithTheme.decorators = [ 38 | (Story) => ( 39 | 46 | 47 | 48 | ), 49 | ]; 50 | 51 | export const DarkMode = Template.bind({}); 52 | 53 | DarkMode.args = { ...Default.args }; 54 | DarkMode.parameters = { backgrounds: { default: 'dark' } }; 55 | 56 | DarkMode.decorators = [ 57 | (Story) => ( 58 | 59 | 60 | 61 | ), 62 | ]; 63 | -------------------------------------------------------------------------------- /src/lib/Checkbox/Checkbox.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, act } from '@testing-library/react'; 2 | import React from 'react'; 3 | 4 | import Checkbox from './Checkbox'; 5 | 6 | describe('components/Checkbox', () => { 7 | it('matches screenshot', () => { 8 | const { asFragment } = render(); 9 | expect(asFragment()).toMatchSnapshot(); 10 | }); 11 | it('onChange event fires', () => { 12 | const mockFn = jest.fn(); 13 | const utils = render(); 14 | const input = utils.getByLabelText('Label Text'); 15 | act(() => { 16 | input.click(); 17 | }); 18 | expect(mockFn.mock.calls.length).toBe(1); 19 | }); 20 | it('should ignore events when disabled', () => { 21 | const mockFn = jest.fn(); 22 | const utils = render( 23 | 24 | ); 25 | const input = utils.getByLabelText('Label Text'); 26 | act(() => { 27 | input.click(); 28 | }); 29 | expect(mockFn.mock.calls.length).toBe(0); 30 | }); 31 | it('renders all colors', () => { 32 | const { asFragment } = render( 33 | <> 34 | 35 | 36 | 37 | 38 | 39 | 40 | ); 41 | expect(asFragment()).toMatchSnapshot(); 42 | }); 43 | it('renders all sizes', () => { 44 | const { asFragment } = render( 45 | <> 46 | 47 | 48 | 49 | 50 | ); 51 | expect(asFragment()).toMatchSnapshot(); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /src/lib/Checkbox/CheckboxGroup/CheckboxGroup.stories.tsx: -------------------------------------------------------------------------------- 1 | import Checkbox from '@lib/Checkbox'; 2 | import { DecaUIProvider } from '@lib/Theme'; 3 | import { ComponentMeta, ComponentStory } from '@storybook/react'; 4 | import React from 'react'; 5 | 6 | export default { 7 | title: 'CheckboxGroup', 8 | component: Checkbox.Group, 9 | } as ComponentMeta; 10 | 11 | const Template: ComponentStory = (args) => ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | 20 | export const Default = Template.bind({}); 21 | Default.args = { 22 | defaultValue: ['A', 'B'], 23 | name: 'FormGroup-Checkbox', 24 | disabled: false, 25 | className: '', 26 | color: 'primary', 27 | }; 28 | 29 | export const SingleDisabled = () => ( 30 | 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | 38 | export const WithTheme = Template.bind({}); 39 | 40 | WithTheme.args = { ...Default.args }; 41 | WithTheme.decorators = [ 42 | (Story) => ( 43 | 50 | 51 | 52 | ), 53 | ]; 54 | 55 | export const DarkMode = Template.bind({}); 56 | 57 | DarkMode.args = { ...Default.args }; 58 | DarkMode.parameters = { backgrounds: { default: 'dark' } }; 59 | 60 | DarkMode.decorators = [ 61 | (Story) => ( 62 | 63 | 64 | 65 | ), 66 | ]; 67 | -------------------------------------------------------------------------------- /src/lib/Checkbox/CheckboxGroup/CheckboxGroup.styles.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from '@lib/Theme/stitches.config'; 2 | 3 | export const StyledCheckboxGroupWrapper = styled('div', { 4 | position: 'relative', 5 | boxSizing: 'border-box', 6 | display: 'flex', 7 | flexDirection: 'column', 8 | gap: '$2', 9 | }); 10 | -------------------------------------------------------------------------------- /src/lib/Checkbox/CheckboxGroup/CheckboxGroup.test.tsx: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | import Checkbox from '@lib/Checkbox'; 3 | import { render, screen } from '@testing-library/react'; 4 | import userEvent from '@testing-library/user-event'; 5 | import React from 'react'; 6 | 7 | describe('components/CheckboxGroup', () => { 8 | it('matches snapshot', () => { 9 | const { asFragment } = render( 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | expect(asFragment()).toMatchSnapshot(); 18 | }); 19 | it('works as an uncontrolled component', async () => { 20 | const user = userEvent.setup(); 21 | render( 22 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | 30 | expect(screen.getByLabelText('Option A')).toBeChecked(); 31 | expect(screen.getByLabelText('Option B')).toBeChecked(); 32 | await user.click(screen.getByLabelText('Option C')); 33 | expect(screen.getByLabelText('Option C')).toBeChecked(); 34 | }); 35 | it('works as a controlled component', async () => { 36 | let value = ['A', 'B']; 37 | const user = userEvent.setup(); 38 | 39 | render( 40 | ) => 43 | (value = [...value, e.target.value]) 44 | } 45 | > 46 | 47 | 48 | 49 | 50 | 51 | ); 52 | 53 | expect(screen.getByLabelText('Option A')).toBeChecked(); 54 | expect(screen.getByLabelText('Option B')).toBeChecked(); 55 | await user.click(screen.getByLabelText('Option C')); 56 | expect(value).toStrictEqual(['A', 'B', 'C']); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/lib/Checkbox/CheckboxGroup/CheckboxGroup.tsx: -------------------------------------------------------------------------------- 1 | import { CheckboxProps } from '@lib/Checkbox'; 2 | import { CSS, StandardColors } from '@lib/Theme/stitches.config'; 3 | import { 4 | PolymorphicRef, 5 | PolymorphicComponentPropWithRef, 6 | uuid, 7 | __DEV__, 8 | } from '@lib/Utils'; 9 | import clsx from 'clsx'; 10 | import React, { useMemo } from 'react'; 11 | import { StyledCheckboxGroupWrapper } from './CheckboxGroup.styles'; 12 | 13 | /** 14 | * CheckboxGroup is a helpful wrapper used to group checkbox components. 15 | */ 16 | interface Props { 17 | /** 18 | * The content of the component. 19 | */ 20 | children?: 21 | | Array>> 22 | | React.ReactElement>; 23 | /** 24 | * Array of checkboxes that are selected by default. Used when component is not controlled. 25 | */ 26 | defaultValue?: Array; 27 | /** 28 | * ClassName applied to the component. 29 | * @default '' 30 | */ 31 | className?: string; 32 | /** 33 | * The name used to reference the value of the control. If you do not provide this prop, it falls back to a randomly generated name. 34 | */ 35 | name?: string; 36 | /** 37 | * Callback fired when a checkbox is selected. 38 | */ 39 | onChange?(e: React.ChangeEvent): void; 40 | /** 41 | * Array of selected checkboxes. Used when component is controlled. 42 | */ 43 | value?: Array; 44 | /** 45 | * Apply disabled state to all checkboxes in the checkbox group component 46 | * @default false 47 | */ 48 | disabled?: boolean; 49 | /** 50 | * Override default CSS style. 51 | */ 52 | css?: CSS; 53 | /** 54 | * Color of checkboxes when active. 55 | */ 56 | color?: StandardColors; 57 | /** 58 | * Size of each checkbox. 59 | */ 60 | size?: 'sm' | 'md' | 'lg'; 61 | } 62 | 63 | export type CheckboxGroupProps = 64 | PolymorphicComponentPropWithRef; 65 | 66 | export type CheckboxGroupComponent = (( 67 | props: CheckboxGroupProps 68 | ) => React.ReactElement | null) & { displayName?: string }; 69 | 70 | const CheckboxGroup: CheckboxGroupComponent = React.forwardRef( 71 | ( 72 | { 73 | children, 74 | defaultValue, 75 | className = '', 76 | name, 77 | onChange, 78 | value, 79 | disabled = false, 80 | as, 81 | css, 82 | color, 83 | size, 84 | ...props 85 | }: CheckboxGroupProps, 86 | ref?: PolymorphicRef 87 | ) => { 88 | const presetId = uuid('checkbox'); 89 | 90 | const getName = useMemo(() => { 91 | if (name) { 92 | return name; 93 | } 94 | return presetId; 95 | }, [name]); 96 | 97 | const preClass = 'decaCheckboxGroup'; 98 | 99 | return ( 100 | 107 | {React.Children.map( 108 | children as React.ReactElement>, 109 | (child: React.ReactElement>) => { 110 | return React.cloneElement(child, { 111 | name: getName, 112 | onChange, 113 | initialCheck: 114 | defaultValue && 115 | defaultValue.includes(child.props.value as string), 116 | checked: value && value.includes(child.props.value as string), 117 | disabled: disabled ? disabled : child.props.disabled, 118 | color, 119 | size, 120 | }); 121 | } 122 | )} 123 | 124 | ); 125 | } 126 | ); 127 | 128 | if (__DEV__) { 129 | CheckboxGroup.displayName = 'DecaUI.CheckboxGroup'; 130 | } 131 | 132 | export default CheckboxGroup; 133 | -------------------------------------------------------------------------------- /src/lib/Checkbox/CheckboxGroup/__snapshots__/CheckboxGroup.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`components/CheckboxGroup matches snapshot 1`] = ` 4 | 5 |
8 |
11 | 19 | 37 |
38 |
41 | 49 | 67 |
68 |
71 | 78 | 96 |
97 |
100 | 107 | 125 |
126 |
127 |
128 | `; 129 | -------------------------------------------------------------------------------- /src/lib/Checkbox/CheckboxGroup/index.ts: -------------------------------------------------------------------------------- 1 | import CheckboxGroup from './CheckboxGroup'; 2 | export * from './CheckboxGroup'; 3 | export { CheckboxGroup }; 4 | export default CheckboxGroup; 5 | -------------------------------------------------------------------------------- /src/lib/Checkbox/index.ts: -------------------------------------------------------------------------------- 1 | import Checkbox from './Checkbox'; 2 | import CheckboxGroup from './CheckboxGroup'; 3 | 4 | Checkbox.Group = CheckboxGroup; 5 | 6 | export * from './Checkbox'; 7 | export { Checkbox }; 8 | export default Checkbox; 9 | -------------------------------------------------------------------------------- /src/lib/Container/Container.cy.tsx: -------------------------------------------------------------------------------- 1 | import { Test } from '../Utils'; 2 | import _cyp from '../../../cypress'; 3 | import React from 'react'; 4 | import Container from './Container'; 5 | import Box from '../Box'; 6 | 7 | const ContainerItem = () => ( 8 | 17 | Content 18 | 19 | ); 20 | 21 | describe('components/Container', () => { 22 | const selector = '[data-testid="test.container"]'; 23 | it('fluid', () => { 24 | cy.baseMount( 25 | 26 | 27 | 28 | ); 29 | cy.get(selector).should('have.css', 'max-width', '100%'); 30 | }); 31 | describe('px', () => { 32 | it('none', () => { 33 | cy.baseMount( 34 | 35 | 36 | 37 | ); 38 | cy.get(selector).should('have.css', 'padding-left', Test.space('n')); 39 | cy.get(selector).should('have.css', 'padding-right', Test.space('n')); 40 | }); 41 | it('sm', () => { 42 | cy.baseMount( 43 | 44 | 45 | 46 | ); 47 | cy.get(selector).should('have.css', 'padding-left', Test.space('2')); 48 | cy.get(selector).should('have.css', 'padding-right', Test.space('2')); 49 | }); 50 | it('md', () => { 51 | cy.baseMount( 52 | 53 | 54 | 55 | ); 56 | cy.get(selector).should('have.css', 'padding-left', Test.space('3')); 57 | cy.get(selector).should('have.css', 'padding-right', Test.space('3')); 58 | }); 59 | it('lg', () => { 60 | cy.baseMount( 61 | 62 | 63 | 64 | ); 65 | cy.get(selector).should('have.css', 'padding-left', Test.space('4')); 66 | cy.get(selector).should('have.css', 'padding-right', Test.space('4')); 67 | }); 68 | }); 69 | describe('breakpoints', () => { 70 | it('xs', () => { 71 | cy.viewport(300, 500); 72 | cy.baseMount( 73 | 74 | 75 | 76 | ); 77 | cy.get(selector).should( 78 | 'have.css', 79 | 'max-width', 80 | Test.breakpoint('xs') + 'px' 81 | ); 82 | }); 83 | it('sm', () => { 84 | cy.viewport(Test.breakpoint('sm'), 500); 85 | cy.baseMount( 86 | 87 | 88 | 89 | ); 90 | cy.get(selector).should( 91 | 'have.css', 92 | 'max-width', 93 | Test.breakpoint('sm') + 'px' 94 | ); 95 | }); 96 | it('md', () => { 97 | cy.viewport(Test.breakpoint('md'), 500); 98 | cy.baseMount( 99 | 100 | 101 | 102 | ); 103 | cy.get(selector).should( 104 | 'have.css', 105 | 'max-width', 106 | Test.breakpoint('sm') + 'px' 107 | ); 108 | }); 109 | it('lg', () => { 110 | cy.viewport(Test.breakpoint('lg'), 500); 111 | cy.baseMount( 112 | 113 | 114 | 115 | ); 116 | cy.get(selector).should( 117 | 'have.css', 118 | 'max-width', 119 | Test.breakpoint('md') + 'px' 120 | ); 121 | }); 122 | it('xl', () => { 123 | cy.viewport(Test.breakpoint('xl'), 500); 124 | cy.baseMount( 125 | 126 | 127 | 128 | ); 129 | cy.get(selector).should( 130 | 'have.css', 131 | 'max-width', 132 | Test.breakpoint('lg') + 'px' 133 | ); 134 | }); 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /src/lib/Container/Container.stories.tsx: -------------------------------------------------------------------------------- 1 | import Box from '@lib/Box'; 2 | import { ComponentMeta, ComponentStory } from '@storybook/react'; 3 | import React from 'react'; 4 | 5 | import Container from './Container'; 6 | 7 | export default { 8 | title: 'Container', 9 | component: Container, 10 | } as ComponentMeta; 11 | 12 | const Template: ComponentStory = (args) => ( 13 | 14 | 22 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris ipsum 23 | justo, interdum eget lacus sit amet, consequat scelerisque lectus. 24 | Suspendisse vel tincidunt purus. 25 | 26 | 27 | ); 28 | 29 | export const Responsive = Template.bind({}); 30 | 31 | Responsive.args = { 32 | responsive: true, 33 | px: 'sm', 34 | fluid: false, 35 | }; 36 | 37 | export const Fluid = Template.bind({}); 38 | 39 | Fluid.args = { 40 | responsive: true, 41 | px: 'sm', 42 | fluid: true, 43 | }; 44 | -------------------------------------------------------------------------------- /src/lib/Container/Container.styles.ts: -------------------------------------------------------------------------------- 1 | import { styled } from '@lib/Theme/stitches.config'; 2 | 3 | export const StyledContainer = styled('div', { 4 | w: '100%', 5 | mr: 'auto', 6 | ml: 'auto', 7 | variants: { 8 | px: { 9 | none: { 10 | px: '$n', 11 | }, 12 | sm: { 13 | px: '$2', 14 | }, 15 | md: { 16 | px: '$3', 17 | }, 18 | lg: { 19 | px: '$4', 20 | }, 21 | }, 22 | responsive: { 23 | true: { 24 | '@n': { 25 | maxWidth: '$breakpoints$xs', 26 | }, 27 | '@xs': { 28 | maxWidth: '$breakpoints$xs', 29 | }, 30 | '@sm': { 31 | maxWidth: '$breakpoints$sm', 32 | }, 33 | '@md': { 34 | maxWidth: '$breakpoints$sm', 35 | }, 36 | '@lg': { 37 | maxWidth: '$breakpoints$md', 38 | }, 39 | '@xl': { 40 | maxWidth: '$breakpoints$lg', 41 | }, 42 | }, 43 | }, 44 | fluid: { 45 | true: { 46 | maxWidth: '100%', 47 | }, 48 | }, 49 | }, 50 | }); 51 | -------------------------------------------------------------------------------- /src/lib/Container/Container.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | import '@testing-library/jest-dom'; 3 | import React from 'react'; 4 | 5 | import Container from './Container'; 6 | 7 | describe('components/Container', () => { 8 | it('renders content', () => { 9 | const { getByText } = render(Children); 10 | expect(getByText('Children')).toBeInTheDocument(); 11 | }); 12 | it('matches snapshot', () => { 13 | const { asFragment } = render(); 14 | expect(asFragment()).toMatchSnapshot(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/lib/Container/Container.tsx: -------------------------------------------------------------------------------- 1 | import { CSS } from '@lib/Theme/stitches.config'; 2 | import { 3 | PolymorphicRef, 4 | PolymorphicComponentPropWithRef, 5 | __DEV__, 6 | } from '@lib/Utils'; 7 | import clsx from 'clsx'; 8 | import React from 'react'; 9 | 10 | import { StyledContainer } from './Container.styles'; 11 | 12 | /** 13 | * The Container component fixes an element's width to the current breakpoint 14 | */ 15 | interface Props { 16 | /** 17 | * Override default CSS style. 18 | */ 19 | css?: CSS; 20 | /** 21 | * The content of the component. 22 | */ 23 | children?: React.ReactNode | undefined; 24 | /** 25 | * ClassName applied to the component. 26 | * @default '' 27 | */ 28 | className?: string; 29 | /** 30 | * padding applied on each side 31 | * @default sm 32 | */ 33 | px?: 'none' | 'sm' | 'md' | 'lg'; 34 | /** 35 | * container max-width changes with breakpoint 36 | * @default true 37 | */ 38 | responsive?: boolean; 39 | /** 40 | * max-width is set to 100% 41 | */ 42 | fluid?: boolean; 43 | } 44 | 45 | export type ContainerProps = 46 | PolymorphicComponentPropWithRef; 47 | 48 | export type ContainerComponent = (( 49 | props: ContainerProps 50 | ) => React.ReactElement | null) & { displayName?: string }; 51 | 52 | const Container: ContainerComponent = React.forwardRef( 53 | ( 54 | { 55 | as, 56 | css, 57 | className = '', 58 | children, 59 | px = 'sm', 60 | responsive = true, 61 | fluid = false, 62 | ...containerProps 63 | }: ContainerProps, 64 | ref?: PolymorphicRef 65 | ) => { 66 | const preClass = 'decaContainer'; 67 | 68 | return ( 69 | 79 | {children} 80 | 81 | ); 82 | } 83 | ); 84 | 85 | if (__DEV__) { 86 | Container.displayName = 'DecaUI.Container'; 87 | } 88 | 89 | export default Container; 90 | -------------------------------------------------------------------------------- /src/lib/Container/__snapshots__/Container.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`components/Container matches snapshot 1`] = ` 4 | 5 |
8 | 9 | `; 10 | -------------------------------------------------------------------------------- /src/lib/Container/index.ts: -------------------------------------------------------------------------------- 1 | import Container from './Container'; 2 | export * from './Container'; 3 | export { Container }; 4 | export default Container; 5 | -------------------------------------------------------------------------------- /src/lib/Grid/Grid.stories.tsx: -------------------------------------------------------------------------------- 1 | import Box from '@lib/Box'; 2 | import Grid from '@lib/Grid'; 3 | import { ComponentMeta, ComponentStory } from '@storybook/react'; 4 | import React from 'react'; 5 | 6 | import GridRuler from './GridRuler'; 7 | 8 | export default { 9 | title: 'Grid', 10 | component: Grid, 11 | argTypes: { 12 | justifyContent: { 13 | description: 'JustifyContent css prop', 14 | table: { 15 | type: { 16 | summary: 17 | 'flex-start | center | flex-end | space-between | space-around | space-evenly', 18 | }, 19 | }, 20 | control: { 21 | type: 'select', 22 | options: [ 23 | 'center', 24 | 'flex-start', 25 | 'flex-end', 26 | 'space-between', 27 | 'space-around', 28 | 'space-evenly', 29 | ], 30 | }, 31 | }, 32 | alignItems: { 33 | description: 'AlignItems css prop', 34 | table: { 35 | type: { 36 | summary: 'flex-start | center | flex-end', 37 | }, 38 | }, 39 | control: { 40 | type: 'radio', 41 | options: ['flex-start', 'center', 'flex-end'], 42 | }, 43 | }, 44 | spacing: { 45 | description: 'How much spacing there should be between columns.', 46 | table: { 47 | type: { 48 | summary: 'none | sm | md | lg', 49 | }, 50 | }, 51 | control: { 52 | type: 'radio', 53 | options: ['none', 'sm', 'md', 'lg'], 54 | }, 55 | }, 56 | }, 57 | } as ComponentMeta; 58 | 59 | const ExampleBox = ({ children }: { children: React.ReactNode }) => ( 60 | 72 | {children} 73 | 74 | ); 75 | 76 | const Template: ComponentStory = (args: any) => ( 77 | 78 | 89 | 90 | 1 91 | 92 | 93 | 2 94 | 95 | 96 | 3 97 | 98 | 99 | 4 100 | 101 | 102 | 103 | ); 104 | 105 | export const Default = Template.bind({}); 106 | (Default.args as any) = { 107 | justifyContent: 'center', 108 | alignItems: 'flex-start', 109 | spacing: 'sm', 110 | n: 12, 111 | xs: 12, 112 | sm: 6, 113 | md: 4, 114 | lg: 3, 115 | xl: 2, 116 | }; 117 | 118 | export const WithGridRuler = Template.bind({}); 119 | WithGridRuler.args = { ...Default.args }; 120 | 121 | WithGridRuler.decorators = [ 122 | (Story, context) => ( 123 | 124 | 125 | 126 | 127 | ), 128 | ]; 129 | -------------------------------------------------------------------------------- /src/lib/Grid/Grid.styles.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from '@lib/Theme/stitches.config'; 2 | import { getStaticColor } from '@lib/Utils'; 3 | import { transparentize } from 'polished'; 4 | 5 | const justifyContentComposer = () => { 6 | const options = [ 7 | 'flex-start', 8 | 'center', 9 | 'flex-end', 10 | 'space-between', 11 | 'space-around', 12 | 'space-evenly', 13 | ]; 14 | const variantObj: Array< 15 | Array> 16 | > = []; 17 | 18 | options.map((i) => { 19 | variantObj.push([ 20 | i as keyof typeof options, 21 | { justifyContent: i } as Record<'justifyContent', keyof typeof options>, 22 | ]); 23 | }); 24 | return Object.fromEntries(variantObj); 25 | }; 26 | 27 | const alignItemsComposer = () => { 28 | const options = ['flex-start', 'center', 'flex-end']; 29 | const variantObj: Array< 30 | Array> 31 | > = []; 32 | 33 | options.map((i) => { 34 | variantObj.push([ 35 | i as keyof typeof options, 36 | { alignItems: i } as Record<'alignItems', keyof typeof options>, 37 | ]); 38 | }); 39 | return Object.fromEntries(variantObj); 40 | }; 41 | 42 | export const StyledGridItem = styled('div', { 43 | display: 'block', 44 | boxSizing: 'border-box', 45 | }); 46 | 47 | export const StyledGridContainer = styled('div', { 48 | display: 'flex', 49 | flexWrap: 'wrap', 50 | boxSizing: 'border-box', 51 | overflow: 'hidden', 52 | variants: { 53 | justifyContent: justifyContentComposer(), 54 | alignItems: alignItemsComposer(), 55 | spacing: { 56 | none: { 57 | m: '$n', 58 | [`& > ${StyledGridItem}`]: { 59 | p: '$n', 60 | }, 61 | }, 62 | sm: { 63 | m: '-$1', 64 | [`& > ${StyledGridItem}`]: { 65 | p: '$1', 66 | }, 67 | }, 68 | md: { 69 | m: '-$3', 70 | [`& > ${StyledGridItem}`]: { 71 | p: '$3', 72 | }, 73 | }, 74 | lg: { 75 | m: '-$5', 76 | [`& > ${StyledGridItem}`]: { 77 | p: '$5', 78 | }, 79 | }, 80 | }, 81 | }, 82 | }); 83 | 84 | export const StyledGridRuler = styled('div', { 85 | position: 'absolute', 86 | top: 0, 87 | zIndex: -1, 88 | display: 'grid', 89 | gridTemplateColumns: 'repeat(12, 1fr)', 90 | width: '100%', 91 | height: '100vh', 92 | variants: { 93 | spacing: { 94 | none: { 95 | gap: '$n', 96 | }, 97 | sm: { 98 | gap: 'calc($1 * 2)', 99 | }, 100 | md: { 101 | gap: 'calc($3 * 2)', 102 | }, 103 | lg: { 104 | gap: 'calc($5 * 2)', 105 | }, 106 | }, 107 | }, 108 | }); 109 | 110 | export const StyledGridRulerItem = styled('div', { 111 | bg: transparentize(0.75, getStaticColor('gray300')), 112 | border: `${transparentize(0.75, getStaticColor('gray600'))} solid 1px`, 113 | }); 114 | -------------------------------------------------------------------------------- /src/lib/Grid/Grid.test.tsx: -------------------------------------------------------------------------------- 1 | import Grid from '@lib/Grid'; 2 | import { render } from '@testing-library/react'; 3 | import '@testing-library/jest-dom'; 4 | import React from 'react'; 5 | 6 | describe('components/Grid', () => { 7 | it('renders content', () => { 8 | const { getByText } = render( 9 | 10 | 1 11 | 2 12 | 3 13 | 14 | ); 15 | expect(getByText('1')).toBeInTheDocument(); 16 | expect(getByText('2')).toBeInTheDocument(); 17 | expect(getByText('3')).toBeInTheDocument(); 18 | }); 19 | it('matches snapshot', () => { 20 | const { asFragment } = render( 21 | 22 | 1 23 | 2 24 | 3 25 | 26 | ); 27 | expect(asFragment()).toMatchSnapshot(); 28 | }); 29 | it('all sizes render properly on grid container', () => { 30 | const { asFragment } = render( 31 | 32 | 1 33 | 2 34 | 3 35 | 36 | ); 37 | expect(asFragment()).toMatchSnapshot(); 38 | }); 39 | it('all sizes render properly on grid item', () => { 40 | const { asFragment } = render( 41 | 42 | 43 | 1 44 | 45 | 2 46 | 3 47 | 48 | ); 49 | expect(asFragment()).toMatchSnapshot(); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/lib/Grid/Grid.tsx: -------------------------------------------------------------------------------- 1 | import { CSS } from '@lib/Theme/stitches.config'; 2 | import { 3 | PolymorphicRef, 4 | PolymorphicComponentPropWithRef, 5 | __DEV__, 6 | MasterComponent, 7 | } from '@lib/Utils'; 8 | import clsx from 'clsx'; 9 | import React from 'react'; 10 | 11 | import { StyledGridItem } from './Grid.styles'; 12 | import GridContainer from './GridContainer'; 13 | 14 | export type Cols = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12; 15 | 16 | /** 17 | * The Grid component acts as a child to the GridContainer component 18 | */ 19 | interface Props { 20 | /** 21 | * Override default CSS style. 22 | */ 23 | css?: CSS; 24 | /** 25 | * The content of the component. 26 | */ 27 | children?: React.ReactNode | undefined; 28 | /** 29 | * ClassName applied to the component. 30 | * @default '' 31 | */ 32 | className?: string; 33 | /** 34 | * How many columns should be taken up by item initially 35 | */ 36 | n?: Cols; 37 | /** 38 | * How many columns should be taken up by item on xs breakpoint 39 | */ 40 | xs?: Cols; 41 | /** 42 | * How many columns should be taken up by item on sm breakpoint 43 | */ 44 | sm?: Cols; 45 | /** 46 | * How many columns should be taken up by item on md breakpoint 47 | */ 48 | md?: Cols; 49 | /** 50 | * How many columns should be taken up by item on lg breakpoint 51 | */ 52 | lg?: Cols; 53 | /** 54 | * How many columns should be taken up by item on xl breakpoint 55 | */ 56 | xl?: Cols; 57 | } 58 | 59 | export type GridProps = 60 | PolymorphicComponentPropWithRef; 61 | 62 | export type GridComponent = (( 63 | props: GridProps 64 | ) => React.ReactElement | null) & { displayName?: string }; 65 | 66 | const Grid: GridComponent = React.forwardRef( 67 | ( 68 | { 69 | as, 70 | css, 71 | className = '', 72 | children, 73 | n, 74 | xs, 75 | sm, 76 | md, 77 | lg, 78 | xl, 79 | ...gridProps 80 | }: GridProps, 81 | ref?: PolymorphicRef 82 | ) => { 83 | const preClass = 'decaGrid'; 84 | 85 | const genGridItemCss = (breakpoint?: Cols, bp?: CSS) => { 86 | if (bp) { 87 | return { 88 | flexBasis: `calc((${breakpoint} / 12) * 100%)`, 89 | maxWidth: `calc((${breakpoint} / 12) * 100%)`, 90 | ...bp, 91 | }; 92 | } 93 | return { 94 | flexBasis: `calc((${breakpoint} / 12) * 100%)`, 95 | maxWidth: `calc((${breakpoint} / 12) * 100%)`, 96 | }; 97 | }; 98 | 99 | const { 100 | '@n': cssN, 101 | '@xs': cssXs, 102 | '@sm': cssSm, 103 | '@md': cssMd, 104 | '@lg': cssLg, 105 | '@xl': cssXl, 106 | ...otherCss 107 | } = (css as CSS) || {}; 108 | 109 | const getCss = { 110 | flexGrow: 0, 111 | '@n': genGridItemCss(n, cssN), 112 | '@xs': genGridItemCss(xs, cssXs), 113 | '@sm': genGridItemCss(sm, cssSm), 114 | '@md': genGridItemCss(md, cssMd), 115 | '@lg': genGridItemCss(lg, cssLg), 116 | '@xl': genGridItemCss(xl, cssXl), 117 | ...otherCss, 118 | }; 119 | 120 | return ( 121 | 128 | {children} 129 | 130 | ); 131 | } 132 | ); 133 | 134 | if (__DEV__) { 135 | Grid.displayName = 'DecaUI.Grid'; 136 | } 137 | 138 | export default Grid as MasterComponent< 139 | HTMLDivElement, 140 | GridProps, 141 | { Container: typeof GridContainer } 142 | >; 143 | -------------------------------------------------------------------------------- /src/lib/Grid/GridContainer.tsx: -------------------------------------------------------------------------------- 1 | import { CSS } from '@lib/Theme/stitches.config'; 2 | import { 3 | __DEV__, 4 | PolymorphicRef, 5 | PolymorphicComponentPropWithRef, 6 | } from '@lib/Utils'; 7 | import clsx from 'clsx'; 8 | import React from 'react'; 9 | 10 | import { Cols, GridProps } from './Grid'; 11 | import { StyledGridContainer } from './Grid.styles'; 12 | 13 | /** 14 | * The GridContainer component serves as a wrapper component to the Grid component 15 | */ 16 | interface Props { 17 | /** 18 | * Override default CSS style. 19 | */ 20 | css?: CSS; 21 | /** 22 | * The content of the component. 23 | */ 24 | children?: React.ReactNode | undefined; 25 | /** 26 | * ClassName applied to the component. 27 | * @default '' 28 | */ 29 | className?: string; 30 | /** 31 | * How much spacing there should be between columns. 32 | */ 33 | spacing?: 'none' | 'sm' | 'md' | 'lg'; 34 | /** 35 | * JustifyContent css prop. 36 | */ 37 | justifyContent?: 38 | | 'flex-start' 39 | | 'center' 40 | | 'flex-end' 41 | | 'space-between' 42 | | 'space-around' 43 | | 'space-evenly'; 44 | /** 45 | * AlignItems css prop. 46 | */ 47 | alignItems?: 'flex-start' | 'center' | 'flex-end'; 48 | /** 49 | * How many columns should be taken up by item initially 50 | */ 51 | n?: Cols; 52 | /** 53 | * How many columns should be taken up by item on xs breakpoint 54 | */ 55 | xs?: Cols; 56 | /** 57 | * How many columns should be taken up by item on sm breakpoint 58 | */ 59 | sm?: Cols; 60 | /** 61 | * How many columns should be taken up by item on md breakpoint 62 | */ 63 | md?: Cols; 64 | /** 65 | * How many columns should be taken up by item on lg breakpoint 66 | */ 67 | lg?: Cols; 68 | /** 69 | * How many columns should be taken up by item on xl breakpoint 70 | */ 71 | xl?: Cols; 72 | } 73 | 74 | export type GridContainerProps = 75 | PolymorphicComponentPropWithRef; 76 | 77 | export type GridContainerComponent = (( 78 | props: GridContainerProps 79 | ) => React.ReactElement | null) & { displayName?: string }; 80 | 81 | const GridContainer: GridContainerComponent = React.forwardRef( 82 | ( 83 | { 84 | as, 85 | css, 86 | className = '', 87 | children, 88 | spacing = 'sm', 89 | justifyContent, 90 | alignItems, 91 | n, 92 | xs, 93 | sm, 94 | md, 95 | lg, 96 | xl, 97 | ...gridContainerProps 98 | }: GridContainerProps, 99 | ref?: PolymorphicRef 100 | ) => { 101 | const preClass = 'decaGridContainer'; 102 | 103 | return ( 104 | 114 | {React.Children.map( 115 | children as React.ReactElement>, 116 | (child: React.ReactElement>) => { 117 | return React.cloneElement(child, { 118 | n: child.props.n ? child.props.n : n, 119 | xs: child.props.xs ? child.props.xs : xs, 120 | sm: child.props.sm ? child.props.sm : sm, 121 | md: child.props.md ? child.props.md : md, 122 | lg: child.props.lg ? child.props.lg : lg, 123 | xl: child.props.xl ? child.props.xl : xl, 124 | }); 125 | } 126 | )} 127 | 128 | ); 129 | } 130 | ); 131 | 132 | if (__DEV__) { 133 | GridContainer.displayName = 'DecaUI.GridContainer'; 134 | } 135 | 136 | export default GridContainer; 137 | -------------------------------------------------------------------------------- /src/lib/Grid/GridRuler.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { __DEV__ } from '@lib/Utils'; 3 | import { StyledGridRuler, StyledGridRulerItem } from './Grid.styles'; 4 | 5 | /** 6 | * The GridRuler component is meant to be used as a developer tool to ensure items are lined up correctly on a 12 column grid. For this reason, refs are not forwarded to this component. 7 | */ 8 | export interface Props { 9 | /** 10 | * How much spacing there should be between columns. 11 | */ 12 | spacing?: 'none' | 'sm' | 'md' | 'lg'; 13 | } 14 | 15 | const GridRuler = ({ spacing = 'sm' }: Props) => { 16 | return ( 17 | 18 | {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map((i) => ( 19 | 20 | ))} 21 | 22 | ); 23 | }; 24 | 25 | if (__DEV__) { 26 | GridRuler.displayName = 'DecaUI.GridRuler'; 27 | } 28 | 29 | export default GridRuler; 30 | -------------------------------------------------------------------------------- /src/lib/Grid/__snapshots__/Grid.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`components/Grid all sizes render properly on grid container 1`] = ` 4 | 5 |
8 |
11 | 1 12 |
13 |
16 | 2 17 |
18 |
21 | 3 22 |
23 |
24 |
25 | `; 26 | 27 | exports[`components/Grid all sizes render properly on grid item 1`] = ` 28 | 29 |
32 |
35 | 1 36 |
37 |
40 | 2 41 |
42 |
45 | 3 46 |
47 |
48 |
49 | `; 50 | 51 | exports[`components/Grid matches snapshot 1`] = ` 52 | 53 |
56 |
59 | 1 60 |
61 |
64 | 2 65 |
66 |
69 | 3 70 |
71 |
72 |
73 | `; 74 | -------------------------------------------------------------------------------- /src/lib/Grid/index.ts: -------------------------------------------------------------------------------- 1 | import Grid from './Grid'; 2 | import GridContainer from './GridContainer'; 3 | 4 | Grid.Container = GridContainer; 5 | 6 | export * from './Grid'; 7 | export * from './GridContainer'; 8 | export { Grid }; 9 | export default Grid; 10 | -------------------------------------------------------------------------------- /src/lib/Input/Input.stories.tsx: -------------------------------------------------------------------------------- 1 | import { DecaUIProvider } from '@lib/Theme'; 2 | import { ComponentMeta, ComponentStory } from '@storybook/react'; 3 | import React from 'react'; 4 | 5 | import Input from './Input'; 6 | 7 | export default { 8 | title: 'Input', 9 | component: Input, 10 | } as ComponentMeta; 11 | 12 | const Template: ComponentStory = (args) => ; 13 | 14 | export const Solid = Template.bind({}); 15 | 16 | Solid.args = { 17 | label: 'Email Address', 18 | size: 'lg', 19 | helperText: 'Please submit query', 20 | variant: 'solid', 21 | placeholder: 'e.g. johndoe@gmail.com', 22 | focusColor: 'primary', 23 | required: true, 24 | disabled: false, 25 | as: 'input', 26 | maxWidth: false, 27 | initialValue: '', 28 | className: '', 29 | pill: false, 30 | }; 31 | 32 | export const Outlined = Template.bind({}); 33 | 34 | Outlined.args = { 35 | label: 'Email Address', 36 | size: 'lg', 37 | helperText: 'Please submit query', 38 | variant: 'outlined', 39 | focusColor: 'primary', 40 | placeholder: 'e.g. johndoe@gmail.com', 41 | required: true, 42 | disabled: false, 43 | as: 'input', 44 | maxWidth: false, 45 | initialValue: '', 46 | className: '', 47 | pill: false, 48 | }; 49 | 50 | export const WithTheme = Template.bind({}); 51 | 52 | WithTheme.args = { ...Outlined.args }; 53 | WithTheme.decorators = [ 54 | (Story) => ( 55 | 65 | 66 | 67 | ), 68 | ]; 69 | 70 | export const DarkMode = Template.bind({}); 71 | 72 | DarkMode.args = { ...Outlined.args }; 73 | DarkMode.parameters = { backgrounds: { default: 'dark' } }; 74 | 75 | DarkMode.decorators = [ 76 | (Story) => ( 77 | 78 | 79 | 80 | ), 81 | ]; 82 | -------------------------------------------------------------------------------- /src/lib/Input/Input.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, fireEvent, act } from '@testing-library/react'; 2 | import React from 'react'; 3 | 4 | import Input from './Input'; 5 | 6 | describe('components/Input', () => { 7 | it('matches snapshot', () => { 8 | const { asFragment } = render( 9 | 14 | ); 15 | expect(asFragment()).toMatchSnapshot(); 16 | }); 17 | it('onFocus event fires', () => { 18 | const mockFn = jest.fn(); 19 | const utils = render(); 20 | const input = utils.getByLabelText('Label Text'); 21 | act(() => { 22 | input.focus(); 23 | }); 24 | expect(mockFn.mock.calls.length).toBe(1); 25 | }); 26 | 27 | it('renders all colors', () => { 28 | const { asFragment } = render( 29 | <> 30 | 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | expect(asFragment()).toMatchSnapshot(); 38 | }); 39 | it('renders all sizes', () => { 40 | const { asFragment } = render( 41 | <> 42 | 43 | 44 | 45 | 46 | ); 47 | expect(asFragment()).toMatchSnapshot(); 48 | }); 49 | it('renders all variants', () => { 50 | const { asFragment } = render( 51 | <> 52 | 53 | 54 | 55 | ); 56 | expect(asFragment()).toMatchSnapshot(); 57 | }); 58 | it('should ignore events when disabled', () => { 59 | const mockFn = jest.fn(); 60 | const utils = render(); 61 | const input = utils.getByLabelText('Label Text'); 62 | fireEvent.change(input, { target: { value: 'new-value' } }); 63 | expect(mockFn.mock.calls.length).toBe(0); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /src/lib/Input/index.ts: -------------------------------------------------------------------------------- 1 | import Input from './Input'; 2 | export * from './Input'; 3 | export { Input }; 4 | export default Input; 5 | -------------------------------------------------------------------------------- /src/lib/Modal/Modal.cy.tsx: -------------------------------------------------------------------------------- 1 | import { Test } from '../Utils'; 2 | import _cyp from '../../../cypress'; 3 | import React from 'react'; 4 | import Modal, { 5 | ModalProps, 6 | ModalHeaderProps, 7 | ModalBodyProps, 8 | ModalFooterProps, 9 | } from '.'; 10 | import Button from '../Button'; 11 | import Text from '../Text'; 12 | import Input from '../Input'; 13 | import DecaUIProvider from '../Theme'; 14 | 15 | interface ModalComposerProps extends ModalProps { 16 | header?: ModalHeaderProps; 17 | body?: ModalBodyProps; 18 | footer?: ModalFooterProps; 19 | } 20 | 21 | const ModalComposer = (props: ModalComposerProps) => ( 22 | 23 | 24 | 25 | Welcome to DecaUI 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 36 | 37 | 38 | 39 | ); 40 | 41 | const modalSelector = '[data-testid="test.modal"]'; 42 | 43 | describe('components/Modal', () => { 44 | describe('base', () => { 45 | it('background-color', () => { 46 | cy.mount(); 47 | cy.get(modalSelector).should( 48 | 'have.css', 49 | 'background-color', 50 | Test.color('white') 51 | ); 52 | }); 53 | }); 54 | describe('state', () => { 55 | it('not exist in DOM when not open', () => { 56 | cy.mount(); 57 | cy.get(modalSelector).should('not.exist'); 58 | }); 59 | it('exists in DOM when open', () => { 60 | cy.mount(); 61 | cy.get(modalSelector).should('exist'); 62 | }); 63 | }); 64 | describe('autoGap', () => { 65 | it('container', () => { 66 | cy.mount(); 67 | cy.get('.decaModal-flexbox').should('have.css', 'gap', Test.space('4')); 68 | }); 69 | it('header', () => { 70 | cy.mount(); 71 | cy.get('.decaModalHeader-root').should( 72 | 'have.css', 73 | 'gap', 74 | Test.space('2') 75 | ); 76 | }); 77 | it('body', () => { 78 | cy.mount(); 79 | cy.get('.decaModalBody-root').should('have.css', 'gap', Test.space('2')); 80 | }); 81 | it('footer', () => { 82 | cy.mount(); 83 | cy.get('.decaModalFooter-root').should( 84 | 'have.css', 85 | 'gap', 86 | Test.space('2') 87 | ); 88 | }); 89 | }); 90 | describe('disabled autoGap', () => { 91 | it('container', () => { 92 | cy.mount(); 93 | cy.get('.decaModal-flexbox').should('have.css', 'gap', Test.space('n')); 94 | }); 95 | it('header', () => { 96 | cy.mount(); 97 | cy.get('.decaModalHeader-root').should( 98 | 'have.css', 99 | 'gap', 100 | Test.space('n') 101 | ); 102 | }); 103 | it('body', () => { 104 | cy.mount(); 105 | cy.get('.decaModalBody-root').should('have.css', 'gap', Test.space('n')); 106 | }); 107 | it('footer', () => { 108 | cy.mount(); 109 | cy.get('.decaModalFooter-root').should( 110 | 'have.css', 111 | 'gap', 112 | Test.space('n') 113 | ); 114 | }); 115 | }); 116 | 117 | describe('padding', () => { 118 | it('default padding', () => { 119 | cy.mount(); 120 | cy.get(modalSelector).should('have.css', 'padding', Test.space('3')); 121 | }); 122 | it('noPadding', () => { 123 | cy.mount(); 124 | cy.get(modalSelector).should('have.css', 'padding', Test.space('n')); 125 | }); 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /src/lib/Modal/Modal.stories.tsx: -------------------------------------------------------------------------------- 1 | import Button from '@lib/Button'; 2 | import Input from '@lib/Input'; 3 | import Modal from '@lib/Modal'; 4 | import Text from '@lib/Text'; 5 | import { DecaUIProvider } from '@lib/Theme'; 6 | import { ComponentMeta, ComponentStory } from '@storybook/react'; 7 | import React, { useState } from 'react'; 8 | 9 | export default { 10 | title: 'Modal', 11 | component: Modal, 12 | } as ComponentMeta; 13 | 14 | const Template: ComponentStory = (args) => { 15 | const [open, setOpen] = useState(false); 16 | 17 | return ( 18 | <> 19 | 20 | 21 | 22 | 23 | Welcome to DecaUI 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 34 | 35 | 36 | 37 | 38 | ); 39 | }; 40 | 41 | export const Default = Template.bind({}); 42 | Default.args = { 43 | noPadding: false, 44 | autoGap: true, 45 | closeButton: true, 46 | }; 47 | 48 | export const WithTheme = Template.bind({}); 49 | WithTheme.args = { ...Default.args }; 50 | WithTheme.decorators = [ 51 | (Story) => ( 52 | 62 | 63 | 64 | ), 65 | ]; 66 | 67 | export const DarkMode = Template.bind({}); 68 | 69 | DarkMode.args = { ...Default.args }; 70 | DarkMode.parameters = { backgrounds: { default: 'dark' } }; 71 | 72 | DarkMode.decorators = [ 73 | (Story) => ( 74 | 75 | 76 | 77 | ), 78 | ]; 79 | -------------------------------------------------------------------------------- /src/lib/Modal/Modal.styles.tsx: -------------------------------------------------------------------------------- 1 | import { styled, theme } from '@lib/Theme/stitches.config'; 2 | import { animated } from '@react-spring/web'; 3 | import { transparentize } from 'polished'; 4 | 5 | export const StyledModalOverlay = styled(animated.div, { 6 | position: 'fixed', 7 | top: 0, 8 | left: 0, 9 | right: 0, 10 | bottom: 0, 11 | bg: transparentize(0.4, theme.colors.black.value), 12 | zIndex: '$10', 13 | }); 14 | 15 | export const StyledModal = styled(animated.div, { 16 | position: 'fixed', 17 | fontFamily: '$normal', 18 | boxShadow: '$default', 19 | br: '$sm', 20 | zIndex: '$max', 21 | color: '$text', 22 | variants: { 23 | noPadding: { 24 | true: { 25 | p: '$n', 26 | }, 27 | false: { 28 | p: '$3', 29 | }, 30 | }, 31 | isDark: { 32 | true: { 33 | bg: '$popperDarkBg', 34 | }, 35 | false: { 36 | bg: '$popperLightBg', 37 | }, 38 | }, 39 | }, 40 | }); 41 | 42 | export const StyledModalFlexbox = styled('div', { 43 | display: 'flex', 44 | justifyContent: 'center', 45 | flexDirection: 'column', 46 | variants: { 47 | autoGap: { 48 | true: { 49 | gap: '$4', 50 | }, 51 | false: { 52 | gap: '$n', 53 | }, 54 | }, 55 | }, 56 | }); 57 | 58 | export const StyledModalHeader = styled('div', { 59 | color: '$text', 60 | display: 'flex', 61 | justifyContent: 'center', 62 | flexDirection: 'column', 63 | variants: { 64 | autoGap: { 65 | true: { 66 | gap: '$2', 67 | }, 68 | false: { 69 | gap: '$n', 70 | }, 71 | }, 72 | }, 73 | }); 74 | 75 | export const StyledModalBody = styled('div', { 76 | color: '$text', 77 | display: 'flex', 78 | justifyContent: 'center', 79 | flexDirection: 'column', 80 | variants: { 81 | autoGap: { 82 | true: { 83 | gap: '$2', 84 | }, 85 | false: { 86 | gap: '$n', 87 | }, 88 | }, 89 | }, 90 | }); 91 | 92 | export const StyledModalFooter = styled('div', { 93 | color: '$text', 94 | display: 'flex', 95 | justifyContent: 'flex-end', 96 | flexDirection: 'row', 97 | variants: { 98 | autoGap: { 99 | true: { 100 | gap: '$2', 101 | }, 102 | false: { 103 | gap: '$n', 104 | }, 105 | }, 106 | }, 107 | }); 108 | -------------------------------------------------------------------------------- /src/lib/Modal/Modal.test.tsx: -------------------------------------------------------------------------------- 1 | import Modal from '@lib/Modal'; 2 | import { render, screen } from '@testing-library/react'; 3 | import React from 'react'; 4 | 5 | describe('components/Modal', () => { 6 | global.ResizeObserver = require('resize-observer-polyfill'); 7 | it('matches snapshot', () => { 8 | const { asFragment } = render(Hello World); 9 | expect(asFragment()).toMatchSnapshot(); 10 | }); 11 | it('renders text when modal is open', () => { 12 | render(content); 13 | expect(screen.queryByText('content')).not.toBe(null); 14 | }); 15 | it('does not render text when modal is closed', () => { 16 | render(content); 17 | expect(screen.queryByText('content')).toBe(null); 18 | }); 19 | it('modal components all render correctly', () => { 20 | const { asFragment } = render( 21 | 22 | Header component 23 | Header component 24 | Header component 25 | 26 | ); 27 | expect(asFragment()).toMatchSnapshot(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/lib/Modal/ModalBody.tsx: -------------------------------------------------------------------------------- 1 | import { CSS } from '@lib/Theme'; 2 | import { 3 | __DEV__, 4 | PolymorphicRef, 5 | PolymorphicComponentPropWithRef, 6 | } from '@lib/Utils'; 7 | import clsx from 'clsx'; 8 | import React, { useContext } from 'react'; 9 | 10 | import { ModalContext, IModalContext } from './Modal'; 11 | import { StyledModalBody } from './Modal.styles'; 12 | 13 | /** 14 | * ModalBody contains the main content of a modal component 15 | */ 16 | interface Props { 17 | /** 18 | * The content of the component. 19 | */ 20 | children?: React.ReactNode | undefined; 21 | /** 22 | * ClassName applied to the component. 23 | * @default '' 24 | */ 25 | className?: string; 26 | /** 27 | * Override default CSS style. 28 | */ 29 | css?: CSS; 30 | /** 31 | * Have gap between all elements. 32 | */ 33 | autoGap?: boolean; 34 | } 35 | 36 | export type ModalBodyProps = 37 | PolymorphicComponentPropWithRef; 38 | 39 | export type ModalBodyComponent = (( 40 | props: ModalBodyProps 41 | ) => React.ReactElement | null) & { displayName?: string }; 42 | 43 | const ModalBody: ModalBodyComponent = React.forwardRef( 44 | ( 45 | { children, className = '', css, as, autoGap, ...props }: ModalBodyProps, 46 | ref?: PolymorphicRef 47 | ) => { 48 | const context = useContext(ModalContext) as IModalContext; 49 | 50 | const preClass = 'decaModalBody'; 51 | 52 | return ( 53 | 61 | {children} 62 | 63 | ); 64 | } 65 | ); 66 | 67 | if (__DEV__) { 68 | ModalBody.displayName = 'DecaUI.ModalBody'; 69 | } 70 | 71 | export default ModalBody; 72 | -------------------------------------------------------------------------------- /src/lib/Modal/ModalFooter.tsx: -------------------------------------------------------------------------------- 1 | import { CSS } from '@lib/Theme/stitches.config'; 2 | import { 3 | __DEV__, 4 | PolymorphicRef, 5 | PolymorphicComponentPropWithRef, 6 | } from '@lib/Utils'; 7 | import clsx from 'clsx'; 8 | import React, { useContext } from 'react'; 9 | 10 | import { ModalContext, IModalContext } from './Modal'; 11 | import { StyledModalFooter } from './Modal.styles'; 12 | 13 | /** 14 | * ModalFooter allows users to place content on the bottom of their modal component 15 | */ 16 | interface Props { 17 | /** 18 | * The content of the component. 19 | */ 20 | children?: React.ReactNode | undefined; 21 | /** 22 | * ClassName applied to the component. 23 | * @default '' 24 | */ 25 | className?: string; 26 | /** 27 | * Override default CSS style. 28 | */ 29 | css?: CSS; 30 | /** 31 | * Have gap between all elements. 32 | */ 33 | autoGap?: boolean; 34 | } 35 | 36 | export type ModalFooterProps = 37 | PolymorphicComponentPropWithRef; 38 | 39 | export type ModalFooterComponent = (( 40 | props: ModalFooterProps 41 | ) => React.ReactElement | null) & { displayName?: string }; 42 | 43 | const ModalFooter: ModalFooterComponent = React.forwardRef( 44 | ( 45 | { 46 | children, 47 | className = '', 48 | css, 49 | as, 50 | autoGap, 51 | ...props 52 | }: ModalFooterProps, 53 | ref?: PolymorphicRef 54 | ) => { 55 | const context = useContext(ModalContext) as IModalContext; 56 | 57 | const preClass = 'decaModalFooter'; 58 | 59 | return ( 60 | 68 | {children} 69 | 70 | ); 71 | } 72 | ); 73 | 74 | if (__DEV__) { 75 | ModalFooter.displayName = 'DecaUI.ModalFooter'; 76 | } 77 | 78 | export default ModalFooter; 79 | -------------------------------------------------------------------------------- /src/lib/Modal/ModalHeader.tsx: -------------------------------------------------------------------------------- 1 | import { CSS } from '@lib/Theme/stitches.config'; 2 | import { 3 | __DEV__, 4 | PolymorphicRef, 5 | PolymorphicComponentPropWithRef, 6 | } from '@lib/Utils'; 7 | import clsx from 'clsx'; 8 | import React, { useContext } from 'react'; 9 | 10 | import { ModalContext, IModalContext } from './Modal'; 11 | import { StyledModalHeader } from './Modal.styles'; 12 | 13 | /** 14 | * ModalHeader allows users to place a header on their modal component 15 | */ 16 | interface Props { 17 | /** 18 | * The content of the component. 19 | */ 20 | children?: React.ReactNode | undefined; 21 | /** 22 | * ClassName applied to the component. 23 | * @default '' 24 | */ 25 | className?: string; 26 | /** 27 | * Override default CSS style. 28 | */ 29 | css?: CSS; 30 | /** 31 | * Have gap between all elements. 32 | */ 33 | autoGap?: boolean; 34 | } 35 | 36 | export type ModalHeaderProps = 37 | PolymorphicComponentPropWithRef; 38 | 39 | export type ModalHeaderComponent = (( 40 | props: ModalHeaderProps 41 | ) => React.ReactElement | null) & { displayName?: string }; 42 | 43 | const ModalHeader: ModalHeaderComponent = React.forwardRef( 44 | ( 45 | { 46 | children, 47 | className = '', 48 | css, 49 | as, 50 | autoGap, 51 | ...props 52 | }: ModalHeaderProps, 53 | ref?: PolymorphicRef 54 | ) => { 55 | const context = useContext(ModalContext) as IModalContext; 56 | 57 | const preClass = 'decaModalHeader'; 58 | 59 | return ( 60 | 68 | {children} 69 | 70 | ); 71 | } 72 | ); 73 | 74 | if (__DEV__) { 75 | ModalHeader.displayName = 'DecaUI.ModalHeader'; 76 | } 77 | 78 | export default ModalHeader; 79 | -------------------------------------------------------------------------------- /src/lib/Modal/__snapshots__/Modal.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`components/Modal matches snapshot 1`] = ``; 4 | 5 | exports[`components/Modal modal components all render correctly 1`] = ``; 6 | -------------------------------------------------------------------------------- /src/lib/Modal/index.ts: -------------------------------------------------------------------------------- 1 | import Modal from './Modal'; 2 | import ModalBody from './ModalBody'; 3 | import ModalFooter from './ModalFooter'; 4 | import ModalHeader from './ModalHeader'; 5 | 6 | Modal.Header = ModalHeader; 7 | Modal.Body = ModalBody; 8 | Modal.Footer = ModalFooter; 9 | 10 | export * from './Modal'; 11 | export * from './ModalHeader'; 12 | export * from './ModalBody'; 13 | export * from './ModalFooter'; 14 | export { Modal }; 15 | export default Modal; 16 | -------------------------------------------------------------------------------- /src/lib/Popover/Popover.cy.tsx: -------------------------------------------------------------------------------- 1 | import { Test } from '../Utils'; 2 | import _cyp from '../../../cypress'; 3 | import React from 'react'; 4 | import Popover, { PopoverProps } from '.'; 5 | import Button from '../Button'; 6 | 7 | const PopoverComposer = (props: PopoverProps) => ( 8 | 9 | 10 | 11 | 12 | This is the content of the popover. 13 | 14 | ); 15 | 16 | describe('components/Popover', () => { 17 | describe('base', () => { 18 | it('background-color', () => { 19 | cy.mount(); 20 | cy.get('button').click(); 21 | cy.get('.decaPopover-root').should( 22 | 'have.css', 23 | 'background-color', 24 | Test.color('popperLightBg') 25 | ); 26 | }); 27 | it('color', () => { 28 | cy.mount(); 29 | cy.get('button').click(); 30 | cy.get('.decaPopover-root').should( 31 | 'have.css', 32 | 'color', 33 | Test.color('black') 34 | ); 35 | }); 36 | }); 37 | describe('state', () => { 38 | it('not exist in DOM when not open', () => { 39 | cy.mount(); 40 | cy.get('.decaPopover-root').should('not.exist'); 41 | }); 42 | it('exists in DOM when open (button clicked)', () => { 43 | cy.mount(); 44 | cy.get('button').click(); 45 | cy.get('.decaPopover-root').should('exist'); 46 | }); 47 | }); 48 | describe('dark mode', () => { 49 | it('background-color', () => { 50 | cy.darkMount(); 51 | cy.get('button').click(); 52 | cy.get('.decaPopover-root').should( 53 | 'have.css', 54 | 'background-color', 55 | Test.color('popperDarkBg') 56 | ); 57 | }); 58 | it('color', () => { 59 | cy.darkMount(); 60 | cy.get('button').click(); 61 | cy.get('.decaPopover-root').should( 62 | 'have.css', 63 | 'color', 64 | Test.color('white') 65 | ); 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/lib/Popover/Popover.stories.tsx: -------------------------------------------------------------------------------- 1 | import Button from '@lib/Button'; 2 | import Popover from '@lib/Popover'; 3 | import { DecaUIProvider } from '@lib/Theme'; 4 | import { ComponentMeta, ComponentStory } from '@storybook/react'; 5 | import React from 'react'; 6 | 7 | export default { 8 | title: 'Popover', 9 | component: Popover, 10 | } as ComponentMeta; 11 | 12 | const Template: ComponentStory = (args) => ( 13 | 14 | 15 | 16 | 17 | This is the content of the popover. 18 | 19 | ); 20 | 21 | export const Default = Template.bind({}); 22 | Default.args = { 23 | placement: 'bottom', 24 | action: 'click', 25 | }; 26 | 27 | export const WithTheme = Template.bind({}); 28 | 29 | WithTheme.args = { ...Default.args }; 30 | WithTheme.decorators = [ 31 | (Story) => ( 32 | 42 | 43 | 44 | ), 45 | ]; 46 | 47 | export const DarkMode = Template.bind({}); 48 | 49 | DarkMode.args = { ...Default.args }; 50 | DarkMode.parameters = { backgrounds: { default: 'dark' } }; 51 | 52 | DarkMode.decorators = [ 53 | (Story) => ( 54 | 55 | 56 | 57 | ), 58 | ]; 59 | -------------------------------------------------------------------------------- /src/lib/Popover/Popover.styles.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from '@lib/Theme/stitches.config'; 2 | import { animated } from '@react-spring/web'; 3 | 4 | export const StyledPopover = styled(animated.div, { 5 | fontFamily: '$normal', 6 | p: '$3', 7 | boxShadow: '$default', 8 | br: '$sm', 9 | color: '$text', 10 | zIndex: '$5', 11 | variants: { 12 | isDark: { 13 | true: { 14 | bg: '$popperDarkBg', 15 | }, 16 | false: { 17 | bg: '$popperLightBg', 18 | }, 19 | }, 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /src/lib/Popover/Popover.test.tsx: -------------------------------------------------------------------------------- 1 | import Button from '@lib/Button'; 2 | import Popover from '@lib/Popover'; 3 | import { render, fireEvent, act, screen } from '@testing-library/react'; 4 | import React from 'react'; 5 | 6 | describe('components/Popover', () => { 7 | global.ResizeObserver = require('resize-observer-polyfill'); 8 | it('matches snapshot', () => { 9 | const { asFragment } = render( 10 | 11 | 12 | 13 | 14 | content 15 | 16 | ); 17 | expect(asFragment()).toMatchSnapshot(); 18 | }); 19 | it('click action', () => { 20 | const utils = render( 21 | 22 | 23 | 24 | 25 | content 26 | 27 | ); 28 | expect(screen.queryByText('content')).toBe(null); 29 | const popoverTrigger = utils.getByText('Open Popover'); 30 | act(() => { 31 | popoverTrigger.click(); 32 | }); 33 | expect(screen.queryByText('content')).not.toBe(null); 34 | }); 35 | it('hover action', async () => { 36 | const utils = render( 37 | 38 | 39 | 40 | 41 | content 42 | 43 | ); 44 | expect(screen.queryByText('content')).toBe(null); 45 | const popoverTrigger = utils.getByText('Open Popover'); 46 | act(() => { 47 | fireEvent.mouseEnter(popoverTrigger); 48 | }); 49 | expect(screen.queryByText('content')).not.toBe(null); 50 | act(() => { 51 | fireEvent.mouseLeave(popoverTrigger); 52 | }); 53 | await act(async () => new Promise((r) => setTimeout(r, 1000))); 54 | expect(screen.queryByText('content')).toBe(null); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/lib/Popover/Popover.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | useFloating, 3 | offset as floatingOffset, 4 | flip, 5 | shift, 6 | autoUpdate, 7 | UseFloatingReturn, 8 | Placement, 9 | } from '@floating-ui/react-dom'; 10 | import { 11 | MasterComponent, 12 | PolymorphicRef, 13 | PolymorphicComponentPropWithRef, 14 | __DEV__, 15 | } from '@lib/Utils'; 16 | import React, { 17 | useState, 18 | useEffect, 19 | useMemo, 20 | SetStateAction, 21 | Dispatch, 22 | } from 'react'; 23 | import PopoverTrigger from './PopoverTrigger'; 24 | import PopoverContent from './PopoverContent'; 25 | 26 | /** 27 | * A Popover can be used to display some content on top of another. 28 | */ 29 | export interface Props { 30 | /** 31 | * The content of the component. It is usually the `Popover.Trigger`, 32 | * and `Popover.Content` 33 | */ 34 | children?: React.ReactNode[]; 35 | /* 36 | * If true, the component is shown. 37 | */ 38 | open?: boolean; 39 | /** 40 | * State dispatcher function (setter in useState) 41 | */ 42 | setOpen?: Dispatch>; 43 | /** 44 | * Placement of the popover component 45 | * @default bottom 46 | */ 47 | placement?: Placement; 48 | /** 49 | * Determines what action needs to take place in order for popover to appear 50 | * @default click 51 | */ 52 | action?: 'click' | 'hover'; 53 | /** 54 | * How far away PopoverContent should be away from PopoverTrigger when opened 55 | * @default 10 56 | */ 57 | offset?: number; 58 | } 59 | 60 | export interface IPopoverContext extends UseFloatingReturn { 61 | triggerRef?: React.Ref; 62 | open?: boolean; 63 | setOpen?: Dispatch>; 64 | mainComponentRef: PolymorphicRef; 65 | action: 'click' | 'hover'; 66 | } 67 | 68 | export const PopoverContext = React.createContext(null); 69 | 70 | export type PopoverProps = 71 | PolymorphicComponentPropWithRef; 72 | 73 | export type PopoverComponent = (( 74 | props: PopoverProps 75 | ) => React.ReactElement | null) & { displayName?: string }; 76 | 77 | const Popover: PopoverComponent = React.forwardRef( 78 | ( 79 | { 80 | children, 81 | open, 82 | setOpen, 83 | placement = 'bottom', 84 | action = 'click', 85 | offset = 10, 86 | }: PopoverProps, 87 | ref?: PolymorphicRef 88 | ) => { 89 | const floatingProps = useFloating({ 90 | placement: placement, 91 | whileElementsMounted: autoUpdate, 92 | strategy: 'absolute', 93 | middleware: [floatingOffset(offset), flip(), shift()], 94 | }); 95 | 96 | const [selfOpen, setSelfOpen] = useState(false); 97 | 98 | const [scrollPos, setScrollPos] = useState(0); 99 | 100 | const isControlledComponent = useMemo(() => open !== undefined, [open]); 101 | 102 | const isScrolling = () => { 103 | if (window.scrollY !== scrollPos) { 104 | isControlledComponent ? setOpen && setOpen(false) : setSelfOpen(false); 105 | setScrollPos(window.scrollY); 106 | } 107 | }; 108 | 109 | const handleEsc = (e: KeyboardEvent) => { 110 | if (e.key === 'Escape') { 111 | isControlledComponent ? setOpen && setOpen(false) : setSelfOpen(false); 112 | } 113 | }; 114 | 115 | useEffect(() => { 116 | window.addEventListener('scroll', isScrolling); 117 | window.addEventListener('keydown', handleEsc); 118 | return () => { 119 | window.removeEventListener('scroll', isScrolling); 120 | window.removeEventListener('keydown', handleEsc); 121 | }; 122 | }, []); 123 | 124 | const [trigger, content] = React.Children.toArray(children); 125 | 126 | const triggerRef = React.useRef(); 127 | 128 | return ( 129 | 139 | {trigger} 140 | {content} 141 | 142 | ); 143 | } 144 | ); 145 | 146 | if (__DEV__) { 147 | Popover.displayName = 'DecaUI.Popover'; 148 | } 149 | 150 | export default Popover as MasterComponent< 151 | HTMLDivElement, 152 | PopoverProps, 153 | { 154 | Trigger: typeof PopoverTrigger; 155 | Content: typeof PopoverContent; 156 | } 157 | >; 158 | -------------------------------------------------------------------------------- /src/lib/Popover/PopoverContent.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeContext } from '@lib/Theme'; 2 | import { CSS } from '@lib/Theme/stitches.config'; 3 | import { 4 | PolymorphicRef, 5 | PolymorphicComponentPropWithRef, 6 | mergeRefs, 7 | useClickOutside, 8 | __DEV__, 9 | } from '@lib/Utils'; 10 | import { animated, useTransition } from '@react-spring/web'; 11 | import clsx from 'clsx'; 12 | import React, { useState, useEffect, useContext } from 'react'; 13 | import ReactDOM from 'react-dom'; 14 | 15 | import { PopoverContext, IPopoverContext } from './Popover'; 16 | import { StyledPopover } from './Popover.styles'; 17 | 18 | /** 19 | * PopoverContent contains the content shown when the trigger is executed 20 | */ 21 | interface Props { 22 | /** 23 | * The content of the component. 24 | */ 25 | children?: React.ReactNode | undefined; 26 | /** 27 | * Override default CSS style. 28 | */ 29 | css?: CSS; 30 | /** 31 | * ClassName applied to the component. 32 | * @default '' 33 | */ 34 | className?: string; 35 | } 36 | 37 | export type PopoverContentProps = 38 | PolymorphicComponentPropWithRef; 39 | 40 | export type PopoverContentComponent = (( 41 | props: PopoverContentProps 42 | ) => React.ReactElement | null) & { displayName?: string }; 43 | 44 | const PopoverContent: PopoverContentComponent = React.forwardRef( 45 | ( 46 | { children, css, className = '', as, ...props }: PopoverContentProps, 47 | ref?: PolymorphicRef 48 | ) => { 49 | const context = useContext(PopoverContext) as IPopoverContext; 50 | 51 | const clickOutsideRef = useClickOutside(() => { 52 | context.setOpen && context.setOpen(false); 53 | }, [context.triggerRef]); 54 | 55 | const transition = useTransition(context.open, { 56 | from: { 57 | scale: 0.75, 58 | opacity: 0, 59 | }, 60 | enter: { 61 | scale: 1, 62 | opacity: 1, 63 | }, 64 | leave: { 65 | scale: 0.75, 66 | opacity: 0, 67 | }, 68 | config: { 69 | tension: 300, 70 | friction: 19, 71 | }, 72 | }); 73 | 74 | const preClass = 'decaPopover'; 75 | 76 | const { dark } = React.useContext(ThemeContext); 77 | 78 | const [DOM, setDOM] = useState(false); 79 | 80 | useEffect(() => { 81 | setDOM(true); 82 | }, []); 83 | 84 | if (DOM) { 85 | return ReactDOM.createPortal( 86 | transition( 87 | (style, item) => 88 | item && ( 89 | 108 | {children} 109 | 110 | ) 111 | ), 112 | document.getElementById('decaUI-provider') 113 | ? (document.getElementById('decaUI-provider') as Element) 114 | : (document.querySelector('body') as Element) 115 | ); 116 | } 117 | return <>; 118 | } 119 | ); 120 | 121 | if (__DEV__) { 122 | PopoverContent.displayName = 'DecaUI.PopoverContent'; 123 | } 124 | 125 | export default PopoverContent; 126 | -------------------------------------------------------------------------------- /src/lib/Popover/PopoverTrigger.tsx: -------------------------------------------------------------------------------- 1 | import { mergeRefs, __DEV__ } from '@lib/Utils'; 2 | import React, { useContext } from 'react'; 3 | 4 | import { PopoverContext, IPopoverContext } from './Popover'; 5 | 6 | /** 7 | * PopoverTrigger opens the popover's content. It must be an interactive element 8 | * such as `button` or `a`. 9 | */ 10 | export interface Props { 11 | /** 12 | * The content of the component. 13 | */ 14 | children?: React.ReactNode | undefined; 15 | } 16 | 17 | const PopoverTrigger = ({ children }: Props) => { 18 | const context = useContext(PopoverContext) as IPopoverContext; 19 | 20 | // enforce single child 21 | const child: any = React.Children.only(children); 22 | 23 | if (context.action === 'click') { 24 | const extendedOnClick = () => { 25 | context.setOpen && context.setOpen((prevState) => !prevState); 26 | child.props.onClick && child.props.onClick(); 27 | }; 28 | 29 | return React.cloneElement(child, { 30 | ...child.props, 31 | onClick: extendedOnClick, 32 | ref: mergeRefs(context.reference, child.ref, context.triggerRef), 33 | }); 34 | } else { 35 | const extendedOnMouseEnter = () => { 36 | context.setOpen && context.setOpen(true); 37 | child.props.onMouseEnter && child.props.onMouseEnter(); 38 | }; 39 | 40 | const extendedOnMouseLeave = () => { 41 | context.setOpen && context.setOpen(false); 42 | child.props.onMouseLeave && child.props.onMouseLeave(); 43 | }; 44 | 45 | return React.cloneElement(child, { 46 | ...child.props, 47 | onMouseEnter: extendedOnMouseEnter, 48 | onMouseLeave: extendedOnMouseLeave, 49 | ref: mergeRefs(context.reference, child.ref, context.triggerRef), 50 | }); 51 | } 52 | }; 53 | 54 | if (__DEV__) { 55 | PopoverTrigger.displayName = 'DecaUI.PopoverTrigger'; 56 | } 57 | 58 | export default PopoverTrigger; 59 | -------------------------------------------------------------------------------- /src/lib/Popover/__snapshots__/Popover.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`components/Popover matches snapshot 1`] = ` 4 | 5 | 11 | 12 | `; 13 | -------------------------------------------------------------------------------- /src/lib/Popover/index.ts: -------------------------------------------------------------------------------- 1 | import Popover from './Popover'; 2 | import PopoverContent from './PopoverContent'; 3 | import PopoverTrigger from './PopoverTrigger'; 4 | 5 | Popover.Trigger = PopoverTrigger; 6 | Popover.Content = PopoverContent; 7 | 8 | export * from './Popover'; 9 | export { Popover }; 10 | export default Popover; 11 | -------------------------------------------------------------------------------- /src/lib/Radio/Radio.cy.tsx: -------------------------------------------------------------------------------- 1 | import { standardColors } from '../Theme'; 2 | import { Test } from '../Utils'; 3 | import _cyp from '../../../cypress'; 4 | import React from 'react'; 5 | import Radio from './Radio'; 6 | 7 | describe('components/Radio', () => { 8 | describe('before click', () => { 9 | it('border-color', () => { 10 | cy.mount(); 11 | 12 | cy.get('label') 13 | .before('border-color') 14 | .should('eq', Test.color('gray600')); 15 | }); 16 | it('disabled', () => { 17 | cy.mount(); 18 | 19 | cy.get('label').should('have.css', 'color', Test.color('gray500')); 20 | 21 | cy.get('label') 22 | .before('border-color') 23 | .should('eq', Test.color('gray400')); 24 | }); 25 | }); 26 | describe('colors', () => { 27 | it('label color', () => { 28 | cy.mount(); 29 | cy.get('label').should('have.css', 'color', Test.color('black')); 30 | }); 31 | standardColors.map((color) => { 32 | describe(color, () => { 33 | it('background-color', () => { 34 | cy.mount(); 35 | 36 | cy.get('label').click(); 37 | 38 | // wait for css transition to finish 39 | cy.wait(250); 40 | cy.get('.decaRadio-circle').should( 41 | 'have.css', 42 | 'background-color', 43 | Test.color(color) 44 | ); 45 | }); 46 | it('border-color', () => { 47 | cy.mount(); 48 | 49 | cy.get('label').click(); 50 | 51 | // wait for css transition to finish 52 | cy.wait(250); 53 | cy.get('label') 54 | .before('border-color') 55 | .should('eq', Test.color(color)); 56 | }); 57 | it('disabled', () => { 58 | cy.mount( 59 | 60 | ); 61 | 62 | cy.get('label').before('opacity').should('eq', '0.6'); 63 | cy.get('.decaRadio-circle').should('have.css', 'opacity', '0.5'); 64 | }); 65 | }); 66 | }); 67 | }); 68 | describe('sizes', () => { 69 | it('sm', () => { 70 | cy.mount(); 71 | cy.get('label').before('width').should('eq', Test.size('2')); 72 | cy.get('label').before('height').should('eq', Test.size('2')); 73 | cy.get('label').before('margin-right').should('eq', Test.space('1')); 74 | cy.get('label').should('have.css', 'font-size', Test.fontSize('caption')); 75 | cy.get('.decaRadio-circle').should('have.css', 'width', Test.size('1')); 76 | }); 77 | it('md', () => { 78 | cy.mount(); 79 | cy.get('label').before('width').should('eq', Test.size('3')); 80 | cy.get('label').before('height').should('eq', Test.size('3')); 81 | cy.get('label').before('margin-right').should('eq', Test.space('2')); 82 | cy.get('label').should('have.css', 'font-size', Test.fontSize('bodySm')); 83 | cy.get('.decaRadio-circle').should('have.css', 'width', Test.size('2')); 84 | }); 85 | it('lg', () => { 86 | cy.mount(); 87 | cy.get('label').before('width').should('eq', Test.size('4')); 88 | cy.get('label').before('height').should('eq', Test.size('4')); 89 | cy.get('label').before('margin-right').should('eq', Test.space('2')); 90 | cy.get('label').should('have.css', 'font-size', Test.fontSize('body')); 91 | cy.get('.decaRadio-circle').should('have.css', 'width', Test.size('3')); 92 | }); 93 | }); 94 | 95 | describe('no label', () => { 96 | describe('should have no margin', () => { 97 | it('sm', () => { 98 | cy.mount(); 99 | cy.get('label').before('margin-right').should('eq', '0px'); 100 | }); 101 | it('md', () => { 102 | cy.mount(); 103 | cy.get('label').before('margin-right').should('eq', '0px'); 104 | }); 105 | it('sm', () => { 106 | cy.mount(); 107 | cy.get('label').before('margin-right').should('eq', '0px'); 108 | }); 109 | }); 110 | }); 111 | 112 | describe('dark mode', () => { 113 | it('label color', () => { 114 | cy.darkMount(); 115 | cy.get('label').should('have.css', 'color', Test.color('white')); 116 | }); 117 | 118 | it('disabled state', () => { 119 | cy.darkMount(); 120 | cy.get('label').before('opacity').should('eq', '0.35'); 121 | cy.get('.decaRadio-circle').should('have.css', 'opacity', '0.35'); 122 | }); 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /src/lib/Radio/Radio.stories.tsx: -------------------------------------------------------------------------------- 1 | import { DecaUIProvider } from '@lib/Theme'; 2 | import { ComponentMeta, ComponentStory } from '@storybook/react'; 3 | import React from 'react'; 4 | 5 | import Radio from './Radio'; 6 | 7 | export default { 8 | title: 'Radio', 9 | component: Radio, 10 | } as ComponentMeta; 11 | 12 | const Template: ComponentStory = (args) => ; 13 | 14 | export const Default = Template.bind({}); 15 | Default.args = { 16 | label: 'Label', 17 | size: 'md', 18 | color: 'primary', 19 | disabled: false, 20 | css: {}, 21 | className: '', 22 | }; 23 | 24 | export const NoLabel = Template.bind({}); 25 | NoLabel.args = { 26 | ...Default.args, 27 | label: '', 28 | }; 29 | 30 | export const WithTheme = Template.bind({}); 31 | 32 | WithTheme.args = { ...Default.args }; 33 | WithTheme.decorators = [ 34 | (Story) => ( 35 | 42 | 43 | 44 | ), 45 | ]; 46 | 47 | export const DarkMode = Template.bind({}); 48 | 49 | DarkMode.args = { ...Default.args }; 50 | DarkMode.parameters = { backgrounds: { default: 'dark' } }; 51 | 52 | DarkMode.decorators = [ 53 | (Story) => ( 54 | 55 | 56 | 57 | ), 58 | ]; 59 | -------------------------------------------------------------------------------- /src/lib/Radio/Radio.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | import React from 'react'; 3 | 4 | import Radio from './Radio'; 5 | 6 | describe('components/Radio', () => { 7 | it('matches snapshot', () => { 8 | const { asFragment } = render(); 9 | expect(asFragment()).toMatchSnapshot(); 10 | }); 11 | it('renders all colors', () => { 12 | const { asFragment } = render( 13 | <> 14 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | expect(asFragment()).toMatchSnapshot(); 22 | }); 23 | it('renders all sizes', () => { 24 | const { asFragment } = render( 25 | <> 26 | 27 | 28 | 29 | 30 | ); 31 | expect(asFragment()).toMatchSnapshot(); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/lib/Radio/RadioGroup/RadioGroup.stories.tsx: -------------------------------------------------------------------------------- 1 | import Radio from '@lib/Radio'; 2 | import { DecaUIProvider } from '@lib/Theme'; 3 | import { ComponentMeta, ComponentStory } from '@storybook/react'; 4 | import React from 'react'; 5 | 6 | export default { 7 | title: 'RadioGroup', 8 | component: Radio.Group, 9 | } as ComponentMeta; 10 | 11 | const Template: ComponentStory = (args) => ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | 20 | export const Default = Template.bind({}); 21 | Default.args = { 22 | defaultValue: 'A', 23 | name: 'FormGroup-Radio', 24 | disabled: false, 25 | className: '', 26 | color: 'primary', 27 | }; 28 | 29 | export const SingleDisabled = () => ( 30 | 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | 38 | export const WithTheme = Template.bind({}); 39 | 40 | WithTheme.args = { ...Default.args }; 41 | WithTheme.decorators = [ 42 | (Story) => ( 43 | 50 | 51 | 52 | ), 53 | ]; 54 | 55 | export const DarkMode = Template.bind({}); 56 | 57 | DarkMode.args = { ...Default.args }; 58 | DarkMode.parameters = { backgrounds: { default: 'dark' } }; 59 | 60 | DarkMode.decorators = [ 61 | (Story) => ( 62 | 63 | 64 | 65 | ), 66 | ]; 67 | -------------------------------------------------------------------------------- /src/lib/Radio/RadioGroup/RadioGroup.styles.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from '@lib/Theme/stitches.config'; 2 | 3 | export const StyledRadioGroupWrapper = styled('div', { 4 | position: 'relative', 5 | boxSizing: 'border-box', 6 | display: 'flex', 7 | flexDirection: 'column', 8 | gap: '$2', 9 | }); 10 | -------------------------------------------------------------------------------- /src/lib/Radio/RadioGroup/RadioGroup.test.tsx: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | import Radio from '@lib/Radio'; 3 | import { render, screen } from '@testing-library/react'; 4 | import userEvent from '@testing-library/user-event'; 5 | import React from 'react'; 6 | 7 | describe('components/RadioGroup', () => { 8 | it('matches snapshot', () => { 9 | const { asFragment } = render( 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | expect(asFragment()).toMatchSnapshot(); 18 | }); 19 | it('works as an uncontrolled component', async () => { 20 | const user = userEvent.setup(); 21 | render( 22 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | 30 | expect(screen.getByLabelText('Option A')).toBeChecked(); 31 | await user.click(screen.getByLabelText('Option C')); 32 | expect(screen.getByLabelText('Option C')).toBeChecked(); 33 | }); 34 | it('works as a controlled component', async () => { 35 | let value = 'A'; 36 | const user = userEvent.setup(); 37 | 38 | render( 39 | ) => 42 | (value = e.target.value) 43 | } 44 | > 45 | 46 | 47 | 48 | 49 | 50 | ); 51 | 52 | expect(screen.getByLabelText('Option A')).toBeChecked(); 53 | await user.click(screen.getByLabelText('Option C')); 54 | expect(value).toBe('C'); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/lib/Radio/RadioGroup/RadioGroup.tsx: -------------------------------------------------------------------------------- 1 | import { CSS, StandardColors } from '@lib/Theme/stitches.config'; 2 | import { RadioProps } from '../Radio'; 3 | import { 4 | PolymorphicRef, 5 | PolymorphicComponentPropWithRef, 6 | uuid, 7 | __DEV__, 8 | } from '@lib/Utils'; 9 | import clsx from 'clsx'; 10 | import React, { useMemo } from 'react'; 11 | 12 | import { StyledRadioGroupWrapper } from './RadioGroup.styles'; 13 | 14 | /** 15 | * RadioGroup is a helpful wrapper used to group Radio button components. 16 | */ 17 | interface Props { 18 | /** 19 | * The content of the component. 20 | */ 21 | children?: 22 | | Array>> 23 | | React.ReactElement>; 24 | /** 25 | * The default value. Used when component is not controlled. 26 | */ 27 | defaultValue?: string; 28 | /** 29 | * ClassName applied to the component. 30 | * @default '' 31 | */ 32 | className?: string; 33 | /** 34 | * The name used to reference the value of the control. If you do not provide this prop, it falls back to a randomly generated name. 35 | */ 36 | name?: string; 37 | /** 38 | * Callback fired when a radio button is selected. 39 | */ 40 | onChange?(e: React.ChangeEvent): void; 41 | /** 42 | * Value of the selected radio button. 43 | */ 44 | value?: string; 45 | /** 46 | * Apply disabled state to all radio buttons in the radio group component 47 | * @default false 48 | */ 49 | disabled?: boolean; 50 | /** 51 | * Color of radio buttons when active. 52 | */ 53 | color?: StandardColors; 54 | /** 55 | * Size of each radio button. 56 | */ 57 | size?: 'sm' | 'md' | 'lg'; 58 | /** 59 | * Override default CSS style. 60 | */ 61 | css?: CSS; 62 | } 63 | 64 | export type RadioGroupProps = 65 | PolymorphicComponentPropWithRef; 66 | 67 | export type RadioGroupComponent = (( 68 | props: RadioGroupProps 69 | ) => React.ReactElement | null) & { displayName?: string }; 70 | 71 | const RadioGroup: RadioGroupComponent = React.forwardRef( 72 | ( 73 | { 74 | children, 75 | defaultValue, 76 | className = '', 77 | name, 78 | onChange, 79 | value, 80 | disabled = false, 81 | color, 82 | size, 83 | as, 84 | css, 85 | ...props 86 | }: RadioGroupProps, 87 | ref?: PolymorphicRef 88 | ) => { 89 | const presetId = uuid('radio'); 90 | 91 | const getName = useMemo(() => { 92 | if (name) { 93 | return name; 94 | } 95 | return presetId; 96 | }, [name]); 97 | 98 | const preClass = 'decaRadioGroup'; 99 | 100 | return ( 101 | 108 | {React.Children.map( 109 | children as React.ReactElement>, 110 | (child: React.ReactElement>) => { 111 | return React.cloneElement(child, { 112 | name: getName, 113 | onChange, 114 | initialSelect: child.props.value === defaultValue, 115 | selected: value ? child.props.value === value : undefined, 116 | disabled: disabled ? disabled : child.props.disabled, 117 | color, 118 | size, 119 | ...child.props, 120 | }); 121 | } 122 | )} 123 | 124 | ); 125 | } 126 | ); 127 | 128 | if (__DEV__) { 129 | RadioGroup.displayName = 'DecaUI.RadioGroup'; 130 | } 131 | 132 | export default RadioGroup; 133 | -------------------------------------------------------------------------------- /src/lib/Radio/RadioGroup/__snapshots__/RadioGroup.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`components/RadioGroup matches snapshot 1`] = ` 4 | 5 |
8 |
11 | 19 |