├── .commitlintrc.js ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ └── release_checklist.md └── PULL_REQUEST_TEMPLATE │ ├── pr_cn.md │ └── pr_en.md ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .stylelintrc.js ├── .travis.yml ├── .umirc.ts ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── components ├── alert │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ └── index.test.tsx │ ├── alert.tsx │ ├── demos │ │ ├── closable.tsx │ │ ├── description.tsx │ │ └── type.tsx │ ├── index.md │ ├── index.tsx │ └── style │ │ ├── _mixins.scss │ │ ├── index.scss │ │ └── index.tsx ├── auto-complete │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ └── index.test.tsx │ ├── autoComplete.tsx │ ├── demos │ │ ├── ajaxSearch.tsx │ │ ├── base.tsx │ │ └── renderOption.tsx │ ├── index.md │ ├── index.tsx │ └── style │ │ └── index.scss ├── button │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ └── index.test.tsx │ ├── button.tsx │ ├── demos │ │ ├── block.tsx │ │ ├── disabled.tsx │ │ ├── size.tsx │ │ └── type.tsx │ ├── index.md │ ├── index.tsx │ └── style │ │ ├── _mixins.scss │ │ ├── index.scss │ │ └── index.tsx ├── hooks │ ├── useClickOutside.ts │ └── useDebounce.ts ├── index.tsx ├── input │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ └── index.test.tsx │ ├── demos │ │ ├── base.tsx │ │ ├── disabled.tsx │ │ ├── icon.tsx │ │ ├── prefix-suffix.tsx │ │ └── size.tsx │ ├── index.md │ ├── index.tsx │ ├── input.tsx │ └── style │ │ └── index.scss ├── menu │ ├── MenuContext.ts │ ├── MenuItem.tsx │ ├── SubMenu.tsx │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ └── index.test.tsx │ ├── demos │ │ ├── defaultOpenKeys.tsx │ │ ├── horizontal.tsx │ │ ├── inline.tsx │ │ └── mode.tsx │ ├── index.md │ ├── index.tsx │ ├── menu.tsx │ └── style │ │ ├── index.scss │ │ └── index.tsx ├── select │ ├── Option.tsx │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ └── index.test.tsx │ ├── demos │ │ ├── base.tsx │ │ ├── disabled.tsx │ │ └── multiple.tsx │ ├── index.md │ ├── index.tsx │ ├── select.tsx │ └── style │ │ ├── index.scss │ │ └── index.tsx ├── style │ ├── core │ │ ├── _motion.scss │ │ ├── _normalize.scss │ │ └── index.scss │ ├── index.scss │ ├── index.tsx │ ├── mixins │ │ ├── _animation.scss │ │ ├── _clearfix.scss │ │ ├── _common.scss │ │ └── index.scss │ └── theme │ │ ├── _default.scss │ │ └── index.scss ├── tabs │ ├── TabPane.tsx │ ├── Tabs.tsx │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ └── index.test.tsx │ ├── demos │ │ ├── base.tsx │ │ ├── disabled.tsx │ │ └── type.tsx │ ├── index.md │ ├── index.tsx │ └── style │ │ ├── index.scss │ │ └── index.tsx ├── tag │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ └── index.test.tsx │ ├── demos │ │ ├── base.tsx │ │ ├── color.tsx │ │ └── size.tsx │ ├── index.md │ ├── index.tsx │ ├── style │ │ ├── _mixins.scss │ │ ├── index.scss │ │ └── index.tsx │ └── tag.tsx └── transition │ ├── index.tsx │ └── transition.tsx ├── docs ├── guide │ ├── README.md │ └── contribute.md └── index.md ├── jest.config.js ├── package.json ├── scripts └── jest │ └── setup.ts ├── tests └── utils.ts ├── tsconfig.build.json ├── tsconfig.json ├── typings.d.ts └── yarn.lock /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * build : 改变了build工具 3 | * ci : 持续集成 4 | * chore : 构建过程或辅助工具的变动 5 | * feat : 新功能 6 | * docs : 仅文档新增/改动 7 | * fix : 修复bug 8 | * perf : 性能优化 9 | * refactor : 某个已有功能重构 10 | * revert : 撤销上一次的 commit 11 | * style : 代码格式改变 12 | * test : 增加测试 13 | */ 14 | module.exports = { 15 | extends: ['@commitlint/config-conventional'], 16 | rules: { 17 | 'type-enum': [ 18 | 2, 19 | 'always', 20 | [ 21 | 'build', 22 | 'ci', 23 | 'chore', 24 | 'docs', 25 | 'feat', 26 | 'fix', 27 | 'perf', 28 | 'refactor', 29 | 'revert', 30 | 'style', 31 | 'test', 32 | ], 33 | ], 34 | }, 35 | } 36 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | /doc-list 4 | /src 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | jest: true, 6 | }, 7 | extends: ['airbnb', 'plugin:react/recommended', 'prettier/react'], 8 | parser: '@typescript-eslint/parser', 9 | parserOptions: { 10 | ecmaFeatures: { 11 | jsx: true, 12 | }, 13 | ecmaVersion: 12, 14 | sourceType: 'module', 15 | }, 16 | plugins: ['react', '@typescript-eslint', 'react-hooks'], 17 | rules: { 18 | semi: 0, 19 | 'react/jsx-filename-extension': 0, 20 | 'react/prop-types': 0, 21 | 'react/button-has-type': 0, 22 | 'react/display-name': 0, 23 | 'react/jsx-props-no-spreading': 0, 24 | 25 | 'jsx-a11y/click-events-have-key-events': 0, 26 | 'jsx-a11y/no-static-element-interactions': 0, 27 | 'jsx-a11y/no-noninteractive-element-interactions': 0, 28 | 29 | indent: 0, 30 | 'no-use-before-define': 0, 31 | 'no-unused-vars': 0, 32 | 'implicit-arrow-linebreak': 0, 33 | 'consistent-return': 0, 34 | 'arrow-parens': 0, 35 | 'object-curly-newline': 0, 36 | 'operator-linebreak': 0, 37 | 'import/no-extraneous-dependencies': 0, 38 | 'import/extensions': 0, 39 | 'import/no-unresolved': 0, 40 | 'import/prefer-default-export': 0, 41 | }, 42 | } 43 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 参与共建 2 | 3 | 想要给 Monki UI 贡献自己的一份力量? 4 | 5 | 我写了一份 **[贡献指南](https://jacky-summer.github.io/monki-ui/guide/contribute)** 来帮助你开始。 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/release_checklist.md: -------------------------------------------------------------------------------- 1 | # Release Checklist ✍️ 2 | 3 | > These steps should be performed when making a new release on [monki-ui](https://github.com/Jacky-Summer/monki-ui). 4 | 5 | ## Deploy to Prod 6 | 7 | - [ ] Create `release/${version-number}` from development branch and push to origin. 8 | - [ ] Create `chore/bump-${version-number}` from above created `release/${version-number}`. 9 | - [ ] Execute `yarn bump-version` to generate CHANGELOG in the `chore/bump-${version-number}` branch and push it to origin. 10 | - [ ] Create a PR to merge `chore/bump-${version-number}` into `release/${version-number}`. ( if have Staging site, the release branch should automatically deployed to the Stag. This personal open source project —— [monki-ui](https://github.com/Jacky-Summer/monki-ui) doesn't have Staging site. ) 11 | - [ ] Create a PR to merge `release/${version-number}` into master branch, it will be automatically deployed in Prod. 12 | 13 | ## Post-release 14 | 15 | - [ ] PR to merge back master to development. 16 | - [ ] Create a release in GitHub, tag release version on the master branch and fill the content from corresponding CHANGELOG. 17 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/pr_cn.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | [[English Template / 英文模板](https://github.com/Jacky-Summer/monki-ui/blob/development/.github/PULL_REQUEST_TEMPLATE/pr_en.md)] 9 | 10 | ### 🤔 这个变动的性质是? 11 | 12 | - [ ] 新特性提交 13 | - [ ] 日常 bug 修复 14 | - [ ] 站点、文档改进 15 | - [ ] 演示代码改进 16 | - [ ] 组件样式/交互改进 17 | - [ ] TypeScript 定义更新 18 | - [ ] 包体积优化 19 | - [ ] 性能优化 20 | - [ ] 功能增强 21 | - [ ] 重构 22 | - [ ] 代码风格优化 23 | - [ ] 测试用例 24 | - [ ] 其他改动(是关于什么的改动?) 25 | 26 | ### 🔗 相关 Issue 27 | 28 | 31 | 32 | ### 💡 需求背景和解决方案 33 | 34 | 39 | 40 | ### 📝 更新日志 41 | 42 | 45 | 46 | | 语言 | 更新描述 | 47 | | ------- | -------- | 48 | | 🇺🇸 英文 | | 49 | | 🇨🇳 中文 | | 50 | 51 | ### PR 标题与 commit 信息开头请遵循如下规范: 52 | 53 | - ✨ feat:新功能 54 | - 🔧 chore:构建过程或辅助工具的变动 55 | - 📝 docs:仅文档新增/改动 56 | - 🐛 fix:修复 bug 57 | - 🚀 perf:性能优化 58 | - 🔨 refactor:某个已有功能重构 59 | - ⏪ revert:代码回滚 60 | - 🎨 style:代码格式改变 61 | - ✅ test:添加缺失的测试或更正现有的测试 62 | - 📦 build:改变了 build 工具 63 | - 👷 ci:持续集成 64 | 65 | 🎉 release(只适用于分支标题):发布版本提交 66 | 67 | ### ☑️ 请求合并前的自查清单 68 | 69 | ⚠️ 请自检并全部**勾选全部选项**。⚠️ 70 | 71 | - [ ] 文档已补充或无须补充 72 | - [ ] 代码演示已提供或无须提供 73 | - [ ] TypeScript 定义已补充或无须补充 74 | - [ ] Changelog 已提供或无须提供 75 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/pr_en.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | [[中文版模板 / Chinese template](https://github.com/Jacky-Summer/monki-ui/blob/development/.github/PULL_REQUEST_TEMPLATE/pr_cn.md)] 9 | 10 | ### 🤔 This is a ... 11 | 12 | - [ ] New feature 13 | - [ ] Bug fix 14 | - [ ] Site / documentation update 15 | - [ ] Demo update 16 | - [ ] Component style update 17 | - [ ] TypeScript definition update 18 | - [ ] Bundle size optimization 19 | - [ ] Performance optimization 20 | - [ ] Enhancement feature 21 | - [ ] Refactoring 22 | - [ ] Code style optimization 23 | - [ ] Test Case 24 | - [ ] Other (about what?) 25 | 26 | ### 🔗 Related issue link 27 | 28 | 31 | 32 | ### 💡 Background and solution 33 | 34 | 39 | 40 | ### 📝 Changelog 41 | 42 | 45 | 46 | | Language | Changelog | 47 | | ---------- | --------- | 48 | | 🇺🇸 English | | 49 | | 🇨🇳 Chinese | | 50 | 51 | ### At the begining of PR title and commit message, please follow below standard: 52 | 53 | - ✨ feat:A new feature 54 | - 🔧 chore:updating chore tasks etc 55 | - 📝 docs:Documentation only changes 56 | - 🐛 fix:A bug fix 57 | - 🚀 perf:A code change that improves performance 58 | - 🔨 refactor:A code change that neither fixes a bug nor adds a feature 59 | - ⏪ revert:code revert to specific commit or version 60 | - 🎨 style:Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) 61 | - ✅ test:Adding missing tests or correcting existing tests 62 | - 📦 build:Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm) 63 | - 👷 ci:Changes to our CI configuration files and scripts (example scopes: Travis, Circle) 64 | 65 | 🎉 release(only used in PR title):submit release version 66 | 67 | ### ☑️ Self Check before Merge 68 | 69 | ⚠️ Please check all items below before review. ⚠️ 70 | 71 | - [ ] Doc is updated/provided or not needed 72 | - [ ] Demo is updated/provided or not needed 73 | - [ ] TypeScript definition is updated/provided or not needed 74 | - [ ] Changelog is provided or not needed 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /npm-debug.log* 6 | /yarn-error.log 7 | /package-lock.json 8 | 9 | # production 10 | /dist 11 | /docs-dist 12 | 13 | # misc 14 | .DS_Store 15 | 16 | # umi 17 | .umi 18 | .umi-production 19 | .umi-test 20 | .env.local 21 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.svg 2 | **/*.ejs 3 | **/*.html 4 | package.json 5 | .umi 6 | .umi-production 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "tabWidth": 2, 4 | "endOfLine": "lf", 5 | "trailingComma": "all", 6 | "printWidth": 90, 7 | "arrowParens": "avoid", 8 | "semi": false, 9 | "bracketSpacing": true, 10 | "overrides": [ 11 | { 12 | "files": ".prettierrc", 13 | "options": { "parser": "json" } 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'stylelint-config-standard', 4 | 'stylelint-config-rational-order', 5 | 'stylelint-config-prettier', 6 | ], 7 | plugins: [ 8 | 'stylelint-order', 9 | 'stylelint-config-rational-order/plugin', 10 | 'stylelint-scss', 11 | ], 12 | rules: { 13 | indentation: 2, // 缩进2个空格 14 | 'string-quotes': 'single', 15 | 'number-leading-zero': 'never', 16 | 'declaration-block-trailing-semicolon': 'always', 17 | 'max-empty-lines': 1, 18 | 'block-closing-brace-empty-line-before': ['never'], 19 | 'declaration-empty-line-before': ['never', { ignore: ['after-declaration'] }], 20 | 'length-zero-no-unit': true, 21 | 'no-eol-whitespace': true, 22 | 'no-missing-end-of-source-newline': true, 23 | 'comment-whitespace-inside': 'always', 24 | 'selector-combinator-space-before': 'always', 25 | 'block-opening-brace-space-before': 'always', 26 | 'comment-whitespace-inside': 'always', 27 | 'declaration-colon-space-after': 'always', 28 | 'declaration-colon-space-before': 'never', 29 | 'declaration-block-semicolon-space-before': 'never', 30 | 'function-comma-space-after': 'always', 31 | 'selector-combinator-space-before': 'always', 32 | 'selector-combinator-space-after': 'always', 33 | 'selector-list-comma-space-after': 'always', 34 | 'selector-descendant-combinator-no-non-space': true, 35 | 'at-rule-no-unknown': null, 36 | 'scss/at-rule-no-unknown': true, 37 | }, 38 | syntax: 'scss', 39 | ignoreFiles: [ 40 | 'src/**/*', 41 | 'node_modules/**/*', 42 | 'dist/**/*', 43 | 'docs-dist/**/*', 44 | 'docs/**/*', 45 | 'tests/**/*', 46 | '**/*.png', 47 | '**/*.jpg', 48 | '**/*.jpeg', 49 | '**/*.PNG', 50 | '**/*.JPG', 51 | '**/*.JPEG', 52 | '**/*.svg', 53 | '**/*.eot', 54 | '**/*.svg', 55 | '**/*.ttf', 56 | '**/*.woff', 57 | '**/*.woff2', 58 | '**/*.otf', 59 | '**/*.txt', 60 | '**/*.js', 61 | '**/*.json', 62 | '**/*.font', 63 | '**/*.md', 64 | '**/*.min.*', 65 | '.*', 66 | ], 67 | } 68 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 12 5 | 6 | cache: yarn 7 | 8 | install: 9 | - yarn 10 | script: 11 | - yarn test 12 | - yarn build 13 | 14 | jobs: 15 | include: 16 | - stage: general 17 | if: branch = development AND commit_message !~ /\[skip publish\]/ 18 | 19 | before_deploy: 20 | - yarn bump-version:prerelease 21 | - yarn build 22 | 23 | deploy: 24 | - provider: npm 25 | email: $NPM_EMAIL 26 | api_key: $NPM_TOKEN 27 | skip_cleanup: true 28 | on: 29 | branch: development 30 | tag: next 31 | 32 | - stage: general 33 | if: branch = master AND commit_message !~ /\[skip publish\]/ 34 | 35 | before_deploy: 36 | - > 37 | if ! [ "${BEFORE_DEPLOY_RUN}" ]; then 38 | export BEFORE_DEPLOY_RUN=1; 39 | git config user.email "builds@travis-ci.com" 40 | git config user.name "Travis CI" 41 | yarn bump-version 42 | git push --verbose --no-verify --follow-tags ${OWNER}${GH_TOKEN}@${GH_REF} HEAD:master 43 | yarn build 44 | fi 45 | deploy: 46 | - provider: pages 47 | github_token: $GH_TOKEN 48 | skip_cleanup: true 49 | keep_history: true 50 | local_dir: docs-dist 51 | on: 52 | branch: master 53 | 54 | - provider: npm 55 | email: $NPM_EMAIL 56 | api_key: $NPM_TOKEN 57 | skip_cleanup: true 58 | keep_history: true 59 | on: 60 | branch: master 61 | -------------------------------------------------------------------------------- /.umirc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'dumi' 2 | 3 | export default defineConfig({ 4 | title: 'Monki UI', 5 | favicon: 'https://img.icons8.com/ultraviolet/2x/year-of-monkey.png', 6 | logo: 'https://img.icons8.com/ultraviolet/2x/year-of-monkey.png', 7 | base: '/monki-ui', 8 | publicPath: '/monki-ui/', 9 | outputPath: 'docs-dist', 10 | mode: 'site', 11 | exportStatic: {}, // 将所有路由输出为 HTML 目录结构,以免刷新页面时 404 12 | resolve: { 13 | includes: ['docs', 'components'], 14 | }, 15 | navs: [ 16 | null, 17 | { 18 | title: 'Github', 19 | path: 'https://github.com/Jacky-Summer/monki-ui', 20 | }, 21 | ], 22 | // more config: https://d.umijs.org/config 23 | }) 24 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | "**/node_modules": true, 4 | "dist": true, 5 | "docs-dist": true, 6 | "yarn.lock": true, 7 | "src/.umi": true 8 | }, 9 | "editor.formatOnSave": true, 10 | "eslint.validate": [ 11 | "javascript", 12 | "javascriptreact", 13 | "typescript", 14 | "typescriptreact" 15 | ], 16 | "css.validate": false, 17 | "less.validate": false, 18 | "editor.codeActionsOnSave": { 19 | "source.fixAll.eslint": true, 20 | "source.fixAll.stylelint": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [1.9.1](https://github.com/Jacky-Summer/monki-ui/compare/v1.9.0...v1.9.1) (2021-06-12) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * no need module ([#79](https://github.com/Jacky-Summer/monki-ui/issues/79)) ([c997dbe](https://github.com/Jacky-Summer/monki-ui/commit/c997dbe514feb0e082dc330c3e51c26df490d820)) 11 | 12 | ## [1.9.0](https://github.com/Jacky-Summer/monki-ui/compare/v1.8.0...v1.9.0) (2021-04-07) 13 | 14 | 15 | ### Features 16 | 17 | * add select component ([#74](https://github.com/Jacky-Summer/monki-ui/issues/74)) ([cd002b6](https://github.com/Jacky-Summer/monki-ui/commit/cd002b6d2146ab6434f02c1515c01440e8e99aa2)) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * select component export name typo ([#76](https://github.com/Jacky-Summer/monki-ui/issues/76)) ([d34d279](https://github.com/Jacky-Summer/monki-ui/commit/d34d2799ea9ee939e05dce89bbf7caeb1812c1f2)) 23 | 24 | ## [1.8.0](https://github.com/Jacky-Summer/monki-ui/compare/v1.7.2...v1.8.0) (2021-03-14) 25 | 26 | 27 | ### Features 28 | 29 | * tag component ([#70](https://github.com/Jacky-Summer/monki-ui/issues/70)) ([c1bd1eb](https://github.com/Jacky-Summer/monki-ui/commit/c1bd1ebcddea4b525df8dfef4659517ee2ee03b7)) 30 | 31 | ### [1.7.2](https://github.com/Jacky-Summer/monki-ui/compare/v1.7.1...v1.7.2) (2021-02-28) 32 | 33 | 34 | ### Bug Fixes 35 | 36 | * before deploy exec twice ([#68](https://github.com/Jacky-Summer/monki-ui/issues/68)) ([e009221](https://github.com/Jacky-Summer/monki-ui/commit/e00922158a4e223b34e69b039d2dbc149e55c7b6)) 37 | 38 | ### [1.7.1](https://github.com/Jacky-Summer/monki-ui/compare/v1.7.0...v1.7.1) (2021-02-28) 39 | 40 | ## [1.7.0](https://github.com/Jacky-Summer/monki-ui/compare/v1.6.0...v1.7.0) (2021-02-28) 41 | 42 | 43 | ### Features 44 | 45 | * add AutoComplete component ([#57](https://github.com/Jacky-Summer/monki-ui/issues/57)) ([b3523d3](https://github.com/Jacky-Summer/monki-ui/commit/b3523d38303411bbb86d5fc5b0ea9c381470a01a)) 46 | 47 | ## [1.6.0](https://github.com/Jacky-Summer/monki-ui/compare/v1.5.0...v1.6.0) (2021-01-30) 48 | 49 | ### Features 50 | 51 | - add input component ([#52](https://github.com/Jacky-Summer/monki-ui/issues/52)) ([1cab7a1](https://github.com/Jacky-Summer/monki-ui/commit/1cab7a1d5b4708f5b1e55dfd2b8c78a74efff480)) 52 | 53 | ### Bug Fixes 54 | 55 | - stylelint scss strict rules to check ([#51](https://github.com/Jacky-Summer/monki-ui/issues/51)) ([603d0b7](https://github.com/Jacky-Summer/monki-ui/commit/603d0b7a81c391c988d0eb1c7136fd0f8f113c98)) 56 | 57 | ## [1.5.0](https://github.com/Jacky-Summer/monki-ui/compare/v1.4.0...v1.5.0) (2021-01-01) 58 | 59 | ### Features 60 | 61 | - tabs component ([#45](https://github.com/Jacky-Summer/monki-ui/issues/45)) ([d806994](https://github.com/Jacky-Summer/monki-ui/commit/d806994daa07afc50cb2d926033c183d0f253ff8)) 62 | 63 | ## [1.4.0](https://github.com/Jacky-Summer/monki-ui/compare/v1.2.0...v1.4.0) (2020-12-05) 64 | 65 | ### Features 66 | 67 | - menu component ([#34](https://github.com/Jacky-Summer/monki-ui/issues/34)) ([0ebaf77](https://github.com/Jacky-Summer/monki-ui/commit/0ebaf77d6402eab075e61a3f06984b052042c24a)) 68 | - upgrade react16 to react17 version ([#28](https://github.com/Jacky-Summer/monki-ui/issues/28)) ([695c6e4](https://github.com/Jacky-Summer/monki-ui/commit/695c6e4c8d3d7036ceae2683dd52a487cc36eeb9)) 69 | 70 | ## [1.3.0](https://github.com/Jacky-Summer/monki-ui/compare/v1.2.0...v1.3.0) (2020-11-29) 71 | 72 | ### Features 73 | 74 | - upgrade react16 to react17 version ([#28](https://github.com/Jacky-Summer/monki-ui/issues/28)) ([695c6e4](https://github.com/Jacky-Summer/monki-ui/commit/695c6e4c8d3d7036ceae2683dd52a487cc36eeb9)) 75 | 76 | ## [1.2.0](https://github.com/Jacky-Summer/monki-ui/compare/v1.0.0...v1.1.0) (2020-11-21) 77 | 78 | ### Features 79 | 80 | - add alert component ([#16](https://github.com/Jacky-Summer/monki-ui/issues/16)) ([561b52c](https://github.com/Jacky-Summer/monki-ui/commit/561b52cc1e4c13519e9651c477c9a6224bd39afd)) 81 | 82 | ## 1.0.0 (2020-11-14) 83 | 84 | ### Features 85 | 86 | - init project ([a828595](https://github.com/Jacky-Summer/monki-ui/commit/a828595a7124ff585062957035e17c35a8b903d4)) 87 | - style structure and whole ui style ([0ef134b](https://github.com/Jacky-Summer/monki-ui/commit/0ef134b3ce9943b57fd09eab3c7ba47c01f6ead5)) 88 | - basic button component ([aacfa5f](https://github.com/Jacky-Summer/monki-ui/commit/aacfa5f078e4b74a18fb99a9dfe7a9259d6afa30)) 89 | - complete the button component ([ad23dda](https://github.com/Jacky-Summer/monki-ui/commit/ad23dda6e26617f5a69dc35f4348bc2841d4d4d1)) 90 | - monki ui lib entry ([699331d](https://github.com/Jacky-Summer/monki-ui/commit/699331d204b637f1cdc395a619af4738ae09f1e5)) 91 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 monki-ui 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

6 | 7 |

Monki UI

8 | 9 |
10 | 11 | [![Build Status](https://travis-ci.com/Jacky-Summer/monki-ui.svg?branch=master)](https://travis-ci.com/Jacky-Summer/monki-ui) [![](https://img.shields.io/npm/v/monki-ui.svg)](https://www.npmjs.com/package/monki-ui) ![](https://img.shields.io/github/license/Jacky-Summer/monki-ui) [![tested with jest](https://img.shields.io/badge/tested_with-jest-99424f.svg)](https://github.com/facebook/jest) 12 | ![david-dev-image](https://img.shields.io/david/dev/Jacky-Summer/monki-ui?style=flat-square) ![david-image](https://img.shields.io/david/Jacky-Summer/monki-ui?style=flat-square) ![](https://img.shields.io/github/stars/Jacky-Summer/monki-ui?style=social) 13 | 14 | Monki UI,是一款基于 Dumi,由 React + TypeScript 开发的个人组件库 🎉。 15 | 16 |
17 | 18 | 该开源项目是我为进阶 React,同时探索组件库设计开发思路所做的,故不可用于生产环境。由于个人设计能力有限,故 UI 设计方面会大量参考[Ant Design 组件库](https://ant.design/index-cn),同时组件的使用方式也会参照 Ant Design 进行实现。如果你也想学习组件开发,欢迎加入或提供意见,你的 star ⭐,是对我最大的鼓励。 19 | 20 | ## ✨ 特性 21 | 22 | - 🌈 提炼组件库设计良好的视觉风格 23 | - 📦 渐进式探索高质量的前端代码的实现 24 | - 🛡 使用 TypeScript 开发,提升开发体验 25 | - ✅ 使用单元测试,为组件稳定性保驾护航 26 | - 📖 提供开发过程的文档思路,助力你学习组件开发 27 | - 🔖 欢迎贡献组件代码,探索最佳实践 28 | 29 | ## 📦 安装 30 | 31 | 使用 npm 或 yarn 安装(推荐) 32 | 33 | ```bash 34 | yarn add monki-ui 35 | ``` 36 | 37 | ```bash 38 | npm install monki-ui 39 | ``` 40 | 41 | ## 🔨 示例 42 | 43 | ```jsx 44 | import { Button } from 'monki-ui' 45 | 46 | const App = () => ( 47 | <> 48 | 49 | 50 | ) 51 | ``` 52 | 53 | 引入样式: 54 | 55 | ```jsx 56 | import 'monki-ui/dist/index.css' 57 | ``` 58 | 59 | ## 计划 60 | 61 | 🚧 开发中...... 62 | 63 | - [ ] 开发 Upload 组件 64 | 65 | ✨ 已完成 66 | 67 | - [x] CSS 样式解决方案、初始化文件结构、UI 设计 68 | - [x] Button 组件开发与测试 69 | - [x] 增加 Travis CI 70 | - [x] 创建入口文件,并发布到 npm 71 | - [x] 开发 Alert 组件 72 | - [x] 开发 Menu 组件 73 | - [x] 开发 Tab 组件 74 | - [x] 开发 Input 组件 75 | - [x] 开发 AutoComplete 组件 76 | - [x] 开发 Tag 组件 77 | - [x] 开发 Select 组件 78 | 79 | ## 开源协议 80 | 81 | 版权 (c) 2020-至今 归 JackySummer 所有. 详情请阅 [LICENSE](./LICENSE). 82 | -------------------------------------------------------------------------------- /components/alert/__tests__/__snapshots__/index.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Alert component should render the correct default alert 1`] = ` 4 | 10 | 18 | 25 | 40 |
43 | 46 | I am ALert 47 | 48 |

51 | This is some desc about the Alert 52 |

53 |
54 |
55 |
56 |
57 |
58 | `; 59 | -------------------------------------------------------------------------------- /components/alert/__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { mount } from 'enzyme' 3 | import Alert, { AlertProps } from '../alert' 4 | 5 | const testProps: AlertProps = { 6 | message: 'This is a Alert', 7 | type: 'success', 8 | closable: true, 9 | onClose: jest.fn(), 10 | } 11 | 12 | jest.useFakeTimers() 13 | 14 | describe('Alert component', () => { 15 | it('should render the correct default alert', () => { 16 | const wrapper = mount( 17 | , 21 | ) 22 | expect(wrapper.find('.mk-alert').hasClass('mk-alert-warning')).toBeTruthy() 23 | expect(wrapper).toMatchSnapshot() 24 | }) 25 | 26 | it('should be closed when click the alert close icon', () => { 27 | const wrapper = mount() 28 | wrapper.find('.mk-alert-close').simulate('click') 29 | jest.advanceTimersByTime(1000) 30 | expect(testProps.onClose).toHaveBeenCalled() 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /components/alert/alert.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState } from 'react' 2 | import classNames from 'classnames' 3 | import CloseOutlined from '@ant-design/icons/CloseOutlined' 4 | import Transition from '../transition' 5 | 6 | export type AlertType = 'success' | 'info' | 'warning' | 'error' 7 | 8 | export interface AlertProps { 9 | message: string 10 | description?: string 11 | type?: AlertType 12 | closable?: boolean 13 | className?: string 14 | onClose?: (e: React.MouseEvent) => void 15 | } 16 | 17 | const Alert: FC = ({ 18 | type, 19 | description, 20 | message, 21 | closable, // Whether Alert can be closed 22 | onClose, 23 | }) => { 24 | const [closed, setClosed] = useState(false) 25 | 26 | const handleClose = (e: React.MouseEvent) => { 27 | setClosed(true) 28 | onClose?.(e) 29 | } 30 | 31 | const classes = classNames('mk-alert', 'className', { 32 | [`mk-alert-${type}`]: type, 33 | }) 34 | const titleClass = classNames('mk-alert-message') 35 | 36 | return ( 37 | 38 |
39 | {message} 40 | {description &&

{description}

} 41 | {closable && ( 42 | 43 | 44 | 45 | )} 46 |
47 |
48 | ) 49 | } 50 | 51 | Alert.defaultProps = { 52 | type: 'warning', 53 | closable: false, 54 | } 55 | 56 | export default Alert 57 | -------------------------------------------------------------------------------- /components/alert/demos/closable.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Alert } from 'monki-ui' 3 | import 'monki-ui/dist/index.css' 4 | 5 | export default () => ( 6 |
7 | 8 | 9 | 15 | 21 |
22 | ) 23 | -------------------------------------------------------------------------------- /components/alert/demos/description.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Alert } from 'monki-ui' 3 | import 'monki-ui/dist/index.css' 4 | 5 | export default () => ( 6 |
7 | 12 | 17 | 22 | 27 |
28 | ) 29 | -------------------------------------------------------------------------------- /components/alert/demos/type.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Alert } from 'monki-ui' 3 | import 'monki-ui/dist/index.css' 4 | 5 | export default () => ( 6 |
7 | 8 | 9 | 10 | 11 |
12 | ) 13 | -------------------------------------------------------------------------------- /components/alert/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Alert 警告提示 3 | group: 4 | title: Alert 警告提示 5 | nav: 6 | title: '组件' 7 | path: /components 8 | --- 9 | 10 | ## 警告样式 11 | 12 | 警告提示分为四种样式,默认是`warning`。 13 | 14 | `success` | `info` | `warning` | `error` 15 | 16 | 17 | 18 | ## 含有辅助性文字介绍的警告提示 19 | 20 | 通过设置`description`属性。 21 | 22 | 23 | 24 | ## 可关闭的警告提示 25 | 26 | 设置`closable`属性和添加`onClose`方法可显示关闭按钮,点击并可关闭警告提示。 27 | 28 | 29 | 30 | ## API 31 | 32 | | 参数 | 说明 | 类型 | 默认值 | 33 | | ----------- | -------------------------------------------------------------------- | ----------------------- | --------- | 34 | | closable | 默认不显示关闭按钮 | boolean | - | 35 | | description | 警告提示的辅助性文字介绍 | ReactNode | - | 36 | | message | 警告提示内容 | ReactNode | - | 37 | | type | 指定警告提示的样式,有四种选择 `success`、`info`、`warning`、`error` | string | `warning` | 38 | | onClose | 关闭时触发的回调函数 | (e: MouseEvent) => void | - | 39 | -------------------------------------------------------------------------------- /components/alert/index.tsx: -------------------------------------------------------------------------------- 1 | import Alert from './alert' 2 | 3 | export default Alert 4 | -------------------------------------------------------------------------------- /components/alert/style/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin alert-style($background, $border, $color) { 2 | color: $color; 3 | background: $background; 4 | border-color: $border; 5 | } 6 | -------------------------------------------------------------------------------- /components/alert/style/index.scss: -------------------------------------------------------------------------------- 1 | @import 'mixins'; 2 | 3 | $alert-colors: ( 4 | 'success': $success-100, 5 | 'info': $blue-100, 6 | 'warning': $warning-100, 7 | 'error': $danger-100, 8 | ); 9 | .mk-alert { 10 | position: relative; 11 | margin-bottom: $alert-margin-bottom; 12 | padding: $alert-padding-y $alert-padding-x; 13 | border: $alert-border-width solid transparent; 14 | border-radius: $alert-border-radius; 15 | .mk-alert-close { 16 | position: absolute; 17 | top: 0; 18 | right: 0; 19 | padding: $alert-close-padding-y $alert-close-padding-x; 20 | color: $font-color-normal; 21 | font-size: $alert-description-font-size; 22 | cursor: pointer; 23 | } 24 | 25 | .mk-alert-desc { 26 | margin: $alert-description-top-margin 0 0; 27 | font-size: $alert-description-font-size; 28 | } 29 | } 30 | @each $color, $value in $alert-colors { 31 | .mk-alert-#{$color} { 32 | @include alert-style($value, darken($value, 5%), $font-color-normal); 33 | 34 | > p { 35 | color: $font-color-light; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /components/alert/style/index.tsx: -------------------------------------------------------------------------------- 1 | import './index.scss' 2 | -------------------------------------------------------------------------------- /components/auto-complete/__tests__/__snapshots__/index.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`AutoComplete test basic AutoComplete behavior 1`] = ` 4 | 26 |
29 | 35 |
38 | 45 |
46 |
47 | 55 | 62 | 77 |
    80 |
  • 85 | ab 86 |
  • 87 |
  • 92 | abc 93 |
  • 94 |
95 |
96 |
97 |
98 |
99 |
100 | `; 101 | -------------------------------------------------------------------------------- /components/auto-complete/__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { mount, ReactWrapper } from 'enzyme' 3 | import AutoComplete, { AutoCompleteProps } from '../autoComplete' 4 | import { waitForWrapperToPaint } from '../../../tests/utils' 5 | 6 | const testArray = [ 7 | { value: 'ab', number: 11 }, 8 | { value: 'abc', number: 1 }, 9 | { value: 'b', number: 4 }, 10 | { value: 'c', number: 15 }, 11 | ] 12 | 13 | let wrapper: ReactWrapper 14 | let inputNode: ReactWrapper 15 | 16 | const testProps: AutoCompleteProps = { 17 | onSearch: query => testArray.filter(item => item.value.includes(query)), 18 | onSelect: jest.fn(), 19 | placeholder: 'This is a placeholder', 20 | } 21 | 22 | describe('AutoComplete', () => { 23 | beforeEach(() => { 24 | wrapper = mount() 25 | inputNode = wrapper.find('input') 26 | }) 27 | 28 | it('test basic AutoComplete behavior', () => { 29 | expect(inputNode.prop('placeholder')).toBe(testProps.placeholder) 30 | inputNode.simulate('change', { target: { value: 'a' } }) 31 | waitForWrapperToPaint(wrapper, 1000) 32 | // should have two suggestion items 33 | expect(wrapper.find('.suggestion-item').length).toEqual(2) 34 | // click the first item 35 | wrapper 36 | .find('.suggestion-item') 37 | .first() 38 | .simulate('click') 39 | expect(testProps.onSelect).toHaveBeenCalledWith({ value: 'ab', number: 11 }) 40 | expect(wrapper).toMatchSnapshot() 41 | }) 42 | 43 | // TODO: test case - should provide keyboard support 44 | // TODO: test case - click outside should hide the dropdown 45 | }) 46 | -------------------------------------------------------------------------------- /components/auto-complete/autoComplete.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | FC, 3 | ReactElement, 4 | KeyboardEvent, 5 | ChangeEvent, 6 | useState, 7 | useEffect, 8 | useRef, 9 | } from 'react' 10 | import classNames from 'classnames' 11 | import { LoadingOutlined } from '@ant-design/icons' 12 | import Transition from '../transition' 13 | import Input, { InputProps } from '../input/input' 14 | import useDebounce from '../hooks/useDebounce' 15 | import useClickOutside from '../hooks/useClickOutside' 16 | 17 | interface DataSourceObject { 18 | value: string 19 | } 20 | 21 | export type DataSourceType = T & DataSourceObject 22 | 23 | export interface AutoCompleteProps extends Omit { 24 | /** 搜索补全项的时候调用 */ 25 | onSearch: (str: string) => DataSourceType[] | Promise 26 | /** 选中下拉选项时触发的回调 */ 27 | onSelect?: (item: DataSourceType) => void 28 | /** 自定义渲染下拉选项,返回 ReactElement */ 29 | renderOption?: (item: DataSourceType) => ReactElement 30 | } 31 | 32 | const AutoComplete: FC = ({ 33 | value = '', 34 | onSearch, 35 | onSelect, 36 | renderOption, 37 | ...restProps 38 | }) => { 39 | const [inputValue, setInputValue] = useState(value as string) 40 | const [showDropdown, setShowDropdown] = useState(false) 41 | const [isLoading, setIsLoading] = useState(false) 42 | const [options, setOptions] = useState([]) 43 | const [highlightIndex, setHighlightIndex] = useState(-1) 44 | const triggerSearch = useRef(false) 45 | const dropdownRef = useRef(null) 46 | const debouncedValue = useDebounce(inputValue, 300) 47 | 48 | // 当点击到AutoComplete组件外的区域,会自动关闭下拉框选项部分 49 | useClickOutside(dropdownRef, () => { 50 | setOptions([]) 51 | setShowDropdown(false) 52 | }) 53 | 54 | useEffect(() => { 55 | if (debouncedValue && triggerSearch.current) { 56 | setOptions([]) 57 | const result = onSearch(debouncedValue) 58 | if (result instanceof Promise) { 59 | setIsLoading(true) 60 | result.then(data => { 61 | setIsLoading(false) 62 | setOptions(data) 63 | if (data.length > 0) { 64 | setShowDropdown(true) 65 | } 66 | }) 67 | } else { 68 | setOptions(result) 69 | if (result.length > 0) { 70 | setShowDropdown(true) 71 | } 72 | } 73 | } else { 74 | setShowDropdown(false) 75 | } 76 | setHighlightIndex(-1) 77 | }, [debouncedValue, onSearch]) 78 | 79 | const handleChange = (e: ChangeEvent) => { 80 | const newValue = e.target.value.trim() 81 | setInputValue(newValue) 82 | triggerSearch.current = true 83 | } 84 | 85 | const handleSelect = (item: DataSourceType) => { 86 | setInputValue(item.value) 87 | setShowDropdown(false) 88 | if (onSelect) { 89 | onSelect(item) 90 | } 91 | triggerSearch.current = false 92 | } 93 | 94 | // eslint-disable-next-line no-confusing-arrow 95 | const renderTemplate = (item: DataSourceType) => 96 | renderOption ? renderOption(item) : item.value 97 | 98 | const highlight = (index: number) => { 99 | let currentIndex = index 100 | if (index < 0) currentIndex = 0 101 | if (index >= options.length) { 102 | currentIndex = options.length - 1 103 | } 104 | setHighlightIndex(currentIndex) 105 | } 106 | 107 | const handleKeyDown = (e: KeyboardEvent) => { 108 | switch (e.key) { 109 | // 回车 110 | case 'Enter': 111 | if (options[highlightIndex]) { 112 | handleSelect(options[highlightIndex]) 113 | } 114 | break 115 | // 上 116 | case 'ArrowUp': 117 | highlight(highlightIndex - 1) 118 | break 119 | // 下 120 | case 'ArrowDown': 121 | highlight(highlightIndex + 1) 122 | break 123 | // ESC 124 | case 'Escape': 125 | setShowDropdown(false) 126 | break 127 | default: 128 | break 129 | } 130 | } 131 | const generateDropdown = () => ( 132 | 133 |
    134 | {isLoading && ( 135 |
    136 | 137 |
    138 | )} 139 | {options.map((item, index) => { 140 | const classes = classNames('suggestion-item', { 141 | 'is-active': index === highlightIndex, // 结合highlightIndex,做高亮处理 142 | }) 143 | return ( 144 | // eslint-disable-next-line react/no-array-index-key 145 |
  • handleSelect(item)}> 146 | {renderTemplate(item)} 147 |
  • 148 | ) 149 | })} 150 |
151 |
152 | ) 153 | 154 | return ( 155 |
156 | 162 | {generateDropdown()} 163 |
164 | ) 165 | } 166 | export default AutoComplete 167 | -------------------------------------------------------------------------------- /components/auto-complete/demos/ajaxSearch.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { AutoComplete } from 'monki-ui' 3 | import { DataSourceType } from 'monki-ui/dist/auto-complete/autoComplete' 4 | 5 | import 'monki-ui/dist/index.css' 6 | 7 | interface GithubUserProps { 8 | login: string 9 | url: string 10 | // eslint-disable-next-line camelcase 11 | avatar_url: string 12 | } 13 | 14 | export default () => { 15 | const renderOption = (item: DataSourceType) => { 16 | const itemWithGithub = item as DataSourceType 17 | return ( 18 | <> 19 | Name: {itemWithGithub.value} 20 | url: {itemWithGithub.url} 21 | 22 | ) 23 | } 24 | const handleSearch = (query: string) => 25 | fetch(`https://api.github.com/search/users?q=${query}`) 26 | .then(res => res.json()) 27 | .then(({ items }) => { 28 | if (typeof items !== 'undefined') { 29 | return items.slice(0, 10).map((item: any) => ({ value: item.login, ...item })) 30 | } 31 | return [] 32 | }) 33 | 34 | return ( 35 |
36 |
请输入任意 Github 用户名
37 | 38 |
39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /components/auto-complete/demos/base.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { AutoComplete } from 'monki-ui' 3 | import 'monki-ui/dist/index.css' 4 | 5 | export default () => { 6 | const strArr = ['a', 'ab', 'aaa', 'b', 'bb', 'bbb', 'c', 'cc', 'ccc', 'd', 'dd', 'ddd'] 7 | 8 | const handleSearch = (query: string) => 9 | strArr.filter(item => item.includes(query)).map(item => ({ value: item })) 10 | 11 | return ( 12 |
13 |
请输入 a 或 b 或 c 或 d
14 | 15 |
16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /components/auto-complete/demos/renderOption.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { AutoComplete } from 'monki-ui' 3 | import { DataSourceType } from 'monki-ui/dist/auto-complete/autoComplete' 4 | import 'monki-ui/dist/index.css' 5 | 6 | interface LakerPlayerProps { 7 | value: string 8 | number: number 9 | } 10 | 11 | export default () => { 12 | const lakersWithNumber = [ 13 | { value: 'a', number: 0 }, 14 | { value: 'aa', number: 1 }, 15 | { value: 'aaa', number: 2 }, 16 | { value: 'b', number: 3 }, 17 | { value: 'bb', number: 4 }, 18 | { value: 'bbb', number: 5 }, 19 | { value: 'c', number: 6 }, 20 | { value: 'cc', number: 7 }, 21 | { value: 'ccc', number: 8 }, 22 | { value: 'd', number: 9 }, 23 | { value: 'dd', number: 10 }, 24 | { value: 'ddd', number: 11 }, 25 | ] 26 | 27 | const handleSearch = (query: string) => 28 | lakersWithNumber.filter(player => player.value.includes(query)) 29 | 30 | const renderOption = (item: DataSourceType) => { 31 | const itemWithNumber = item as DataSourceType 32 | return ( 33 | <> 34 | 字母: {itemWithNumber.value} 35 | 数字编号: {itemWithNumber.number} 36 | 37 | ) 38 | } 39 | 40 | return ( 41 |
42 |
请输入 a 或 b 或 c 或 d
43 | 44 |
45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /components/auto-complete/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: AutoComplete 自动完成 3 | group: 4 | title: AutoComplete 自动完成 5 | nav: 6 | title: '组件' 7 | path: /components 8 | --- 9 | 10 | 输入框自动完成功能。 11 | 12 | ### 何时使用 13 | 14 | - 需要一个输入框而不是选择器。 15 | 16 | - 需要输入建议/辅助提示。 17 | 18 | 和 Select 的区别是: 19 | 20 | - AutoComplete 是一个带提示的文本输入框,用户可以自由输入,关键词是辅助输入。 21 | 22 | - Select 是在限定的可选项中进行选择,关键词是选择。 23 | 24 | ### 基本使用 25 | 26 | 27 | 28 | ### 自定义渲染下拉选项 29 | 30 | 31 | 32 | ### ajax 请求下拉选项 33 | 34 | 35 | 36 | ### API 37 | 38 | | 参数 | 说明 | 类型 | 39 | | ------------ | ------------------------------------- | --------------- | 40 | | onSearch | 搜索补全项的时候调用 | function(value) | 41 | | onSelect | 被选中时调用,参数为选中项的 value 值 | function(value) | 42 | | renderOption | 自定义渲染下拉选项 | function(value) | 43 | -------------------------------------------------------------------------------- /components/auto-complete/index.tsx: -------------------------------------------------------------------------------- 1 | import AutoComplete from './autoComplete' 2 | 3 | export default AutoComplete 4 | -------------------------------------------------------------------------------- /components/auto-complete/style/index.scss: -------------------------------------------------------------------------------- 1 | .mk-auto-complete { 2 | position: relative; 3 | } 4 | .mk-suggestion-list { 5 | position: absolute; 6 | top: calc(100% + 8px); 7 | left: 0; 8 | z-index: 100; 9 | width: 100%; 10 | padding-left: 0; 11 | white-space: nowrap; 12 | list-style: none; 13 | background: $white; 14 | border: $menu-border-width solid $menu-border-color; 15 | box-shadow: $submenu-box-shadow; 16 | .suggestion-loading-icon { 17 | display: flex; 18 | align-items: center; 19 | justify-content: center; 20 | min-height: 75px; 21 | 22 | .anticon { 23 | font-size: 40px; 24 | } 25 | } 26 | .suggestion-item { 27 | padding: $menu-item-padding-y $menu-item-padding-x; 28 | color: $body-color; 29 | cursor: pointer; 30 | transition: $menu-transition; 31 | &.is-active { 32 | color: $white !important; 33 | background: $menu-item-active-color !important; 34 | } 35 | &:hover { 36 | color: $menu-item-active-color !important; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /components/button/__tests__/__snapshots__/index.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Button component should render Buttons of different size correctly 1`] = ` 4 |
5 | 13 | 21 | 29 |
30 | `; 31 | 32 | exports[`Button component should render Buttons of different type correctly 1`] = ` 33 |
34 | 42 | 50 | 58 | 66 | 74 | 82 | 90 |
91 | `; 92 | -------------------------------------------------------------------------------- /components/button/__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow, render } from 'enzyme' 3 | import Button, { ButtonProps } from '../button' 4 | 5 | const testProps: ButtonProps = { 6 | type: 'primary', 7 | size: 'lg', 8 | className: 'monkey', 9 | } 10 | 11 | const disabledProps: ButtonProps = { 12 | disabled: true, 13 | onClick: jest.fn(), 14 | } 15 | 16 | describe('Button component', () => { 17 | it('should render Buttons of different type correctly', () => { 18 | const wrapper = render( 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
, 28 | ) 29 | expect(wrapper).toMatchSnapshot() 30 | }) 31 | 32 | it('should render a link when btnType equals link and href is provided', () => { 33 | const wrapper = shallow( 34 | , 37 | ) 38 | const element = wrapper.first() 39 | expect(element.name()).toEqual('a') 40 | expect(element.hasClass('mk-btn mk-btn-link')) 41 | }) 42 | 43 | it('should render the correct component based on different props', () => { 44 | const wrapper = shallow() 45 | const element = wrapper.find('.monkey') 46 | expect(element.hasClass('mk-btn-primary mk-btn-lg')).toBeTruthy() 47 | }) 48 | 49 | it('should render Buttons of different size correctly', () => { 50 | const wrapper = render( 51 |
52 | 55 | 58 | 61 |
, 62 | ) 63 | expect(wrapper).toMatchSnapshot() 64 | }) 65 | 66 | it('should render disabled button when disabled set to true', () => { 67 | const wrapper = shallow() 68 | const element = wrapper.find('button') 69 | expect(element.prop('disabled')).toBeTruthy() 70 | element.simulate('click') 71 | expect(disabledProps.onClick).not.toHaveBeenCalled() 72 | }) 73 | 74 | it('should not render as link button when href is undefined', async () => { 75 | const wrapper = shallow( 76 | , 79 | ) 80 | expect(wrapper.find('a').exists()).toBeFalsy() 81 | expect(wrapper.find('button').exists()).toBeTruthy() 82 | }) 83 | }) 84 | -------------------------------------------------------------------------------- /components/button/button.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | import classNames from 'classnames' 3 | 4 | const isString = (children: React.ReactNode) => { 5 | if (typeof children === 'string') { 6 | return {children} 7 | } 8 | return children 9 | } 10 | 11 | export type ButtonType = 12 | | 'default' 13 | | 'primary' 14 | | 'info' 15 | | 'warning' 16 | | 'danger' 17 | | 'dashed' 18 | | 'link' 19 | | 'text' 20 | 21 | export type ButtonSize = 'lg' | 'md' | 'sm' 22 | export type ButtonHTMLTypes = 'submit' | 'button' | 'reset' 23 | 24 | interface BaseButtonProps { 25 | type?: ButtonType 26 | size?: ButtonSize 27 | disabled?: boolean 28 | block?: boolean 29 | className?: string 30 | href?: string 31 | icon?: React.ReactNode 32 | children?: React.ReactNode 33 | } 34 | 35 | type NativeButtonProps = { 36 | htmlType?: ButtonHTMLTypes 37 | target?: string 38 | onClick?: React.MouseEventHandler 39 | } & BaseButtonProps & 40 | Omit, 'type'> 41 | 42 | type AnchorButtonProps = { 43 | href?: string 44 | onClick?: React.MouseEventHandler 45 | } & BaseButtonProps & 46 | Omit, 'type'> 47 | 48 | export type ButtonProps = Partial 49 | 50 | const Button: FC = ({ 51 | type, 52 | htmlType, 53 | size, 54 | disabled, 55 | block, 56 | className, 57 | href, 58 | children, 59 | ...restProps 60 | }) => { 61 | const classes = classNames('mk-btn', className, { 62 | [`mk-btn-${type}`]: type, 63 | [`mk-btn-${size}`]: size, 64 | 'mk-btn-block': block, 65 | }) 66 | 67 | if (type === 'link' && href) { 68 | return ( 69 | 70 | {children} 71 | 72 | ) 73 | } 74 | 75 | const kids = isString(children) 76 | 77 | return ( 78 | 86 | ) 87 | } 88 | 89 | Button.defaultProps = { 90 | disabled: false, 91 | type: 'default', 92 | size: 'md', 93 | block: false, 94 | htmlType: 'button' as ButtonProps['htmlType'], 95 | } 96 | 97 | export default Button 98 | -------------------------------------------------------------------------------- /components/button/demos/block.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Button } from 'monki-ui' 3 | import 'monki-ui/dist/index.css' 4 | 5 | export default () => ( 6 |
7 | 10 |
11 | ) 12 | -------------------------------------------------------------------------------- /components/button/demos/disabled.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Button } from 'monki-ui' 3 | import 'monki-ui/dist/index.css' 4 | 5 | export default () => ( 6 |
7 | 10 | 13 | 16 |
17 | ) 18 | -------------------------------------------------------------------------------- /components/button/demos/size.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Button } from 'monki-ui' 3 | import 'monki-ui/dist/index.css' 4 | 5 | export default () => ( 6 |
7 | 10 | 13 | 14 |
15 | ) 16 | -------------------------------------------------------------------------------- /components/button/demos/type.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Button } from 'monki-ui' 3 | import 'monki-ui/dist/index.css' 4 | 5 | export default () => ( 6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 | 17 |
18 | ) 19 | -------------------------------------------------------------------------------- /components/button/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Button 按钮 3 | group: 4 | title: Button 按钮 5 | nav: 6 | title: '组件' 7 | path: /components 8 | --- 9 | 10 | ### 按钮类型 11 | 12 | 按钮分为五种类型`type`,默认是`default`。 13 | 14 | `default` | `primary` | `info` | `warning` | `danger` | `dashed` | `link` | `text` 15 | 16 | 17 | 18 | ### 按钮尺寸 19 | 20 | 尺寸`size`分为大、中、小,默认尺寸是中。 21 | 22 | `lg` | `md` | `sm` 23 | 24 | 25 | 26 | ### 不可用状态 27 | 28 | 添加 `disabled` 属性即可让按钮处于不可用状态,同时按钮样式也会改变。 29 | 30 | 31 | 32 | ### Block 按钮 33 | 34 | `block`属性将使按钮适合其父宽度,默认不设置。 35 | 36 | 37 | 38 | ### API 39 | 40 | 通过设置 Button 的属性来产生不同的按钮样式,推荐顺序为:`type` > `size` -> `disabled`。 41 | 42 | 按钮的属性说明如下: 43 | 44 | | 属性 | 说明 | 类型 | 默认值 | 45 | | -------- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------- | --------- | 46 | | block | 将按钮宽度调整为其父宽度的选项 | boolean | false | 47 | | disabled | 按钮失效状态 | boolean | false | 48 | | href | 点击跳转的地址,指定此属性 button 的行为和 a 链接一致 | string | - | 49 | | htmlType | 设置 `button` 原生的 `type` 值 | string | `button` | 50 | | size | 设置按钮大小 | `lg` \| `md` \| `sm` | `md` | 51 | | type | 设置按钮类型 | `primary` \| `info` \| `warning` \| `danger` \| `dashed` \| `link` \| `text` \| `default` | `default` | 52 | | onClick | 点击按钮时的回调 | (event) => void | - | 53 | 54 | 支持原生 button 的其他所有属性。 55 | -------------------------------------------------------------------------------- /components/button/index.tsx: -------------------------------------------------------------------------------- 1 | import Button from './button' 2 | 3 | export default Button 4 | -------------------------------------------------------------------------------- /components/button/style/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin button-size($padding-y, $padding-x, $font-size, $border-radius) { 2 | padding: $padding-y $padding-x; 3 | font-size: $font-size; 4 | border-radius: $border-radius; 5 | } 6 | 7 | @mixin button-style( 8 | $background, 9 | $border, 10 | $color, 11 | $hover-background: lighten($background, 7.5%), 12 | $hover-border: lighten($border, 10%), 13 | $hover-color: $color 14 | ) { 15 | color: $color; 16 | background: $background; 17 | border-color: $border; 18 | &:hover { 19 | color: $hover-color; 20 | background: $hover-background; 21 | border-color: $hover-border; 22 | } 23 | &:focus, &.focus { 24 | color: $hover-color; 25 | background: $hover-background; 26 | border-color: $hover-border; 27 | } 28 | &:disabled, &.disabled { 29 | color: $color; 30 | background: $background; 31 | border-color: $border; 32 | } 33 | } 34 | 35 | [id^='components-button-demo-'] .mk-btn { 36 | margin-right: 8px; 37 | margin-bottom: 12px; 38 | } 39 | -------------------------------------------------------------------------------- /components/button/style/index.scss: -------------------------------------------------------------------------------- 1 | @import 'mixins'; 2 | 3 | .mk-btn { 4 | position: relative; 5 | display: inline-block; 6 | color: $body-color; 7 | font-weight: $btn-font-weight; 8 | line-height: $btn-line-height; 9 | white-space: nowrap; 10 | text-align: center; 11 | vertical-align: middle; 12 | background-image: none; 13 | border: $btn-border-width solid transparent; 14 | box-shadow: $btn-box-shadow; 15 | cursor: pointer; 16 | transition: $btn-transition; 17 | @include button-size($btn-padding-y, $btn-padding-x, $btn-font-size, $border-radius); 18 | &.disabled, &[disabled] { 19 | box-shadow: none; 20 | cursor: not-allowed; 21 | opacity: $btn-disabled-opacity; 22 | > * { 23 | pointer-events: none; 24 | } 25 | } 26 | } 27 | // type 28 | .mk-btn-default { 29 | @include button-style($white, $gray-400, $body-color, $white, $primary, $primary); 30 | } 31 | 32 | .mk-btn-primary { 33 | @include button-style($primary, $primary, $white); 34 | } 35 | 36 | .mk-btn-info { 37 | @include button-style($info, $info, $white); 38 | } 39 | 40 | .mk-btn-warning { 41 | @include button-style($warning, $warning, $white); 42 | } 43 | 44 | .mk-btn-danger { 45 | @include button-style($danger, $danger, $white); 46 | } 47 | 48 | .mk-btn-text { 49 | @include button-style(transparent, transparent, $body-color); 50 | box-shadow: none; 51 | } 52 | 53 | .mk-btn-dashed { 54 | @include button-style($white, $gray-400, $body-color, $white, $primary, $primary); 55 | border-style: dotted; 56 | } 57 | 58 | .mk-btn-link { 59 | color: $btn-link-color; 60 | font-weight: $font-weight-normal; 61 | text-decoration: $link-decoration; 62 | background-color: transparent; 63 | box-shadow: none; 64 | &:hover { 65 | color: $btn-link-hover-color; 66 | text-decoration: $link-hover-decoration; 67 | } 68 | &:focus, &.focus { 69 | text-decoration: $link-hover-decoration; 70 | box-shadow: none; 71 | } 72 | &:disabled, &.disabled { 73 | color: $btn-link-disabled-color; 74 | pointer-events: none; 75 | } 76 | } 77 | 78 | // size 79 | .mk-btn-lg { 80 | @include button-size( 81 | $btn-padding-y-lg, 82 | $btn-padding-x-lg, 83 | $btn-font-size-lg, 84 | $btn-border-radius-lg 85 | ); 86 | } 87 | 88 | .mk-btn-md { 89 | @include button-size( 90 | $btn-padding-y-md, 91 | $btn-padding-x-md, 92 | $btn-font-size-md, 93 | $btn-border-radius-md 94 | ); 95 | } 96 | 97 | .mk-btn-sm { 98 | @include button-size( 99 | $btn-padding-y-sm, 100 | $btn-padding-x-sm, 101 | $btn-font-size-sm, 102 | $btn-border-radius-sm 103 | ); 104 | } 105 | 106 | // shape 107 | .mk-btn-circle { 108 | min-width: 32px; 109 | padding-right: 0; 110 | padding-left: 0; 111 | text-align: center; 112 | border-radius: 50%; 113 | } 114 | 115 | .mk-btn-round { 116 | border-radius: 30%; 117 | } 118 | 119 | // block 120 | .mk-btn-block { 121 | width: 100%; 122 | } 123 | 124 | // icon 125 | // .mk-btn-icon-only { 126 | // width: 32px; 127 | // height: 32px; 128 | // padding: 2.4px 0; 129 | // font-size: 16px; 130 | // border-radius: 2px; 131 | // } 132 | 133 | // .mk-btn > .anticon + span, 134 | // .mk-btn > span + .anticon { 135 | // margin-left: 8px; 136 | // } 137 | -------------------------------------------------------------------------------- /components/button/style/index.tsx: -------------------------------------------------------------------------------- 1 | import './index.scss' 2 | -------------------------------------------------------------------------------- /components/hooks/useClickOutside.ts: -------------------------------------------------------------------------------- 1 | import { RefObject, useEffect } from 'react' 2 | 3 | function useClickOutside(ref: RefObject, handler: Function) { 4 | useEffect(() => { 5 | const listener = (event: MouseEvent) => { 6 | const isClickOutside = !ref.current?.contains?.(event.target as HTMLElement) 7 | if (isClickOutside && ref.current) { 8 | handler(event) 9 | } 10 | } 11 | 12 | document.addEventListener('click', listener) 13 | 14 | return () => { 15 | document.removeEventListener('click', listener) 16 | } 17 | }, [ref, handler]) 18 | } 19 | 20 | export default useClickOutside 21 | -------------------------------------------------------------------------------- /components/hooks/useDebounce.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | 3 | function useDebounce(value: T, delay?: number): T { 4 | const [debouncedValue, setDebouncedValue] = useState(value) 5 | 6 | useEffect(() => { 7 | const timer = setTimeout(() => setDebouncedValue(value), delay || 500) 8 | 9 | return () => { 10 | clearTimeout(timer) 11 | } 12 | }, [value, delay]) 13 | 14 | return debouncedValue 15 | } 16 | 17 | export default useDebounce 18 | -------------------------------------------------------------------------------- /components/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as Button } from './button' 2 | export { default as Alert } from './alert' 3 | export { default as Transition } from './transition' 4 | export { default as Menu } from './menu' 5 | export { default as Tabs } from './tabs' 6 | export { default as Input } from './input' 7 | export { default as AutoComplete } from './auto-complete' 8 | export { default as Tag } from './tag' 9 | export { default as Select } from './select' 10 | -------------------------------------------------------------------------------- /components/input/__tests__/__snapshots__/index.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Input component should render prefix and suffix element on prefix/suffix property 1`] = ` 4 | 10 |
13 |
16 | https:// 17 |
18 | 23 |
26 | .com 27 |
28 |
29 |
30 | `; 31 | 32 | exports[`Input component should render the correct default Input 1`] = ` 33 | 37 |
40 | 45 |
46 |
47 | `; 48 | 49 | exports[`Input component should render the disabled Input on disabled property 1`] = ` 50 | 55 |
58 | 64 |
65 |
66 | `; 67 | 68 | exports[`Input component should support size 1`] = ` 69 | 74 |
77 | 82 |
83 |
84 | `; 85 | -------------------------------------------------------------------------------- /components/input/__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { mount } from 'enzyme' 3 | import Input, { InputProps } from '../input' 4 | 5 | const defaultProps: InputProps = { 6 | onChange: jest.fn(), 7 | placeholder: 'this is input', 8 | } 9 | 10 | describe('Input component', () => { 11 | it('should render the correct default Input', () => { 12 | const wrapper = mount() 13 | expect(wrapper.find('input.mk-input-inner').prop('placeholder')).toEqual( 14 | 'this is input', 15 | ) 16 | expect(wrapper).toMatchSnapshot() 17 | }) 18 | 19 | it('should support size', () => { 20 | const wrapper = mount() 21 | expect(wrapper.find('.mk-input-wrapper').hasClass('input-size-lg')).toBeTruthy() 22 | expect(wrapper).toMatchSnapshot() 23 | }) 24 | 25 | it('should render the disabled Input on disabled property', () => { 26 | const wrapper = mount() 27 | expect(wrapper.find('.mk-input-wrapper').hasClass('is-disabled')).toBeTruthy() 28 | expect(wrapper).toMatchSnapshot() 29 | }) 30 | 31 | it('should render prefix and suffix element on prefix/suffix property', () => { 32 | const wrapper = mount() 33 | expect(wrapper.find('.mk-input-group-prefix').text()).toEqual('https://') 34 | expect(wrapper.find('.mk-input-group-suffix').text()).toEqual('.com') 35 | expect(wrapper).toMatchSnapshot() 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /components/input/demos/base.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Input } from 'monki-ui' 3 | import 'monki-ui/dist/index.css' 4 | 5 | export default () => ( 6 |
7 | 8 |
9 | ) 10 | -------------------------------------------------------------------------------- /components/input/demos/disabled.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Input } from 'monki-ui' 3 | import 'monki-ui/dist/index.css' 4 | 5 | export default () => ( 6 |
7 | 8 |
9 | ) 10 | -------------------------------------------------------------------------------- /components/input/demos/icon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Input } from 'monki-ui' 3 | import { CaretDownOutlined } from '@ant-design/icons' 4 | import 'monki-ui/dist/index.css' 5 | 6 | export default () => ( 7 |
8 | } /> 9 |
10 | ) 11 | -------------------------------------------------------------------------------- /components/input/demos/prefix-suffix.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Input } from 'monki-ui' 3 | import 'monki-ui/dist/index.css' 4 | 5 | export default () => ( 6 |
7 | 8 | 9 |
10 | ) 11 | -------------------------------------------------------------------------------- /components/input/demos/size.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Input } from 'monki-ui' 3 | import 'monki-ui/dist/index.css' 4 | 5 | export default () => ( 6 |
7 | 8 | 9 | 10 |
11 | ) 12 | -------------------------------------------------------------------------------- /components/input/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Input 输入框 3 | group: 4 | title: Input 输入框 5 | nav: 6 | title: '组件' 7 | path: /components 8 | --- 9 | 10 | 通过鼠标或键盘输入内容,是最基础的表单域的包装。 11 | 12 | **何时使用** 13 | 14 | - 需要用户输入表单域内容时。 15 | - 提供组合型输入框,带搜索的输入框,还可以进行大小选择。 16 | 17 | ### 基本使用 18 | 19 | 20 | 21 | ### 三种大小 22 | 23 | 输入框定义了三种尺寸 size(大`lg`、默认`md`、小`sm`) 24 | 25 | 26 | 27 | ### 禁用 28 | 29 | 设置禁用状态 `disabled`,默认为 false 30 | 31 | 32 | 33 | ### 前缀与后缀 34 | 35 | 在输入框上添加前缀`prefix`或后缀`suffix`图标。 36 | 37 | 38 | 39 | ### 图标 40 | 41 | 如添加一个向下箭头的图标 42 | 43 | 44 | 45 | ### API 46 | 47 | | 参数 | 说明 | 类型 | 默认值 | 48 | | -------- | ---------------------------------------------- | -------------------- | ------ | 49 | | disabled | 是否禁用状态,默认为 false | boolean | false | 50 | | id | 输入框的 id | string | - | 51 | | prefix | 带有前缀图标的 input | ReactNode | - | 52 | | size | 控件大小。 | `lg` \| `md` \| `sm` | - | 53 | | suffix | 带有后缀图标的 input | ReactNode | - | | 54 | | type | 声明 input 类型,同原生 input 标签的 type 属性 | string | `text` | 55 | | value | 输入框内容 | string | - | 56 | | icon | 图标 | ReactNode | - | 57 | | onChange | 输入框内容变化时的回调 | function(e) | - | 58 | 59 | Input 的其他属性和 React 自带的 [input](https://facebook.github.io/react/docs/events.html#supported-events) 一致。 60 | -------------------------------------------------------------------------------- /components/input/index.tsx: -------------------------------------------------------------------------------- 1 | import Input from './input' 2 | 3 | export default Input 4 | -------------------------------------------------------------------------------- /components/input/input.tsx: -------------------------------------------------------------------------------- 1 | import React, { InputHTMLAttributes, ReactNode, forwardRef } from 'react' 2 | import classNames from 'classnames' 3 | 4 | export type InputSize = 'lg' | 'md' | 'sm' 5 | 6 | export interface InputProps 7 | extends Omit, 'size' | 'prefix' | 'suffix'> { 8 | size?: InputSize 9 | disabled?: boolean 10 | prefix?: ReactNode // 前缀 11 | suffix?: ReactNode // 后缀 12 | icon?: ReactNode // 图标 13 | onChange?: (event: React.ChangeEvent) => void 14 | } 15 | 16 | const Input = forwardRef((props, ref) => { 17 | const { size, disabled, prefix, suffix, style, icon, ...restProps } = props 18 | const classes = classNames('mk-input-wrapper', { 19 | [`input-size-${size}`]: size, 20 | 'is-disabled': disabled, 21 | 'input-group': prefix || suffix, 22 | 'input-group-suffix': !!suffix, 23 | 'input-group-prefix': !!prefix, 24 | }) 25 | 26 | return ( 27 |
28 | {prefix &&
{prefix}
} 29 | {icon &&
{icon}
} 30 | 31 | {suffix &&
{suffix}
} 32 |
33 | ) 34 | }) 35 | 36 | export default Input 37 | -------------------------------------------------------------------------------- /components/input/style/index.scss: -------------------------------------------------------------------------------- 1 | .mk-input-wrapper { 2 | position: relative; 3 | display: flex; 4 | width: 100%; 5 | margin-bottom: 10px; 6 | .icon-wrapper { 7 | position: absolute; 8 | top: 0; 9 | right: 0; 10 | display: flex; 11 | align-items: center; 12 | justify-content: center; 13 | width: 35px; 14 | height: 100%; 15 | color: $input-color; 16 | svg { 17 | color: $font-color-lighter; 18 | } 19 | } 20 | } 21 | 22 | .mk-input-inner { 23 | width: 100%; 24 | padding: $input-padding-y $input-padding-x; 25 | color: $input-color; 26 | font-weight: $input-font-weight; 27 | font-size: $input-font-size; 28 | font-family: $input-font-family; 29 | line-height: $input-line-height; 30 | background-color: $input-bg; 31 | background-clip: padding-box; 32 | border: $input-border-width solid $input-border-color; 33 | border-radius: $input-border-radius; 34 | box-shadow: $input-box-shadow; 35 | transition: $input-transition; 36 | 37 | &:focus { 38 | color: $input-focus-color; 39 | background-color: $input-focus-bg; 40 | border-color: $input-focus-border-color; 41 | outline: 0; 42 | box-shadow: $input-focus-box-shadow; 43 | } 44 | &::placeholder { 45 | color: $input-placeholder-color; 46 | opacity: 1; 47 | } 48 | &:disabled, &[readonly] { 49 | background-color: $input-disabled-bg; 50 | border-color: $input-disabled-border-color; 51 | cursor: not-allowed; 52 | opacity: 1; 53 | } 54 | } 55 | .icon-wrapper + .mk-input-inner { 56 | padding-right: 35px; 57 | } 58 | .mk-input-group-prefix, .mk-input-group-suffix { 59 | display: flex; 60 | align-items: center; 61 | margin-bottom: 0; 62 | padding: $input-padding-y $input-padding-x; 63 | color: $input-group-addon-color; 64 | font-weight: $font-weight-normal; 65 | font-size: $input-font-size; 66 | line-height: $input-line-height; 67 | white-space: nowrap; 68 | text-align: center; 69 | background-color: $input-group-addon-bg; 70 | border: $input-border-width solid $input-group-addon-border-color; 71 | border-radius: $input-border-radius; 72 | } 73 | 74 | .input-size-sm .mk-input-inner { 75 | padding: $input-padding-y-sm $input-padding-x-sm; 76 | font-size: $input-font-size-sm; 77 | border-radius: $input-border-radius-sm; 78 | } 79 | 80 | .input-size-lg .mk-input-inner { 81 | padding: $input-padding-y-lg $input-padding-x-lg; 82 | font-size: $input-font-size-lg; 83 | border-radius: $input-border-radius-lg; 84 | } 85 | .mk-input-group-suffix + .btn { 86 | padding: 0; 87 | border: 0; 88 | } 89 | .input-group > .mk-input-group-prefix, .input-group.input-group-suffix > .mk-input-inner { 90 | @include border-right-radius(0); 91 | } 92 | 93 | .input-group > .mk-input-group-suffix, .input-group.input-group-prefix > .mk-input-inner { 94 | @include border-left-radius(0); 95 | } 96 | -------------------------------------------------------------------------------- /components/menu/MenuContext.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { MenuMode, SelectCallback } from './menu' 3 | 4 | export interface IMenuContext { 5 | key: string 6 | onClick?: SelectCallback 7 | mode?: MenuMode 8 | defaultOpenKeys?: string[] 9 | } 10 | 11 | const MenuContext = React.createContext({ key: '0' }) 12 | 13 | export default MenuContext 14 | -------------------------------------------------------------------------------- /components/menu/MenuItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useContext, CSSProperties } from 'react' 2 | import classNames from 'classnames' 3 | import MenuContext from './MenuContext' 4 | 5 | export interface MenuItemProps { 6 | index?: string 7 | className?: string 8 | style?: CSSProperties 9 | disabled?: boolean 10 | } 11 | 12 | const MenuItem: FC = ({ 13 | index, 14 | className, 15 | style, 16 | disabled, 17 | children, 18 | }) => { 19 | const context = useContext(MenuContext) 20 | const classes = classNames('mk-menu-item', className, { 21 | 'is-disabled': disabled, 22 | // eslint-disable-next-line react/destructuring-assignment 23 | 'is-active': context.key === index, 24 | }) 25 | 26 | const handleClick = () => { 27 | const { onClick } = context 28 | if (onClick && !disabled && typeof index === 'string') { 29 | onClick(index) 30 | } 31 | } 32 | 33 | return ( 34 |
  • 35 | {children} 36 |
  • 37 | ) 38 | } 39 | 40 | MenuItem.displayName = 'MenuItem' 41 | 42 | export default MenuItem 43 | -------------------------------------------------------------------------------- /components/menu/SubMenu.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useContext, useState, FunctionComponentElement } from 'react' 2 | import classNames from 'classnames' 3 | import { DownOutlined } from '@ant-design/icons' 4 | import MenuContext from './MenuContext' 5 | import { MenuItemProps } from './MenuItem' 6 | import Transition from '../transition' 7 | 8 | export interface SubMenuProps { 9 | index?: string 10 | title: string 11 | className?: string 12 | style?: React.CSSProperties 13 | } 14 | 15 | const SubMenu: FC = ({ index, title, className, style, children }) => { 16 | const { key, mode, defaultOpenKeys } = useContext(MenuContext) 17 | const openedSubMenus = defaultOpenKeys as Array 18 | const isOpened = index && mode === 'inline' ? openedSubMenus.includes(index) : false 19 | const [menuOpen, setOpen] = useState(isOpened) 20 | const classes = classNames('mk-menu-item mk-submenu-item', className, { 21 | 'is-active': key === index, 22 | 'is-opened': menuOpen, 23 | 'is-inline': mode === 'inline', 24 | }) 25 | 26 | const handleClick = (e: React.MouseEvent) => { 27 | e.preventDefault() 28 | setOpen(!menuOpen) 29 | } 30 | 31 | let timer: number 32 | const handleMouse = (e: React.MouseEvent, toggle: boolean) => { 33 | clearTimeout(timer) 34 | e.preventDefault() 35 | timer = window.setTimeout(() => { 36 | setOpen(toggle) 37 | }, 100) 38 | } 39 | 40 | const handleEvents = 41 | mode !== 'inline' 42 | ? { 43 | onMouseEnter: (e: React.MouseEvent) => handleMouse(e, true), 44 | onMouseLeave: (e: React.MouseEvent) => handleMouse(e, false), 45 | } 46 | : {} 47 | 48 | const clickEvents = mode === 'inline' ? { onClick: handleClick } : {} 49 | 50 | const renderChildren = () => { 51 | const subMenuClasses = classNames('mk-submenu', { 52 | 'menu-opened': menuOpen, 53 | }) 54 | const childrenComponent = React.Children.map(children, (child, i) => { 55 | const childElement = child as FunctionComponentElement 56 | if (childElement.type.displayName === 'MenuItem') { 57 | return React.cloneElement(childElement, { 58 | index: `${index}-${i}`, 59 | }) 60 | } 61 | // eslint-disable-next-line no-console 62 | console.error('Warning: SubMenu has a child which is not a MenuItem component') 63 | }) 64 | 65 | return ( 66 | 67 |
      {childrenComponent}
    68 |
    69 | ) 70 | } 71 | 72 | return ( 73 |
  • 74 |
    75 | {title} 76 | 77 |
    78 | {renderChildren()} 79 |
  • 80 | ) 81 | } 82 | 83 | SubMenu.displayName = 'SubMenu' 84 | 85 | export default SubMenu 86 | -------------------------------------------------------------------------------- /components/menu/__tests__/__snapshots__/index.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Menu component render should render the correct default menu 1`] = ` 4 | 9 |
      12 | 16 |
    • 20 | menu1 21 |
    • 22 |
      23 | 28 |
    • 32 | menu2 33 |
    • 34 |
      35 | 39 |
    • 43 | menu3 44 |
    • 45 |
      46 |
    47 |
    48 | `; 49 | 50 | exports[`Menu component render should render the correct default menu with SubMenu 1`] = ` 51 | 56 |
      59 | 63 |
    • 67 | menu1 68 |
    • 69 |
      70 | 75 |
    • 79 | menu2 80 |
    • 81 |
      82 | 86 |
    • 90 | menu3 91 |
    • 92 |
      93 | 98 |
    • 103 |
      106 | menu4 107 | 110 | 134 | 139 | 162 | 177 | 178 | 179 | 180 | 181 |
      182 | 190 | 197 | 212 | 213 | 214 |
    • 215 |
      216 | 221 |
    • 226 |
      229 | menu5 230 | 233 | 257 | 262 | 285 | 300 | 301 | 302 | 303 | 304 |
      305 | 313 | 320 | 335 | 336 | 337 |
    • 338 |
      339 |
    340 |
    341 | `; 342 | 343 | exports[`Menu component render should render the correct inline menu 1`] = ` 344 | 349 |
      352 | 356 |
    • 360 | menu1 361 |
    • 362 |
      363 | 368 |
    • 372 | menu2 373 |
    • 374 |
      375 | 379 |
    • 383 | menu3 384 |
    • 385 |
      386 |
    387 |
    388 | `; 389 | 390 | exports[`Menu component render should render the correct inline menu with SubMenu 1`] = ` 391 | 396 |
      399 | 403 |
    • 407 | menu1 408 |
    • 409 |
      410 | 415 |
    • 419 | menu2 420 |
    • 421 |
      422 | 426 |
    • 430 | menu3 431 |
    • 432 |
      433 | 438 |
    • 441 |
      445 | menu4 446 | 449 | 473 | 478 | 501 | 516 | 517 | 518 | 519 | 520 |
      521 | 529 | 536 | 551 | 552 | 553 |
    • 554 |
      555 | 560 |
    • 563 |
      567 | menu5 568 | 571 | 595 | 600 | 623 | 638 | 639 | 640 | 641 | 642 |
      643 | 651 | 658 | 673 | 674 | 675 |
    • 676 |
      677 |
    678 |
    679 | `; 680 | -------------------------------------------------------------------------------- /components/menu/__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { mount } from 'enzyme' 3 | import Menu, { MenuProps } from '../menu' 4 | 5 | const { SubMenu } = Menu 6 | 7 | const generateMenu = (props: MenuProps = {}) => ( 8 | 9 | menu1 10 | menu2 11 | menu3 12 | 13 | ) 14 | 15 | const generateMenuWithSubMenu = (props: MenuProps = {}) => ( 16 | 17 | menu1 18 | menu2 19 | menu3 20 | 21 | subMenu1 22 | 23 | 24 | subMenu2 25 | 26 | 27 | ) 28 | 29 | describe('Menu component render', () => { 30 | it('should render the correct default menu', () => { 31 | const wrapper = mount(generateMenu()) 32 | 33 | expect(wrapper.find('.mk-menu').hasClass('mk-menu-horizontal')).toBeTruthy() 34 | expect( 35 | wrapper 36 | .find('.mk-menu-item') 37 | .first() 38 | .hasClass('is-active'), 39 | ).toBeTruthy() 40 | 41 | expect(wrapper).toMatchSnapshot() 42 | }) 43 | 44 | it('should render the correct inline menu', () => { 45 | const wrapper = mount( 46 | generateMenu({ 47 | mode: 'inline', 48 | }), 49 | ) 50 | 51 | expect(wrapper.find('.mk-menu').hasClass('mk-menu-inline')).toBeTruthy() 52 | expect(wrapper.find('.mk-menu').hasClass('mk-menu-horizontal')).toBeFalsy() 53 | expect( 54 | wrapper 55 | .find('.mk-menu-item') 56 | .first() 57 | .hasClass('is-active'), 58 | ).toBeTruthy() 59 | expect(wrapper).toMatchSnapshot() 60 | }) 61 | 62 | it('should render the correct default menu with SubMenu', () => { 63 | const wrapper = mount(generateMenuWithSubMenu()) 64 | expect(wrapper.find('.mk-menu-item').length).toEqual(5) 65 | expect(wrapper.find('.mk-submenu-item').length).toEqual(2) 66 | expect(wrapper).toMatchSnapshot() 67 | }) 68 | 69 | it('should render the correct inline menu with SubMenu', () => { 70 | const wrapper = mount( 71 | generateMenuWithSubMenu({ 72 | mode: 'inline', 73 | }), 74 | ) 75 | expect(wrapper.find('.mk-menu').hasClass('mk-menu-inline')).toBeTruthy() 76 | expect(wrapper.find('.mk-menu-item').length).toEqual(5) 77 | expect(wrapper.find('.mk-submenu-item').length).toEqual(2) 78 | expect(wrapper).toMatchSnapshot() 79 | }) 80 | 81 | it('should render disabled MenuItem correctly', () => { 82 | const onClick = jest.fn() 83 | const wrapper = mount( 84 | generateMenu({ 85 | onClick, 86 | }), 87 | ) 88 | expect( 89 | wrapper 90 | .find('.mk-menu-item') 91 | .at(1) 92 | .hasClass('is-disabled'), 93 | ).toBeTruthy() 94 | expect( 95 | wrapper 96 | .find('.is-disabled') 97 | .first() 98 | .simulate('click'), 99 | ) 100 | expect(onClick).not.toHaveBeenCalled() 101 | }) 102 | 103 | it('click items should change active and call the right callback', () => { 104 | const onClick = jest.fn() 105 | const wrapper = mount( 106 | generateMenu({ 107 | onClick, 108 | }), 109 | ) 110 | const menuItem1 = wrapper.find('.mk-menu-item').at(0) 111 | const menuItem3 = wrapper.find('.mk-menu-item').at(2) 112 | expect(menuItem1.hasClass('is-active')).toBeTruthy() 113 | menuItem3.simulate('click') 114 | 115 | expect(onClick).toHaveBeenCalled() 116 | expect( 117 | wrapper 118 | .find('.mk-menu-item') 119 | .at(2) 120 | .hasClass('is-active'), 121 | ).toBeTruthy() 122 | 123 | expect( 124 | wrapper 125 | .find('.mk-menu-item') 126 | .at(0) 127 | .hasClass('is-active'), 128 | ).toBeFalsy() 129 | }) 130 | 131 | it('set defaultOpenKeys in mode horizontal not working', () => { 132 | const wrapper = mount( 133 | generateMenuWithSubMenu({ 134 | defaultOpenKeys: ['3'], 135 | }), 136 | ) 137 | expect(wrapper.find('.mk-submenu')).toEqual({}) 138 | }) 139 | 140 | it('should accept defaultOpenKeys in mode inline', () => { 141 | const wrapper = mount( 142 | generateMenuWithSubMenu({ 143 | mode: 'inline', 144 | defaultOpenKeys: ['3'], 145 | }), 146 | ) 147 | expect(wrapper.find('.mk-submenu').length).toEqual(1) 148 | }) 149 | 150 | it('should render defaultSelectedKey on default menu correctly', () => { 151 | const wrapper = mount( 152 | generateMenu({ 153 | defaultSelectedKey: '2', 154 | }), 155 | ) 156 | expect( 157 | wrapper 158 | .find('.mk-menu-item') 159 | .at(0) 160 | .hasClass('is-active'), 161 | ).toBeFalsy() 162 | expect( 163 | wrapper 164 | .find('.mk-menu-item') 165 | .at(2) 166 | .hasClass('is-active'), 167 | ).toBeTruthy() 168 | }) 169 | }) 170 | -------------------------------------------------------------------------------- /components/menu/demos/defaultOpenKeys.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Menu } from 'monki-ui' 3 | import 'monki-ui/dist/index.css' 4 | 5 | const { SubMenu } = Menu 6 | 7 | export default () => ( 8 | 9 | 菜单1 10 | 11 | 菜单2-1 12 | 菜单2-2 13 | 14 | 菜单3 15 | 菜单4 16 | 17 | ) 18 | -------------------------------------------------------------------------------- /components/menu/demos/horizontal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Menu } from 'monki-ui' 3 | import 'monki-ui/dist/index.css' 4 | 5 | export default () => { 6 | const onClick = (index: string) => { 7 | console.log('click: ', index) 8 | } 9 | return ( 10 | 11 | 菜单1 12 | 菜单2 13 | 菜单3 14 | 菜单4 15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /components/menu/demos/inline.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Menu } from 'monki-ui' 3 | import 'monki-ui/dist/index.css' 4 | 5 | export default () => ( 6 | 7 | 菜单1 8 | 菜单2 9 | 菜单3 10 | 菜单4 11 | 12 | ) 13 | -------------------------------------------------------------------------------- /components/menu/demos/mode.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { Menu, Button } from 'monki-ui' 3 | import { MenuMode } from 'monki-ui/dist/menu/menu' 4 | import 'monki-ui/dist/index.css' 5 | 6 | const { SubMenu } = Menu 7 | 8 | export default () => { 9 | const [mode, setMode] = useState('horizontal') 10 | const toggleMode = () => { 11 | if (mode === 'horizontal') { 12 | setMode('inline') 13 | } else { 14 | setMode('horizontal') 15 | } 16 | } 17 | 18 | return ( 19 |
    20 |
    21 | 24 | mode: {mode} 25 |
    26 | 27 | 菜单1 28 | 29 | 菜单2-1 30 | 菜单2-2 31 | 32 | 菜单3 33 | 菜单4 34 | 35 |
    36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /components/menu/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Menu 导航菜单 3 | group: 4 | title: Menu 导航菜单 5 | nav: 6 | title: '组件' 7 | path: /components 8 | --- 9 | 10 | ### 顶部导航 11 | 12 | 水平的顶部导航菜单。`mode`值为`horizontal`。 13 | 14 | 15 | 16 | ### 内嵌菜单 17 | 18 | 垂直菜单,子菜单内嵌在菜单区域。`mode`值为`inline`。 19 | 20 | 21 | 22 | ### 只展开当前父级菜单 23 | 24 | 通过`defaultOpenKeys`设置当前展开的 SubMenu 菜单项 key 数组 25 | 26 | 点击菜单,收起其他展开的所有菜单,保持菜单聚焦简洁。 27 | 28 | 29 | 30 | ### 切换菜单类型 31 | 32 | 展示动态切换模式。 33 | 34 | 35 | 36 | ### API 37 | 38 | #### Menu 39 | 40 | | 参数 | 说明 | 类型 | 默认值 | 41 | | ------------------- | -------------------------------------- | ------------------------------------------ | -------- | 42 | | defaultOpenKeys | 初始展开的 SubMenu 菜单项 key 数组 | string\[] | - | 43 | | defaultSelectedKeys | 初始选中的菜单项 key 数组 | string | - | 44 | | mode | 菜单类型,现在支持水平、和内嵌模式三种 | `horizontal` \| `inline` | `inline` | 45 | | style | 根节点样式 | CSSProperties | - | 46 | | onClick | 点击 MenuItem 调用此函数 | function({ item, key, keyPath, domEvent }) | - | 47 | 48 | #### Menu.Item 49 | 50 | | 参数 | 说明 | 类型 | 默认值 | 51 | | -------- | --------------- | ------- | ------ | 52 | | disabled | 是否禁用 | boolean | false | 53 | | index | item 的唯一标志 | string | - | 54 | 55 | #### Menu.SubMenu 56 | 57 | | 参数 | 说明 | 类型 | 默认值 | 58 | | -------- | -------------- | ----------------------------- | ------ | 59 | | children | 子菜单的菜单项 | Array<MenuItem \| SubMenu> | - | 60 | | index | 唯一标志 | string | - | 61 | | title | 子菜单项值 | ReactNode | - | 62 | -------------------------------------------------------------------------------- /components/menu/index.tsx: -------------------------------------------------------------------------------- 1 | import Menu from './menu' 2 | 3 | export default Menu 4 | -------------------------------------------------------------------------------- /components/menu/menu.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState, CSSProperties } from 'react' 2 | import classNames from 'classnames' 3 | import MenuContext, { IMenuContext } from './MenuContext' 4 | import MenuItem, { MenuItemProps } from './MenuItem' 5 | import SubMenu, { SubMenuProps } from './SubMenu' 6 | 7 | export type MenuMode = 'horizontal' | 'inline' // 水平 | 内嵌 8 | export type SelectCallback = (selectedKey: string) => void 9 | 10 | export interface MenuProps { 11 | className?: string 12 | style?: CSSProperties 13 | mode?: MenuMode // 菜单类型 14 | onClick?: SelectCallback // 点击 MenuItem 调用此函数 15 | defaultSelectedKey?: string // 初始选中的菜单项 key 16 | defaultOpenKeys?: string[] // 初始展开的 SubMenu 菜单项 key 数组 只在纵向模式下生效 17 | } 18 | 19 | const RootMenu: FC = ({ 20 | className, 21 | style, 22 | mode, 23 | onClick, 24 | defaultSelectedKey, 25 | defaultOpenKeys, 26 | children, 27 | }) => { 28 | const [currentSelectedKey, setCurrentSelectedKey] = useState(defaultSelectedKey) 29 | const classes = classNames('mk-menu', className, { 30 | 'mk-menu-horizontal': mode === 'horizontal', 31 | 'mk-menu-inline': mode === 'inline', 32 | }) 33 | 34 | const handleClick = (key: string) => { 35 | setCurrentSelectedKey(key) 36 | if (typeof onClick === 'function') { 37 | onClick(key) 38 | } 39 | } 40 | 41 | const menuContext: IMenuContext = { 42 | key: currentSelectedKey || '0', 43 | onClick: handleClick, 44 | mode, 45 | defaultOpenKeys, 46 | } 47 | 48 | const renderChildren = () => 49 | React.Children.map(children, (child, index) => { 50 | // child 是 ReactNode 类型,先断言成 FunctionComponentElement 类型,再拿到 displayName 内置属性 51 | const childElement = child as React.FunctionComponentElement 52 | const { displayName } = childElement.type 53 | if (displayName === 'MenuItem' || displayName === 'SubMenu') { 54 | // 添加 index 属性,利用遍历时可以拿到的 index 变量来设置,这样使用时则无需给 MenuItem 传入 index 属性 55 | return React.cloneElement(childElement, { 56 | index: index.toString(), 57 | }) 58 | } 59 | // eslint-disable-next-line no-console 60 | console.error('Warning: Menu has a child which is not a MenuItem component') 61 | }) 62 | 63 | return ( 64 |
      65 | {renderChildren()} 66 |
    67 | ) 68 | } 69 | 70 | RootMenu.defaultProps = { 71 | mode: 'horizontal', 72 | defaultSelectedKey: '0', 73 | defaultOpenKeys: [], 74 | } 75 | 76 | export type IMenuComponent = FC & { 77 | Item: FC 78 | SubMenu: FC 79 | } 80 | 81 | const Menu = RootMenu as IMenuComponent 82 | Menu.Item = MenuItem 83 | Menu.SubMenu = SubMenu 84 | 85 | export default Menu 86 | -------------------------------------------------------------------------------- /components/menu/style/index.scss: -------------------------------------------------------------------------------- 1 | /* stylelint-disable */ 2 | .mk-menu { 3 | display: flex; 4 | flex-wrap: wrap; 5 | margin-bottom: 30px; 6 | padding-left: 0; 7 | list-style: none; 8 | border-bottom: $menu-border-width solid $menu-border-color; 9 | box-shadow: $menu-box-shadow; 10 | > .mk-menu-item { 11 | padding: $menu-item-padding-y $menu-item-padding-x; 12 | cursor: pointer; 13 | transition: $menu-transition; 14 | &:hover, 15 | &:focus { 16 | text-decoration: none; 17 | } 18 | &.is-disabled { 19 | color: $menu-item-disabled-color; 20 | cursor: default; 21 | pointer-events: none; 22 | } 23 | &.is-active, 24 | &:hover { 25 | color: $menu-item-active-color; 26 | border-bottom: $menu-item-active-border-width solid $menu-item-active-color; 27 | } 28 | } 29 | .mk-submenu-item { 30 | position: relative; 31 | .mk-submenu-title { 32 | display: flex; 33 | align-items: center; 34 | } 35 | .arrow-icon { 36 | margin-left: 10px; 37 | font-size: 12px; 38 | transition: transform 0.25s ease-in-out; 39 | } 40 | &:hover { 41 | .arrow-icon { 42 | transform: rotate(180deg); 43 | } 44 | } 45 | } 46 | .is-inline { 47 | .arrow-icon { 48 | transform: rotate(0deg) !important; 49 | } 50 | } 51 | .is-inline.is-opened { 52 | .arrow-icon { 53 | transform: rotate(180deg) !important; 54 | } 55 | } 56 | .mk-submenu { 57 | padding-left: 0; 58 | white-space: nowrap; 59 | list-style: none; 60 | transition: $menu-transition; 61 | .mk-menu-item { 62 | padding: $menu-item-padding-y $menu-item-padding-x; 63 | color: $body-color; 64 | cursor: pointer; 65 | transition: $menu-transition; 66 | &.is-active, 67 | &:hover { 68 | color: $menu-item-active-color !important; 69 | } 70 | } 71 | } 72 | .mk-submenu.menu-opened { 73 | display: block; 74 | } 75 | } 76 | .mk-menu-horizontal { 77 | > .mk-menu-item { 78 | border-bottom: $menu-item-active-border-width solid transparent; 79 | } 80 | .mk-submenu { 81 | position: absolute; 82 | top: calc(100% + 8px); 83 | left: 0; 84 | z-index: 100; 85 | background: $white; 86 | border: $menu-border-width solid $menu-border-color; 87 | box-shadow: $submenu-box-shadow; 88 | } 89 | } 90 | .mk-menu-inline { 91 | flex-direction: column; 92 | margin: 10px 20px; 93 | border-right: $menu-border-width solid $menu-border-color; 94 | border-bottom: 0; 95 | > .mk-menu-item { 96 | border-left: $menu-item-active-border-width solid transparent; 97 | &.is-active, 98 | &:hover { 99 | border-bottom: 0; 100 | border-left: $menu-item-active-border-width solid $menu-item-active-color; 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /components/menu/style/index.tsx: -------------------------------------------------------------------------------- 1 | import './index.scss' 2 | -------------------------------------------------------------------------------- /components/select/Option.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useContext } from 'react' 2 | import classNames from 'classnames' 3 | import { CheckOutlined } from '@ant-design/icons' 4 | import { SelectContext } from './select' 5 | 6 | export interface SelectOptionProps { 7 | index?: string 8 | /** 默认根据此属性值进行筛选,该值不能相同 */ 9 | value: string 10 | /** 选项的标签,若不设置则默认与 value 相同 */ 11 | label?: string 12 | /** 是否禁用该选项 */ 13 | disabled?: boolean 14 | } 15 | 16 | export const Option: FC = ({ 17 | value, 18 | label, 19 | disabled, 20 | children, 21 | index, 22 | }) => { 23 | const { onSelect, selectedValues, multiple } = useContext(SelectContext) 24 | const isSelected = selectedValues.includes(value) 25 | const classes = classNames('mk-select-item', { 26 | 'is-disabled': disabled, 27 | 'is-selected': isSelected, 28 | }) 29 | // eslint-disable-next-line no-shadow 30 | const handleClick = (e: React.MouseEvent, value: string, isSelected: boolean) => { 31 | e.preventDefault() 32 | if (onSelect && !disabled) { 33 | onSelect(value, isSelected) 34 | } 35 | } 36 | 37 | return ( 38 |
  • { 42 | handleClick(e, value, isSelected) 43 | }} 44 | > 45 | {children || label || value} 46 | {multiple && isSelected && } 47 |
  • 48 | ) 49 | } 50 | 51 | Option.displayName = 'Option' 52 | 53 | export default Option 54 | -------------------------------------------------------------------------------- /components/select/__tests__/__snapshots__/index.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Select component should render the correct Select component 1`] = ` 4 | 11 |
    14 |
    18 | } 21 | name="mk-select" 22 | onChange={[Function]} 23 | placeholder="test" 24 | value="" 25 | > 26 |
    29 |
    32 | 33 | 56 | 61 | 84 | 99 | 100 | 101 | 102 | 103 |
    104 | 112 |
    113 |
    114 |
    115 | 123 | 130 | 145 | 146 | 147 |
    148 |
    149 | `; 150 | -------------------------------------------------------------------------------- /components/select/__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { mount } from 'enzyme' 3 | import Select, { SelectProps } from '../select' 4 | 5 | const { Option } = Select 6 | 7 | const testProps: SelectProps = { 8 | defaultValue: '', 9 | placeholder: 'test', 10 | onChange: jest.fn(), 11 | onVisibleChange: jest.fn(), 12 | } 13 | 14 | const multipleProps: SelectProps = { 15 | ...testProps, 16 | multiple: true, 17 | } 18 | 19 | describe('Select component', () => { 20 | it('should render the correct Select component', () => { 21 | const wrapper = mount( 22 | , 27 | ) 28 | expect(wrapper.find('.mk-select-input').length).toBe(1) 29 | expect(wrapper.find('.mk-select-dropdown').length).toBe(0) 30 | expect(wrapper).toMatchSnapshot() 31 | }) 32 | 33 | // TODO: add more test case 34 | }) 35 | -------------------------------------------------------------------------------- /components/select/demos/base.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Select } from 'monki-ui' 3 | import 'monki-ui/dist/index.css' 4 | 5 | export default () => { 6 | const handleChange = (selectedValue: string, selectedValues: string[]) => { 7 | console.log(selectedValue, selectedValues) 8 | } 9 | 10 | const handleVisibleChange = (visible: boolean) => { 11 | console.log('visible', visible) 12 | } 13 | 14 | return ( 15 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /components/select/demos/disabled.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Select } from 'monki-ui' 3 | import 'monki-ui/dist/index.css' 4 | 5 | export default () => ( 6 |
    7 | 12 |
    13 | ) 14 | -------------------------------------------------------------------------------- /components/select/demos/multiple.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Select } from 'monki-ui' 3 | import 'monki-ui/dist/index.css' 4 | 5 | export default () => { 6 | const handleChange = (selectedValue: string, selectedValues: string[]) => { 7 | console.log(selectedValue, selectedValues) 8 | } 9 | 10 | const handleVisibleChange = (visible: boolean) => { 11 | console.log('visible', visible) 12 | } 13 | 14 | return ( 15 | 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /components/select/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Select 选择器 3 | group: 4 | title: Select 选择器 5 | nav: 6 | title: '组件' 7 | path: /components 8 | --- 9 | 10 | ### 何时使用 11 | 12 | - 弹出一个下拉菜单给用户选择操作,用于代替原生的选择器,或者需要一个更优雅的多选器时。 13 | - 当选项少时(少于 5 项),建议直接将选项平铺,使用 Radio 是更好的选择。 14 | 15 | ### 基本使用 16 | 17 | 18 | 19 | ### 多选 20 | 21 | 多选,从已有条目中选择。设置`multiple`属性 22 | 23 | 24 | 25 | ### 禁用 26 | 27 | 28 | 29 | ### API 30 | 31 | #### Select 32 | 33 | | 参数 | 说明 | 类型 | 默认值 | 34 | | --------------- | --------------------- | ------------------------------------------------ | ------- | 35 | | defaultValue | 默认选中的选项 | string \| string\[] | - | 36 | | placeholder | 选择框默认文本 | string | - | 37 | | disabled | 是否禁用 | boolean | `false` | 38 | | multiple | 是否支持多选 | boolean | - | 39 | | onChange | 选中值发生变化时触发 | function(value:string, selectedValues:string\[]) | - | 40 | | onVisibleChange | 下拉框出现/隐藏时触发 | function(visible:boolean) | - | 41 | 42 | #### Select.Option 43 | 44 | | 参数 | 说明 | 类型 | 默认值 | 45 | | -------- | --------------------------------------- | ------- | ------ | 46 | | index | item 的唯一标志 | string | - | 47 | | value | 默认根据此属性值进行筛选,该值不能相同 | string | - | 48 | | label | 选项的标签,若不设置则默认与 value 相同 | string | - | 49 | | disabled | 是否禁用 | boolean | false | 50 | -------------------------------------------------------------------------------- /components/select/index.tsx: -------------------------------------------------------------------------------- 1 | import Select from './select' 2 | 3 | export default Select 4 | -------------------------------------------------------------------------------- /components/select/select.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | FC, 3 | useState, 4 | ChangeEvent, 5 | createContext, 6 | useRef, 7 | FunctionComponentElement, 8 | useEffect, 9 | } from 'react' 10 | import classNames from 'classnames' 11 | import { DownOutlined } from '@ant-design/icons' 12 | import Input from '../input' 13 | import Tag from '../tag' 14 | import useClickOutside from '../hooks/useClickOutside' 15 | import Transition from '../transition' 16 | import Option, { SelectOptionProps } from './Option' 17 | 18 | export interface SelectProps { 19 | /** 默认选中的选项 可以是字符串或者字符串数组 */ 20 | defaultValue?: string | string[] 21 | placeholder?: string 22 | /** 是否禁用 */ 23 | disabled?: boolean 24 | /** 是否支持多选 */ 25 | multiple?: boolean 26 | /** input 的 name 属性 */ 27 | name?: string 28 | /** 选中值发生变化时触发 */ 29 | onChange?: (selectedValue: string, selectedValues: string[]) => void 30 | /** 下拉框出现/隐藏时触发 */ 31 | onVisibleChange?: (visible: boolean) => void 32 | } 33 | 34 | export interface ISelectContext { 35 | onSelect?: (value: string, isSelected?: boolean) => void 36 | selectedValues: string[] 37 | multiple?: boolean 38 | } 39 | 40 | export const SelectContext = createContext({ selectedValues: [] }) 41 | 42 | export const RootSelect: FC = props => { 43 | const { 44 | defaultValue, 45 | placeholder, 46 | children, 47 | multiple, 48 | name, 49 | disabled, 50 | onChange, 51 | onVisibleChange, 52 | } = props 53 | const inputRef = useRef(null) 54 | const containerRef = useRef(null) 55 | const containerWidth = useRef(0) 56 | const [selectedValues, setSelectedValues] = useState( 57 | Array.isArray(defaultValue) ? defaultValue : [], 58 | ) 59 | const [options, setOptions] = useState([]) 60 | const [menuOpen, setMenuOpen] = useState(false) 61 | const [value, setValue] = useState( 62 | typeof defaultValue === 'string' ? defaultValue : '', 63 | ) 64 | 65 | const handleOptionClick = (optionValue: string, isSelected?: boolean) => { 66 | let updatedValues = [optionValue] 67 | if (!multiple) { 68 | setMenuOpen(false) 69 | setValue(optionValue) 70 | if (onVisibleChange) { 71 | onVisibleChange(false) 72 | } 73 | } else { 74 | // 多选模式 75 | setValue('') 76 | // 如果当前点击的选项已被选中,则去除选中状态;如果之前未被选中,则设为选中状态 77 | updatedValues = isSelected 78 | ? selectedValues.filter(v => v !== optionValue) 79 | : [...selectedValues, optionValue] 80 | setSelectedValues(updatedValues) 81 | } 82 | 83 | if (onChange) { 84 | onChange(optionValue, updatedValues) 85 | } 86 | } 87 | 88 | const handleInputValueChange = (e: ChangeEvent) => { 89 | const newValue = e.target.value.trim() 90 | if (multiple) return 91 | setValue(newValue) 92 | } 93 | 94 | useEffect(() => { 95 | const tempArr: string[] = [] 96 | React.Children.map(children, child => { 97 | const childElement = child as FunctionComponentElement 98 | const { 99 | // eslint-disable-next-line no-shadow 100 | props: { value = '' }, 101 | } = childElement 102 | 103 | if (value !== 'disabled' && value) tempArr.push(value) 104 | }) 105 | setOptions(tempArr) 106 | }, [children]) 107 | 108 | useEffect(() => { 109 | // focus input 110 | if (inputRef.current) { 111 | inputRef.current.focus() 112 | if (multiple && selectedValues.length > 0) { 113 | inputRef.current.placeholder = '' 114 | } else if (placeholder) inputRef.current.placeholder = placeholder 115 | } 116 | }, [selectedValues, multiple, placeholder]) 117 | 118 | useEffect(() => { 119 | if (containerRef.current) { 120 | containerWidth.current = containerRef.current.getBoundingClientRect().width 121 | } 122 | }) 123 | 124 | useClickOutside(containerRef, () => { 125 | if (!multiple && options.includes(value)) { 126 | setValue('') 127 | } 128 | setMenuOpen(false) 129 | if (onVisibleChange && menuOpen) { 130 | onVisibleChange(false) 131 | } 132 | }) 133 | 134 | const passedContext: ISelectContext = { 135 | onSelect: handleOptionClick, 136 | selectedValues, 137 | multiple, 138 | } 139 | 140 | const handleClick = (e: React.MouseEvent) => { 141 | e.preventDefault() 142 | if (!disabled) { 143 | setMenuOpen(!menuOpen) 144 | if (onVisibleChange) { 145 | onVisibleChange(!menuOpen) 146 | } 147 | } 148 | } 149 | 150 | const generateOptions = () => 151 | React.Children.map(children, (child, i) => { 152 | const childElement = child as FunctionComponentElement 153 | if (childElement.type.displayName === 'Option') { 154 | return React.cloneElement(childElement, { 155 | index: `select-${i}`, 156 | }) 157 | } 158 | // eslint-disable-next-line no-console 159 | console.error('Warning: Select has a child which is not a Option component') 160 | }) 161 | 162 | const containerClass = classNames('mk-select', { 163 | 'menu-is-open': menuOpen, 164 | 'is-disabled': disabled, 165 | 'is-multiple': multiple, 166 | }) 167 | 168 | return ( 169 |
    170 |
    171 | } 180 | autoComplete="off" 181 | /> 182 |
    183 | 184 | 185 |
      {generateOptions()}
    186 |
    187 |
    188 | {multiple && ( 189 |
    193 | {selectedValues.map((selectedValue, index) => ( 194 | { 199 | handleOptionClick(selectedValue, true) 200 | }} 201 | closable 202 | /> 203 | ))} 204 |
    205 | )} 206 |
    207 | ) 208 | } 209 | 210 | RootSelect.defaultProps = { 211 | name: 'mk-select', 212 | } 213 | export type ISelectComponent = FC & { 214 | Option: FC 215 | } 216 | const Select = RootSelect as ISelectComponent 217 | Select.Option = Option 218 | 219 | export default Select 220 | -------------------------------------------------------------------------------- /components/select/style/index.scss: -------------------------------------------------------------------------------- 1 | .mk-select { 2 | position: relative; 3 | 4 | input { 5 | &[readonly] { 6 | background-color: $input-bg; 7 | border-color: $input-border-color; 8 | cursor: pointer; 9 | opacity: 1; 10 | } 11 | &:disabled { 12 | background-color: $input-disabled-bg; 13 | border-color: $input-disabled-border-color; 14 | cursor: not-allowed; 15 | opacity: 1; 16 | } 17 | } 18 | 19 | .mk-input-wrapper { 20 | cursor: pointer; 21 | &:hover { 22 | input { 23 | border-color: $primary !important; 24 | } 25 | } 26 | } 27 | 28 | .icon-wrapper { 29 | transform: rotate(0deg) !important; 30 | transition: transform .25s ease-in-out; 31 | } 32 | } 33 | 34 | .mk-select.menu-is-open { 35 | .icon-wrapper { 36 | transform: rotate(180deg) !important; 37 | } 38 | } 39 | 40 | .mk-select-dropdown { 41 | position: absolute; 42 | top: calc(100% + 8px); 43 | left: 0; 44 | z-index: 100; 45 | width: 100%; 46 | padding-left: 0; 47 | white-space: nowrap; 48 | list-style: none; 49 | background: $white; 50 | border: $menu-border-width solid $menu-border-color; 51 | box-shadow: $submenu-box-shadow; 52 | .mk-select-item { 53 | display: flex; 54 | align-items: center; 55 | justify-content: space-between; 56 | padding: $menu-item-padding-y $menu-item-padding-x; 57 | color: $body-color; 58 | cursor: pointer; 59 | transition: $menu-transition; 60 | &.is-selected { 61 | color: $menu-item-active-color; 62 | font-weight: $font-weight-bold; 63 | } 64 | &.is-disabled { 65 | color: $menu-item-disabled-color; 66 | cursor: default; 67 | pointer-events: none; 68 | } 69 | &:hover { 70 | background-color: rgba($primary, .1); 71 | } 72 | } 73 | } 74 | 75 | .mk-selected-tags { 76 | position: absolute; 77 | top: 0; 78 | left: 0; 79 | z-index: 100; 80 | display: flex; 81 | flex-wrap: wrap; 82 | align-items: center; 83 | justify-content: center; 84 | max-width: 100%; 85 | height: 100%; 86 | .mk-tag { 87 | height: 80%; 88 | margin-left: 4px; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /components/select/style/index.tsx: -------------------------------------------------------------------------------- 1 | import './index.scss' 2 | -------------------------------------------------------------------------------- /components/style/core/_motion.scss: -------------------------------------------------------------------------------- 1 | @include zoom-animation('top', scaleY(0), scaleY(1), center top); 2 | @include zoom-animation('left', scale(.45, .45), scale(1, 1), top left); 3 | @include zoom-animation('right', scale(.45, .45), scale(1, 1), top right); 4 | @include zoom-animation('bottom', scaleY(0), scaleY(1), center bottom); 5 | -------------------------------------------------------------------------------- /components/style/core/_normalize.scss: -------------------------------------------------------------------------------- 1 | // stylelint-disable at-rule-no-vendor-prefix, declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix 2 | 3 | // Reboot 4 | // 5 | // Normalization of HTML elements, manually forked from Normalize.css to remove 6 | // styles targeting irrelevant browsers while applying new styles. 7 | // 8 | // Normalize is licensed MIT. https://github.com/necolas/normalize.css 9 | 10 | // Document 11 | // 12 | // Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`. 13 | *, *::before, *::after { 14 | box-sizing: border-box; 15 | } 16 | 17 | // Body 18 | // 19 | // 1. Remove the margin in all browsers. 20 | // 2. As a best practice, apply a default `background-color`. 21 | // 3. Prevent adjustments of font size after orientation changes in IE on Windows Phone and in iOS. 22 | // 4. Change the default tap highlight to be completely transparent in iOS. 23 | body { 24 | margin: 0; // 1 25 | color: $body-color; 26 | font-weight: $font-weight-base; 27 | font-size: $font-size-base; 28 | font-family: $font-family-base; 29 | line-height: $line-height-base; 30 | text-align: $body-text-align; 31 | background-color: $body-bg; // 2 32 | -webkit-text-size-adjust: 100%; // 3 33 | -webkit-tap-highlight-color: rgba($black, 0); // 4 34 | } 35 | 36 | // Future-proof rule: in browsers that support :focus-visible, suppress the focus outline 37 | // on elements that programmatically receive focus but wouldn't normally show a visible 38 | // focus outline. In general, this would mean that the outline is only applied if the 39 | // interaction that led to the element receiving programmatic focus was a keyboard interaction, 40 | // or the browser has somehow determined that the user is primarily a keyboard user and/or 41 | // wants focus outlines to always be presented. 42 | // 43 | // See https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible 44 | // and https://developer.paciellogroup.com/blog/2018/03/focus-visible-and-backwards-compatibility/ 45 | 46 | [tabindex='-1']:focus:not(:focus-visible) { 47 | outline: 0 !important; 48 | } 49 | 50 | // Content grouping 51 | // 52 | // 1. Reset Firefox's gray color 53 | // 2. Set correct height and prevent the `size` attribute to make the `hr` look like an input field 54 | // See https://www.w3schools.com/tags/tryit.asp?filename=tryhtml_hr_size 55 | 56 | hr { 57 | margin: $hr-margin-y 0; 58 | color: $hr-color; // 1 59 | background-color: currentColor; 60 | border: 0; 61 | opacity: $hr-opacity; 62 | } 63 | 64 | hr:not([size]) { 65 | height: $hr-height; // 2 66 | } 67 | 68 | // Typography 69 | // 70 | // 1. Remove top margins from headings 71 | // By default, `

    `-`

    ` all receive top and bottom margins. We nuke the top 72 | // margin for easier control within type scales as it avoids margin collapsing. 73 | 74 | %heading { 75 | margin-top: 0; // 1 76 | margin-bottom: $headings-margin-bottom; 77 | color: $headings-color; 78 | font-weight: $headings-font-weight; 79 | font-family: $headings-font-family; 80 | font-style: $headings-font-style; 81 | line-height: $headings-line-height; 82 | } 83 | 84 | h1 { 85 | @extend %heading; 86 | font-size: $h1-font-size; 87 | } 88 | 89 | h2 { 90 | @extend %heading; 91 | font-size: $h2-font-size; 92 | } 93 | 94 | h3 { 95 | @extend %heading; 96 | font-size: $h3-font-size; 97 | } 98 | 99 | h4 { 100 | @extend %heading; 101 | font-size: $h4-font-size; 102 | } 103 | 104 | h5 { 105 | @extend %heading; 106 | font-size: $h5-font-size; 107 | } 108 | 109 | h6 { 110 | @extend %heading; 111 | font-size: $h6-font-size; 112 | } 113 | 114 | // Reset margins on paragraphs 115 | // 116 | // Similarly, the top margin on `

    `s get reset. However, we also reset the 117 | // bottom margin to use `rem` units instead of `em`. 118 | 119 | p { 120 | margin-top: 0; 121 | margin-bottom: $paragraph-margin-bottom; 122 | } 123 | 124 | // Abbreviations 125 | // 126 | // 1. Duplicate behavior to the data-* attribute for our tooltip plugin 127 | // 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 128 | // 3. Add explicit cursor to indicate changed behavior. 129 | // 4. Prevent the text-decoration to be skipped. 130 | 131 | abbr[title], abbr[data-original-title] { 132 | // 1 133 | text-decoration: underline; // 2 134 | text-decoration: underline dotted; // 2 135 | cursor: help; // 3 136 | text-decoration-skip-ink: none; // 4 137 | } 138 | 139 | address { 140 | margin-bottom: 1rem; 141 | font-style: normal; 142 | line-height: inherit; 143 | } 144 | 145 | ol, ul { 146 | padding-left: 2rem; 147 | } 148 | 149 | ol, ul, dl { 150 | margin-top: 0; 151 | margin-bottom: 1rem; 152 | } 153 | 154 | ol ol, ul ul, ol ul, ul ol { 155 | margin-bottom: 0; 156 | } 157 | 158 | dt { 159 | font-weight: $dt-font-weight; 160 | } 161 | 162 | // 1. Undo browser default 163 | 164 | dd { 165 | margin-bottom: .5rem; 166 | margin-left: 0; // 1 167 | } 168 | 169 | blockquote { 170 | margin: 0 0 1rem; 171 | } 172 | 173 | // Add the correct font weight in Chrome, Edge, and Safari 174 | 175 | b, strong { 176 | font-weight: $font-weight-bolder; 177 | } 178 | 179 | // Add the correct font size in all browsers 180 | 181 | small { 182 | font-size: $small-font-size; 183 | } 184 | 185 | // Prevent `sub` and `sup` elements from affecting the line height in 186 | // all browsers. 187 | 188 | sub, sup { 189 | position: relative; 190 | font-size: $sub-sup-font-size; 191 | line-height: 0; 192 | vertical-align: baseline; 193 | } 194 | 195 | sub { 196 | bottom: -.25em; 197 | } 198 | sup { 199 | top: -.5em; 200 | } 201 | 202 | // Links 203 | 204 | a { 205 | color: $link-color; 206 | text-decoration: $link-decoration; 207 | 208 | &:hover { 209 | color: $link-hover-color; 210 | text-decoration: $link-hover-decoration; 211 | } 212 | } 213 | 214 | // And undo these styles for placeholder links/named anchors (without href). 215 | // It would be more straightforward to just use a[href] in previous block, but that 216 | // causes specificity issues in many other styles that are too complex to fix. 217 | // See https://github.com/twbs/bootstrap/issues/19402 218 | 219 | a:not([href]) { 220 | &, &:hover { 221 | color: inherit; 222 | text-decoration: none; 223 | } 224 | } 225 | 226 | // Code 227 | 228 | pre, code, kbd, samp { 229 | font-size: 1em; // Correct the odd `em` font sizing in all browsers. 230 | font-family: $font-family-monospace; 231 | } 232 | 233 | // 1. Remove browser default top margin 234 | // 2. Reset browser default of `1em` to use `rem`s 235 | // 3. Don't allow content to break outside 236 | 237 | code { 238 | color: $code-color; 239 | font-size: $code-font-size; 240 | word-wrap: break-word; 241 | 242 | // Streamline the style when inside anchors to avoid broken underline and more 243 | a > & { 244 | color: inherit; 245 | } 246 | } 247 | 248 | pre { 249 | display: block; 250 | margin-top: 0; // 1 251 | margin-bottom: 1rem; // 2 252 | overflow: auto; // 3 253 | color: $pre-color; 254 | font-size: $code-font-size; 255 | 256 | // Account for some code outputs that place code tags in pre tags 257 | code { 258 | color: inherit; 259 | font-size: inherit; 260 | word-break: normal; 261 | } 262 | } 263 | 264 | // Figures 265 | 266 | // Apply a consistent margin strategy (matches our type styles). 267 | 268 | figure { 269 | margin: 0 0 1rem; 270 | } 271 | 272 | // Images and content 273 | 274 | img { 275 | vertical-align: middle; 276 | } 277 | 278 | // 1. Workaround for the SVG overflow bug in IE 11 is still required. 279 | // See https://github.com/twbs/bootstrap/issues/26878 280 | 281 | svg { 282 | overflow: hidden; // 1 283 | vertical-align: middle; 284 | } 285 | 286 | // Tables 287 | 288 | // Prevent double borders 289 | 290 | table { 291 | border-collapse: collapse; 292 | } 293 | 294 | caption { 295 | padding-top: .5rem; 296 | padding-bottom: .5rem; 297 | color: $text-muted; 298 | text-align: left; 299 | caption-side: bottom; 300 | } 301 | 302 | // Matches default `` alignment by inheriting from the ``, or the 303 | // closest parent with a set `text-align`. 304 | 305 | th { 306 | text-align: inherit; 307 | } 308 | 309 | // Forms 310 | 311 | // 1. Allow labels to use `margin` for spacing. 312 | 313 | label { 314 | display: inline-block; // 1 315 | margin-bottom: .5rem; 316 | } 317 | 318 | // Remove the default `border-radius` that macOS Chrome adds. 319 | // 320 | // Details at https://github.com/twbs/bootstrap/issues/24093 321 | 322 | button { 323 | // stylelint-disable-next-line property-blacklist 324 | border-radius: 0; 325 | } 326 | 327 | // 1. Remove the margin in Firefox and Safari 328 | 329 | input, button, select, optgroup, textarea { 330 | margin: 0; 331 | font-size: 14px; 332 | font-family: sans-serif; 333 | line-height: 1.5715; 334 | } 335 | 336 | // Show the overflow in Edge 337 | 338 | button, input { 339 | overflow: visible; 340 | } 341 | 342 | // Remove the inheritance of text transform in Firefox 343 | 344 | button, select { 345 | text-transform: none; 346 | } 347 | 348 | // Remove the inheritance of word-wrap in Safari. 349 | // 350 | // Details at https://github.com/twbs/bootstrap/issues/24990 351 | 352 | select { 353 | word-wrap: normal; 354 | } 355 | 356 | // Remove the dropdown arrow in Chrome from inputs built with datalists. 357 | // 358 | // Source: https://stackoverflow.com/a/54997118 359 | 360 | [list]::-webkit-calendar-picker-indicator { 361 | display: none; 362 | } 363 | 364 | // 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` 365 | // controls in Android 4. 366 | // 2. Correct the inability to style clickable types in iOS and Safari. 367 | // 3. Opinionated: add "hand" cursor to non-disabled button elements. 368 | 369 | button, [type='button'], [type='reset'], [type='submit'] { 370 | -webkit-appearance: button; // 2 371 | 372 | @if $enable-pointer-cursor-for-buttons { 373 | &:not(:disabled) { 374 | cursor: pointer; // 3 375 | } 376 | } 377 | } 378 | 379 | // Remove inner border and padding from Firefox, but don't restore the outline like Normalize. 380 | 381 | ::-moz-focus-inner { 382 | padding: 0; 383 | border-style: none; 384 | } 385 | 386 | // Remove the default appearance of temporal inputs to avoid a Mobile Safari 387 | // bug where setting a custom line-height prevents text from being vertically 388 | // centered within the input. 389 | // See https://bugs.webkit.org/show_bug.cgi?id=139848 390 | // and https://github.com/twbs/bootstrap/issues/11266 391 | 392 | input[type='date'], input[type='time'], input[type='datetime-local'], input[type='month'] { 393 | -webkit-appearance: textfield; 394 | } 395 | 396 | // 1. Remove the default vertical scrollbar in IE. 397 | // 2. Textareas should really only resize vertically so they don't break their (horizontal) containers. 398 | 399 | textarea { 400 | overflow: auto; // 1 401 | resize: vertical; // 2 402 | } 403 | 404 | // 1. Browsers set a default `min-width: min-content;` on fieldsets, 405 | // unlike e.g. `

    `s, which have `min-width: 0;` by default. 406 | // So we reset that to ensure fieldsets behave more like a standard block element. 407 | // See https://github.com/twbs/bootstrap/issues/12359 408 | // and https://html.spec.whatwg.org/multipage/#the-fieldset-and-legend-elements 409 | // 2. Reset the default outline behavior of fieldsets so they don't affect page layout. 410 | 411 | fieldset { 412 | min-width: 0; // 1 413 | margin: 0; // 2 414 | padding: 0; // 2 415 | border: 0; // 2 416 | } 417 | 418 | // 1. By using `float: left`, the legend will behave like a block element 419 | // 2. Correct the color inheritance from `fieldset` elements in IE. 420 | // 3. Correct the text wrapping in Edge and IE. 421 | 422 | legend { 423 | float: left; // 1 424 | width: 100%; 425 | margin-bottom: $legend-margin-bottom; 426 | padding: 0; 427 | color: inherit; // 2 428 | font-weight: $legend-font-weight; 429 | font-size: $legend-font-size; 430 | line-height: inherit; 431 | white-space: normal; // 3 432 | } 433 | 434 | mark { 435 | padding: $mark-padding; 436 | background-color: $mark-bg; 437 | } 438 | 439 | // Add the correct vertical alignment in Chrome, Firefox, and Opera. 440 | 441 | progress { 442 | vertical-align: baseline; 443 | } 444 | 445 | // Fix height of inputs with a type of datetime-local, date, month, week, or time 446 | // See https://github.com/twbs/bootstrap/issues/18842 447 | 448 | ::-webkit-datetime-edit { 449 | overflow: visible; 450 | line-height: 0; 451 | } 452 | 453 | // 1. Correct the outline style in Safari. 454 | // 2. This overrides the extra rounded corners on search inputs in iOS so that our 455 | // `.form-control` class can properly style them. Note that this cannot simply 456 | // be added to `.form-control` as it's not specific enough. For details, see 457 | // https://github.com/twbs/bootstrap/issues/11586. 458 | 459 | [type='search'] { 460 | outline-offset: -2px; // 1 461 | -webkit-appearance: textfield; // 2 462 | } 463 | 464 | // Remove the inner padding in Chrome and Safari on macOS. 465 | 466 | ::-webkit-search-decoration { 467 | -webkit-appearance: none; 468 | } 469 | 470 | // Remove padding around color pickers in webkit browsers 471 | 472 | ::-webkit-color-swatch-wrapper { 473 | padding: 0; 474 | } 475 | 476 | // 1. Change font properties to `inherit` in Safari. 477 | // 2. Correct the inability to style clickable types in iOS and Safari. 478 | 479 | ::-webkit-file-upload-button { 480 | font: inherit; // 1 481 | -webkit-appearance: button; // 2 482 | } 483 | 484 | // Work around a Firefox/IE bug where the transparent `button` background 485 | // results in a loss of the default `button` focus styles. 486 | // 487 | // Credit: https://github.com/suitcss/base/ 488 | 489 | button:focus { 490 | outline: 1px dotted; 491 | outline: 5px auto -webkit-focus-ring-color; 492 | } 493 | 494 | // Correct element displays 495 | 496 | output { 497 | display: inline-block; 498 | } 499 | 500 | // 1. Add the correct display in all browsers 501 | 502 | summary { 503 | display: list-item; // 1 504 | cursor: pointer; 505 | } 506 | 507 | // Add the correct display for template & main in IE 11 508 | 509 | template { 510 | display: none; 511 | } 512 | 513 | main { 514 | display: block; 515 | } 516 | 517 | // Always hide an element with the `hidden` HTML attribute. 518 | 519 | [hidden] { 520 | display: none !important; 521 | } 522 | -------------------------------------------------------------------------------- /components/style/core/index.scss: -------------------------------------------------------------------------------- 1 | @import '../mixins'; 2 | @import 'normalize'; 3 | @import 'motion'; 4 | -------------------------------------------------------------------------------- /components/style/index.scss: -------------------------------------------------------------------------------- 1 | @import './theme'; 2 | 3 | @import './core'; 4 | 5 | // button 6 | @import '../button/style'; 7 | 8 | // alert 9 | @import '../alert/style'; 10 | 11 | // menu 12 | @import '../menu/style'; 13 | 14 | // Tabs 15 | @import '../tabs/style'; 16 | 17 | // Input 18 | @import '../input/style'; 19 | 20 | // AutoComplete 21 | @import '../auto-complete/style'; 22 | 23 | // Tag 24 | @import '../tag/style'; 25 | 26 | // Select 27 | @import '../select/style'; 28 | -------------------------------------------------------------------------------- /components/style/index.tsx: -------------------------------------------------------------------------------- 1 | import './index.scss' 2 | -------------------------------------------------------------------------------- /components/style/mixins/_animation.scss: -------------------------------------------------------------------------------- 1 | @mixin zoom-animation( 2 | $direction: 'top', 3 | $scaleStart: scaleY(0), 4 | $scaleEnd: scaleY(1), 5 | $origin: center top 6 | ) { 7 | .zoom-in-#{$direction}-enter { 8 | transform: $scaleStart; 9 | opacity: 0; 10 | } 11 | .zoom-in-#{$direction}-enter-active { 12 | transform: $scaleEnd; 13 | transform-origin: $origin; 14 | opacity: 1; 15 | transition: transform 300ms cubic-bezier(.23, 1, .32, 1) 100ms, 16 | opacity 300ms cubic-bezier(.23, 1, .32, 1) 100ms; 17 | } 18 | .zoom-in-#{$direction}-exit { 19 | opacity: 1; 20 | } 21 | .zoom-in-#{$direction}-exit-active { 22 | transform: $scaleStart; 23 | transform-origin: $origin; 24 | opacity: 0; 25 | transition: transform 300ms cubic-bezier(.23, 1, .32, 1) 100ms, 26 | opacity 300ms cubic-bezier(.23, 1, .32, 1) 100ms; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /components/style/mixins/_clearfix.scss: -------------------------------------------------------------------------------- 1 | @mixin clearfix() { 2 | &::after { 3 | display: block; 4 | clear: both; 5 | height: 0; 6 | visibility: hidden; 7 | content: ''; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /components/style/mixins/_common.scss: -------------------------------------------------------------------------------- 1 | @mixin border-top-radius($radius) { 2 | border-top-left-radius: $radius; 3 | border-top-right-radius: $radius; 4 | } 5 | 6 | @mixin border-right-radius($radius) { 7 | border-top-right-radius: $radius; 8 | border-bottom-right-radius: $radius; 9 | } 10 | 11 | @mixin border-left-radius($radius) { 12 | border-top-left-radius: $radius; 13 | border-bottom-left-radius: $radius; 14 | } 15 | -------------------------------------------------------------------------------- /components/style/mixins/index.scss: -------------------------------------------------------------------------------- 1 | // Mixins 2 | @import 'clearfix'; 3 | @import 'common'; 4 | @import 'animation'; 5 | -------------------------------------------------------------------------------- /components/style/theme/_default.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * 基础变量引用 vikingship 中的 _variables.scss 并修改 3 | */ 4 | $white: #fff !default; 5 | $gray-100: #f8f9fa !default; 6 | $gray-200: #e9ecef !default; 7 | $gray-300: #dee2e6 !default; 8 | $gray-400: #ced4da !default; 9 | $gray-500: #adb5bd !default; 10 | $gray-600: #6c757d !default; 11 | $gray-700: #495057 !default; 12 | $gray-800: #343a40 !default; 13 | $gray-900: #212529 !default; 14 | $black: #000 !default; 15 | 16 | $blue: #1890ff !default; 17 | $indigo: #6610f2 !default; 18 | $purple: #6f42c1 !default; 19 | $pink: #d63384 !default; 20 | $red: #dc3545 !default; 21 | $orange: #fd7e14 !default; 22 | $yellow: #fadb14 !default; 23 | $green: #b7eb8f !default; 24 | $teal: #20c997 !default; 25 | $cyan: #17a2b8 !default; 26 | 27 | $primary: $blue !default; 28 | $secondary: $gray-600 !default; 29 | $success: $green !default; 30 | $info: $cyan !default; 31 | $warning: $yellow !default; 32 | $danger: $red !default; 33 | $light: $gray-100 !default; 34 | $dark: $gray-800 !default; 35 | 36 | $red-100: #fff1f0 !default; 37 | $blue-100: #e6f7ff !default; 38 | $yellow-100: #feffe6 !default; 39 | $green-100: #f0ffe4 !default; 40 | 41 | // system color 42 | $primary-100: $blue-100 !default; 43 | $success-100: $green-100 !default; 44 | $warning-100: $yellow-100 !default; 45 | $danger-100: $red-100 !default; 46 | 47 | $theme-colors: ( 48 | 'primary': $primary, 49 | 'secondary': $secondary, 50 | 'success': $success, 51 | 'info': $info, 52 | 'warning': $warning, 53 | 'danger': $danger, 54 | 'light': $light, 55 | 'dark': $dark, 56 | ); 57 | 58 | $font-family-sans-serif: 'sans-serif', -apple-system, blinkmacsystemfont, 'Segoe UI', 59 | roboto, 'Helvetica Neue', arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 60 | 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji' !default; 61 | $font-family-monospace: sfmono-regular, menlo, monaco, consolas, 'Liberation Mono', 62 | 'Courier New', monospace !default; 63 | $font-family-base: $font-family-sans-serif !default; 64 | 65 | // 字体大小 66 | $font-size-base: 1rem !default; // Assumes the browser default, typically `16px` 67 | $font-size-lg: $font-size-base * 1.25 !default; 68 | $font-size-md: $font-size-base * 1 !default; 69 | $font-size-sm: $font-size-base * .875 !default; 70 | $font-size-root: null !default; 71 | 72 | // 字体粗细 73 | $font-weight-lighter: lighter !default; 74 | $font-weight-light: 300 !default; 75 | $font-weight-normal: 400 !default; 76 | $font-weight-bold: 700 !default; 77 | $font-weight-bolder: bolder !default; 78 | $font-weight-base: $font-weight-normal !default; 79 | 80 | // 字体颜色 81 | $font-color-normal: rgba(0, 0, 0, .85); 82 | $font-color-light: rgba(0, 0, 0, .65); 83 | $font-color-lighter: rgba(0, 0, 0, .45); 84 | 85 | // 行高 86 | $line-height-base: 1.5 !default; 87 | $line-height-lg: 2 !default; 88 | $line-height-sm: 1.25 !default; 89 | 90 | // 标题大小 91 | $h1-font-size: $font-size-base * 2.5 !default; 92 | $h2-font-size: $font-size-base * 2 !default; 93 | $h3-font-size: $font-size-base * 1.75 !default; 94 | $h4-font-size: $font-size-base * 1.5 !default; 95 | $h5-font-size: $font-size-base * 1.25 !default; 96 | $h6-font-size: $font-size-base !default; 97 | 98 | // 链接 99 | $link-color: $primary !default; 100 | $link-decoration: none !default; 101 | $link-hover-color: darken($link-color, 15%) !default; 102 | $link-hover-decoration: underline !default; 103 | 104 | // body 105 | $body-bg: $white !default; 106 | $body-color: $gray-900 !default; 107 | $body-text-align: null !default; 108 | 109 | // Spacing 110 | $spacer: 1rem !default; 111 | 112 | $headings-margin-bottom: $spacer / 2 !default; 113 | $headings-font-family: null !default; 114 | $headings-font-style: null !default; 115 | $headings-font-weight: 500 !default; 116 | $headings-line-height: 1.2 !default; 117 | $headings-color: null !default; 118 | 119 | // Paragraphs 120 | 121 | $paragraph-margin-bottom: 1rem !default; 122 | 123 | // 字体其他部分 heading list hr 等等 124 | $headings-margin-bottom: $spacer / 2 !default; 125 | $headings-font-family: null !default; 126 | $headings-font-style: null !default; 127 | $headings-font-weight: 500 !default; 128 | $headings-line-height: 1.2 !default; 129 | $headings-color: null !default; 130 | 131 | $display1-size: 6rem !default; 132 | $display2-size: 5.5rem !default; 133 | $display3-size: 4.5rem !default; 134 | $display4-size: 3.5rem !default; 135 | 136 | $display1-weight: 300 !default; 137 | $display2-weight: 300 !default; 138 | $display3-weight: 300 !default; 139 | $display4-weight: 300 !default; 140 | $display-line-height: $headings-line-height !default; 141 | 142 | $lead-font-size: $font-size-base * 1.25 !default; 143 | $lead-font-weight: 300 !default; 144 | 145 | $small-font-size: .875em !default; 146 | 147 | $sub-sup-font-size: .75em !default; 148 | 149 | $text-muted: $gray-600 !default; 150 | 151 | $initialism-font-size: $small-font-size !default; 152 | 153 | $blockquote-small-color: $gray-600 !default; 154 | $blockquote-small-font-size: $small-font-size !default; 155 | $blockquote-font-size: $font-size-base * 1.25 !default; 156 | 157 | $hr-color: inherit !default; 158 | $hr-height: 1px !default; 159 | $hr-opacity: .25 !default; 160 | 161 | $legend-margin-bottom: .5rem !default; 162 | $legend-font-size: 1.5rem !default; 163 | $legend-font-weight: null !default; 164 | 165 | $mark-padding: .2em !default; 166 | 167 | $dt-font-weight: $font-weight-bold !default; 168 | 169 | $nested-kbd-font-weight: $font-weight-bold !default; 170 | 171 | $list-inline-padding: .5rem !default; 172 | 173 | $mark-bg: #fcf8e3 !default; 174 | 175 | $hr-margin-y: $spacer !default; 176 | 177 | // Code 178 | 179 | $code-font-size: $small-font-size !default; 180 | $code-color: $pink !default; 181 | $pre-color: null !default; 182 | 183 | // options 可配置选项 184 | $enable-pointer-cursor-for-buttons: true !default; 185 | 186 | // 边框 和 border radius 187 | 188 | $border-width: 1px !default; 189 | $border-color: $gray-300 !default; 190 | 191 | $border-radius: .25rem !default; 192 | $border-radius-lg: .3rem !default; 193 | $border-radius-md: .25rem !default; 194 | $border-radius-sm: .2rem !default; 195 | 196 | // 不同类型的 box shadow 197 | $box-shadow-sm: 0 .125rem .25rem rgba($black, .075) !default; 198 | $box-shadow: 0 .5rem 1rem rgba($black, .15) !default; 199 | $box-shadow-lg: 0 1rem 3rem rgba($black, .175) !default; 200 | $box-shadow-inset: inset 0 1px 2px rgba($black, .075) !default; 201 | 202 | // 按钮 203 | // 按钮基本属性 204 | $btn-font-weight: 400; 205 | $btn-padding-y: .375rem !default; 206 | $btn-padding-x: .75rem !default; 207 | $btn-font-family: $font-family-base !default; 208 | $btn-font-size: $font-size-base !default; 209 | $btn-line-height: $line-height-base !default; 210 | 211 | // 不同大小按钮的 padding 和 font size 212 | $btn-padding-y-sm: .25rem !default; 213 | $btn-padding-x-sm: .5rem !default; 214 | $btn-font-size-sm: $font-size-sm !default; 215 | 216 | $btn-padding-y-md: .4rem !default; 217 | $btn-padding-x-md: .75rem !default; 218 | $btn-font-size-md: $font-size-md !default; 219 | 220 | $btn-padding-y-lg: .5rem !default; 221 | $btn-padding-x-lg: 1rem !default; 222 | $btn-font-size-lg: $font-size-lg !default; 223 | 224 | // 按钮边框 225 | $btn-border-width: $border-width !default; 226 | 227 | // 按钮其他 228 | $btn-box-shadow: inset 0 1px 0 rgba($white, .15), 0 1px 1px rgba($black, .075) !default; 229 | $btn-disabled-opacity: .65 !default; 230 | 231 | // Alert 232 | $alert-padding-y: .75rem !default; 233 | $alert-padding-x: 1.25rem !default; 234 | $alert-close-padding-y: .5rem !default; 235 | $alert-close-padding-x: 1rem !default; 236 | $alert-margin-bottom: 1rem !default; 237 | $alert-border-radius: $border-radius !default; 238 | $alert-title-font-weight: $font-weight-bold !default; 239 | $alert-border-width: $border-width !default; 240 | $alert-description-font-size: $font-size-sm !default; 241 | $alert-description-top-margin: .3rem !default; 242 | 243 | // 链接按钮 244 | $btn-link-color: $link-color !default; 245 | $btn-link-hover-color: $link-hover-color !default; 246 | $btn-link-disabled-color: $gray-600 !default; 247 | 248 | // 按钮 radius 249 | $btn-border-radius: $border-radius !default; 250 | $btn-border-radius-lg: $border-radius-lg !default; 251 | $btn-border-radius-md: $border-radius-md !default; 252 | $btn-border-radius-sm: $border-radius-sm !default; 253 | 254 | $btn-transition: color .15s ease-in-out, background-color .15s ease-in-out, 255 | border-color .15s ease-in-out, box-shadow .15s ease-in-out !default; 256 | 257 | // menu 258 | $menu-border-width: $border-width !default; 259 | $menu-border-color: $border-color !default; 260 | $menu-box-shadow: inset 0 1px 0 rgba($white, .15), 0 1px 1px rgba($black, .075) !default; 261 | $menu-transition: color .15s ease-in-out, border-color .15s ease-in-out !default; 262 | 263 | // menu-item 264 | $menu-item-padding-y: .5rem !default; 265 | $menu-item-padding-x: 1rem !default; 266 | $menu-item-active-color: $primary !default; 267 | $menu-item-active-border-width: 2px !default; 268 | $menu-item-disabled-color: $gray-600 !default; 269 | 270 | // sub-menu 271 | // submenu 272 | $submenu-box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .12), 0 0 6px 0 rgba(0, 0, 0, .04); 273 | 274 | // input 275 | $input-padding-y: $btn-padding-y !default; 276 | $input-padding-x: $btn-padding-x !default; 277 | $input-font-family: $btn-font-family !default; 278 | $input-font-size: $btn-font-size !default; 279 | $input-font-weight: $font-weight-base !default; 280 | $input-line-height: $btn-line-height !default; 281 | 282 | $input-padding-y-sm: $btn-padding-y-sm !default; 283 | $input-padding-x-sm: $btn-padding-x-sm !default; 284 | $input-font-size-sm: $btn-font-size-sm !default; 285 | 286 | $input-padding-y-lg: $btn-padding-y-lg !default; 287 | $input-padding-x-lg: $btn-padding-x-lg !default; 288 | $input-font-size-lg: $btn-font-size-lg !default; 289 | 290 | $input-bg: $white !default; 291 | $input-disabled-bg: $gray-200 !default; 292 | $input-disabled-border-color: null !default; 293 | 294 | $input-color: $gray-700 !default; 295 | $input-border-color: $gray-400 !default; 296 | $input-border-width: $border-width !default; 297 | $input-box-shadow: $box-shadow-inset !default; 298 | 299 | $input-border-radius: $border-radius !default; 300 | $input-border-radius-lg: $border-radius-lg !default; 301 | $input-border-radius-sm: $border-radius-sm !default; 302 | 303 | $input-focus-bg: $input-bg !default; 304 | $input-focus-border-color: lighten($primary, 25%) !default; 305 | $input-focus-width: .2rem !default; 306 | $input-focus-color: $input-color !default; 307 | $input-focus-shadow-color: rgba($primary, .25) !default; 308 | $input-focus-box-shadow: 0 0 0 $input-focus-width $input-focus-shadow-color !default; 309 | 310 | $input-placeholder-color: $gray-600 !default; 311 | $input-plaintext-color: $body-color !default; 312 | 313 | $input-height-border: $input-border-width * 2 !default; 314 | 315 | $input-transition: border-color .3s ease-in-out, box-shadow .3s ease-in-out !default; 316 | 317 | $input-group-addon-color: $input-color !default; 318 | $input-group-addon-bg: $gray-200 !default; 319 | $input-group-addon-border-color: $input-border-color !default; 320 | 321 | // Progress bars 322 | 323 | $progress-font-size: $font-size-base * .75 !default; 324 | $progress-bg: $gray-200 !default; 325 | $progress-border-radius: $border-radius !default; 326 | $progress-bar-color: $white !default; 327 | $progress-bar-transition: width .6s ease !default; 328 | 329 | // navs 330 | 331 | $nav-link-padding-y: .5rem !default; 332 | $nav-link-padding-x: 1rem !default; 333 | $nav-link-disabled-color: $gray-600 !default; 334 | 335 | $nav-tabs-border-color: $gray-300 !default; 336 | $nav-tabs-border-width: $border-width !default; 337 | $nav-tabs-border-radius: $border-radius !default; 338 | $nav-tabs-link-hover-border-color: $gray-200 $gray-200 $nav-tabs-border-color !default; 339 | $nav-tabs-link-hover-color: $primary !default; 340 | $nav-tabs-link-active-color: $primary !default; 341 | $nav-tabs-link-active-bg: $body-bg !default; 342 | $nav-tabs-link-active-border-color: $gray-300 $gray-300 $nav-tabs-link-active-bg !default; 343 | $nav-tabs-content-margin: 1rem !default; 344 | 345 | // Tag 346 | 347 | $tag-padding-y: .04rem !default; 348 | $tag-padding-x: .45rem !default; 349 | $tag-font-size: $font-size-base !default; 350 | 351 | $tag-padding-y-sm: 1px !default; 352 | $tag-padding-x-sm: 3px !default; 353 | $tag-font-size-sm: $font-size-sm !default; 354 | 355 | $tag-padding-y-lg: .12rem !default; 356 | $tag-padding-x-lg: .6rem !default; 357 | $tag-font-size-lg: $font-size-base !default; 358 | -------------------------------------------------------------------------------- /components/style/theme/index.scss: -------------------------------------------------------------------------------- 1 | @import 'default'; 2 | -------------------------------------------------------------------------------- /components/tabs/TabPane.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | 3 | export interface Props { 4 | tab: React.ReactNode 5 | disabled?: boolean 6 | } 7 | 8 | const TabPane: FC = ({ children }) => ( 9 |
    {children}
    10 | ) 11 | 12 | export default TabPane 13 | -------------------------------------------------------------------------------- /components/tabs/Tabs.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, FunctionComponentElement, MouseEvent, useState } from 'react' 2 | import classNames from 'classnames' 3 | import TabPane, { Props as TabPaneProps } from './TabPane' 4 | 5 | export type TabsType = 'line' | 'card' 6 | 7 | export interface TabsProps { 8 | className?: string 9 | defaultActiveKey?: number 10 | type?: TabsType 11 | onTabClick?: (selectedKey: number) => void 12 | } 13 | 14 | const RootTab: FC = ({ 15 | className, 16 | defaultActiveKey, 17 | type, 18 | onTabClick, 19 | children, 20 | }) => { 21 | const [activeKey, setActiveKey] = useState(defaultActiveKey) 22 | const navClass = classNames('mk-tabs-nav', { 23 | 'nav-line': type === 'line', 24 | 'nav-card': type === 'card', 25 | }) 26 | 27 | const handleClick = (e: MouseEvent, index: number, disabled: boolean | undefined) => { 28 | if (!disabled) { 29 | setActiveKey(index) 30 | if (onTabClick) { 31 | onTabClick(index) 32 | } 33 | } 34 | } 35 | 36 | const renderNavLinks = () => 37 | React.Children.map(children, (child, index) => { 38 | const childElement = child as FunctionComponentElement 39 | const { tab, disabled } = childElement.props 40 | const classes = classNames('mk-tabs-nav-item', { 41 | 'is-active': activeKey === index, 42 | disabled, 43 | }) 44 | 45 | return ( 46 |
  • { 51 | handleClick(e, index, disabled) 52 | }} 53 | > 54 | {tab} 55 |
  • 56 | ) 57 | }) 58 | 59 | const renderContent = () => 60 | React.Children.map(children, (child, index) => { 61 | if (index === activeKey) { 62 | return child 63 | } 64 | }) 65 | 66 | return ( 67 |
    68 |
      {renderNavLinks()}
    69 |
    {renderContent()}
    70 |
    71 | ) 72 | } 73 | 74 | RootTab.defaultProps = { 75 | defaultActiveKey: 0, 76 | type: 'line', 77 | } 78 | 79 | export type ITabComponent = FC & { 80 | TabPane: FC 81 | } 82 | 83 | const Tabs = RootTab as ITabComponent 84 | Tabs.TabPane = TabPane 85 | 86 | export default Tabs 87 | -------------------------------------------------------------------------------- /components/tabs/__tests__/__snapshots__/index.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Tabs component should render the correct default Tabs 1`] = ` 4 | 9 |
    12 |
      15 |
    • 20 | Tab 1 21 |
    • 22 |
    • 27 | Tab 2 28 |
    • 29 |
    • 34 | Tab 3 35 |
    • 36 |
    37 |
    40 | 44 |
    47 | Content of Tab Pane 2 48 |
    49 |
    50 |
    51 |
    52 |
    53 | `; 54 | -------------------------------------------------------------------------------- /components/tabs/__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { mount } from 'enzyme' 3 | import Tabs, { TabsProps } from '../Tabs' 4 | 5 | const { TabPane } = Tabs 6 | 7 | const testProps: TabsProps = { 8 | defaultActiveKey: 1, 9 | onTabClick: jest.fn(), 10 | } 11 | 12 | const generateTabs = (props: TabsProps = {}) => ( 13 | 14 | 15 | Content of Tab Pane 1 16 | 17 | Content of Tab Pane 2 18 | Content of Tab Pane 3 19 | 20 | ) 21 | 22 | describe('Tabs component', () => { 23 | it('should render the correct default Tabs', () => { 24 | const wrapper = mount(generateTabs()) 25 | const activeElement = wrapper.find('.mk-tabs-nav-item').at(1) 26 | expect(wrapper.find('.mk-tabs-nav').hasClass('nav-line')) 27 | expect(activeElement.hasClass('is-active')).toBeTruthy() 28 | expect(activeElement.text()).toEqual('Tab 2') 29 | expect(wrapper.find('.mk-tab-panel').text()).toEqual('Content of Tab Pane 2') 30 | expect(wrapper).toMatchSnapshot() 31 | }) 32 | 33 | it('click tabItem should switch to content', () => { 34 | const wrapper = mount(generateTabs()) 35 | wrapper 36 | .find('.mk-tabs-nav-item') 37 | .at(2) 38 | .simulate('click') 39 | 40 | expect(testProps.onTabClick).toHaveBeenCalled() 41 | expect( 42 | wrapper 43 | .find('.mk-tabs-nav-item') 44 | .at(2) 45 | .hasClass('is-active'), 46 | ).toBeTruthy() 47 | expect( 48 | wrapper 49 | .find('.mk-tabs-nav-item') 50 | .at(1) 51 | .hasClass('is-active'), 52 | ).toBeFalsy() 53 | }) 54 | 55 | it('click disabled tabItem should not works', () => { 56 | const wrapper = mount(generateTabs()) 57 | wrapper 58 | .find('.mk-tabs-nav-item') 59 | .at(0) 60 | .simulate('click') 61 | 62 | expect( 63 | wrapper 64 | .find('.mk-tabs-nav-item') 65 | .at(0) 66 | .hasClass('is-active'), 67 | ).toBeFalsy() 68 | expect( 69 | wrapper 70 | .find('.mk-tabs-nav-item') 71 | .at(1) 72 | .hasClass('is-active'), 73 | ).toBeTruthy() 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /components/tabs/demos/base.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Tabs } from 'monki-ui' 3 | import 'monki-ui/dist/index.css' 4 | 5 | const { TabPane } = Tabs 6 | 7 | export default () => { 8 | const handleClick = (key: number) => { 9 | console.log(key) 10 | } 11 | 12 | return ( 13 | 14 | Content of Tab Pane 1 15 | Content of Tab Pane 2 16 | Content of Tab Pane 3 17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /components/tabs/demos/disabled.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Tabs } from 'monki-ui' 3 | import 'monki-ui/dist/index.css' 4 | 5 | const { TabPane } = Tabs 6 | 7 | export default () => ( 8 | 9 | Content of Tab Pane 1 10 | Content of Tab Pane 2 11 | 12 | Content of Tab Pane 3 13 | 14 | 15 | ) 16 | -------------------------------------------------------------------------------- /components/tabs/demos/type.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Tabs } from 'monki-ui' 3 | import 'monki-ui/dist/index.css' 4 | 5 | const { TabPane } = Tabs 6 | 7 | export default () => ( 8 | 9 | Content of Tab Pane 1 10 | Content of Tab Pane 2 11 | Content of Tab Pane 3 12 | 13 | ) 14 | -------------------------------------------------------------------------------- /components/tabs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Tabs 标签页 3 | group: 4 | title: Tabs 标签页 5 | nav: 6 | title: '组件' 7 | path: /components 8 | --- 9 | 10 | ### 基本 11 | 12 | 默认选中第一项。 13 | 14 | 15 | 16 | ### 禁用 17 | 18 | 禁用某一项。 19 | 20 | 21 | 22 | ### 卡片式页签 23 | 24 | 另一种样式的页签,不提供对应的垂直样式。 25 | 26 | 27 | 28 | ### API 29 | 30 | #### Tabs 31 | 32 | | 参数 | 说明 | 类型 | 默认值 | 33 | | ---------------- | -------------------------------------------- | --------------------- | ------ | 34 | | defaultActiveKey | 初始化选中面板的 key,如果没有设置 activeKey | number | 0 | 35 | | type | 页签的基本样式,可选 line、card 类型 | string | `line` | 36 | | onTabClick | tab 被点击的回调 | function(key: number) | - | 37 | 38 | #### Tabs.TabPane 39 | 40 | | 参数 | 说明 | 类型 | 默认值 | 41 | | -------- | ---------------- | --------- | ------ | 42 | | disabled | 是否禁用 | boolean | false | 43 | | tab | 选项卡头显示文字 | ReactNode | - | 44 | -------------------------------------------------------------------------------- /components/tabs/index.tsx: -------------------------------------------------------------------------------- 1 | import Tabs from './Tabs' 2 | 3 | export default Tabs 4 | -------------------------------------------------------------------------------- /components/tabs/style/index.scss: -------------------------------------------------------------------------------- 1 | .mk-tabs-nav { 2 | display: flex; 3 | flex-wrap: wrap; 4 | margin-bottom: 0; 5 | padding-left: 0; 6 | list-style: none; 7 | border-bottom: $nav-tabs-border-width solid $nav-tabs-border-color; 8 | } 9 | .mk-tabs-nav-item { 10 | display: block; 11 | padding: $nav-link-padding-y $nav-link-padding-x; 12 | cursor: pointer; 13 | &:hover, &:focus { 14 | color: $nav-tabs-link-hover-color; 15 | } 16 | &.disabled { 17 | color: $nav-link-disabled-color; 18 | background-color: transparent; 19 | border-color: transparent; 20 | cursor: default; 21 | pointer-events: none; 22 | } 23 | &.is-active { 24 | color: $nav-tabs-link-active-color; 25 | } 26 | } 27 | .nav-line { 28 | .mk-tabs-nav-item { 29 | &.is-active { 30 | border-bottom: $nav-tabs-border-width * 2 solid $nav-tabs-link-active-color; 31 | } 32 | } 33 | } 34 | 35 | .nav-card { 36 | .mk-tabs-nav-item { 37 | margin-bottom: -$nav-tabs-border-width; 38 | border: $nav-tabs-border-width solid transparent; 39 | &.is-active { 40 | @include border-top-radius($nav-tabs-border-radius); 41 | background-color: $nav-tabs-link-active-bg; 42 | border-color: $nav-tabs-link-active-border-color; 43 | } 44 | } 45 | } 46 | 47 | .mk-tabs-content { 48 | margin-top: $nav-tabs-content-margin; 49 | } 50 | -------------------------------------------------------------------------------- /components/tabs/style/index.tsx: -------------------------------------------------------------------------------- 1 | import './index.scss' 2 | -------------------------------------------------------------------------------- /components/tag/__tests__/__snapshots__/index.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Tag render normal Tag correctly 1`] = ` 4 | 9 | 17 | 24 | 39 |
    42 | 45 | Tag1 46 | 47 |
    48 |
    49 |
    50 |
    51 |
    52 | `; 53 | -------------------------------------------------------------------------------- /components/tag/__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { mount } from 'enzyme' 3 | import Tag from '../tag' 4 | 5 | describe('Tag', () => { 6 | it('render normal Tag correctly', () => { 7 | const wrapper = mount(normal) 8 | 9 | expect(wrapper).toMatchSnapshot() 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /components/tag/demos/base.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Tag } from 'monki-ui' 3 | import 'monki-ui/dist/index.css' 4 | 5 | export default () => { 6 | const handleClick = () => { 7 | console.log('close') 8 | } 9 | 10 | return ( 11 | <> 12 | 13 | 14 | 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /components/tag/demos/color.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Tag } from 'monki-ui' 3 | import 'monki-ui/dist/index.css' 4 | 5 | export default () => ( 6 | <> 7 | 8 | 9 | 10 | 11 | ) 12 | -------------------------------------------------------------------------------- /components/tag/demos/size.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Tag } from 'monki-ui' 3 | import 'monki-ui/dist/index.css' 4 | 5 | export default () => ( 6 | <> 7 | 8 | 9 | 10 | 11 | ) 12 | -------------------------------------------------------------------------------- /components/tag/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Tag 标签 3 | group: 4 | title: Tag 标签 5 | nav: 6 | title: '组件' 7 | path: /components 8 | --- 9 | 10 | 进行标记和分类的小标签。 11 | 12 | ## 何时使用 13 | 14 | - 用于标记事物的属性和维度。 15 | - 进行分类。 16 | 17 | ## 基本 18 | 19 | 20 | 21 | ## 颜色 22 | 23 | 24 | 25 | ## 尺寸 26 | 27 | 28 | 29 | ## API 30 | 31 | | 参数 | 说明 | 类型 | 默认值 | 32 | | -------- | ---------------- | ---------- | ------ | 33 | | closable | 标签是否可以关闭 | boolean | false | 34 | | color | 标签色 | string | - | 35 | | text | Tag 的文本 | string | - | 36 | | size | Tag 的尺寸 | string | - | 37 | | onClose | 关闭时的回调 | () => void | - | 38 | -------------------------------------------------------------------------------- /components/tag/index.tsx: -------------------------------------------------------------------------------- 1 | import Tag from './tag' 2 | 3 | export default Tag 4 | -------------------------------------------------------------------------------- /components/tag/style/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin tag-size($padding-y, $padding-x, $font-size) { 2 | padding: $padding-y $padding-x; 3 | font-size: $font-size; 4 | } 5 | -------------------------------------------------------------------------------- /components/tag/style/index.scss: -------------------------------------------------------------------------------- 1 | @import 'mixins'; 2 | 3 | .mk-tag { 4 | display: inline-block; 5 | box-sizing: border-box; 6 | vertical-align: middle; 7 | border-radius: 3px; 8 | @include tag-size($tag-padding-y, $tag-padding-x, $tag-font-size); 9 | 10 | .tag-text { 11 | vertical-align: middle; 12 | } 13 | 14 | .tag-close-icon { 15 | margin-left: 4px; 16 | vertical-align: middle; 17 | } 18 | } 19 | 20 | .tag-lg { 21 | @include tag-size($tag-padding-y-lg, $tag-padding-x-lg, $tag-font-size-lg); 22 | } 23 | .tag-sm { 24 | @include tag-size($tag-padding-y-sm, $tag-padding-x-sm, $tag-font-size-sm); 25 | } 26 | 27 | @each $key, $val in $theme-colors { 28 | .tag-#{$key} { 29 | color: $val; 30 | background-color: rgba($val, .1); 31 | border: 1px solid rgba($val, .2); 32 | 33 | .mk-icon { 34 | &:hover { 35 | color: darken($val, 10%); 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /components/tag/style/index.tsx: -------------------------------------------------------------------------------- 1 | import './index.scss' 2 | -------------------------------------------------------------------------------- /components/tag/tag.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState } from 'react' 2 | import classNames from 'classnames' 3 | import CloseOutlined from '@ant-design/icons/CloseOutlined' 4 | import Transition from '../transition' 5 | 6 | export type ColorProps = 'primary' | 'success' | 'warning' | 'danger' 7 | 8 | export interface TagProps { 9 | /** 自定义类名 */ 10 | className?: string 11 | /** Tag的主题色 */ 12 | color?: ColorProps 13 | /** Tag的尺寸 */ 14 | size?: 'lg' | 'sm' 15 | /** Tag的文本 */ 16 | text: string 17 | /** 关闭Tag */ 18 | onClose?: () => void 19 | /** 是否显示关闭图标 */ 20 | closable?: boolean 21 | } 22 | 23 | export const Tag: FC = props => { 24 | const [hide, setHide] = useState(false) 25 | const { className, color, size, text, onClose, closable, ...restProps } = props 26 | const classes = classNames('mk-tag', className, { 27 | [`tag-${color}`]: color, 28 | [`tag-${size}`]: size, 29 | }) 30 | const handleClose = (e: React.MouseEvent) => { 31 | if (onClose) { 32 | onClose() 33 | } 34 | setHide(true) 35 | } 36 | return ( 37 | 38 |
    39 | {text} 40 | {closable && } 41 |
    42 |
    43 | ) 44 | } 45 | 46 | Tag.defaultProps = { 47 | color: 'primary', 48 | closable: false, 49 | } 50 | 51 | export default Tag 52 | -------------------------------------------------------------------------------- /components/transition/index.tsx: -------------------------------------------------------------------------------- 1 | import Transition from './transition' 2 | 3 | export default Transition 4 | -------------------------------------------------------------------------------- /components/transition/transition.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | import { CSSTransition } from 'react-transition-group' 3 | import { CSSTransitionProps } from 'react-transition-group/CSSTransition' 4 | 5 | type AnimationName = 6 | | 'zoom-in-top' 7 | | 'zoom-in-left' 8 | | 'zoom-in-bottom' 9 | | 'zoom-in-right' 10 | 11 | type TransitionProps = CSSTransitionProps & { 12 | animation?: AnimationName 13 | wrapper?: boolean 14 | classNames?: string 15 | } 16 | 17 | const Transition: FC = ({ 18 | classNames, 19 | animation, 20 | wrapper, // 方法被包裹元素设置了transition属性冲突,在外层加div不覆盖 21 | children, 22 | ...restProps 23 | }) => ( 24 | 25 | {wrapper ?
    children
    : children} 26 |
    27 | ) 28 | 29 | Transition.defaultProps = { 30 | unmountOnExit: true, 31 | appear: true, 32 | wrapper: false, 33 | } 34 | 35 | export default Transition 36 | -------------------------------------------------------------------------------- /docs/guide/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: '关于 Monki UI' 3 | nav: 4 | title: 开发指南 5 | --- 6 | 7 | # Monki UI 8 | 9 | `monki-ui`,是一款基于 Dumi,由 React + TypeScript 开发的个人组件库 🎉。该开源项目是我为进阶 React,同时探索组件库设计开发思路所做的,故不可用于生产环境。由于个人设计能力有限,故 UI 设计方面会大量参考[Ant Design 组件库](https://ant.design/index-cn)。如果你也想学习组件开发,欢迎加入或提供意见,该项目会长期更新,你的 star ⭐,是对我最大的鼓励。 10 | 11 |
    12 | 13 |
    14 | 15 | ## ✨ 特性 16 | 17 | - 🌈 提炼组件库设计良好的视觉风格 18 | - 📦 渐进式探索高质量的前端代码的实现 19 | - 🛡 使用 TypeScript 开发,提升开发体验 20 | - ✅ 使用单元测试,为组件稳定性保驾护航 21 | - 📖 提供开发过程的文档思路,助力你学习组件开发 22 | - 🔖 该项目会长期维护,并不断探索最佳实践 23 | 24 | ## 作者 25 | 26 | 技术社区名字:JackySummer 27 | 28 | - [掘金](https://juejin.im/user/1257497033714477/activities) 29 | - [Github](https://github.com/Jacky-Summer) 30 | - [个人博客](https://jacky-summer.github.io/) 31 | - [SegmentFault](https://segmentfault.com/u/jackysummer) 32 | - [公众号-前端精神时光屋](https://raw.githubusercontent.com/jacky-summer/personal-blog/master/%E5%9B%BE%E7%89%87%E6%96%87%E4%BB%B6/fe-house-qrcode.jpg) 33 | -------------------------------------------------------------------------------- /docs/guide/contribute.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: '贡献指南' 3 | nav: 4 | title: 开发指南 5 | --- 6 | 7 | 10 | 11 | # 参与贡献 12 | 13 | 如果你也想参与开发,请阅读如下说明: 14 | 15 | ## 分支管理 16 | 17 | 该项目长期维护两个分支 `master` 和 `development`。如果你要提一个 pull request,那么请基于 `development` 分支来做,我会不定时合并 development 到 master,并发布版本与同步到 npm。 18 | 19 | ## Bugs 20 | 21 | 该项目使用 [GitHub Issues](https://github.com/Jacky-Summer/monki-ui/issues) 来做 bug 追踪。 22 | 23 | ## 第一次贡献 24 | 25 | 如果你还不清楚怎么在 GitHub 上提 Pull Request ,可以阅读下面这篇文章来学习: 26 | 27 | [如何优雅地在 GitHub 上贡献代码](https://segmentfault.com/a/1190000000736629) 28 | 29 | ## Pull Request 30 | 31 | ### 🤔 这个变动的性质是? 32 | 33 | - [ ] 新特性提交 34 | - [ ] 日常 bug 修复 35 | - [ ] 站点、文档改进 36 | - [ ] 演示代码改进 37 | - [ ] 组件样式/交互改进 38 | - [ ] TypeScript 定义更新 39 | - [ ] 包体积优化 40 | - [ ] 性能优化 41 | - [ ] 功能增强 42 | - [ ] 重构 43 | - [ ] 代码风格优化 44 | - [ ] 测试用例 45 | - [ ] 其他改动(是关于什么的改动?) 46 | 47 | ### 任何 PR 请基于 `development` 分支来做,`code review` 通过后会合并。 48 | 49 | ### PR 标题与 commit 信息开头请遵循如下规范: 50 | 51 | - ✨ feat:新功能 52 | - 🔧 chore:构建过程或辅助工具的变动 53 | - 📝 docs:仅文档新增/改动 54 | - 🐛 fix:修复 bug 55 | - 🚀 perf:性能优化 56 | - 🔨 refactor:某个已有功能重构 57 | - ⏪ revert:代码回滚 58 | - 🎨 style:代码格式改变 59 | - ✅ test:添加缺失的测试或更正现有的测试 60 | - 📦 build:改变了 build 工具 61 | - 👷 ci:持续集成 62 | 63 | 🎉 release(only used in PR title):发布版本提交 64 | 65 | 更多详细小贴士请阅读[pull request 模板](https://github.com/Jacky-Summer/monki-ui/blob/development/.github/PULL_REQUEST_TEMPLATE/pr_cn.md) 66 | 67 | **在你发送 Pull Request 之前**,请确认你是按照下面的步骤来做的: 68 | 69 | 1. 基于`development`分支做修改。 70 | 2. 在项目根目录下运行了 `yarn install`。 71 | 3. 如果你修复了一个 bug 或者新增了一个功能,请确保写了相应的测试,这很重要。 72 | 4. 确认所有的测试都是通过的 `yarn test`。 小贴士:开发过程中可以用 `yarn test --watch TestName` 来运行指定的测试。 73 | 5. 运行 `yarn test -u` 来更新 [jest snapshot](http://facebook.github.io/jest/docs/en/snapshot-testing.html#snapshot-testing-with-jest) 并且把这些更新也提交上来(如果有的话)。 74 | 6. 确保你的代码通过了 lint 检查 `yarn lint`. 小贴士: Lint 会在你 `git commit` 的时候自动运行。 75 | 76 | ## 开发流程 77 | 78 | 在你 clone 了 monki-ui 的代码并且使用 `yarn install` 安装完依赖后,你还可以运行下面几个常用的命令: 79 | 80 | 1. `yarn start` 在本地运行 Monki UI 的网站。 81 | 2. `yarn lint` 检查代码风格。 82 | 3. `yarn test` 运行测试。 83 | 4. `yarn build:lib` 编译 TypeScript 与 CSS 代码到 dist 目录。 84 | 5. `yarn build:docs` 构建 monki-ui 的组件库文档版本到 docs-dist 目录。 85 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Monki UI - 基于 React 和 TypeScript 实现的 UI 组件库 3 | hero: 4 | title: Monki UI 5 | desc: 🏆 个人开源项目,使用 React + TypeScript 打造自己的 UI 组件库 6 | actions: 7 | - text: 开始使用 8 | link: /guide 9 | features: 10 | - icon: https://gw.alipayobjects.com/zos/bmw-prod/881dc458-f20b-407b-947a-95104b5ec82b/k79dm8ih_w144_h144.png 11 | title: 开箱即用 12 | desc: 使用方式简单,项目长期维护,并在开发中不断探索最佳实践 13 | - icon: https://gw.alipayobjects.com/zos/bmw-prod/d60657df-0822-4631-9d7c-e7a869c2f21c/k79dmz3q_w126_h126.png 14 | title: React 进阶必备 15 | desc: 使用 React Hook 打造自己的组件库,React 进阶学习的必经之路 16 | - icon: https://gw.alipayobjects.com/os/q/cms/images/k9zij2bh/67f75d56-0d62-47d6-a8a5-dbd0cb79a401_w96_h96.png 17 | title: TypeScript 18 | desc: 代码全部使用 TypeScript 开发,提供完整的组件和 API 类型定义 19 | footer: Monki UI MIT Licensed | Copyright © 2020
    Powered by [dumi](https://d.umijs.org) 20 | --- 21 | 22 | ## 我的技术社区 23 | 24 | - [掘金](https://juejin.im/user/1257497033714477/activities) 25 | - [Github](https://github.com/Jacky-Summer) 26 | - [个人博客](https://jacky-summer.github.io/) 27 | - [SegmentFault](https://segmentfault.com/u/jackysummer) 28 | - [公众号-前端精神时光屋](https://raw.githubusercontent.com/jacky-summer/personal-blog/master/%E5%9B%BE%E7%89%87%E6%96%87%E4%BB%B6/fe-house-qrcode.jpg) 29 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: [''], 3 | setupFiles: ['/scripts/jest/setup.ts'], 4 | transform: { 5 | '\\.(ts|tsx)$': 'ts-jest', 6 | }, 7 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$', 8 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'], 9 | snapshotSerializers: ['enzyme-to-json/serializer'], 10 | moduleNameMapper: { 11 | '\\.(css|scss)$': 'identity-obj-proxy', 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monki-ui", 3 | "version": "1.9.1", 4 | "description": "React components library", 5 | "title": "Monki UI", 6 | "author": "JackySummer", 7 | "main": "dist/index.js", 8 | "types": "dist/index.d.ts", 9 | "keywords": [ 10 | "monki-ui", 11 | "component", 12 | "design", 13 | "frontend", 14 | "react", 15 | "react-component", 16 | "ui" 17 | ], 18 | "homepage": "https://jacky-summer.github.io/monki-ui", 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/Jacky-Summer/monki-ui" 22 | }, 23 | "license": "MIT", 24 | "scripts": { 25 | "start": "dumi dev", 26 | "test": "jest", 27 | "clean": "rimraf ./dist", 28 | "build": "yarn build:lib && yarn build:docs", 29 | "build:docs": "dumi build", 30 | "build:lib": "yarn clean && yarn build-ts && yarn build-css", 31 | "build-ts": "tsc -p tsconfig.build.json", 32 | "build-css": "dart-sass ./components/style/index.scss ./dist/index.css", 33 | "prettier": "prettier --write \"**/*.{js,jsx,tsx,ts,scss,md,json}\"", 34 | "lint": "yarn lint:eslint && yarn lint:stylelint", 35 | "lint:eslint": "eslint -c .eslintrc.js --ext .ts,.tsx,.js ./components --fix", 36 | "lint:stylelint": "stylelint **/*.{html,css,scss} --fix", 37 | "typecheck": "tsc --noEmit -p ./tsconfig.json", 38 | "commit": "git-cz", 39 | "bump-version": "standard-version --message 'chore(release): %s\n\n[skip publish]' --no-verify", 40 | "bump-version:prerelease": "yarn bump-version --skip.changelog=true --skip.commit=true --skip.tag=true --prerelease ${TRAVIS_BUILD_NUMBER}" 41 | }, 42 | "husky": { 43 | "hooks": { 44 | "pre-commit": "lint-staged", 45 | "commit-msg": "commitlint --config .commitlintrc.js -E HUSKY_GIT_PARAMS" 46 | } 47 | }, 48 | "lint-staged": { 49 | "*.{js,jsx,md,json}": [ 50 | "prettier --write" 51 | ], 52 | "*.ts?(x)": [ 53 | "prettier --parser=typescript --write" 54 | ], 55 | "*.{scss,css}": [ 56 | "stylelint --fix" 57 | ] 58 | }, 59 | "config": { 60 | "commitizen": { 61 | "path": "./node_modules/cz-conventional-changelog" 62 | } 63 | }, 64 | "files": [ 65 | "dist" 66 | ], 67 | "devDependencies": { 68 | "@commitlint/cli": "^11.0.0", 69 | "@commitlint/config-conventional": "^11.0.0", 70 | "@types/cheerio": "^0.22.22", 71 | "@types/classnames": "^2.2.11", 72 | "@types/enzyme": "^3.10.8", 73 | "@types/jest": "^26.0.15", 74 | "@types/react-transition-group": "^4.4.0", 75 | "@typescript-eslint/eslint-plugin": "^4.4.0", 76 | "@typescript-eslint/parser": "^4.4.0", 77 | "@umijs/plugin-sass": "^1.1.1", 78 | "@wojtekmaj/enzyme-adapter-react-17": "0.3.2", 79 | "commitizen": "^4.2.1", 80 | "cz-conventional-changelog": "^3.3.0", 81 | "dart-sass": "^1.25.0", 82 | "dumi": "^1.0.10", 83 | "enzyme": "^3.11.0", 84 | "enzyme-adapter-react-16": "^1.15.5", 85 | "enzyme-to-json": "^3.6.1", 86 | "eslint": "^7.11.0", 87 | "eslint-config-airbnb": "^18.2.0", 88 | "eslint-config-prettier": "^6.12.0", 89 | "eslint-plugin-react-hooks": "^4.1.2", 90 | "gh-pages": "^3.0.0", 91 | "husky": "^4.3.0", 92 | "identity-obj-proxy": "^3.0.0", 93 | "jest": "^26.6.3", 94 | "lint-staged": "^10.0.7", 95 | "prettier": "^1.19.1", 96 | "react": "17.0.1", 97 | "react-dom": "17.0.1", 98 | "rimraf": "^3.0.2", 99 | "standard-version": "^9.0.0", 100 | "stylelint": "^13.7.2", 101 | "stylelint-config-prettier": "^8.0.2", 102 | "stylelint-config-rational-order": "^0.1.2", 103 | "stylelint-config-standard": "^20.0.0", 104 | "stylelint-order": "^4.1.0", 105 | "stylelint-scss": "^3.18.0", 106 | "ts-jest": "^26.4.3", 107 | "typescript": "^4.2.2", 108 | "yorkie": "^2.0.0" 109 | }, 110 | "dependencies": { 111 | "@ant-design/icons": "^4.3.0", 112 | "classnames": "^2.2.6", 113 | "react-transition-group": "^4.4.1" 114 | }, 115 | "resolutions": { 116 | "remark": "12.0.0" 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /scripts/jest/setup.ts: -------------------------------------------------------------------------------- 1 | import { configure } from 'enzyme' 2 | import Adapter from '@wojtekmaj/enzyme-adapter-react-17' 3 | 4 | configure({ adapter: new Adapter() }) 5 | -------------------------------------------------------------------------------- /tests/utils.ts: -------------------------------------------------------------------------------- 1 | import { act } from 'react-dom/test-utils' 2 | import { ReactWrapper } from 'enzyme' 3 | 4 | jest.useFakeTimers() 5 | 6 | export const waitForWrapperToPaint = async (wrapper: ReactWrapper, delay = 500) => { 7 | await act(async () => { 8 | jest.advanceTimersByTime(delay) 9 | wrapper.update() 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "ESNext", 5 | "target": "ES5", 6 | "declaration": true, 7 | "jsx": "react", 8 | "moduleResolution": "Node", 9 | "allowSyntheticDefaultImports": true 10 | }, 11 | "include": ["components/"], 12 | "exclude": [ 13 | "components/**/__tests__", 14 | "components/**/demos", 15 | "components/**/style", 16 | "components/**/*.md" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2019", 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "importHelpers": true, 7 | "strictNullChecks": true, 8 | "jsx": "react", 9 | "esModuleInterop": true, 10 | "sourceMap": true, 11 | "baseUrl": "./", 12 | "strict": true, 13 | "allowSyntheticDefaultImports": true 14 | }, 15 | "exclude": ["node_modules", "dist", "typings"] 16 | } 17 | -------------------------------------------------------------------------------- /typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css' 2 | declare module '*.less' 3 | --------------------------------------------------------------------------------