├── .all-contributorsrc ├── .changeset ├── README.md └── config.json ├── .commitlintrc.json ├── .eslintignore ├── .eslintrc ├── .github └── workflows │ └── next.yml ├── .gitignore ├── .huskyrc.json ├── .lintstagedrc ├── .npmignore ├── .prettierignore ├── .prettierrc ├── .storybook ├── main.js └── preview.tsx ├── .vscode ├── extensions.json └── settings.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── clean-package.config.json ├── jest.config.js ├── lerna-debug.log ├── lerna.json ├── package.json ├── packages ├── alert │ ├── CHANGELOG.md │ ├── README.md │ ├── __tests__ │ │ └── alert.test.tsx │ ├── package.json │ ├── src │ │ ├── Alert.tsx │ │ ├── icons.tsx │ │ └── index.ts │ ├── stories │ │ └── alert.stories.tsx │ └── tsconfig.json ├── avatar │ ├── CHANGELOG.md │ ├── README.md │ ├── __tests__ │ │ ├── avatar-group.test.tsx │ │ └── avatar.test.tsx │ ├── package.json │ ├── src │ │ ├── avatar.tsx │ │ ├── avatarGroup.tsx │ │ ├── index.ts │ │ └── randomBgColors.ts │ ├── stories │ │ └── avatar.stories.tsx │ └── tsconfig.json ├── breadcrumb │ ├── CHANGELOG.md │ ├── README.md │ ├── __tests__ │ │ └── breadcrumb.test.tsx │ ├── package.json │ ├── src │ │ ├── breadcrumb.tsx │ │ └── index.ts │ ├── stories │ │ └── breadcrumb.stories.tsx │ └── tsconfig.json ├── button │ ├── CHANGELOG.md │ ├── README.md │ ├── __tests__ │ │ └── button.test.tsx │ ├── package.json │ ├── src │ │ ├── button-effects.ts │ │ ├── button.tsx │ │ ├── icon-button.tsx │ │ └── index.ts │ ├── stories │ │ ├── button.stories.tsx │ │ └── icon-button.stories.tsx │ └── tsconfig.json ├── checkbox │ ├── CHANGELOG.md │ ├── README.md │ ├── __tests__ │ │ └── checkbox.test.tsx │ ├── package.json │ ├── src │ │ ├── checkbox-group.tsx │ │ ├── checkbox-icon.tsx │ │ ├── checkbox-types.ts │ │ ├── checkbox.tsx │ │ ├── index.ts │ │ ├── use-checkbox-group.ts │ │ └── use-checkbox.ts │ ├── stories │ │ └── checkbox.stories.tsx │ └── tsconfig.json ├── clickable │ ├── CHANGELOG.md │ ├── README.md │ ├── __tests__ │ │ └── clickable.test.tsx │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── use-clickable.tsx │ ├── stories │ │ └── use-clickable.stories.tsx │ └── tsconfig.json ├── close-button │ ├── CHANGELOG.md │ ├── README.md │ ├── __tests__ │ │ └── close-button.test.tsx │ ├── package.json │ ├── src │ │ ├── close-button.tsx │ │ └── index.ts │ ├── stories │ │ └── close-button.stories.tsx │ └── tsconfig.json ├── color │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ ├── color.ts │ │ └── index.ts │ ├── stories │ │ └── color.stories.tsx │ └── tsconfig.json ├── container │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── container.tsx │ │ └── index.ts │ ├── stories │ │ └── container.stories.tsx │ └── tsconfig.json ├── core │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── counter │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── use-counter.ts │ ├── stories │ │ └── use-counter.stories.tsx │ ├── tests │ │ └── use-counter.test.tsx │ └── tsconfig.json ├── descendant │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── use-descendant.tsx │ ├── stories │ │ └── use-descendant.stories.tsx │ └── tsconfig.json ├── drawer │ ├── CHANGELOG.md │ ├── README.md │ ├── __tests__ │ │ └── drawer.test.tsx │ ├── package.json │ ├── src │ │ ├── drawer.tsx │ │ └── index.ts │ ├── stories │ │ └── drawer.stories.tsx │ └── tsconfig.json ├── focus-lock │ ├── CHANGELOG.md │ ├── README.md │ ├── __tests__ │ │ └── focus-lock.test.tsx │ ├── package.json │ ├── src │ │ ├── focus-lock.tsx │ │ └── index.ts │ └── tsconfig.json ├── form-control │ ├── CHANGELOG.md │ ├── README.md │ ├── __tests__ │ │ └── form-control.test.tsx │ ├── package.json │ ├── src │ │ ├── form-control.tsx │ │ ├── form-error.tsx │ │ ├── form-helper.tsx │ │ ├── form-label.tsx │ │ ├── index.ts │ │ └── use-form-control.ts │ ├── stories │ │ └── form-control.stories.tsx │ └── tsconfig.json ├── hooks │ ├── CHANGELOG.md │ ├── README.md │ ├── __tests__ │ │ └── use-controllable.test.tsx │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── use-animation.ts │ │ ├── use-boolean.ts │ │ ├── use-callback-ref.ts │ │ ├── use-clipboard.ts │ │ ├── use-controllable.ts │ │ ├── use-disclosure.ts │ │ ├── use-event-listener.ts │ │ ├── use-focus-effect.ts │ │ ├── use-focus-on-pointer-down.ts │ │ ├── use-force-update.ts │ │ ├── use-id.ts │ │ ├── use-interval.ts │ │ ├── use-latest-ref.ts │ │ ├── use-safe-layout-effect.ts │ │ ├── use-timeout.ts │ │ ├── use-unmount-effect.ts │ │ └── use-update-effect.ts │ └── tsconfig.json ├── icon │ ├── CHANGELOG.md │ ├── README.md │ ├── __tests__ │ │ ├── create-icon.test.tsx │ │ └── icon.test.tsx │ ├── package.json │ ├── src │ │ ├── create-icon.tsx │ │ ├── icon.tsx │ │ └── index.ts │ ├── stories │ │ └── icon.stories.tsx │ └── tsconfig.json ├── icons │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ ├── Add.tsx │ │ ├── ArrowBack.tsx │ │ ├── ArrowDown.tsx │ │ ├── ArrowForward.tsx │ │ ├── ArrowLeft.tsx │ │ ├── ArrowRight.tsx │ │ ├── ArrowUp.tsx │ │ ├── ArrowUpDown.tsx │ │ ├── AtSign.tsx │ │ ├── Attachment.tsx │ │ ├── Bell.tsx │ │ ├── Calendar.tsx │ │ ├── Chat.tsx │ │ ├── Check.tsx │ │ ├── CheckCircle.tsx │ │ ├── ChevronDown.tsx │ │ ├── ChevronLeft.tsx │ │ ├── ChevronRight.tsx │ │ ├── ChevronUp.tsx │ │ ├── Close.tsx │ │ ├── Copy.tsx │ │ ├── Delete.tsx │ │ ├── Download.tsx │ │ ├── DragHandle.tsx │ │ ├── Edit.tsx │ │ ├── Email.tsx │ │ ├── ExternalLink.tsx │ │ ├── Info.tsx │ │ ├── InfoOutline.tsx │ │ ├── Link.tsx │ │ ├── Lock.tsx │ │ ├── Minus.tsx │ │ ├── Moon.tsx │ │ ├── NotAllowed.tsx │ │ ├── Phone.tsx │ │ ├── PlusSquare.tsx │ │ ├── Question.tsx │ │ ├── QuestionOutline.tsx │ │ ├── Repeat.tsx │ │ ├── RepeatClock.tsx │ │ ├── Search.tsx │ │ ├── Search2.tsx │ │ ├── Settings.tsx │ │ ├── SmallAdd.tsx │ │ ├── SmallClose.tsx │ │ ├── Spinner.tsx │ │ ├── Star.tsx │ │ ├── Sun.tsx │ │ ├── Time.tsx │ │ ├── TriangleDown.tsx │ │ ├── TriangleUp.tsx │ │ ├── Unlock.tsx │ │ ├── UpDown.tsx │ │ ├── View.tsx │ │ ├── ViewOff.tsx │ │ ├── Warning.tsx │ │ ├── WarningTwo.tsx │ │ └── index.ts │ ├── stories │ │ └── icons.stories.tsx │ └── tsconfig.json ├── image │ ├── CHANGELOG.md │ ├── README.md │ ├── __tests__ │ │ ├── image.test.tsx │ │ └── lazy-image.test.tsx │ ├── package.json │ ├── src │ │ ├── image.tsx │ │ ├── index.ts │ │ ├── lazy-image.tsx │ │ └── use-image.tsx │ ├── stories │ │ ├── image.stories.tsx │ │ └── lazy-image.stories.tsx │ └── tsconfig.json ├── input │ ├── CHANGELOG.md │ ├── README.md │ ├── __tests__ │ │ └── input.test.tsx │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── input-addon.tsx │ │ └── input.tsx │ ├── stories │ │ └── input.stories.tsx │ ├── tsconfig.json │ └── tsup.config.bundled_otmscst7zn.mjs ├── layout │ ├── CHANGELOG.md │ ├── README.md │ ├── __tests__ │ │ └── layout.test.tsx │ ├── package.json │ ├── src │ │ ├── badge.tsx │ │ ├── box.tsx │ │ ├── divider.tsx │ │ ├── index.ts │ │ └── stack.tsx │ ├── stories │ │ ├── badge.stories.tsx │ │ ├── box.stories.tsx │ │ ├── divider.stories.tsx │ │ └── stack.stories.tsx │ └── tsconfig.json ├── modal │ ├── CHANGELOG.md │ ├── README.md │ ├── __tests__ │ │ └── modal.test.tsx │ ├── package.json │ ├── src │ │ ├── alert-dialog.tsx │ │ ├── index.ts │ │ ├── modal-manager.ts │ │ ├── modal-transition.tsx │ │ ├── modal.tsx │ │ └── use-modal.ts │ ├── stories │ │ └── modal.stories.tsx │ └── tsconfig.json ├── number-input │ ├── CHANGELOG.md │ ├── README.md │ ├── __tests__ │ │ └── number-input.test.tsx │ ├── package.json │ ├── src │ │ ├── icons.tsx │ │ ├── index.ts │ │ ├── number-input.tsx │ │ ├── use-number-input.ts │ │ ├── use-spinner.ts │ │ └── utils.ts │ ├── stories │ │ └── number-input.stories.tsx │ └── tsconfig.json ├── popover │ ├── CHANGELOG.md │ ├── README.md │ ├── __tests__ │ │ └── popover.test.tsx │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── popover-anchor.tsx │ │ ├── popover-arrow.tsx │ │ ├── popover-body.tsx │ │ ├── popover-close-button.tsx │ │ ├── popover-content.tsx │ │ ├── popover-context.ts │ │ ├── popover-footer.tsx │ │ ├── popover-header.tsx │ │ ├── popover-transition.tsx │ │ ├── popover-trigger.tsx │ │ ├── popover.tsx │ │ └── use-popover.ts │ ├── stories │ │ └── popover.stories.tsx │ └── tsconfig.json ├── popper │ ├── CHANGELOG.md │ ├── README.md │ ├── __tests__ │ │ └── popper.test.tsx │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── popper.modifiers.ts │ │ ├── popper.placement.ts │ │ ├── popper.utils.ts │ │ └── use-popper.ts │ ├── stories │ │ └── popper.stories.tsx │ └── tsconfig.json ├── portal │ ├── CHANGELOG.md │ ├── README.md │ ├── __tests__ │ │ └── portal.test.tsx │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── portal-manager.tsx │ │ └── portal.tsx │ ├── stories │ │ └── portal.stories.tsx │ └── tsconfig.json ├── progress │ ├── CHANGELOG.md │ ├── README.md │ ├── __tests__ │ │ └── progress.test.tsx │ ├── package.json │ ├── src │ │ ├── circular-progress.tsx │ │ ├── index.ts │ │ ├── progress.tsx │ │ └── progress.utils.tsx │ ├── stories │ │ ├── circular-progress.stories.tsx │ │ └── progress.stories.tsx │ └── tsconfig.json ├── radio │ ├── CHANGELOG.md │ ├── README.md │ ├── __tests__ │ │ ├── radio-group.test.tsx │ │ └── radio.test.tsx │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── radio-group.tsx │ │ ├── radio.tsx │ │ ├── use-radio-group.ts │ │ └── use-radio.ts │ ├── stories │ │ └── radio.stories.tsx │ └── tsconfig.json ├── react-types │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.d.ts │ └── tsconfig.json ├── react-utils │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── children.ts │ │ ├── context.ts │ │ ├── index.ts │ │ ├── refs.ts │ │ └── types.ts │ └── tsconfig.json ├── spinner │ ├── CHANGELOG.md │ ├── README.md │ ├── __tests__ │ │ └── spinner.test.tsx │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── spinner.tsx │ ├── stories │ │ └── spinner.stories.tsx │ └── tsconfig.json ├── system │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── create-component.tsx │ │ ├── index.ts │ │ ├── jsx.tsx │ │ ├── system.tsx │ │ ├── system.types.tsx │ │ └── system.utils.ts │ ├── stories │ │ └── system.stories.tsx │ └── tsconfig.json ├── toast │ ├── CHANGELOG.md │ ├── README.md │ ├── __tests__ │ │ ├── standalone-toast.test.tsx │ │ └── use-toast.test.tsx │ ├── package.json │ ├── src │ │ ├── create-standalone-toast.tsx │ │ ├── index.ts │ │ ├── toast.component.tsx │ │ ├── toast.placement.ts │ │ ├── toast.provider.tsx │ │ ├── toast.tsx │ │ ├── toast.types.ts │ │ ├── toast.utils.ts │ │ ├── use-toast-provider.tsx │ │ └── use-toast.tsx │ ├── stories │ │ └── toast.stories.tsx │ └── tsconfig.json ├── transition │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── collapse.tsx │ │ ├── fade.tsx │ │ ├── index.ts │ │ ├── scale-fade.tsx │ │ ├── slide-fade.tsx │ │ ├── slide.tsx │ │ └── transition-utils.tsx │ ├── stories │ │ ├── collapse.stories.tsx │ │ ├── fade.stories.tsx │ │ ├── modal.tsx │ │ ├── scale-fade.stories.tsx │ │ ├── slide-fade.stories.tsx │ │ └── slide.stories.tsx │ └── tsconfig.json ├── utils │ ├── CHANGELOG.md │ ├── README.md │ ├── __tests__ │ │ ├── array.test.ts │ │ ├── assertion.test.ts │ │ ├── dom.test.ts │ │ ├── functions.test.ts │ │ ├── number.test.ts │ │ └── object.test.ts │ ├── package.json │ ├── src │ │ ├── array.ts │ │ ├── assertions.ts │ │ ├── dom-query.ts │ │ ├── dom.ts │ │ ├── focus.ts │ │ ├── functions.ts │ │ ├── index.ts │ │ ├── lazy.ts │ │ ├── logger.ts │ │ ├── numbers.ts │ │ ├── objects.ts │ │ ├── react-helpers.ts │ │ ├── tabbable.ts │ │ └── types.ts │ ├── stories │ │ └── merge-refs.stories.tsx │ └── tsconfig.json └── visually-hidden │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── react-shim.js │ ├── src │ ├── index.ts │ └── visually-hidden.tsx │ ├── stories │ └── visually-hidden.stories.tsx │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── postcss.config.js ├── public ├── full-logo.png └── sitemap.xml ├── react-shim.js ├── release-template.md ├── scripts └── setup-test.ts ├── tailwind.config.js ├── tooling ├── babel-plugin │ ├── .babelrc │ ├── .eslintrc │ ├── CHANGELOG.md │ ├── package.json │ └── src │ │ └── index.js └── test-utils │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ └── src │ ├── accessibility.tsx │ ├── focus.ts │ ├── hooks.ts │ ├── index.ts │ ├── mock-image.ts │ ├── press.ts │ ├── render.tsx │ ├── test-utils.ts │ ├── user-event.ts │ └── utils.ts ├── tsconfig.json ├── tsup.config.ts └── turbo.json /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "nature-ui", 3 | "projectOwner": "DNature", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": true, 11 | "commitConvention": "gitmoji", 12 | "contributors": [ 13 | { 14 | "login": "DNature", 15 | "name": "Divine Hycenth", 16 | "avatar_url": "https://avatars.githubusercontent.com/u/49137104?v=4", 17 | "profile": "https://divinehycenth.com/", 18 | "contributions": [ 19 | "code", 20 | "content", 21 | "design", 22 | "doc", 23 | "maintenance", 24 | "talk" 25 | ] 26 | } 27 | ], 28 | "contributorsPerLine": 7 29 | } 30 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.6.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "linked": [], 6 | "access": "restricted", 7 | "baseBranch": "main", 8 | "updateInternalDependencies": "patch", 9 | "ignore": [] 10 | } 11 | -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"], 3 | "rules": { 4 | "type-enum": [ 5 | 1, 6 | "always", 7 | [ 8 | "bug", 9 | "build", 10 | "ci", 11 | "docs", 12 | "feat", 13 | "fix", 14 | "perf", 15 | "refactor", 16 | "revert", 17 | "style", 18 | "test", 19 | "docs", 20 | "merge", 21 | "chore", 22 | "sprint" 23 | ] 24 | ] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | coverage 4 | build 5 | *.config.js -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | node_modules 6 | /.pnp 7 | .pnp.js 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | /dist 15 | tooling/**/dist 16 | dist 17 | packages/**/tsconfig.tsbuildinfo 18 | tsconfig.tsbuildinfo 19 | 20 | # misc 21 | .DS_Store 22 | .env.local 23 | .env.development.local 24 | .env.test.local 25 | .env.production.local 26 | .env 27 | 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | *.backup 32 | *.log 33 | 34 | storybook-static 35 | 36 | # Local Netlify folder 37 | .netlify -------------------------------------------------------------------------------- /.huskyrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 4 | "pre-commit": "lint-staged" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "**/*.{ts,tsx,js,jsx}": [ 3 | "prettier --write" 4 | ], 5 | "**/*.{md,yml,json,babelrc,eslintrc,prettierrc}": [ 6 | "prettier --write" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /test 3 | *.npmrc 4 | *.npmignore 5 | /__tests__ 6 | *.backup 7 | *.log -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | coverage 4 | .next 5 | build 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "arrowParens": "always", 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "jsxSingleQuote": true, 7 | "proseWrap": "always", 8 | "bracketSpacing": true, 9 | "endOfLine": "lf", 10 | "trailingComma": "all", 11 | "jsxBracketSameLine": false 12 | } 13 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | stories: ['../packages/**/*.stories.@(ts|tsx|mdx)'], 6 | addons: [ 7 | '@storybook/addon-links', 8 | '@storybook/addon-essentials', 9 | '@storybook/addon-a11y', 10 | { 11 | name: '@storybook/addon-postcss', 12 | options: { 13 | cssLoaderOptions: { 14 | importLoaders: 1, 15 | }, 16 | postcssLoaderOptions: { 17 | implementation: require('postcss'), 18 | }, 19 | }, 20 | }, 21 | ], 22 | typescript: { 23 | reactDocgen: false, 24 | }, 25 | core: { 26 | builder: 'webpack5', 27 | }, 28 | webpackFinal: (config) => { 29 | config.resolve.alias = { 30 | ...config.resolve.alias, 31 | '@nature-ui/core': path.resolve(__dirname, '../packages/core/src'), 32 | }; 33 | config.resolve.extensions.push('.ts', '.tsx'); 34 | 35 | return config; 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /.storybook/preview.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import 'tailwindcss/tailwind.css'; 3 | 4 | const withNature: React.FC = (StoryFn) => { 5 | return ( 6 | 7 | {/* */} 8 |
9 | 10 |
11 | {/*
*/} 12 |
13 | ); 14 | }; 15 | 16 | export const decorators = [withNature]; 17 | 18 | export const parameters = { 19 | controls: { 20 | expanded: true, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "bradlc.vscode-tailwindcss", 5 | "christian-kohler.path-intellisense", 6 | "eamodio.gitlens", 7 | "esbenp.prettier-vscode", 8 | "formulahendry.auto-rename-tag", 9 | "jpoissonnier.vscode-styled-components", 10 | "rvest.vs-code-prettier-eslint", 11 | "steoates.autoimport" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": true, 4 | "source.organizeImports": true 5 | }, 6 | "files.exclude": { 7 | "**/.git": true, 8 | "**/.svn": true, 9 | "**/.hg": true, 10 | "**/CVS": true, 11 | "**/.DS_Store": true 12 | }, 13 | "typescript.tsdk": "node_modules/typescript/lib" 14 | } 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Segun Adebayo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /clean-package.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "replace": { 3 | "main": "dist/index.js", 4 | "module": "dist/index.mjs", 5 | "types": "dist/index.d.ts", 6 | "exports": { 7 | ".": { 8 | "types": "./dist/index.d.ts", 9 | "import": "./dist/index.mjs", 10 | "require": "./dist/index.js" 11 | }, 12 | "./package.json": "./package.json" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const { defaults: tsjPreset } = require('ts-jest/presets'); 2 | 3 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 4 | module.exports = { 5 | preset: 'ts-jest', 6 | testEnvironment: 'jsdom', 7 | collectCoverageFrom: ['packages/**/*.{ts,tsx}'], 8 | transform: { 9 | ...tsjPreset.transform, 10 | // '^.+\\.tsx?$': [ 11 | // 'ts-jest', 12 | // { 13 | // isolatedModules: true, 14 | // // ts-jest configuration goes here 15 | // }, 16 | // ], 17 | }, 18 | setupFilesAfterEnv: ['./scripts/setup-test.ts'], 19 | }; 20 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["packages/*", "tooling/*"], 3 | "version": "independent", 4 | "npmClient": "yarn", 5 | "useWorkspace": true, 6 | "registry": "http://localhost:4873", 7 | "command": { 8 | "publish": { 9 | "conventionalCommit": true, 10 | "message": "chore(release): publish", 11 | "ignoreChanges": ["**/stories/**", "**/tests/**"] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/alert/README.md: -------------------------------------------------------------------------------- 1 | # Alert `@nature-ui/alert` 2 | 3 | > # Alert 4 | 5 | A react component used to alert users of a particular screen area that needs 6 | user action 7 | 8 | ## Installation 9 | 10 | ```sh 11 | npm i @nature-ui/alert 12 | 13 | # or 14 | 15 | yarn add @nature-ui/alert 16 | ``` 17 | 18 | ## Usage 19 | 20 | ```js 21 | import Alert from '@nature-ui/alert'; 22 | 23 | () => { 24 | return ( 25 | 26 | This is a success alert! 27 | 28 | ); 29 | }; 30 | ``` 31 | -------------------------------------------------------------------------------- /packages/alert/__tests__/alert.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen, testA11y } from '@nature-ui/test-utils'; 2 | 3 | import { 4 | Alert, 5 | AlertDescription, 6 | AlertIcon, 7 | AlertTitle, 8 | AlertWrapper, 9 | } from '../src'; 10 | 11 | describe('@nature-ui/alert ', () => { 12 | test('should have no accessibility issue', async () => { 13 | await testA11y( 14 | 15 | 16 | Alert title 17 | Alert description 18 | , 19 | ); 20 | 21 | await testA11y(Alert description); 22 | }); 23 | 24 | test('should have role="alert"', async () => { 25 | render(Alert description); 26 | 27 | expect(screen.getByRole('alert')).toBeInTheDocument(); 28 | }); 29 | 30 | test('should have role="alert"', () => { 31 | render( 32 | 33 | 34 | Alert title 35 | Alert description 36 | , 37 | ); 38 | 39 | expect(screen.getByRole('alert')).toBeInTheDocument(); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /packages/alert/src/icons.tsx: -------------------------------------------------------------------------------- 1 | import { Icon, IconProps } from '@nature-ui/icon'; 2 | import React from 'react'; 3 | 4 | export const CheckIcon: React.FC = (props) => ( 5 | 6 | 10 | 11 | ); 12 | 13 | export const InfoIcon: React.FC = (props) => ( 14 | 15 | 19 | 20 | ); 21 | 22 | export const WarningIcon: React.FC = (props) => ( 23 | 24 | 28 | 29 | ); 30 | -------------------------------------------------------------------------------- /packages/alert/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Alert'; 2 | -------------------------------------------------------------------------------- /packages/alert/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src", "index.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/avatar/README.md: -------------------------------------------------------------------------------- 1 | # @nature-ui/avatar 2 | 3 | The Avatar component is used to represent user, and displays the profile 4 | picture, initials or fallback icon. 5 | 6 | ## Installation 7 | 8 | ```sh 9 | yarn add @nature-ui/avatar 10 | 11 | # or 12 | 13 | npm i @nature-ui/avatar 14 | ``` 15 | 16 | ## Import components 17 | 18 | ```jsx 19 | import { Avatar, AvatarGroup } from '@nature-ui/avatar'; 20 | ``` 21 | 22 | ## Basic Usage 23 | 24 | Simply import the `Avatar` component and pass it the image `src` and name of the 25 | user in the avatar. 26 | 27 | ```jsx 28 | 29 | ``` 30 | 31 | Stack Avatars in a group by using the `AvatarGroup` component 32 | 33 | ```jsx 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ``` 43 | -------------------------------------------------------------------------------- /packages/avatar/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './avatar'; 2 | export * from './avatarGroup'; 3 | -------------------------------------------------------------------------------- /packages/avatar/src/randomBgColors.ts: -------------------------------------------------------------------------------- 1 | import { randomColor, isDark } from '@nature-ui/color'; 2 | 3 | export const randomBgColors = (name?: string) => { 4 | const bg = name ? randomColor({ string: name }) : 'rgb(226, 232, 240)'; 5 | const isBgDark = isDark(bg); 6 | 7 | const color = name ? (isBgDark ? '#fff' : '#1A202C') : '#fff'; 8 | const borderColor = '#fff'; 9 | 10 | return { 11 | bg, 12 | color, 13 | borderColor, 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /packages/avatar/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src", "index.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/breadcrumb/__tests__/breadcrumb.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@nature-ui/test-utils'; 2 | import { Breadcrumb, BreadcrumbItem, BreadcrumbLink } from '../src'; 3 | 4 | describe('@nature-ui/breadcrumb', () => { 5 | test('has the proper aria-attributes', () => { 6 | const { getByText, getAllByRole, getByLabelText } = render( 7 | 8 | 9 | Link 1 10 | 11 | 12 | Link 2 13 | 14 | 15 | Link 3 16 | 17 | , 18 | ); 19 | 20 | // surrounding `nav` has aria-label="breadcrumb" 21 | getByLabelText('breadcrumb', { selector: 'nav' }); 22 | 23 | // `isCurrentPage` link has aria-current="page" 24 | const currentPageLink = getByText('Link 3'); 25 | 26 | expect(currentPageLink).toHaveAttribute('aria-current', 'page'); 27 | 28 | // separator receives presentation="role" 29 | expect(getAllByRole('presentation')).toHaveLength(2); 30 | }); 31 | 32 | test('seperator can be changed', () => { 33 | const { getAllByText } = render( 34 | 35 | 36 | Link 1 37 | 38 | 39 | Link 2 40 | 41 | , 42 | ); 43 | 44 | expect(getAllByText('-')).toHaveLength(1); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /packages/breadcrumb/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './breadcrumb'; 2 | -------------------------------------------------------------------------------- /packages/breadcrumb/stories/breadcrumb.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter, Link } from 'react-router-dom'; 3 | import { Breadcrumb, BreadcrumbItem, BreadcrumbLink } from '../src'; 4 | 5 | export default { 6 | title: 'Breadcrumb', 7 | }; 8 | 9 | export const Default = () => ( 10 | 11 | 12 | 13 | 14 | Home 15 | 16 | 17 | 18 | 19 | 20 | About 21 | 22 | 23 | 24 | 25 | 29 | Contact 30 | 31 | 32 | 33 | 34 | ); 35 | 36 | export const Separator = () => ( 37 | 38 | 39 | Home 40 | 41 | 42 | 43 | About 44 | 45 | 46 | 47 | Current 48 | 49 | 50 | ); 51 | -------------------------------------------------------------------------------- /packages/breadcrumb/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src", "index.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/button/src/button-effects.ts: -------------------------------------------------------------------------------- 1 | import { css, keyframes } from '@nature-ui/system'; 2 | 3 | export const _ripple = keyframes` 4 | 50% { 5 | opacity: 0.3; 6 | } 7 | 100% { 8 | opacity: 0; 9 | transform: translate(-50%, -50%) scale(10); 10 | } 11 | `; 12 | 13 | export const rippleEffect = css` 14 | line-height: 1.2; 15 | 16 | &::after { 17 | content: ''; 18 | position: absolute; 19 | top: 50%; 20 | left: 50%; 21 | width: 1em; 22 | height: 1em; 23 | background: currentColor; 24 | border-radius: 50%; 25 | opacity: 0; 26 | } 27 | 28 | &:focus:not(:active)::after { 29 | animation: 0.3s ${_ripple}; 30 | } 31 | `; 32 | -------------------------------------------------------------------------------- /packages/button/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './button'; 2 | export * from './icon-button'; 3 | -------------------------------------------------------------------------------- /packages/button/stories/icon-button.stories.tsx: -------------------------------------------------------------------------------- 1 | import { PhoneIcon, SearchIcon } from '@nature-ui/icons'; 2 | import { Stack } from '@nature-ui/layout'; 3 | import { Meta, Story } from '@storybook/react'; 4 | import React from 'react'; 5 | import { HiHeart } from 'react-icons/hi'; 6 | import { IconButton, IconButtonProps } from '../src'; 7 | 8 | export default { 9 | title: 'Button/IconButton', 10 | component: IconButton, 11 | } as Meta; 12 | 13 | export const iconButton: Story = (args) => ( 14 | 15 | } {...args} /> 16 | 17 | } 21 | {...args} 22 | /> 23 | 24 | 30 | 31 | 32 | 33 | ); 34 | -------------------------------------------------------------------------------- /packages/button/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src", "index.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/checkbox/README.md: -------------------------------------------------------------------------------- 1 | # @nature-ui/checkbox 2 | 3 | Checkbox component is used in forms when a user needs to select multiple values 4 | from several options. 5 | 6 | ## Installation 7 | 8 | ```sh 9 | yarn add @nature-ui/checkbox 10 | 11 | # or 12 | 13 | npm i @nature-ui/checkbox 14 | ``` 15 | 16 | ## Import component 17 | 18 | ```jsx 19 | import { Checkbox, CheckboxGroup } from '@nature-ui/checkbox'; 20 | ``` 21 | 22 | ## Usage 23 | 24 | ```jsx 25 | This is a checkbox 26 | ``` 27 | 28 | ### CheckboxGroup 29 | 30 | CheckboxGroup is used to bind multiple checkboxes into a group, and it indicates 31 | whether one or more options are selected. 32 | 33 | ```jsx 34 | 35 | 36 | One 37 | Two 38 | Three 39 | 40 | 41 | ``` 42 | -------------------------------------------------------------------------------- /packages/checkbox/src/checkbox-group.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, __DEV__ } from '@nature-ui/utils'; 2 | import React from 'react'; 3 | 4 | import { 5 | useCheckboxGroup, 6 | UseCheckboxGroupProps, 7 | UseCheckboxGroupReturn, 8 | } from './use-checkbox-group'; 9 | 10 | export interface CheckboxGroupProps extends UseCheckboxGroupProps { 11 | children?: React.ReactNode; 12 | } 13 | 14 | export type CheckboxGroupContext = Pick< 15 | UseCheckboxGroupReturn, 16 | 'onChange' | 'value' | 'size' | 'color' | 'hoverColor' 17 | > & {}; 18 | 19 | const [CheckboxGroupContextProvider, useCheckboxGroupContext] = 20 | createContext({ 21 | name: 'CheckboxGroupContext', 22 | strict: false, 23 | }); 24 | 25 | export { useCheckboxGroupContext }; 26 | 27 | /** 28 | * Used for multiple checkboxes which are bound in one group 29 | * and it indicates whether one or more options are selected 30 | * 31 | */ 32 | export const CheckboxGroup = (props: CheckboxGroupProps) => { 33 | const { children, color, hoverColor, size } = props; 34 | const { value, onChange } = useCheckboxGroup(props); 35 | 36 | const group = React.useMemo( 37 | () => ({ 38 | onChange, 39 | value, 40 | color, 41 | hoverColor, 42 | size, 43 | }), 44 | [onChange, value], 45 | ); 46 | 47 | return ( 48 | 49 | {children} 50 | 51 | ); 52 | }; 53 | 54 | if (__DEV__) { 55 | CheckboxGroup.displayName = 'CheckboxGroup'; 56 | } 57 | -------------------------------------------------------------------------------- /packages/checkbox/src/checkbox-icon.tsx: -------------------------------------------------------------------------------- 1 | import { Icon, SvgIconProps } from '@nature-ui/icon'; 2 | 3 | const CheckIcon = (props: SvgIconProps) => { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | ); 11 | }; 12 | 13 | export const MinusIcon = (props: SvgIconProps) => { 14 | return ( 15 | 16 | 17 | 18 | ); 19 | }; 20 | 21 | export type CheckboxIconProps = SvgIconProps & { 22 | isChecked?: boolean; 23 | isIndeterminate?: boolean; 24 | }; 25 | 26 | /** 27 | * CheckboxIcon 28 | * 29 | * Icon for visually indicating the checked or indeterminate 30 | * state of a checkbox 31 | */ 32 | export const CheckboxIcon = (props: CheckboxIconProps) => { 33 | const { isChecked, isIndeterminate, ...rest } = props; 34 | 35 | if (isChecked) { 36 | return ; 37 | } 38 | 39 | if (isIndeterminate) { 40 | return ; 41 | } 42 | 43 | return null; 44 | }; 45 | -------------------------------------------------------------------------------- /packages/checkbox/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './checkbox'; 2 | export * from './checkbox-group'; 3 | export * from './checkbox-types'; 4 | export * from './use-checkbox'; 5 | export * from './use-checkbox-group'; 6 | -------------------------------------------------------------------------------- /packages/checkbox/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src", "index.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/clickable/README.md: -------------------------------------------------------------------------------- 1 | # @nature-ui/clickable 2 | 3 | React hook that implements all the interactions of a native `button` component 4 | with support for making it focusable even if it's disabled. 5 | 6 | It can be used with both native button elements or other elements (like `div`). 7 | 8 | ## Installation 9 | 10 | ```jsx 11 | import { useClickable } from '@nature-ui/clickable'; 12 | ``` 13 | 14 | ## Usage 15 | 16 | ```jsx 17 | // create a clickable primitive 18 | const Clickable = (props) => { 19 | const clickable = useClickable(props); 20 | return 34 | 35 | counter.update(e.target.value)} 39 | /> 40 | 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /packages/counter/tests/use-counter.test.tsx: -------------------------------------------------------------------------------- 1 | import { invoke, renderHook } from '@nature-ui/test-utils'; 2 | import { useCounter } from '../src'; 3 | 4 | test('should increment', () => { 5 | const { result } = renderHook(() => useCounter({ defaultValue: 0 })); 6 | invoke(result.current.increment); 7 | expect(result.current.valueAsNumber).toBe(1); 8 | }); 9 | 10 | test('should decrement', () => { 11 | const { result } = renderHook(() => useCounter({ defaultValue: 0 })); 12 | invoke(result.current.decrement); 13 | expect(result.current.valueAsNumber).toBe(-1); 14 | }); 15 | 16 | test('should increment with step', () => { 17 | const { result } = renderHook(() => useCounter({ defaultValue: 0, step: 5 })); 18 | invoke(result.current.decrement); 19 | expect(result.current.valueAsNumber).toBe(-5); 20 | }); 21 | 22 | test('should not exceed max', () => { 23 | const { result } = renderHook(() => 24 | useCounter({ defaultValue: 0, step: 5, max: 4 }), 25 | ); 26 | invoke(result.current.increment); 27 | expect(result.current.valueAsNumber).toBe(4); 28 | }); 29 | 30 | test('should exceed max but be out-of-range', () => { 31 | const { result } = renderHook(() => 32 | useCounter({ defaultValue: 0, step: 5, max: 4, keepWithinRange: false }), 33 | ); 34 | invoke(result.current.increment); 35 | expect(result.current.valueAsNumber).toBe(5); 36 | expect(result.current.isOutOfRange).toBeTruthy(); 37 | }); 38 | 39 | test('should increment with small step', () => { 40 | const { result } = renderHook(() => 41 | useCounter({ defaultValue: 0.2, step: 0.1, max: 4 }), 42 | ); 43 | invoke(result.current.increment); 44 | expect(result.current.valueAsNumber).toBe(0.3); 45 | }); 46 | -------------------------------------------------------------------------------- /packages/counter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src", "index.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/descendant/README.md: -------------------------------------------------------------------------------- 1 | # Descendant 2 | 3 | A descendant index solution for better accessibility support in compound 4 | components. 5 | 6 | > **Note 🚨:** This package is primarily intended for internal use by the Nature 7 | > UI library. You should not use it directly in your production projects. 8 | 9 | ## Installation 10 | 11 | ```sh 12 | yarn add @nature-ui/descendant 13 | 14 | # or 15 | 16 | npm i @nature-ui/descendant 17 | ``` 18 | -------------------------------------------------------------------------------- /packages/descendant/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './use-descendant'; 2 | -------------------------------------------------------------------------------- /packages/descendant/stories/use-descendant.stories.tsx: -------------------------------------------------------------------------------- 1 | import { createContext } from '@nature-ui/utils'; 2 | import React from 'react'; 3 | 4 | import { DescendantContext, useDescendant, useDescendants } from '../src'; 5 | 6 | export default { 7 | title: 'Descendants', 8 | component: useDescendants, 9 | }; 10 | 11 | type Context = DescendantContext; 12 | 13 | const [Provider, useDescendantCtx] = createContext({ 14 | name: 'DescendantContext', 15 | }); 16 | 17 | const Option = ({ 18 | value, 19 | disabled, 20 | focusable, 21 | }: { 22 | value?: string; 23 | disabled?: boolean; 24 | focusable?: boolean; 25 | }) => { 26 | const context = useDescendantCtx(); 27 | 28 | const ref = React.useRef(null); 29 | 30 | const index = useDescendant({ 31 | element: ref.current, 32 | value, 33 | disabled, 34 | focusable, 35 | context, 36 | }); 37 | 38 | return ( 39 |
40 | Option {index + 1} 41 |
42 | ); 43 | }; 44 | 45 | const Select = ({ children }: { children?: React.ReactNode }) => { 46 | const context = useDescendants(); 47 | 48 | return {children}; 49 | }; 50 | 51 | export const Default = () => ( 52 | 61 | ); 62 | -------------------------------------------------------------------------------- /packages/descendant/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src", "index.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/drawer/README.md: -------------------------------------------------------------------------------- 1 | # Drawer 2 | 3 | The Drawer component is a panel that slides out from the edge of the screen. It 4 | can be useful when you need users to complete a task or view some details 5 | without leaving the current page. 6 | 7 | ## Installation 8 | 9 | ```sh 10 | yarn add @nature-ui/drawer 11 | ``` 12 | 13 | ## Import component 14 | 15 | ```jsx 16 | import { Drawer, DrawerContent, DrawerOverlay } from '@nature-ui/drawer'; 17 | ``` 18 | 19 | ## Usage 20 | 21 | ```jsx 22 | <> 23 | 24 | setOpen(false)}> 25 | 26 | 27 |
This is the drawer content
28 | 29 |
30 |
31 |
32 | 33 | ``` 34 | -------------------------------------------------------------------------------- /packages/drawer/__tests__/drawer.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen, testA11y } from '@nature-ui/test-utils'; 2 | import React from 'react'; 3 | import { 4 | Drawer, 5 | DrawerBody, 6 | DrawerContent, 7 | DrawerHeader, 8 | DrawerOverlay, 9 | DrawerProps, 10 | } from '../src'; 11 | 12 | const SimpleDrawer = (props: { 13 | placement?: DrawerProps['placement']; 14 | isOpen?: boolean; 15 | }) => { 16 | const [isOpen, setIsOpen] = React.useState(props.isOpen || false); 17 | const onClose = () => setIsOpen(false); 18 | 19 | return ( 20 | 21 | 22 | 23 | Basic Drawer 24 | 25 |

Some contents...

26 |

Some contents...

27 |

Some contents...

28 |
29 |
30 |
31 | ); 32 | }; 33 | 34 | it('does not render when isOpen is false', () => { 35 | render(); 36 | 37 | expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); 38 | }); 39 | 40 | it('does renders when isOpen is true', () => { 41 | render(); 42 | 43 | expect(screen.queryByRole('dialog')).toBeInTheDocument(); 44 | }); 45 | 46 | it('passes a11y test', async () => { 47 | await testA11y(); 48 | }); 49 | 50 | it("renders on the correct side under 'ltr' direction", () => { 51 | render(); 52 | 53 | expect(screen.queryByRole('dialog')).toHaveStyle('left: 0'); 54 | }); 55 | -------------------------------------------------------------------------------- /packages/drawer/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './drawer'; 2 | -------------------------------------------------------------------------------- /packages/drawer/stories/drawer.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Button, ButtonProps } from '@nature-ui/button'; 2 | import React from 'react'; 3 | import { Drawer, DrawerContent, DrawerOverlay } from '../src'; 4 | 5 | export default { 6 | title: 'Drawer', 7 | component: Drawer, 8 | }; 9 | 10 | const StyledButton = (args: ButtonProps) => ( 11 | 47 | 48 | 49 | 50 | 51 | Confirmation! 52 | Are you sure you want to have that milkshake? 53 | 54 | 55 | ``` 56 | -------------------------------------------------------------------------------- /packages/popover/__tests__/popover.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@nature-ui/test-utils'; 2 | import { usePopover } from '../src'; 3 | 4 | // TODO: Test this component 5 | const Component = () => { 6 | const { getTriggerProps, getPopoverProps, onClose } = usePopover(); 7 | 8 | return ( 9 |
10 | 11 |
Popover content
12 | 13 |
14 | ); 15 | }; 16 | 17 | describe('@nature-ui/popover', () => { 18 | test('Popover renders correctly', async () => { 19 | render(); 20 | expect(screen.getByText('Open')).toBeInTheDocument(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /packages/popover/src/index.ts: -------------------------------------------------------------------------------- 1 | export { Popover } from './popover'; 2 | export type { PopoverProps } from './popover'; 3 | export { PopoverAnchor } from './popover-anchor'; 4 | export { PopoverArrow } from './popover-arrow'; 5 | export type { PopoverArrowProps } from './popover-arrow'; 6 | export { PopoverBody } from './popover-body'; 7 | export type { PopoverBodyProps } from './popover-body'; 8 | export { 9 | PopoverCloseButton, 10 | type PopoverCloseButtonProps, 11 | } from './popover-close-button'; 12 | export { PopoverContent } from './popover-content'; 13 | export type { PopoverContentProps } from './popover-content'; 14 | export { PopoverFooter } from './popover-footer'; 15 | export type { PopoverFooterProps } from './popover-footer'; 16 | export { PopoverHeader } from './popover-header'; 17 | export type { PopoverHeaderProps } from './popover-header'; 18 | export { PopoverTrigger } from './popover-trigger'; 19 | export { 20 | usePopover, 21 | type UsePopoverProps, 22 | type UsePopoverReturn, 23 | } from './use-popover'; 24 | -------------------------------------------------------------------------------- /packages/popover/src/popover-anchor.tsx: -------------------------------------------------------------------------------- 1 | import { Children, cloneElement } from 'react'; 2 | import { usePopoverContext } from './popover-context'; 3 | 4 | /** 5 | * PopoverAnchor is element that is used as the positioning reference 6 | * for the popover. 7 | */ 8 | 9 | export function PopoverAnchor(props: React.PropsWithChildren<{}>) { 10 | // enforce a single child 11 | const child: any = Children.only(props.children); 12 | const { getAnchorProps } = usePopoverContext(); 13 | 14 | return cloneElement(child, getAnchorProps(child.props, child.ref)); 15 | } 16 | 17 | PopoverAnchor.displayName = 'PopoverAnchor'; 18 | -------------------------------------------------------------------------------- /packages/popover/src/popover-arrow.tsx: -------------------------------------------------------------------------------- 1 | import { HTMLNatureProps, clsx, nature } from '@nature-ui/system'; 2 | import { usePopoverContext } from './popover-context'; 3 | 4 | export interface PopoverArrowProps extends HTMLNatureProps<'div'> {} 5 | 6 | export function PopoverArrow(props: PopoverArrowProps) { 7 | const { getArrowProps, getArrowInnerProps } = usePopoverContext(); 8 | return ( 9 | 13 | 20 | 21 | ); 22 | } 23 | 24 | PopoverArrow.displayName = 'PopoverArrow'; 25 | -------------------------------------------------------------------------------- /packages/popover/src/popover-body.tsx: -------------------------------------------------------------------------------- 1 | import { HTMLNatureProps, clsx, forwardRef, nature } from '@nature-ui/system'; 2 | import { usePopoverContext } from './popover-context'; 3 | 4 | export interface PopoverBodyProps extends HTMLNatureProps<'div'> {} 5 | /** 6 | * PopoverBody is the main content area for the popover. Should contain 7 | * at least one interactive element. 8 | */ 9 | 10 | export const PopoverBody = forwardRef( 11 | function PopoverBody(props, ref) { 12 | const { getBodyProps } = usePopoverContext(); 13 | 14 | return ( 15 | 19 | ); 20 | }, 21 | ); 22 | 23 | PopoverBody.displayName = 'PopoverBody'; 24 | -------------------------------------------------------------------------------- /packages/popover/src/popover-close-button.tsx: -------------------------------------------------------------------------------- 1 | import { CloseButton, CloseButtonProps } from '@nature-ui/close-button'; 2 | import { clsx, forwardRef } from '@nature-ui/system'; 3 | import { usePopoverContext } from './popover-context'; 4 | 5 | export type PopoverCloseButtonProps = CloseButtonProps; 6 | 7 | export const PopoverCloseButton = forwardRef( 8 | function PopoverCloseButton(props, ref) { 9 | const { onClose } = usePopoverContext(); 10 | return ( 11 | 21 | ); 22 | }, 23 | ); 24 | 25 | PopoverCloseButton.displayName = 'PopoverCloseButton'; 26 | -------------------------------------------------------------------------------- /packages/popover/src/popover-context.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from '@nature-ui/react-utils'; 2 | import { UsePopoverReturn } from './use-popover'; 3 | 4 | export const [PopoverProvider, usePopoverContext] = 5 | createContext({ 6 | name: 'PopoverContext', 7 | errorMessage: 8 | 'usePopoverContext: `context` is undefined. Seems you forgot to wrap all popover components within ``', 9 | }); 10 | -------------------------------------------------------------------------------- /packages/popover/src/popover-footer.tsx: -------------------------------------------------------------------------------- 1 | import { HTMLNatureProps, clsx, nature } from '@nature-ui/system'; 2 | 3 | export interface PopoverFooterProps extends HTMLNatureProps<'footer'> {} 4 | 5 | export function PopoverFooter(props: PopoverFooterProps) { 6 | return ( 7 | 14 | ); 15 | } 16 | 17 | PopoverFooter.displayName = 'PopoverFooter'; 18 | -------------------------------------------------------------------------------- /packages/popover/src/popover-header.tsx: -------------------------------------------------------------------------------- 1 | import { HTMLNatureProps, clsx, forwardRef, nature } from '@nature-ui/system'; 2 | import { usePopoverContext } from './popover-context'; 3 | 4 | export interface PopoverHeaderProps extends HTMLNatureProps<'header'> {} 5 | /** 6 | * PopoverHeader is the accessible header or label 7 | * for the popover's content, and it is first announced by screenreaders. 8 | */ 9 | 10 | export const PopoverHeader = forwardRef( 11 | function PopoverHeader(props, ref) { 12 | const { getHeaderProps } = usePopoverContext(); 13 | 14 | return ( 15 | 22 | ); 23 | }, 24 | ); 25 | 26 | PopoverHeader.displayName = 'PopoverHeader'; 27 | -------------------------------------------------------------------------------- /packages/popover/src/popover-trigger.tsx: -------------------------------------------------------------------------------- 1 | import { Children, cloneElement } from "react" 2 | import { usePopoverContext } from "./popover-context" 3 | 4 | /** 5 | * PopoverTrigger opens the popover's content. It must be an interactive element 6 | * such as `button` or `a`. 7 | */ 8 | 9 | export function PopoverTrigger(props: { children: React.ReactNode }) { 10 | // enforce a single child 11 | const child: any = Children.only(props.children) 12 | const { getTriggerProps } = usePopoverContext() 13 | return cloneElement(child, getTriggerProps(child.props, child.ref)) 14 | } 15 | 16 | PopoverTrigger.displayName = "PopoverTrigger" 17 | -------------------------------------------------------------------------------- /packages/popover/src/popover.tsx: -------------------------------------------------------------------------------- 1 | import { MaybeRenderProp } from '@nature-ui/react-types'; 2 | import { runIfFn } from '@nature-ui/utils'; 3 | import { PopoverProvider } from './popover-context'; 4 | import { UsePopoverProps, usePopover } from './use-popover'; 5 | 6 | export interface PopoverProps extends UsePopoverProps { 7 | /** 8 | * The content of the popover. It is usually the `PopoverTrigger`, 9 | * and `PopoverContent` 10 | */ 11 | children?: MaybeRenderProp<{ 12 | isOpen: boolean; 13 | onClose: () => void; 14 | forceUpdate: (() => void) | undefined; 15 | }>; 16 | } 17 | 18 | /** 19 | * Popover is used to bring attention to specific user interface elements, 20 | * typically to suggest an action or to guide users through a new experience. 21 | * 22 | * @see Docs https://nature-ui.com/docs/components/popover 23 | */ 24 | export function Popover(props: PopoverProps) { 25 | const { children, ...rest } = props; 26 | const context = usePopover({ ...rest, direction: 'ltr' }); 27 | 28 | return ( 29 | 30 | {runIfFn(children, { 31 | isOpen: context.isOpen, 32 | onClose: context.onClose, 33 | forceUpdate: context.forceUpdate, 34 | })} 35 | 36 | ); 37 | } 38 | 39 | Popover.displayName = 'Popover'; 40 | -------------------------------------------------------------------------------- /packages/popover/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src", "index.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/popper/README.md: -------------------------------------------------------------------------------- 1 | # Popper 2 | 3 | A React hooks wrapper for popper.js to dynamic positioning of containers around 4 | a reference. 5 | 6 | ## Installation 7 | 8 | ```sh 9 | yarn add @nature-ui/popper 10 | ``` 11 | -------------------------------------------------------------------------------- /packages/popper/__tests__/popper.test.tsx: -------------------------------------------------------------------------------- 1 | import { getBoxShadow, toTransformOrigin } from '../src/popper.utils'; 2 | 3 | describe('@nature-ui/popper', () => { 4 | describe('toTransformOrigin', () => { 5 | test.each` 6 | origin | transform 7 | ${'top'} | ${'bottom center'} 8 | ${'top-start'} | ${'bottom left'} 9 | ${'top-end'} | ${'bottom right'} 10 | ${'bottom'} | ${'top center'} 11 | ${'bottom-start'} | ${'top left'} 12 | ${'bottom-end'} | ${'top right'} 13 | ${'left'} | ${'right center'} 14 | ${'left-start'} | ${'right top'} 15 | ${'left-end'} | ${'right bottom'} 16 | ${'right'} | ${'left center'} 17 | ${'right-start'} | ${'left top'} 18 | ${'right-end'} | ${'left bottom'} 19 | `( 20 | `given position of $origin should return $transform placement`, 21 | ({ origin, transform }) => { 22 | expect(toTransformOrigin(origin)).toBe(transform); 23 | }, 24 | ); 25 | }); 26 | 27 | describe('getBoxShadow', () => { 28 | test.each` 29 | placement | boxShadow 30 | ${'top'} | ${'2px 2px 2px 0 var(--popper-arrow-shadow-color)'} 31 | ${'bottom'} | ${'-1px -1px 1px 0 var(--popper-arrow-shadow-color)'} 32 | ${'right'} | ${'-1px 1px 1px 0 var(--popper-arrow-shadow-color)'} 33 | ${'left'} | ${'1px -1px 1px 0 var(--popper-arrow-shadow-color)'} 34 | `( 35 | `given placement of $origin should return $boxShadow`, 36 | ({ placement, boxShadow }) => { 37 | expect(getBoxShadow(placement)).toBe(boxShadow); 38 | }, 39 | ); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /packages/popper/src/index.ts: -------------------------------------------------------------------------------- 1 | export type { Placement, PlacementWithLogical } from './popper.placement'; 2 | export { cssVars as popperCSSVars } from './popper.utils'; 3 | export { usePopper } from './use-popper'; 4 | export type { UsePopperProps, UsePopperReturn } from './use-popper'; 5 | -------------------------------------------------------------------------------- /packages/popper/src/popper.placement.ts: -------------------------------------------------------------------------------- 1 | import type { Placement } from '@popperjs/core'; 2 | 3 | type Logical = 4 | | 'start-start' 5 | | 'start-end' 6 | | 'end-start' 7 | | 'end-end' 8 | | 'start' 9 | | 'end'; 10 | 11 | type PlacementWithLogical = Placement | Logical; 12 | 13 | export type { Placement, PlacementWithLogical }; 14 | 15 | const logicals: Record = { 16 | 'start-start': { ltr: 'left-start', rtl: 'right-start' }, 17 | 'start-end': { ltr: 'left-end', rtl: 'right-end' }, 18 | 'end-start': { ltr: 'right-start', rtl: 'left-start' }, 19 | 'end-end': { ltr: 'right-end', rtl: 'left-end' }, 20 | start: { ltr: 'left', rtl: 'right' }, 21 | end: { ltr: 'right', rtl: 'left' }, 22 | }; 23 | 24 | const opposites: Partial> = { 25 | 'auto-start': 'auto-end', 26 | 'auto-end': 'auto-start', 27 | 'top-start': 'top-end', 28 | 'top-end': 'top-start', 29 | 'bottom-start': 'bottom-end', 30 | 'bottom-end': 'bottom-start', 31 | }; 32 | 33 | export function getPopperPlacement( 34 | placement: PlacementWithLogical, 35 | dir: 'ltr' | 'rtl' = 'ltr', 36 | ): Placement { 37 | const value = ((logicals as any)[placement]?.[dir] || placement) as Placement; 38 | if (dir === 'ltr') return value; 39 | return (opposites as any)[placement] ?? value; 40 | } 41 | -------------------------------------------------------------------------------- /packages/popper/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src", "index.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/portal/README.md: -------------------------------------------------------------------------------- 1 | # Portal 2 | 3 | A wrapper for rendering components in React Portals, with support for nested 4 | portals and stacking. No need to use `z-index` at all with this portal, that's 5 | right! 6 | 7 | ## Installation 8 | 9 | ```sh 10 | yarn add @nature-ui/portal 11 | 12 | # or 13 | 14 | npm i @nature-ui/portal 15 | ``` 16 | 17 | ## Import components 18 | 19 | ```jsx 20 | import { Portal, PortalManager } from '@nature-ui/portal'; 21 | ``` 22 | 23 | Render the `PortalManager` once at the root of your application 24 | 25 | ```jsx 26 | function App() { 27 | return ( 28 | <> 29 | 30 | {/* Your app goes here */} 31 | 32 | ); 33 | } 34 | ``` 35 | 36 | ### Basic usage 37 | 38 | Portals are render into the portal manager's node by default not 39 | `document.body`. 40 | 41 | > It'll only render into `document.body` if you don't include `PortalManager`. 42 | 43 | ```jsx 44 |
45 |

Welcome

46 | This text has been portaled 47 |
48 | ``` 49 | 50 | ### Nested portals 51 | 52 | Nesting portal can be very useful to build complex widgets like nested menu, 53 | popovers, modal, etc. 54 | 55 | ```jsx 56 | 57 | This is a portal. 58 | This is a nested portal 59 | 60 | ``` 61 | -------------------------------------------------------------------------------- /packages/portal/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './portal-manager'; 2 | export * from './portal'; 3 | -------------------------------------------------------------------------------- /packages/portal/src/portal-manager.tsx: -------------------------------------------------------------------------------- 1 | import { createContext } from '@nature-ui/react-utils'; 2 | import { __DEV__ } from '@nature-ui/utils'; 3 | import React from 'react'; 4 | 5 | interface PortalManagerContext { 6 | zIndex?: number; 7 | } 8 | 9 | const [PortalManagerContextProvider, usePortalManager] = 10 | createContext({ 11 | strict: false, 12 | name: 'PortalManagerContext', 13 | }); 14 | 15 | export { usePortalManager }; 16 | 17 | export interface PortalManagerProps { 18 | /** 19 | * Child elements of the Portal manager 20 | * Ideally, it should be at the top-level 21 | * of your application 22 | */ 23 | children?: React.ReactNode; 24 | /** 25 | * [Z-Index war] If your has multiple elements 26 | * with z-index clashing, you might need to 27 | * apply a z-index to the Portal manager 28 | */ 29 | zIndex?: number; 30 | } 31 | 32 | /** 33 | * PortalManager 34 | * 35 | * Used to manage multiple portals within an application. 36 | * It must be render only once, at the root of your application. 37 | * 38 | * Inspired by BaseWeb's LayerManager component 39 | */ 40 | export const PortalManager = (props: PortalManagerProps) => { 41 | const { children, zIndex } = props; 42 | return ( 43 | 44 | {children} 45 | 46 | ); 47 | }; 48 | 49 | if (__DEV__) { 50 | PortalManager.displayName = 'PortalManager'; 51 | } 52 | -------------------------------------------------------------------------------- /packages/portal/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src", "index.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/progress/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './circular-progress'; 2 | export * from './progress'; 3 | -------------------------------------------------------------------------------- /packages/progress/stories/circular-progress.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from '@storybook/react'; 2 | import React from 'react'; 3 | import { CircularProgress, CircularProgressProps } from '../src'; 4 | 5 | export default { 6 | title: 'Progress/Circular', 7 | component: CircularProgress, 8 | } as Meta; 9 | 10 | type ProgressType = Story; 11 | 12 | const Template: ProgressType = (args) => ; 13 | 14 | export const Basic: ProgressType = Template.bind({}); 15 | Basic.args = { 16 | value: 25, 17 | }; 18 | 19 | export const WithSize: ProgressType = Template.bind({}); 20 | WithSize.args = { 21 | value: 25, 22 | size: 80, 23 | }; 24 | 25 | export const WithThickness: ProgressType = Template.bind({}); 26 | WithThickness.args = { 27 | value: 25, 28 | thickness: 5, 29 | size: 50, 30 | }; 31 | 32 | export const withColorScheme: ProgressType = Template.bind({}); 33 | withColorScheme.args = { 34 | value: 25, 35 | colorScheme: 'text-purple-400', 36 | size: 50, 37 | }; 38 | 39 | export const showPercent: ProgressType = Template.bind({}); 40 | showPercent.args = { 41 | value: 80, 42 | showPercent: true, 43 | size: 50, 44 | }; 45 | 46 | export const inDeterminate: ProgressType = Template.bind({}); 47 | inDeterminate.args = { 48 | size: 50, 49 | isIndeterminate: true, 50 | }; 51 | -------------------------------------------------------------------------------- /packages/progress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src", "index.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/radio/README.md: -------------------------------------------------------------------------------- 1 | # Radio 2 | 3 | Radio component is used in forms when a user needs to select a single value from 4 | several options. 5 | 6 | ## Installation 7 | 8 | ```sh 9 | yarn add @nature-ui/radio 10 | 11 | # or 12 | 13 | npm i @nature-ui/radio 14 | ``` 15 | 16 | ## Import component 17 | 18 | ```jsx 19 | import { Radio } from '@nature-ui/radio'; 20 | ``` 21 | 22 | ## Usage 23 | 24 | ```jsx 25 | This is a radio 26 | ``` 27 | 28 | ### RadioGroup 29 | 30 | RadioGroup is used to bind multiple radios into a group, and it indicates 31 | whether which option is selected. 32 | 33 | ```jsx 34 | 35 | One 36 | Two 37 | Three 38 | 39 | ``` 40 | -------------------------------------------------------------------------------- /packages/radio/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './radio'; 2 | export * from './use-radio'; 3 | export * from './use-radio-group'; 4 | export * from './radio-group'; 5 | -------------------------------------------------------------------------------- /packages/radio/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src", "index.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/react-types/README.md: -------------------------------------------------------------------------------- 1 | # @chakra-ui/react-types 2 | 3 | A Quick description of the component 4 | 5 | > This is an internal utility, not intended for public usage. 6 | 7 | ## Installation 8 | 9 | ```sh 10 | yarn add @chakra-ui/react-types 11 | # or 12 | npm i @chakra-ui/react-types 13 | ``` 14 | 15 | ## Contribution 16 | 17 | Yes please! See the 18 | [contributing guidelines](https://github.com/chakra-ui/chakra-ui/blob/master/CONTRIBUTING.md) 19 | for details. 20 | 21 | ## Licence 22 | 23 | This project is licensed under the terms of the 24 | [MIT license](https://github.com/chakra-ui/chakra-ui/blob/master/LICENSE). 25 | -------------------------------------------------------------------------------- /packages/react-types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nature-ui/react-types", 3 | "version": "0.0.4", 4 | "description": "", 5 | "keywords": [ 6 | "react", 7 | "types", 8 | "shared" 9 | ], 10 | "author": "Segun Adebayo ", 11 | "homepage": "https://github.com/nature-ui/nature-ui#readme", 12 | "license": "MIT", 13 | "types": "src/index.d.ts", 14 | "sideEffects": false, 15 | "files": [ 16 | "dist", 17 | "src" 18 | ], 19 | "publishConfig": { 20 | "access": "public" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/nature-ui/nature-ui.git", 25 | "directory": "packages/utilities/react-types" 26 | }, 27 | "bugs": { 28 | "url": "https://github.com/nature-ui/nature-ui/issues" 29 | }, 30 | "peerDependencies": { 31 | "react": ">=18" 32 | }, 33 | "devDependencies": { 34 | "react": "^18.0.0" 35 | }, 36 | "tsup": { 37 | "clean": true, 38 | "target": "es2019", 39 | "format": [ 40 | "cjs", 41 | "esm" 42 | ] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/react-types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src", "index.d.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/react-utils/README.md: -------------------------------------------------------------------------------- 1 | # @nature-ui/react-utils 2 | 3 | This package contains all React-related utilities and types that may be consumed 4 | by other packages. 5 | 6 | > This is an internal utility, not intended for public usage. 7 | 8 | ## Installation 9 | 10 | ```sh 11 | yarn add @nature-ui/react-utils 12 | # or 13 | npm i @nature-ui/react-utils 14 | ``` 15 | -------------------------------------------------------------------------------- /packages/react-utils/src/children.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /** 4 | * Gets only the valid children of a component, 5 | * and ignores any nullish or falsy child. 6 | * 7 | * @param children the children 8 | */ 9 | export function getValidChildren(children: React.ReactNode) { 10 | return React.Children.toArray(children).filter((child) => 11 | React.isValidElement(child), 12 | ) as React.ReactElement[]; 13 | } 14 | -------------------------------------------------------------------------------- /packages/react-utils/src/context.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export interface CreateContextOptions { 4 | /** 5 | * If `true`, React will throw if context is `null` or `undefined` 6 | * In some cases, you might want to support nested context, so you can set it to `false` 7 | */ 8 | strict?: boolean; 9 | /** 10 | * Error message to throw if the context is `undefined` 11 | */ 12 | errorMessage?: string; 13 | /** 14 | * The display name of the context 15 | */ 16 | name?: string; 17 | } 18 | 19 | type CreateContextReturn = [React.Provider, () => T, React.Context]; 20 | 21 | /** 22 | * Creates a named context, provider, and hook. 23 | * 24 | * @param options create context options 25 | */ 26 | export function createContext(options: CreateContextOptions = {}) { 27 | const { 28 | strict = true, 29 | errorMessage = 'useContext: `context` is undefined. Seems you forgot to wrap component within the Provider', 30 | name, 31 | } = options; 32 | 33 | const Context = React.createContext(undefined); 34 | 35 | Context.displayName = name; 36 | 37 | function useContext() { 38 | const context = React.useContext(Context); 39 | 40 | if (!context && strict) { 41 | const error = new Error(errorMessage); 42 | error.name = 'ContextError'; 43 | Error.captureStackTrace?.(error, useContext); 44 | throw error; 45 | } 46 | 47 | return context; 48 | } 49 | 50 | return [ 51 | Context.Provider, 52 | useContext, 53 | Context, 54 | ] as CreateContextReturn; 55 | } 56 | -------------------------------------------------------------------------------- /packages/react-utils/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './refs'; 2 | export * from './context'; 3 | export * from './types'; 4 | export * from './children'; 5 | -------------------------------------------------------------------------------- /packages/react-utils/src/refs.ts: -------------------------------------------------------------------------------- 1 | import { isFunction } from '@nature-ui/utils'; 2 | 3 | type ReactRef = 4 | | React.Ref 5 | | React.RefObject 6 | | React.MutableRefObject; 7 | 8 | /** 9 | * Assigns a value to a ref function or object 10 | * 11 | * @param ref the ref to assign to 12 | * @param value the value 13 | */ 14 | export function assignRef(ref: ReactRef | undefined, value: T) { 15 | if (ref == null) return; 16 | 17 | if (isFunction(ref)) { 18 | ref(value); 19 | return; 20 | } 21 | 22 | try { 23 | // @ts-ignore 24 | ref.current = value; 25 | } catch (error) { 26 | throw new Error(`Cannot assign value '${value}' to ref '${ref}'`); 27 | } 28 | } 29 | 30 | /** 31 | * Combine multiple React refs into a single ref function. 32 | * This is used mostly when you need to allow consumers forward refs to 33 | * internal components 34 | * 35 | * @param refs refs to assign to value to 36 | */ 37 | export function mergeRefs(...refs: (ReactRef | undefined)[]) { 38 | return (node: T | null) => { 39 | refs.forEach((ref) => assignRef(ref, node)); 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /packages/react-utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src", "index.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/spinner/README.md: -------------------------------------------------------------------------------- 1 | # Spinner 2 | 3 | Spinners provide a visual cue that an action is processing, awaiting a course of 4 | change or a result. 5 | 6 | ## Installation 7 | 8 | ```sh 9 | yarn add @nature-ui/spinner 10 | 11 | # or 12 | 13 | npm i @nature-ui/spinner 14 | ``` 15 | 16 | ## Import component 17 | 18 | ```jsx 19 | import { Spinner } from '@nature-ui/spinner'; 20 | ``` 21 | 22 | ## Usage 23 | 24 | ```jsx 25 | 26 | ``` 27 | 28 | ## Spinner with different sizes 29 | 30 | Change the size of the spinner by passing the `size` prop. 31 | 32 | ```jsx 33 | <> 34 | 35 | 36 | 37 | 38 | 39 | ``` 40 | 41 | ## Spinner with color 42 | 43 | Change the background color of the moving section of the spinner by passing the 44 | `color` prop. 45 | 46 | ```jsx 47 | 48 | ``` 49 | -------------------------------------------------------------------------------- /packages/spinner/__tests__/spinner.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, testA11y } from '@nature-ui/test-utils'; 2 | import { Spinner } from '../src'; 3 | 4 | test('Spinner renders correctly', async () => { 5 | const { container } = render(); 6 | await testA11y(container); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/spinner/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './spinner'; 2 | -------------------------------------------------------------------------------- /packages/spinner/stories/spinner.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Spinner } from '../src'; 3 | 4 | export default { 5 | title: 'Spinner', 6 | }; 7 | 8 | export const Default = () => ; 9 | 10 | export const WithColor = () => ; 11 | 12 | export const WithSizes = () => ( 13 |
14 | 15 | 16 | 17 | 18 |
19 | ); 20 | -------------------------------------------------------------------------------- /packages/spinner/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src", "index.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/system/README.md: -------------------------------------------------------------------------------- 1 | # `@nature-ui/spinner` 2 | 3 | > TODO: description 4 | 5 | ## Usage 6 | 7 | ```jsx 8 | import spinner from '@nature-ui/spinner'; 9 | 10 | // TODO: DEMONSTRATE API 11 | ``` 12 | -------------------------------------------------------------------------------- /packages/system/src/index.ts: -------------------------------------------------------------------------------- 1 | export { css, keyframes } from '@emotion/css'; 2 | export * from './jsx'; 3 | export * from './system'; 4 | export * from './system.types'; 5 | export * from './system.utils'; 6 | -------------------------------------------------------------------------------- /packages/system/src/jsx.tsx: -------------------------------------------------------------------------------- 1 | import { Interpolation, Theme, jsx as emotion } from '@emotion/react'; 2 | import { Dict } from '@nature-ui/utils'; 3 | import React from 'react'; 4 | 5 | export const jsx = ( 6 | props: Dict, 7 | type: React.ElementType = 'div', 8 | ...children: React.ReactNode[] 9 | ) => { 10 | if (props.as) { 11 | type = props.as; 12 | } 13 | return emotion.apply(type, [type, props, ...children]); 14 | }; 15 | 16 | declare module 'react' { 17 | interface Attributes { 18 | css?: Interpolation; 19 | } 20 | } 21 | 22 | export default jsx; 23 | -------------------------------------------------------------------------------- /packages/system/src/system.tsx: -------------------------------------------------------------------------------- 1 | import { As } from '@nature-ui/utils'; 2 | import clsx from 'clsx'; 3 | import { createComponent } from './create-component'; 4 | import { NatureComponent, PropsOf } from './system.types'; 5 | import { DOMElements, domElements } from './system.utils'; 6 | 7 | export const natureComp = (component: T) => { 8 | return createComponent(component)(); 9 | }; 10 | 11 | type NatureJSXElements = { 12 | [Tag in DOMElements]: NatureComponent; 13 | }; 14 | 15 | export type HTMLNatureProps = Omit< 16 | PropsOf, 17 | T extends 'svg' ? 'ref' | 'children' : 'ref' 18 | > & { as?: As }; 19 | 20 | type CreateNatureComponent = { 21 | (component: T): NatureComponent; 22 | }; 23 | 24 | export const nature = natureComp as unknown as CreateNatureComponent & 25 | NatureJSXElements; 26 | 27 | domElements.forEach((tag) => { 28 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 29 | // @ts-ignore 30 | nature[tag] = nature(tag); 31 | }); 32 | 33 | export { clsx }; 34 | -------------------------------------------------------------------------------- /packages/system/stories/system.stories.tsx: -------------------------------------------------------------------------------- 1 | import { css, nature } from '../src'; 2 | 3 | export default { 4 | title: 'System', 5 | }; 6 | 7 | const Heading = nature('h1'); 8 | 9 | const Btn = nature('button'); 10 | 11 | export const withHeading1 = () => ( 12 |
13 | 19 | Welcome 20 | 21 | 28 | Welcome 29 | 30 |
31 | ); 32 | 33 | export const withHeading = () => ( 34 |
35 | Welcome 36 | 41 | Welcome 42 | 43 |
44 | ); 45 | 46 | export const Basic = () => ( 47 | 54 | Hello world 55 | 56 | ); 57 | 58 | export const WithAs = () => ( 59 | 65 | Hello world 66 | 67 | ); 68 | -------------------------------------------------------------------------------- /packages/system/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src", "index.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/toast/__tests__/standalone-toast.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@nature-ui/test-utils'; 2 | import { act } from '@testing-library/react'; 3 | import { createStandaloneToast } from '../src'; 4 | 5 | describe('Standalone Toast', () => { 6 | it('should render a toast correctly', async () => { 7 | const { ToastContainer, toast } = createStandaloneToast(); 8 | const title = 'Yay!'; 9 | const description = 'Something awesome happened'; 10 | 11 | render(); 12 | 13 | await act(async () => { 14 | toast({ 15 | title, 16 | description, 17 | duration: 4000, 18 | isClosable: true, 19 | }); 20 | }); 21 | 22 | const allByTitle = await screen.findAllByRole('alert', { name: title }); 23 | const allByDescription = await screen.findAllByText(description); 24 | 25 | expect(allByTitle).toHaveLength(1); 26 | expect(allByDescription).toHaveLength(1); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /packages/toast/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create-standalone-toast'; 2 | export * from './toast'; 3 | export * from './toast.placement'; 4 | export * from './toast.provider'; 5 | export * from './toast.types'; 6 | export * from './use-toast'; 7 | -------------------------------------------------------------------------------- /packages/toast/src/toast.placement.ts: -------------------------------------------------------------------------------- 1 | export type LogicalToastPosition = 2 | | 'top-start' 3 | | 'top-end' 4 | | 'bottom-start' 5 | | 'bottom-end'; 6 | 7 | export type ToastPositionWithLogical = 8 | | LogicalToastPosition 9 | | 'top' 10 | | 'bottom' 11 | | 'top-left' 12 | | 'top-right' 13 | | 'bottom-left' 14 | | 'bottom-right'; 15 | 16 | export type ToastPosition = Exclude< 17 | ToastPositionWithLogical, 18 | LogicalToastPosition 19 | >; 20 | 21 | export type WithoutLogicalPosition = Omit & { 22 | position?: ToastPosition; 23 | }; 24 | 25 | type LogicalPlacementMap = Record< 26 | LogicalToastPosition, 27 | { ltr: ToastPosition; rtl: ToastPosition } 28 | >; 29 | 30 | export function getToastPlacement( 31 | position: ToastPositionWithLogical | undefined, 32 | dir: 'ltr' | 'rtl', 33 | ): ToastPosition | undefined { 34 | if (!position) return; 35 | 36 | const logicals: LogicalPlacementMap = { 37 | 'top-start': { ltr: 'top-left', rtl: 'top-right' }, 38 | 'top-end': { ltr: 'top-right', rtl: 'top-left' }, 39 | 'bottom-start': { ltr: 'bottom-left', rtl: 'bottom-right' }, 40 | 'bottom-end': { ltr: 'bottom-right', rtl: 'bottom-left' }, 41 | }; 42 | 43 | const logical = logicals[position as keyof typeof logicals]; 44 | return logical?.[dir] ?? position; 45 | } 46 | -------------------------------------------------------------------------------- /packages/toast/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src", "index.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/transition/README.md: -------------------------------------------------------------------------------- 1 | # `@nature-ui/transition` 2 | 3 | > TODO: description 4 | 5 | ## Usage 6 | 7 | ```jsx 8 | import { Fade } from '@nature-ui/transition'; 9 | 10 | // TODO: DEMONSTRATE API 11 | ``` 12 | -------------------------------------------------------------------------------- /packages/transition/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './collapse'; 2 | export * from './fade'; 3 | export * from './scale-fade'; 4 | export * from './slide'; 5 | export * from './slide-fade'; 6 | -------------------------------------------------------------------------------- /packages/transition/stories/fade.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@nature-ui/button'; 2 | import { useBoolean } from '@nature-ui/hooks'; 3 | import { Meta, Story } from '@storybook/react'; 4 | import React from 'react'; 5 | 6 | import { Fade, FadeProps } from '../src'; 7 | import { Modal } from './modal'; 8 | 9 | export default { 10 | title: 'Transition/Fade', 11 | component: Fade, 12 | } as Meta; 13 | 14 | const Template: Story = (args) => { 15 | const [open, { toggle }] = useBoolean(false); 16 | return ( 17 | <> 18 | 19 | 20 | 21 | Lorem Ipsum is simply dummy text of the printing and typesetting 22 | industry. 23 | 24 | 25 | 26 | ); 27 | }; 28 | 29 | export const Basic = Template.bind({}); 30 | -------------------------------------------------------------------------------- /packages/transition/stories/modal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const modalStyles: React.CSSProperties = { 4 | maxWidth: '630px', 5 | minWidth: '320px', 6 | background: 'skyblue', 7 | minHeight: 300, 8 | height: 'auto', 9 | padding: '1rem', 10 | borderRadius: '5px', 11 | margin: '1rem', 12 | }; 13 | 14 | type DivProps = React.HTMLAttributes; 15 | 16 | export const Modal = (props: DivProps) => ( 17 |
25 |

Animate Me

26 |

27 | Lorem ipsum, dolor sit amet consectetur adipisicing elit. Eius porro quas 28 | tempora delectus magni esse vero dolorum laudantium veritatis illum. 29 |

30 |
31 | ); 32 | -------------------------------------------------------------------------------- /packages/transition/stories/scale-fade.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@nature-ui/button'; 2 | import { useBoolean } from '@nature-ui/hooks'; 3 | import { Meta, Story } from '@storybook/react'; 4 | import React from 'react'; 5 | 6 | import { ScaleFade, ScaleFadeProps } from '../src'; 7 | 8 | export default { 9 | title: 'Transition/ScaleFade', 10 | component: ScaleFade, 11 | } as Meta; 12 | 13 | export const Basic: Story = (args) => { 14 | const [isOpen, { toggle }] = useBoolean(); 15 | 16 | return ( 17 | <> 18 | 19 | 29 | Lorem Ipsum is simply dummy text of the printing and typesetting 30 | industry. 31 | 32 | 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /packages/transition/stories/slide-fade.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@nature-ui/button'; 2 | import { useBoolean } from '@nature-ui/hooks'; 3 | import { Meta, Story } from '@storybook/react'; 4 | import React from 'react'; 5 | import { SlideFade, SlideFadeProps } from '../src'; 6 | 7 | export default { 8 | title: 'Transition/SlideFade', 9 | component: SlideFade, 10 | } as Meta; 11 | 12 | const Template: Story = (args) => { 13 | const [open, { toggle }] = useBoolean(false); 14 | 15 | return ( 16 | <> 17 | 18 | 28 | Lorem Ipsum is simply dummy text of the printing and typesetting 29 | industry. 30 | 31 | 32 | ); 33 | }; 34 | 35 | export const Basic = Template.bind({}); 36 | 37 | export const WithCustomTransition = Template.bind({}); 38 | WithCustomTransition.args = { 39 | transition: { enter: { duration: 0.3 }, exit: { duration: 0.5 } }, 40 | }; 41 | 42 | export const WithTransitionEnd = Template.bind({}); 43 | WithTransitionEnd.args = { 44 | style: { 45 | display: 'block', 46 | }, 47 | transitionEnd: { 48 | exit: { display: 'none' }, 49 | }, 50 | }; 51 | 52 | export const WithoutReverseProp = Template.bind({}); 53 | WithoutReverseProp.args = { 54 | reverse: false, 55 | style: { 56 | display: 'block', 57 | }, 58 | transitionEnd: { exit: { display: 'none' } }, 59 | }; 60 | -------------------------------------------------------------------------------- /packages/transition/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src", "index.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/utils/README.md: -------------------------------------------------------------------------------- 1 | # `@nature-ui/utils` 2 | 3 | Shared utilities for various Nature UI components 4 | 5 | > **Note **: This package is intended for internal use by the Nature UI 6 | > components. 7 | 8 | ## Installation 9 | 10 | ```sh 11 | yarn add @nature-ui/utils 12 | 13 | // TODO: DEMONSTRATE API 14 | ``` 15 | 16 | ## Usage 17 | 18 | ```jsx 19 | import utils from '@nature-ui/utils'; 20 | ``` 21 | -------------------------------------------------------------------------------- /packages/utils/__tests__/array.test.ts: -------------------------------------------------------------------------------- 1 | import { addItem, removeItem } from '../src'; 2 | 3 | const array = [1, 2, 3, 4, 5, 6, 7, 8]; 4 | 5 | describe('remove and add operations', () => { 6 | test('should add new item to the end of array', () => { 7 | const result = addItem(array, 9); 8 | 9 | expect(result).toStrictEqual([...array, 9]); 10 | }); 11 | 12 | test('should remove a given item from array', () => { 13 | const result = removeItem(array, 8); 14 | 15 | expect(result).toStrictEqual([1, 2, 3, 4, 5, 6, 7]); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /packages/utils/__tests__/dom.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ariaAttr, 3 | dataAttr, 4 | getOwnerDocument, 5 | getWindow, 6 | normalizeEventKey, 7 | } from '../src'; 8 | 9 | describe('dome tests', () => { 10 | test('should get window object', () => { 11 | expect(getWindow()).toBe(window); 12 | }); 13 | 14 | test('should normalize keyboard events', () => { 15 | const keyboardEvent: any = { 16 | key: 'Left', 17 | keyCode: 38, 18 | }; 19 | 20 | expect(normalizeEventKey(keyboardEvent)).toBe('ArrowLeft'); 21 | }); 22 | 23 | test('should return data attribute value from boolean', () => { 24 | expect(dataAttr(true)).toBe(''); 25 | }); 26 | 27 | test('should return aria attribute value from boolean', () => { 28 | expect(ariaAttr(false)).toBeUndefined(); 29 | }); 30 | 31 | test('should get document object', () => { 32 | expect(getOwnerDocument()).toBe(document); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /packages/utils/__tests__/functions.test.ts: -------------------------------------------------------------------------------- 1 | import { callAllHandler, runIfFn } from '../src'; 2 | 3 | describe('Functions test', () => { 4 | test('should run function if function else return value', () => { 5 | expect(runIfFn(() => 1 + 1)).toStrictEqual(2); 6 | expect(runIfFn(2)).toStrictEqual(2); 7 | }); 8 | 9 | test('should call all passed functions on event triggered', () => { 10 | const event = { target: { value: 1 } }; 11 | 12 | let val1 = 0, 13 | val2 = 0; 14 | 15 | const func1 = (_event: any) => { 16 | val1 = _event.target.value + 1; 17 | 18 | return val1; 19 | }; 20 | const func2 = (_event: any) => { 21 | val2 = _event.target.value + 2; 22 | 23 | return val2; 24 | }; 25 | 26 | callAllHandler(func1, func2)(event); 27 | 28 | expect(val1).toStrictEqual(2); 29 | expect(val2).toStrictEqual(3); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /packages/utils/__tests__/number.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | clampValue, 3 | countDecimalPlaces, 4 | toPrecision, 5 | valueToPercent, 6 | } from '../src'; 7 | 8 | describe('Number test', () => { 9 | test('should round number to specific prevision', () => { 10 | expect(toPrecision(1.4567, 2)).toStrictEqual('1.46'); 11 | }); 12 | 13 | test('should return number of decimal places', () => { 14 | expect(countDecimalPlaces(1.1231)).toStrictEqual(4); 15 | }); 16 | 17 | test('should return percent of value in a specific range', () => { 18 | expect(valueToPercent(5, 0, 10)).toStrictEqual(50); 19 | }); 20 | 21 | test('should clamp value to specified minimum', () => { 22 | expect(clampValue(5, 6, 10)).toStrictEqual(6); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /packages/utils/__tests__/object.test.ts: -------------------------------------------------------------------------------- 1 | import { omit } from '../src'; 2 | 3 | const object = { 4 | a: 1, 5 | b: 2, 6 | c: { d: 3 }, 7 | }; 8 | 9 | describe('Object tests', () => { 10 | test('should return object with omitted property', () => { 11 | expect(omit(object, ['a'])).toStrictEqual({ 12 | b: 2, 13 | c: { d: 3 }, 14 | }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /packages/utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nature-ui/utils", 3 | "version": "2.2.13", 4 | "description": "Utility functions", 5 | "author": "Divine Hycenth ", 6 | "homepage": "https://github.com/DNature/nature-ui#readme", 7 | "license": "MIT", 8 | "main": "dist/index.js", 9 | "directories": { 10 | "lib": "dist", 11 | "test": "__tests__" 12 | }, 13 | "files": [ 14 | "dist" 15 | ], 16 | "publishConfig": { 17 | "access": "public" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/DNature/nature-ui.git" 22 | }, 23 | "scripts": { 24 | "build": "tsup src --dts", 25 | "build:fast": "tsup src", 26 | "dev": "pnpm build:fast -- --watch", 27 | "clean": "rimraf dist .turbo", 28 | "typecheck": "tsc --noEmit", 29 | "prepack": "clean-package" 30 | }, 31 | "bugs": { 32 | "url": "https://github.com/DNature/nature-ui/issues" 33 | }, 34 | "peerDependencies": { 35 | "react": ">=18" 36 | }, 37 | "devDependencies": { 38 | "clean-package": "^2.1.1", 39 | "react": "^18.0.0" 40 | }, 41 | "gitHead": "cafae1d72b2a7027028454c22d4bbaebfdafba5f", 42 | "clean-package": "../../clean-package.config.json", 43 | "tsup": { 44 | "clean": true, 45 | "target": "es2019", 46 | "format": [ 47 | "cjs", 48 | "esm" 49 | ] 50 | }, 51 | "module": "dist/index.mjs", 52 | "types": "dist/index.d.ts", 53 | "exports": { 54 | ".": { 55 | "types": "./dist/index.d.ts", 56 | "import": "./dist/index.mjs", 57 | "require": "./dist/index.js" 58 | }, 59 | "./package.json": "./package.json" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/utils/src/array.ts: -------------------------------------------------------------------------------- 1 | export const addItem = (array: T[], item: T) => { 2 | return [...array, item]; 3 | }; 4 | 5 | export const removeItem = (array: T[], item: T) => { 6 | return array.filter((eachItem) => eachItem !== item); 7 | }; 8 | -------------------------------------------------------------------------------- /packages/utils/src/dom-query.ts: -------------------------------------------------------------------------------- 1 | import { isFocusable, isTabbable } from './tabbable'; 2 | 3 | const SELECTORS = [ 4 | 'input', 5 | 'select', 6 | 'textarea', 7 | 'a[href]', 8 | 'area[href]', 9 | 'button', 10 | '[tabindex]', 11 | 'audio[controls]', 12 | 'video[controls]', 13 | '[contenteditable]:not([contenteditable=false])', 14 | ]; 15 | 16 | const selector = SELECTORS.join(); 17 | 18 | export const getAllFocusable = (container: T) => { 19 | const allFocusable = Array.from(container.querySelectorAll(selector)); 20 | 21 | allFocusable.unshift(container); 22 | 23 | return allFocusable 24 | .filter(isFocusable) 25 | .filter((el) => window.getComputedStyle(el).display !== 'none'); 26 | }; 27 | 28 | export const getAllTabbable = ( 29 | container: T, 30 | fallbackToFocusable?: boolean, 31 | ) => { 32 | const allFocusable = Array.from(container.querySelectorAll(selector)); 33 | const allTabbable = allFocusable.filter(isTabbable); 34 | 35 | if (isTabbable(container)) { 36 | allTabbable.unshift(container); 37 | } 38 | 39 | if (!allTabbable.length && fallbackToFocusable) { 40 | return allFocusable; 41 | } 42 | 43 | return allTabbable; 44 | }; 45 | 46 | export const getFirstTabbableIn = ( 47 | container: T, 48 | fallbackToFocusable?: boolean, 49 | ): T | null => { 50 | const [first] = getAllTabbable(container, fallbackToFocusable); 51 | 52 | return first || null; 53 | }; 54 | -------------------------------------------------------------------------------- /packages/utils/src/functions.ts: -------------------------------------------------------------------------------- 1 | import { isFunction } from './assertions'; 2 | import { FunctionArguments } from './types'; 3 | 4 | export type MaybeFunction = 5 | | T 6 | | ((...args: Args) => T); 7 | 8 | export const runIfFn = ( 9 | valueOrFn: T | ((...args: U[]) => T), 10 | ...args: U[] 11 | ): T => { 12 | return isFunction(valueOrFn) ? valueOrFn(...args) : valueOrFn; 13 | }; 14 | 15 | export const callAllHandler = void>( 16 | ...fns: (T | undefined)[] 17 | ) => { 18 | return (event: FunctionArguments[0]): any => { 19 | fns.some((fn) => { 20 | fn?.(event); 21 | 22 | return event?.defaultPrevented; 23 | }); 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /packages/utils/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './array'; 2 | export * from './assertions'; 3 | export * from './dom'; 4 | export * from './dom-query'; 5 | export * from './focus'; 6 | export * from './functions'; 7 | export * from './lazy'; 8 | export * from './logger'; 9 | export * from './numbers'; 10 | export * from './objects'; 11 | export * from './react-helpers'; 12 | export * from './tabbable'; 13 | export * from './types'; 14 | -------------------------------------------------------------------------------- /packages/utils/src/lazy.ts: -------------------------------------------------------------------------------- 1 | export type LazyMode = 'unmount' | 'keepMounted'; 2 | 3 | interface LazyOptions { 4 | enabled?: boolean; 5 | isSelected?: boolean; 6 | wasSelected?: boolean; 7 | mode?: LazyMode; 8 | } 9 | 10 | /** 11 | * Determines whether the children of a disclosure widget 12 | * should be rendered or not, depending on the lazy behavior. 13 | * 14 | * Used in accordion, tabs, popover, menu and other disclosure 15 | * widgets. 16 | */ 17 | export function lazyDisclosure(options: LazyOptions) { 18 | const { wasSelected, enabled, isSelected, mode = 'unmount' } = options; 19 | 20 | // if not lazy, always render the disclosure's content 21 | if (!enabled) return true; 22 | 23 | // if the disclosure is selected, render the disclosure's content 24 | if (isSelected) return true; 25 | 26 | // if the disclosure was selected but not active, keep its content active 27 | if (mode === 'keepMounted' && wasSelected) return true; 28 | 29 | return false; 30 | } 31 | -------------------------------------------------------------------------------- /packages/utils/src/logger.ts: -------------------------------------------------------------------------------- 1 | import { __DEV__ } from './assertions'; 2 | 3 | export const warn = (options: { condition: boolean; message: string }) => { 4 | if (options.condition && __DEV__) { 5 | console.warn(options.message); 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /packages/utils/src/objects.ts: -------------------------------------------------------------------------------- 1 | import { Dict, Omit } from './types'; 2 | 3 | export const omit = ( 4 | object: T, 5 | keys: K[], 6 | ) => { 7 | const result: Dict = {}; 8 | 9 | Object.keys(object).forEach((key) => { 10 | if (!keys.includes(key as any)) { 11 | result[key] = object[key]; 12 | } 13 | }); 14 | 15 | return result as Omit; 16 | }; 17 | 18 | export const objectKeys = (obj: T) => 19 | Object.keys(obj) as unknown as (keyof T)[]; 20 | -------------------------------------------------------------------------------- /packages/utils/src/types.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export type Merge = Omit> & T2; 4 | 5 | export type SafeMerge = P & Omit; 6 | 7 | export type UnionStringArray> = T[number]; 8 | 9 | export type Omit = Pick>; 10 | 11 | export type As

= React.ElementType

; 12 | 13 | export type AnyFunction = (...args: T[]) => any; 14 | 15 | export type FunctionArguments = T extends ( 16 | ...args: infer R 17 | ) => any 18 | ? R 19 | : never; 20 | export type Dict = Record; 21 | 22 | export type ReactNodeOrRenderProp

= 23 | | React.ReactNode 24 | | ((props: P) => React.ReactNode); 25 | 26 | export type Booleanish = boolean | 'true' | 'false'; 27 | 28 | export type ObjectOrArray = 29 | | T[] 30 | | Record; 31 | 32 | export type StringOrNumber = string | number; 33 | 34 | export type EventKeys = 35 | | 'ArrowDown' 36 | | 'ArrowUp' 37 | | 'ArrowLeft' 38 | | 'ArrowRight' 39 | | 'Enter' 40 | | 'Space' 41 | | 'Tab' 42 | | 'Backspace' 43 | | 'Control' 44 | | 'Meta' 45 | | 'Home' 46 | | 'End' 47 | | 'PageDown' 48 | | 'PageUp' 49 | | 'Delete' 50 | | 'Escape' 51 | | ' ' 52 | | 'Shift'; 53 | -------------------------------------------------------------------------------- /packages/utils/stories/merge-refs.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { mergeRefs } from '../src'; 4 | 5 | export default { 6 | title: 'Utils/MergeRefs', 7 | }; 8 | 9 | const Refs = React.forwardRef( 10 | ( 11 | props: { 12 | children: React.ReactElement; 13 | }, 14 | forwardedRef: React.Ref, 15 | ) => { 16 | type ChildElement = React.ReactElement<{ ref: React.Ref }>; 17 | const _child = React.Children.only(props.children) as ChildElement; 18 | 19 | const ref = React.useRef(null); 20 | 21 | const _ref = mergeRefs(ref, _child.props.ref); 22 | 23 | console.log({ ref }); 24 | 25 | return ( 26 |

27 | {React.cloneElement(_child, { 28 | ref: _ref, 29 | })} 30 |
31 | ); 32 | }, 33 | ); 34 | 35 | Refs.displayName = 'Refs'; 36 | 37 | export const Default = () => ( 38 | 39 |
Hello world from ref component
40 |
41 | ); 42 | -------------------------------------------------------------------------------- /packages/utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src", "index.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/visually-hidden/README.md: -------------------------------------------------------------------------------- 1 | # Visually Hidden 2 | 3 | The visually hidden component styles itself so that it’s content is not visible, 4 | but it is available to assistive technologies like screen readers and other text 5 | to speech programs. 6 | 7 | ## Installation 8 | 9 | ```sh 10 | yarn add @nature-ui/visually-hidden 11 | 12 | # or 13 | 14 | npm i @nature-ui/visually-hidden 15 | ``` 16 | 17 | ## Import component 18 | 19 | ```jsx 20 | import { VisuallyHidden } from '@nature-ui/visually-hidden'; 21 | ``` 22 | 23 | ## Basic usage 24 | 25 | ```jsx 26 | // it renders a `span` by default 27 | This content will be hidden on screen 28 | 29 | // for visually hidden input fields 30 | 31 | ``` 32 | 33 | ## References 34 | 35 | - https://snook.ca/archives/html_and_css/hiding-content-for-accessibility 36 | - https://a11yproject.com/posts/how-to-hide-content/ 37 | -------------------------------------------------------------------------------- /packages/visually-hidden/react-shim.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | export { React } 3 | -------------------------------------------------------------------------------- /packages/visually-hidden/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './visually-hidden'; 2 | export { default } from './visually-hidden'; 3 | -------------------------------------------------------------------------------- /packages/visually-hidden/src/visually-hidden.tsx: -------------------------------------------------------------------------------- 1 | /** ** */ 2 | import { forwardRef, nature, PropsOf } from '@nature-ui/system'; 3 | import { __DEV__ } from '@nature-ui/utils'; 4 | import React from 'react'; 5 | 6 | const Span = nature('span'); 7 | const Input = nature('input'); 8 | 9 | /** 10 | * Styles to visually hide an element 11 | * but make it accessible to screen-readers. 12 | */ 13 | export const visuallyHiddenStyle: React.CSSProperties = { 14 | border: 0, 15 | clip: 'rect(0px, 0px, 0px, 0px)', 16 | height: '1px', 17 | width: '1px', 18 | margin: '--1px', 19 | padding: '0px', 20 | overflow: 'hidden', 21 | whiteSpace: 'nowrap', 22 | position: 'absolute', 23 | }; 24 | 25 | /** 26 | * Visually hidden component used to hide 27 | * elements on screen 28 | */ 29 | export const VisuallyHidden = (props: PropsOf) => ( 30 | 31 | ); 32 | 33 | if (__DEV__) { 34 | VisuallyHidden.displayName = 'VisuallyHidden'; 35 | } 36 | 37 | /** 38 | * Visually hidden input component for designing 39 | * custom input components using the html `input` 40 | * as a proxy 41 | */ 42 | export const VisuallyHiddenInput = forwardRef, 'input'>( 43 | (props, ref) => ( 44 | 45 | ), 46 | ); 47 | 48 | if (__DEV__) { 49 | VisuallyHiddenInput.displayName = 'VisuallyHiddenInput'; 50 | } 51 | export default VisuallyHidden; 52 | -------------------------------------------------------------------------------- /packages/visually-hidden/stories/visually-hidden.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { VisuallyHidden, VisuallyHiddenInput } from '../src'; 3 | 4 | export default { 5 | title: 'Visually Hidden', 6 | }; 7 | 8 | export const hiddenSpan = () => ( 9 | This is visually hidden 10 | ); 11 | 12 | export const hiddenInput = () => ( 13 | console.log(e.target.checked)} 17 | /> 18 | ); 19 | -------------------------------------------------------------------------------- /packages/visually-hidden/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src", "index.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/**" 3 | - "tooling/**" -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | } 6 | } -------------------------------------------------------------------------------- /public/full-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nature-UI/nature-ui/b46dd781be47f510a9deedf011404022ed36f4d1/public/full-logo.png -------------------------------------------------------------------------------- /react-shim.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | export { React } 3 | -------------------------------------------------------------------------------- /release-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@nature-ui/alert': minor, 3 | '@nature-ui/alert-dialog': minor, 4 | '@nature-ui/avatar': minor, 5 | '@nature-ui/breadcrumb': minor, 6 | '@nature-ui/button': minor, 7 | '@nature-ui/checkbox': minor, 8 | '@nature-ui/clickable': minor, 9 | '@nature-ui/close-button': minor, 10 | '@nature-ui/color': minor, 11 | '@nature-ui/container': minor, 12 | '@nature-ui/core': minor, 13 | '@nature-ui/counter': minor, 14 | '@nature-ui/descendant': minor, 15 | '@nature-ui/drawer': minor, 16 | '@nature-ui/focus-lock': minor, 17 | '@nature-ui/form-control': minor, 18 | '@nature-ui/hooks': minor, 19 | '@nature-ui/icon': minor, 20 | '@nature-ui/icons': minor, 21 | '@nature-ui/image': minor, 22 | '@nature-ui/input': minor, 23 | '@nature-ui/layout': minor, 24 | '@nature-ui/modal': minor, 25 | '@nature-ui/number-input': minor, 26 | '@nature-ui/popover': minor, 27 | '@nature-ui/portal': minor, 28 | '@nature-ui/progress': minor, 29 | '@nature-ui/radio': minor, 30 | '@nature-ui/react-utils': minor, 31 | '@nature-ui/spinner': minor, 32 | '@nature-ui/system': minor, 33 | '@nature-ui/test-utils': minor, 34 | '@nature-ui/toast': minor, 35 | '@nature-ui/transition': minor, 36 | '@nature-ui/utils': minor, 37 | '@nature-ui/visually-hidden': minor, 38 | --- 39 | -------------------------------------------------------------------------------- /scripts/setup-test.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/extend-expect'; 2 | const { getComputedStyle } = window; 3 | window.getComputedStyle = (elt) => getComputedStyle(elt); 4 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['packages/**/*.{ts,tsx,js}'], 3 | plugins: [], 4 | theme: { 5 | extend: {}, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /tooling/babel-plugin/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": 8 8 | }, 9 | "loose": true 10 | } 11 | ] 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /tooling/babel-plugin/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "node": true 6 | }, 7 | "parser": "@babel/eslint-parser", 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "jsx": true 11 | }, 12 | "ecmaVersion": 2018, 13 | "sourceType": "module", 14 | "allowImportExportEverywhere": true 15 | }, 16 | "extends": ["plugin:react/recommended", "plugin:prettier/recommended"], 17 | "plugins": ["react"], 18 | "rules": { 19 | "react/prop-types": "off", 20 | "prettier/prettier": [ 21 | "error", 22 | { 23 | "endOfLine": "auto" 24 | } 25 | ] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tooling/babel-plugin/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @nature-ui/babel-plugin 2 | 3 | ## 2.2.0 4 | 5 | ### Minor Changes 6 | 7 | - v2 8 | 9 | ## 2.1.0 10 | 11 | ### Minor Changes 12 | 13 | - v2 14 | 15 | ## 2.0.0 16 | 17 | ### Major Changes 18 | 19 | - v2 20 | 21 | ## 1.0.0 22 | 23 | ### Major Changes 24 | 25 | - v2 26 | 27 | ## 0.0.0 28 | 29 | ### Major Changes 30 | 31 | - Releasing v2 32 | 33 | ## 0.0.0-dev-20230301133840 34 | 35 | ### Patch Changes 36 | 37 | - update react version 38 | 39 | ## 0.0.0-dev-20230301120121 40 | 41 | ### Patch Changes 42 | 43 | - dev 44 | 45 | ## 0.0.0-dev-20221217102739 46 | 47 | ### Patch Changes 48 | 49 | - Optimization 50 | 51 | ## 0.0.0-dev-20221025084126 52 | 53 | ### Patch Changes 54 | 55 | - Implemented PNPM, Turborepo, tsup and clean. 56 | 57 | ## 1.0.1 58 | 59 | ### Patch Changes 60 | 61 | - Fix webpack console warnings 62 | -------------------------------------------------------------------------------- /tooling/babel-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nature-ui/babel-plugin", 3 | "version": "2.2.0", 4 | "description": "Mark custom forwardRef and memo as pure for tree shaking", 5 | "main": "dist/index.js", 6 | "publishConfig": { 7 | "access": "public" 8 | }, 9 | "files": [ 10 | "dist" 11 | ], 12 | "scripts": { 13 | "test": "jest --passWithNoTests", 14 | "prepublish": "pnpm build", 15 | "lint": "eslint --ext .js src", 16 | "build": "babel src -d dist" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+ssh://git@github.com/Nature-UI/nature-ui.git" 21 | }, 22 | "keywords": [ 23 | "nature-ui", 24 | "babel-plugin" 25 | ], 26 | "author": "Divine Hycenth ", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/Nature-UI/nature-ui/issues" 30 | }, 31 | "homepage": "https://github.com/Nature-UI/nature-ui#readme", 32 | "dependencies": { 33 | "@babel/helper-annotate-as-pure": "^7.18.6", 34 | "@babel/helper-plugin-utils": "^7.19.0" 35 | }, 36 | "peerDependencies": { 37 | "@babel/core": "^7.19.3" 38 | }, 39 | "devDependencies": { 40 | "@babel/core": "^7.19.3", 41 | "@babel/eslint-parser": "^7.19.1", 42 | "@nature-ui/system": "workspace:*", 43 | "clean-package": "^2.1.1" 44 | }, 45 | "jest": { 46 | "testEnvironment": "node", 47 | "testRegex": "/test/.*\\.test\\.js$", 48 | "collectCoverageFrom": [ 49 | "src/**/*.js", 50 | "!src/log.js" 51 | ] 52 | }, 53 | "gitHead": "cafae1d72b2a7027028454c22d4bbaebfdafba5f" 54 | } 55 | -------------------------------------------------------------------------------- /tooling/babel-plugin/src/index.js: -------------------------------------------------------------------------------- 1 | import { types } from '@babel/core'; 2 | import annotateAsPure from '@babel/helper-annotate-as-pure'; 3 | import { declare } from '@babel/helper-plugin-utils'; 4 | 5 | const PURE_CALLS = new Map([ 6 | ['@nature-ui/system', ['forwardRef', 'memo']], 7 | ['@nature-ui/core', ['forwardRef', 'memo']], 8 | ]); 9 | 10 | export default declare((api) => { 11 | api.assertVersion(7); 12 | 13 | return { 14 | name: 'transform-react-pure-annotations', 15 | visitor: { 16 | CallExpression(path) { 17 | if (isImported(path)) { 18 | annotateAsPure(path); 19 | } 20 | }, 21 | }, 22 | }; 23 | }); 24 | 25 | function isImported(path) { 26 | if (!types.isMemberExpression(path.node.callee)) { 27 | const callee = path.get('callee'); 28 | for (const [module, methods] of PURE_CALLS) { 29 | for (const method of methods) { 30 | if (callee.referencesImport(module, method)) { 31 | return true; 32 | } 33 | } 34 | } 35 | 36 | return false; 37 | } 38 | return false; 39 | } 40 | -------------------------------------------------------------------------------- /tooling/test-utils/README.md: -------------------------------------------------------------------------------- 1 | # `@nature-ui/test-utils` 2 | 3 | > TODO: description 4 | 5 | ## Usage 6 | 7 | ```jsx 8 | import { render } from '@nature-ui/test-utils'; 9 | 10 | // TODO: DEMONSTRATE API 11 | ``` 12 | -------------------------------------------------------------------------------- /tooling/test-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nature-ui/test-utils", 3 | "version": "2.2.13", 4 | "description": "Testing utilities for Nature UI UI's components", 5 | "keywords": [ 6 | "nature ui", 7 | "test utils", 8 | "testing", 9 | "utils", 10 | "testing library", 11 | "react testing library" 12 | ], 13 | "author": "Divine Hycenth ", 14 | "homepage": "https://github.com/DNature/nature-ui", 15 | "license": "MIT", 16 | "main": "src/index.ts", 17 | "directories": { 18 | "lib": "src" 19 | }, 20 | "files": [ 21 | "src" 22 | ], 23 | "publishConfig": { 24 | "access": "public" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/DNature/nature-ui.git" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/DNature/nature-ui/issues" 32 | }, 33 | "dependencies": { 34 | "@nature-ui/hooks": "workspace:*", 35 | "@nature-ui/utils": "workspace:*", 36 | "@testing-library/jest-dom": "^5.16.5", 37 | "@testing-library/react": "^13.4.0", 38 | "@testing-library/react-hooks": "^8.0.1", 39 | "@testing-library/user-event": "^14.4.3", 40 | "@types/jest-axe": "^3.5.4", 41 | "jest-axe": "^6.0.0", 42 | "react-test-renderer": "^18.2.0" 43 | }, 44 | "devDependencies": { 45 | "@nature-ui/system": "workspace:*", 46 | "clean-package": "^2.1.1", 47 | "react": "^18.0.0" 48 | }, 49 | "peerDependencies": { 50 | "react": ">=18" 51 | }, 52 | "gitHead": "cafae1d72b2a7027028454c22d4bbaebfdafba5f" 53 | } 54 | -------------------------------------------------------------------------------- /tooling/test-utils/src/accessibility.tsx: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom/extend-expect" 2 | import { RenderOptions } from "@testing-library/react" 3 | import { axe, toHaveNoViolations, JestAxeConfigureOptions } from "jest-axe" 4 | import * as React from "react" 5 | import { render } from "./render" 6 | 7 | expect.extend(toHaveNoViolations) 8 | 9 | export async function testA11y( 10 | ui: React.ReactElement | HTMLElement, 11 | options: RenderOptions & { axeOptions?: JestAxeConfigureOptions } = {}, 12 | ) { 13 | const { axeOptions, ...rest } = options 14 | const container = React.isValidElement(ui) ? render(ui, rest).container : ui 15 | const results = await axe(container, axeOptions) 16 | expect(results).toHaveNoViolations() 17 | } 18 | -------------------------------------------------------------------------------- /tooling/test-utils/src/focus.ts: -------------------------------------------------------------------------------- 1 | import { getActiveElement, isFocusable } from '@nature-ui/utils'; 2 | import { act } from '@testing-library/react'; 3 | 4 | export function focus(el: HTMLElement) { 5 | if (getActiveElement(el) === el) return; 6 | if (!isFocusable(el)) return; 7 | act(() => { 8 | el.focus(); 9 | }); 10 | } 11 | 12 | export function blur(el?: HTMLElement | null) { 13 | if (el == null) el = document.activeElement as HTMLElement; 14 | if (el.tagName === 'BODY') return; 15 | if (getActiveElement(el) !== el) return; 16 | act(() => { 17 | if (el && 'blur' in el) el.blur(); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /tooling/test-utils/src/hooks.ts: -------------------------------------------------------------------------------- 1 | import { renderHook, act } from "@testing-library/react-hooks" 2 | 3 | export const hooks = { 4 | render: renderHook, 5 | act, 6 | } 7 | -------------------------------------------------------------------------------- /tooling/test-utils/src/index.ts: -------------------------------------------------------------------------------- 1 | export { act, fireEvent, screen, waitFor } from '@testing-library/react'; 2 | export * from './accessibility'; 3 | export { blur, focus } from './focus'; 4 | export * from './hooks'; 5 | export * from './press'; 6 | export { render } from './render'; 7 | export * from './test-utils'; 8 | import { mockImage } from './mock-image'; 9 | 10 | export const mocks = { 11 | image: mockImage, 12 | }; 13 | -------------------------------------------------------------------------------- /tooling/test-utils/src/mock-image.ts: -------------------------------------------------------------------------------- 1 | type Status = 'loaded' | 'error'; 2 | 3 | const originalImage = window.Image; 4 | 5 | export function mockImage() { 6 | let status: Status; 7 | 8 | //@ts-expect-error 9 | window.Image = class Image { 10 | onload: VoidFunction = () => { 11 | console.log('called'); 12 | }; 13 | 14 | onerror: VoidFunction = () => {}; 15 | 16 | src = ''; 17 | 18 | alt = ''; 19 | 20 | hasAttribute(name: string) { 21 | return name in this; 22 | } 23 | 24 | getAttribute(name: string) { 25 | return name in this ? (this as any)[name] : null; 26 | } 27 | 28 | constructor() { 29 | setTimeout(() => { 30 | if (status === 'error') { 31 | this.onerror(); 32 | } else { 33 | this.onload(); 34 | } 35 | }, mockImage.DELAY); 36 | return this; 37 | } 38 | }; 39 | 40 | return { 41 | simulate(value: Status) { 42 | status = value; 43 | }, 44 | restore() { 45 | window.Image = originalImage; 46 | }, 47 | }; 48 | } 49 | 50 | mockImage.restore = () => { 51 | window.Image = originalImage; 52 | }; 53 | 54 | mockImage.DELAY = 100; 55 | -------------------------------------------------------------------------------- /tooling/test-utils/src/press.ts: -------------------------------------------------------------------------------- 1 | import { fireEvent } from '@testing-library/react'; 2 | 3 | export const press = ( 4 | key: string, 5 | element?: Element | null, 6 | options: KeyboardEventInit = {}, 7 | ) => { 8 | if (!element) return; 9 | 10 | if (document.activeElement !== element) { 11 | fireEvent.focus(element); 12 | } 13 | 14 | fireEvent.keyDown(element, { 15 | key, 16 | ...options, 17 | }); 18 | fireEvent.keyUp(element, { 19 | key, 20 | ...options, 21 | }); 22 | }; 23 | 24 | const createPress = (key: string, defaultOptions: KeyboardEventInit = {}) => { 25 | return (element?: Element | null, options: KeyboardEventInit = {}) => 26 | press(key, element, { 27 | ...defaultOptions, 28 | ...options, 29 | }); 30 | }; 31 | 32 | press.Escape = createPress('Escape'); 33 | press.Backspace = createPress('Backspace'); 34 | press.Delete = createPress('Delete'); 35 | press.Tab = createPress('Tab'); 36 | press.ShiftTab = createPress('Tab', { shiftKey: true }); 37 | press.Enter = createPress('Enter'); 38 | press.Space = createPress(' '); 39 | press.ArrowUp = createPress('ArrowUp'); 40 | press.ArrowRight = createPress('ArrowRight'); 41 | press.ArrowDown = createPress('ArrowDown'); 42 | press.ArrowLeft = createPress('ArrowLeft'); 43 | press.End = createPress('End'); 44 | press.Home = createPress('Home'); 45 | press.PageUp = createPress('PageUp'); 46 | press.PageDown = createPress('PageDown'); 47 | -------------------------------------------------------------------------------- /tooling/test-utils/src/render.tsx: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/extend-expect'; 2 | import { render as rtlRender } from '@testing-library/react'; 3 | import { toHaveNoViolations } from 'jest-axe'; 4 | import React from 'react'; 5 | import { userEvent } from './user-event'; 6 | 7 | expect.extend(toHaveNoViolations); 8 | 9 | export function render( 10 | ui: React.ReactElement, 11 | { ...options }: any = {}, 12 | ): ReturnType & { user: ReturnType } { 13 | const { wrapper: Wrapper = React.Fragment, ...rtlOptions } = options; 14 | const user = userEvent.setup(); 15 | 16 | const result = rtlRender( 17 | <> 18 | {ui} 19 | , 20 | rtlOptions, 21 | ); 22 | return { user, ...result }; 23 | } 24 | -------------------------------------------------------------------------------- /tooling/test-utils/src/test-utils.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/extend-expect'; 2 | import { fireEvent } from '@testing-library/react'; 3 | import { toHaveNoViolations } from 'jest-axe'; 4 | 5 | expect.extend(toHaveNoViolations); 6 | 7 | export { act as invoke, renderHook } from '@testing-library/react-hooks'; 8 | 9 | export { default as userEvent } from '@testing-library/user-event'; 10 | export const escape = (ui: HTMLElement) => 11 | fireEvent.keyDown(ui, { 12 | key: 'Escape', 13 | keyCode: 27, 14 | }); 15 | -------------------------------------------------------------------------------- /tooling/test-utils/src/user-event.ts: -------------------------------------------------------------------------------- 1 | import { act } from '@testing-library/react'; 2 | import $userEvent from '@testing-library/user-event'; 3 | import { press } from './press'; 4 | import { sleep } from './utils'; 5 | 6 | type Writeable = { -readonly [P in keyof T]: T[P] }; 7 | 8 | type PatchResult = Omit, 'setup'> & { 9 | press: typeof press; 10 | setup: (...args: any[]) => PatchResult; 11 | }; 12 | 13 | function patch($value: any) { 14 | const result = Object.entries($value).reduce((acc, [key, value]) => { 15 | if (key === 'setup') { 16 | //@ts-expect-error 17 | acc[key] = (...args: any[]) => ({ ...patch(value(...args)), press }); 18 | } else { 19 | acc[key] = async (...args: any[]) => { 20 | act(() => { 21 | //@ts-expect-error 22 | value(...args); 23 | }); 24 | await sleep(); 25 | }; 26 | } 27 | 28 | return acc; 29 | }, {} as any); 30 | 31 | return result as PatchResult; 32 | } 33 | 34 | const userEvent = { ...patch($userEvent), press }; 35 | 36 | export { userEvent }; 37 | -------------------------------------------------------------------------------- /tooling/test-utils/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { act } from '@testing-library/react'; 2 | 3 | export function queue(): Promise { 4 | return act(() => Promise.resolve()); 5 | } 6 | 7 | export function nextTick(): Promise { 8 | return act( 9 | () => 10 | new Promise((resolve) => requestAnimationFrame(() => resolve())), 11 | ); 12 | } 13 | 14 | export async function sleep(ms = 16): Promise { 15 | await act(() => new Promise((resolve) => setTimeout(resolve, ms))); 16 | await nextTick(); 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "lib": ["dom", "esnext"], 6 | "declaration": true, 7 | "sourceMap": true, 8 | "moduleResolution": "node", 9 | "skipLibCheck": true, 10 | "strict": true, 11 | "isolatedModules": true, 12 | "jsx": "react-jsx", 13 | "esModuleInterop": true, 14 | "resolveJsonModule": true, 15 | "allowSyntheticDefaultImports": true, 16 | "downlevelIteration": true, 17 | "removeComments": true, 18 | "noImplicitAny": false, 19 | "noImplicitReturns": false, 20 | "noFallthroughCasesInSwitch": true 21 | // "types": ["node", "jest", "@testing-library/jest-dom"] 22 | }, 23 | "exclude": ["**/node_modules"], 24 | "include": ["@types", "packages", "tooling", "scripts", "react-shim.js"] 25 | } 26 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | clean: true, 5 | format: ['cjs', 'esm'], 6 | outExtension(ctx) { 7 | return { js: `.${ctx.format}.js` }; 8 | }, 9 | target: 'es2019', 10 | // inject: process.env.JSX ? [findUpSync("react-shim.js")!] : undefined, 11 | }); 12 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turborepo.org/schema.json", 3 | "pipeline": { 4 | "build": { 5 | "outputs": ["dist/**"], 6 | "dependsOn": ["^build"] 7 | }, 8 | "build:fast": { 9 | "outputs": ["dist/**"], 10 | "dependsOn": ["^build:fast"] 11 | }, 12 | "dev": { 13 | "cache": false 14 | }, 15 | "typecheck": { 16 | "cache": false, 17 | "dependsOn": ["^typecheck"] 18 | } 19 | }, 20 | "globalDependencies": ["tsconfig.json"] 21 | } 22 | --------------------------------------------------------------------------------