├── .babelrc ├── .commitlintrc.js ├── .editorconfig ├── .eslintrc ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .stylelintrc ├── .travis.yml ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── components ├── Affix │ ├── __test__ │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ └── index.test.tsx │ └── index.tsx ├── BackTop │ ├── __test__ │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ └── index.test.tsx │ ├── index.scss │ └── index.tsx ├── Breadcrumb │ ├── __test__ │ │ └── index.test.tsx │ ├── index.scss │ └── index.tsx ├── Button │ ├── __test__ │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ └── index.test.tsx │ ├── index.scss │ └── index.tsx ├── Checkbox │ ├── __test__ │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ └── index.test.tsx │ ├── checkbox.tsx │ ├── checkboxItem.tsx │ ├── index.scss │ └── index.tsx ├── Dropdown │ ├── __test__ │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ └── index.test.tsx │ ├── dropdown.tsx │ ├── index.scss │ └── index.tsx ├── Icon │ ├── __test__ │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ └── index.test.tsx │ ├── index.scss │ └── index.tsx ├── Layout │ ├── Col.tsx │ ├── Row.tsx │ ├── index.scss │ └── index.tsx ├── Modal │ ├── __test__ │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ └── index.test.tsx │ ├── confirm.tsx │ ├── index.scss │ ├── index.tsx │ └── modal.tsx ├── Overlay │ ├── __test__ │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ └── index.test.tsx │ ├── index.scss │ ├── index.tsx │ └── overlay.tsx ├── Panel │ ├── index.scss │ └── index.tsx ├── Popover │ ├── __test__ │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ └── index.test.tsx │ ├── index.scss │ ├── index.tsx │ └── popover.tsx ├── Portal │ ├── __test__ │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ └── index.test.tsx │ ├── index.tsx │ └── portal.tsx ├── Progress │ ├── __test__ │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ └── index.test.tsx │ ├── index.scss │ ├── index.tsx │ └── progress.tsx ├── Radio │ ├── __test__ │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ └── index.test.tsx │ ├── index.scss │ ├── index.tsx │ ├── radio.tsx │ └── radioItem.tsx ├── Spin │ ├── __test__ │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ └── index.test.tsx │ ├── index.scss │ ├── index.tsx │ └── spin.tsx ├── Tabs │ ├── __test__ │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ └── index.test.tsx │ ├── index.scss │ ├── index.tsx │ ├── tabpane.tsx │ └── tabs.tsx ├── Tooltip │ ├── __test__ │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ └── index.test.tsx │ ├── index.scss │ ├── index.tsx │ └── tooltip.tsx ├── index.tsx ├── styles │ ├── _base.scss │ ├── _components.scss │ ├── _normalize.scss │ ├── index.scss │ ├── mixins │ │ ├── _clearfix.scss │ │ ├── _depth.scss │ │ ├── _ellipsis.scss │ │ ├── _highlight.scss │ │ ├── _modal.scss │ │ └── _onePxBorder.scss │ └── themes │ │ ├── _color.scss │ │ ├── _default.scss │ │ └── _font.scss └── utils │ ├── __test__ │ └── tool.test.ts │ ├── scrollTo.ts │ ├── tool.ts │ ├── type.ts │ └── use.ts ├── config └── index.js ├── docs ├── about │ ├── CHANGELOG.md │ ├── CONTRIBUTION.md │ ├── changeLog.tsx │ ├── contribution.tsx │ ├── install.md │ ├── install.tsx │ ├── usage.md │ └── usage.tsx ├── app.tsx ├── components │ ├── Affix │ │ ├── README.md │ │ ├── demo.scss │ │ ├── index.tsx │ │ └── simpleAffix.tsx │ ├── BackTop │ │ ├── README.md │ │ ├── customBackTop.tsx │ │ ├── demo.scss │ │ ├── index.tsx │ │ └── simpleBackTop.tsx │ ├── Breadcrumb │ │ ├── README.md │ │ ├── demo.scss │ │ ├── index.tsx │ │ └── simple.tsx │ ├── Button │ │ ├── README.md │ │ ├── demo.scss │ │ ├── index.tsx │ │ └── simpleButton.tsx │ ├── Checkbox │ │ ├── README.md │ │ ├── demo.tsx │ │ └── index.tsx │ ├── Dropdown │ │ ├── README.md │ │ ├── basic.tsx │ │ ├── demo.tsx │ │ ├── index.scss │ │ └── index.tsx │ ├── Icon │ │ ├── README.md │ │ ├── demo.scss │ │ ├── iconList.ts │ │ ├── index.tsx │ │ └── simpleIcon.tsx │ ├── Layout │ │ ├── README.md │ │ ├── demo.tsx │ │ ├── index.scss │ │ └── index.tsx │ ├── Modal │ │ ├── README.md │ │ ├── alert.tsx │ │ ├── basicModal.tsx │ │ ├── customFooter.tsx │ │ ├── index.tsx │ │ └── mask.tsx │ ├── Overlay │ │ ├── README.md │ │ ├── demo.tsx │ │ ├── index.scss │ │ └── index.tsx │ ├── Panel │ │ ├── README.md │ │ ├── index.tsx │ │ └── simplePanel.tsx │ ├── Popover │ │ ├── README.md │ │ ├── basic.tsx │ │ ├── index.tsx │ │ └── simpleDemo.tsx │ ├── Progress │ │ ├── README.md │ │ ├── color.tsx │ │ ├── index.tsx │ │ ├── render.tsx │ │ ├── shape.tsx │ │ ├── simple.tsx │ │ ├── size.tsx │ │ └── status.tsx │ ├── Radio │ │ ├── README.md │ │ ├── buttonDemo.tsx │ │ ├── demo.tsx │ │ └── index.tsx │ ├── Spin │ │ ├── README.md │ │ ├── container.tsx │ │ ├── index.tsx │ │ ├── simpleDemo.tsx │ │ ├── sizeDemo.tsx │ │ └── tipDemo.tsx │ ├── Tabs │ │ ├── README.md │ │ ├── demo.scss │ │ ├── index.tsx │ │ └── simpleTabs.tsx │ └── Tooltip │ │ ├── README.md │ │ ├── basic.tsx │ │ ├── index.tsx │ │ └── simpleDemo.tsx ├── images │ └── snake.png ├── index.html ├── index.scss ├── layout │ ├── block │ │ ├── index.scss │ │ └── index.tsx │ ├── docs.scss │ ├── docsNav.tsx │ ├── index.tsx │ └── prism │ │ ├── index.tsx │ │ ├── prism.js │ │ └── prism.scss ├── routes │ ├── about.ts │ ├── components.ts │ └── index.tsx └── types │ └── index.d.ts ├── jest.config.js ├── package.json ├── postcss.config.js ├── scripts ├── publishdoc.js └── release.sh ├── setup.js ├── tsconfig.json ├── tslint.json ├── types ├── affix.d.ts ├── backtop.d.ts ├── breadcrumb.d.ts ├── button.d.ts ├── checkbox.d.ts ├── dropdown.d.ts ├── icon.d.ts ├── index.d.ts ├── layout.d.tsx ├── modal.d.ts ├── overlay.d.ts ├── panel.d.ts ├── popover.d.ts ├── portal.d.ts ├── progress.d.ts ├── radio.d.ts ├── spin.d.ts ├── tabs.d.ts └── tooltip.d.ts └── webpack ├── addImportLoader.js ├── docs.config.js ├── webpack.dev.conf.js ├── webpack.dll.config.js └── webpack.prod.conf.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react", 4 | ] 5 | } -------------------------------------------------------------------------------- /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserPreset: { 3 | parserOpts: { 4 | headerPattern: /^(\w*)(?:\((.*)\))?:[ ]?(.*)$/, 5 | headerCorrespondence: ['type', 'scope', 'subject'] 6 | } 7 | }, 8 | rules: { 9 | 'type-empty': [2, 'never'], 10 | 'type-case': [2, 'always', 'lower-case'], 11 | 'subject-empty': [2, 'never'], 12 | 'type-enum': [2, 'always', [ 13 | 'feat', 14 | 'fix', 15 | 'docs', 16 | 'style', 17 | 'refactor', 18 | 'test', 19 | 'chore', 20 | ]] 21 | } 22 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # 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 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:prettier/recommended"], 3 | "plugins": ["react-hooks"], 4 | "rules": { 5 | "react-hooks/rules-of-hooks": "error", 6 | "react-hooks/exhaustive-deps": "warn" 7 | }, 8 | "parser": "babel-eslint" 9 | } 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | What is the current behavior? 2 | 3 | If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem. 4 | 5 | What is the expected behavior? -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | *.pid.lock 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # nyc test coverage 19 | .nyc_output 20 | node_modules/ 21 | 22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 23 | .grunt 24 | npm-debug.log 25 | 26 | # node-waf configuration 27 | .lock-wscript 28 | 29 | # Compiled binary addons (http://nodejs.org/api/addons.html) 30 | build/Release 31 | 32 | # Dependency directories 33 | node_modules 34 | jspm_packages 35 | 36 | # Optional npm cache directory 37 | .npm 38 | 39 | # Optional eslint cache 40 | .eslintcache 41 | 42 | # Optional REPL history 43 | .node_repl_history 44 | 45 | # Output of 'npm pack' 46 | *.tgz 47 | 48 | # Yarn Integrity file 49 | .yarn-integrity 50 | yarn-error.log 51 | yarn.lock 52 | 53 | jest_0 54 | 55 | # editor 56 | .idea/ 57 | 58 | # eslint 59 | .DS_Store 60 | 61 | # session 62 | sessions 63 | 64 | # dist directory 65 | build 66 | server/static 67 | server/views 68 | 69 | monitor 70 | 71 | dist 72 | lib 73 | 74 | docsDist 75 | .markdownlint.json 76 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | build 5 | .build 6 | *.json 7 | # etc.. 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": true, 3 | "jsxBracketSameLine": false, 4 | "printWidth": 100, 5 | // "proseWrap": "always", 6 | "semi": false, 7 | "singleQuote": true, 8 | "tabWidth": 2 9 | } 10 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["stylelint-scss"], 3 | "extends": [ 4 | "stylelint-config-standard", 5 | "stylelint-config-recommended-scss" 6 | ], 7 | "rules": { 8 | "at-rule-no-unknown": null, 9 | "color-hex-case": null, 10 | "block-closing-brace-newline-after": null, 11 | "at-rule-empty-line-before":null, 12 | "number-no-trailing-zeros": null, 13 | "no-empty-source": null, 14 | "unit-case": null, 15 | "scss/at-rule-no-unknown": true, 16 | "font-family-no-missing-generic-family-keyword": null 17 | } 18 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | 5 | notifications: 6 | email: 7 | - 1025687605@qq.com 8 | - muyy95@gmail.com 9 | - 1171009543@qq.com 10 | 11 | node_js: 12 | - 10 13 | 14 | before_install: 15 | - yarn add codecov.io coveralls 16 | 17 | after_success: 18 | - cat ./coverage/lcov.info | ./node_modules/codecov.io/bin/codecov.io.js 19 | - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js 20 | 21 | branches: 22 | only: 23 | - master 24 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Jest All", 11 | "program": "${workspaceFolder}/node_modules/.bin/jest", 12 | "args": [ 13 | "--runInBand" 14 | ], 15 | "console": "integratedTerminal", 16 | "internalConsoleOptions": "neverOpen", 17 | "disableOptimisticBPs": true, 18 | "windows": { 19 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 20 | } 21 | }, 22 | { 23 | "type": "node", 24 | "request": "launch", 25 | "name": "Jest Current File", 26 | "program": "${workspaceFolder}/node_modules/.bin/jest", 27 | "args": [ 28 | "${relativeFile}" 29 | ], 30 | "console": "integratedTerminal", 31 | "internalConsoleOptions": "neverOpen", 32 | "disableOptimisticBPs": true, 33 | "windows": { 34 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 35 | } 36 | } 37 | ] 38 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 front-swordsman 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 | [![Build Status](https://travis-ci.org/ming-cult/snake-design.png)](https://travis-ci.org/ming-cult/snake-design) 2 | 3 |
4 | 5 |
6 | 7 | ## 介绍 8 | 9 | 基于 `React hooks` 开发的 `PC` 端组件库。[文档地址](https://ming-cult.github.io/snake-design) 10 | 11 | ### 特性 12 | 13 | - 组件均基于 hooks 开发 14 | - 除了特殊说明都为受控组件 15 | - 基于 TDD 开发, 90% 以上的测试覆盖率 16 | 17 | > [组件开发进度](https://github.com/ming-cult/snake-design/projects/1) 18 | 19 | ## 开发进度 20 | 21 | - [x] Icon 图标 22 | - [x] Breadcrumb 面包屑 23 | - [x] Button 按钮 24 | - [x] BackTop 回到顶部 25 | - [x] Affix 固钉 26 | - [x] Radio 单选框 27 | - [x] Checkbox 复选框 28 | - [x] Overlay 弹出层 29 | - [x] Tabs 标签页 30 | - [x] Layout(Row, Col) 布局 31 | - [x] Modal 模态框 32 | - [x] DropDown 下拉框 33 | - [x] Popover 气泡卡片 34 | - [x] Tooltip 文字提示 35 | - [x] Spin 加载 36 | - [ ] Input 37 | - [ ] Avatar 38 | - [ ] Badge 39 | - [ ] Card 40 | - [ ] Collapse 41 | - [ ] Divide 42 | - [ ] Menu 43 | - [ ] Pagination 44 | - [ ] Progress 45 | - [ ] Slide 46 | - [ ] Step 47 | - [ ] Spin 48 | - [ ] Switch 49 | - [ ] Tag 50 | - [ ] Upload 51 | - [ ] Popup 52 | - [ ] Tooltip 53 | - [ ] DataPicker 54 | - [ ] TimePicker 55 | - [ ] Cascader 56 | - [ ] Select 57 | - [ ] Table 58 | - [ ] AutoComplete 59 | 60 | ## 命令说明 61 | 62 | - `yarn start`: 启动项目 63 | - `yarn test`: 运行单元测试 64 | - `yarn test:coverage`: 查看代码覆盖率 65 | - `yarn build`: 编译打包组件 66 | - `yarn publish`: 自动化发布版本 67 | -------------------------------------------------------------------------------- /components/Affix/__test__/__snapshots__/index.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`render Affix render default 1`] = ` 4 |
5 |
8 |
11 | Affix Top 12 |
13 |
14 |
15 | `; 16 | -------------------------------------------------------------------------------- /components/Affix/__test__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | // import { render, fireEvent } from 'react-testing-library' 3 | import { render } from 'react-testing-library' 4 | import Affix from '../index' 5 | 6 | describe('render Affix', () => { 7 | it('render default', () => { 8 | const { container } = render(Affix Top) 9 | expect(container).toMatchSnapshot() 10 | }) 11 | // 如何测试 getBoundingCientRect, 后续填坑。 12 | // it('fire scroll', () => { 13 | // const { getByText } = render(Affix Top) 14 | // // fireEvent.scroll(window, { 15 | // // top: 0 16 | // // }) 17 | 18 | // expect(getByText('Affix Top').parentNode.parentNode).toHaveStyle('position: relative') 19 | // }) 20 | }) 21 | -------------------------------------------------------------------------------- /components/Affix/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { AffixProps } from 'types/affix.d' 3 | import { throttle } from '../utils/tool' 4 | 5 | const { useState, useEffect, useRef } = React 6 | 7 | const Affix = (userProps: AffixProps, ref: any) => { 8 | const props = { 9 | ...userProps 10 | } 11 | const { offsetTop, offsetBottom, children, target, onChange, className, style } = props 12 | 13 | const placeholderRef = ref || useRef(null) 14 | const wrapperRef = useRef(null) 15 | 16 | const [positionStyle, setPositionStyle] = useState({}) 17 | // 滚动元素 18 | let scrollElm: Window | HTMLElement = window 19 | // 是否是绝对布局模式 20 | let fixed = false 21 | 22 | const handleScroll = () => { 23 | const rect = placeholderRef.current.getBoundingClientRect() 24 | let { top, bottom } = rect 25 | const style: React.CSSProperties = {} 26 | let containerTop = 0 // 容器距离视口上侧的距离 27 | let containerBottom = 0 // 容器距离视口下侧的距离 28 | 29 | if (scrollElm !== window) { 30 | const containerRect = (scrollElm as HTMLElement).getBoundingClientRect() 31 | containerTop = containerRect.top 32 | containerBottom = containerRect.bottom 33 | 34 | top = top - containerTop // 距离容器顶部的距离 35 | bottom = containerBottom - bottom // 距离容器底部的距离 36 | } else { 37 | bottom = window.innerHeight - bottom 38 | } 39 | 40 | if (top <= offsetTop || bottom <= offsetBottom) { 41 | if (!fixed) { 42 | style.position = 'fixed' 43 | style.top = offsetTop !== undefined ? offsetTop + containerTop : null 44 | style.bottom = 45 | offsetBottom !== undefined 46 | ? scrollElm !== window 47 | ? window.innerHeight - (containerBottom - offsetBottom) 48 | : bottom 49 | : null 50 | 51 | // 在子节点移开父节点后保持原来占位 52 | const { width, height } = wrapperRef.current.getBoundingClientRect() 53 | placeholderRef.current.style.height = `${height}px` 54 | placeholderRef.current.style.width = `${width}px` 55 | onChange && onChange(true) 56 | fixed = true 57 | setPositionStyle(style) 58 | } 59 | } else { 60 | if (fixed) { 61 | style.position = 'relative' 62 | onChange && onChange(false) 63 | fixed = false 64 | setPositionStyle(style) 65 | } 66 | } 67 | } 68 | 69 | const scroll = throttle(handleScroll, 20) 70 | 71 | useEffect(() => { 72 | if (target) scrollElm = target() 73 | // 让按钮点击调整 offsetTop 操作立马生效 74 | handleScroll() 75 | ;(scrollElm as any).addEventListener('scroll', scroll) 76 | 77 | return () => { 78 | ;(scrollElm as any).removeEventListener('scroll', scroll) 79 | } 80 | }, [offsetTop, offsetBottom]) 81 | 82 | return ( 83 |
84 |
85 | {children} 86 |
87 |
88 | ) 89 | } 90 | 91 | export default React.forwardRef(Affix) 92 | -------------------------------------------------------------------------------- /components/BackTop/__test__/__snapshots__/index.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`BackTop Test snapshot with children 1`] = ` 4 |
5 |
8 |
9 | 回到顶部 10 |
11 |
12 |
13 | `; 14 | 15 | exports[`BackTop Test snapshot without children 1`] = ` 16 |
17 |
20 | 24 | 27 | 28 |
29 |
30 | `; 31 | -------------------------------------------------------------------------------- /components/BackTop/__test__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { render, fireEvent } from 'react-testing-library' 3 | import BackTop from '../index' 4 | 5 | describe('BackTop Test', () => { 6 | it('snapshot without children', () => { 7 | const { container } = render() 8 | expect(container).toMatchSnapshot() 9 | }) 10 | 11 | it('snapshot with children', () => { 12 | const { container } = render( 13 | 14 |
回到顶部
15 |
16 | ) 17 | expect(container).toMatchSnapshot() 18 | }) 19 | 20 | it('click backTop back to the top', () => { 21 | const { getByText } = render(回到顶部) 22 | window.scrollTo(0, 500) 23 | const back = getByText(/回到顶部/i) as HTMLElement 24 | fireEvent.click(back) 25 | expect(document.documentElement.scrollTop).toBe(0) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /components/BackTop/index.scss: -------------------------------------------------------------------------------- 1 | $backtop: 'snake-design-backtop'; 2 | 3 | .#{$backtop} { 4 | &-default { 5 | position: fixed; 6 | right: 100px; 7 | bottom: 50px; 8 | width: 40px; 9 | height: 40px; 10 | line-height: 40px; 11 | border-radius: 50%; 12 | background: rgba(0, 0, 0, 0.45); 13 | text-align: center; 14 | } 15 | 16 | &-hide { 17 | display: none; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /components/BackTop/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { BacktopProps } from 'types/backtop' 3 | import cx from 'classnames' 4 | import { scrollToY } from '../utils/scrollTo' 5 | import Icon from '../Icon' 6 | import { throttle } from '../utils/tool' 7 | import './index.scss' 8 | 9 | const { useState, useEffect } = React 10 | 11 | const defaultProps = { 12 | prefixCls: 'snake-design-backtop', 13 | visibilityHeight: 400 14 | } 15 | 16 | const BackTop = (userProps: BacktopProps, ref: any) => { 17 | const props = { 18 | ...userProps, 19 | ...defaultProps 20 | } 21 | 22 | const { prefixCls, children, visibilityHeight } = props 23 | 24 | const [show, setShow] = useState(false) 25 | 26 | const scrollLogic = () => { 27 | if (window.scrollY >= visibilityHeight) { 28 | setShow(true) 29 | } else { 30 | setShow(false) 31 | } 32 | } 33 | 34 | useEffect(() => { 35 | const scroll = throttle(scrollLogic, 20) 36 | window.addEventListener('scroll', scroll) 37 | return () => { 38 | window.removeEventListener('scroll', scroll) 39 | } 40 | }, []) 41 | 42 | const backTopFn = () => { 43 | // window.scrollTo({ 44 | // top: 0, 45 | // behavior: 'smooth' 46 | // }) 47 | scrollToY(0) 48 | } 49 | 50 | return ( 51 |
58 | {children || } 59 |
60 | ) 61 | } 62 | 63 | export default React.forwardRef(BackTop) 64 | -------------------------------------------------------------------------------- /components/Breadcrumb/__test__/index.test.tsx: -------------------------------------------------------------------------------- 1 | /* 单测要点: 2 | (1)点击能跳转; 3 | (2)className 和 style 被设置进去 4 | */ 5 | 6 | import * as React from 'react' 7 | import { render, fireEvent } from 'react-testing-library' 8 | import Breadcrumb from '../index' 9 | const { useState } = React 10 | 11 | const defaultDataSource = [ 12 | { 13 | link: 'customer', 14 | content: '客户' 15 | }, 16 | { 17 | link: 'customer-list', 18 | content: '客户列表' 19 | }, 20 | { 21 | link: 'customer-xiaochuan', 22 | content: '小船出海有限公司' 23 | } 24 | ] 25 | 26 | const Demo = () => { 27 | const [dataSource, setDataSource] = useState(defaultDataSource) 28 | const handleClick = (index: number) => { 29 | setDataSource(dataSource.slice(0, index + 1)) 30 | } 31 | return 32 | } 33 | 34 | describe('Breadcrumb Test', () => { 35 | it('renders default correctly', () => { 36 | const { getByText } = render() 37 | expect(getByText('客户列表').getAttribute('href')).toBe('customer-list') 38 | }) 39 | it('onClick works correctly', () => { 40 | const { getByText, queryByText } = render() 41 | expect(queryByText('小船出海有限公司')).toBeTruthy() 42 | fireEvent.click(getByText('客户列表')) 43 | expect(queryByText('小船出海有限公司')).toBeFalsy() 44 | }) 45 | it('className and style have been set correctly', () => { 46 | const { container } = render( 47 | 54 | ) 55 | 56 | const wrapper = container.firstChild as HTMLElement 57 | expect(wrapper.classList.contains('my-breadcrumb')).toBe(true) 58 | expect(wrapper.style.color).toBe('red') 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /components/Breadcrumb/index.scss: -------------------------------------------------------------------------------- 1 | @import '../styles/themes/default'; 2 | 3 | .snake-breadcrumb { 4 | &-default { 5 | font-size: 16px; 6 | } 7 | 8 | &-large { 9 | font-size: 24px; 10 | } 11 | 12 | &-small { 13 | font-size: 12px; 14 | } 15 | } 16 | 17 | .snake-breadcrumb-item-content { 18 | color: $brand; 19 | cursor: pointer; 20 | 21 | &:hover { 22 | color: $brand; 23 | opacity: 0.8; 24 | } 25 | } 26 | 27 | .snake-breadcrumb-item-active { 28 | .snake-breadcrumb-item-content { 29 | color: $textContent; 30 | cursor: initial; 31 | } 32 | } 33 | 34 | .snake-breadcrumb-separator { 35 | // cursor: pointer; 36 | margin: 0 4px; 37 | } 38 | -------------------------------------------------------------------------------- /components/Breadcrumb/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { getCx } from '../utils/tool' 3 | import { BreadcrumbProps, BreadcrumbItemProps } from 'types/breadcrumb.d' 4 | import './index.scss' 5 | 6 | const { useCallback } = React 7 | 8 | const defaultProps = { 9 | prefixCls: 'snake-breadcrumb', 10 | size: 'default', 11 | separator: '/', 12 | expandMax: 5 13 | } 14 | 15 | const Breadcrumb = (userProps: BreadcrumbProps) => { 16 | const props = { 17 | ...defaultProps, 18 | ...userProps 19 | } 20 | const { prefixCls, separator, onClick, style, dataSource, size, className } = props 21 | const cx = useCallback(getCx(prefixCls), [prefixCls]) 22 | 23 | return ( 24 |
25 | {dataSource.map((item: BreadcrumbItemProps, index: number) => { 26 | const isActive = index === dataSource.length - 1 27 | let aProps: any = {} 28 | if (!isActive) { 29 | aProps = onClick 30 | ? { 31 | onClick: () => onClick(index, item.link) 32 | } 33 | : { 34 | href: item.link 35 | } 36 | } 37 | return ( 38 | 44 | 45 | {item.content} 46 | 47 | {!isActive && {separator}} 48 | 49 | ) 50 | })} 51 |
52 | ) 53 | } 54 | 55 | export default Breadcrumb 56 | -------------------------------------------------------------------------------- /components/Button/__test__/__snapshots__/index.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`render button render button with child-icon 1`] = ` 4 |
5 | 20 |
21 | `; 22 | 23 | exports[`render button render button with children 1`] = ` 24 |
25 | 33 |
34 | `; 35 | 36 | exports[`render button render button with type-icon 1`] = ` 37 |
38 | 53 |
54 | `; 55 | 56 | exports[`render button render default 1`] = ` 57 |
58 | 64 |
65 | `; 66 | 67 | exports[`render text button render only text 1`] = ` 68 | 77 | `; 78 | -------------------------------------------------------------------------------- /components/Button/__test__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { render, fireEvent } from 'react-testing-library' 3 | import Button from '../index' 4 | import Icon from '../../Icon' 5 | 6 | describe('render button', () => { 7 | it('render default', () => { 8 | const wrapper = render() 13 | expect(childrenBtn.container).toMatchSnapshot() 14 | }) 15 | it('render button with type-icon', () => { 16 | const iconBtn = render() 17 | expect(iconBtn.container).toMatchSnapshot() 18 | }) 19 | it('render button with child-icon', () => { 20 | const iconBtn = render( 21 | 25 | ) 26 | expect(iconBtn.container).toMatchSnapshot() 27 | }) 28 | }) 29 | 30 | describe('render text button', () => { 31 | it('render only text', () => { 32 | const wrapper = render() 33 | expect(wrapper.container).toMatchSnapshot() 34 | }) 35 | }) 36 | 37 | describe('onClick event', () => { 38 | const fn = jest.fn() 39 | it('onClick should not work when loading', () => { 40 | const { getByText, queryByText } = render( 41 | 44 | ) 45 | expect(queryByText('点击按钮')).toBeTruthy() 46 | fireEvent.click(getByText('点击按钮')) 47 | expect(fn).not.toBeCalled() 48 | }) 49 | it('onClick should not work when disabled', () => { 50 | const { getByText, queryByText } = render( 51 | 54 | ) 55 | expect(queryByText('点击按钮')).toBeTruthy() 56 | fireEvent.click(getByText('点击按钮')) 57 | expect(fn).not.toBeCalled() 58 | }) 59 | it('onClick should be fired', () => { 60 | const { getByText, queryByText } = render() 61 | expect(queryByText('点击按钮')).toBeTruthy() 62 | fireEvent.click(getByText('点击按钮')) 63 | expect(fn).toBeCalled() 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /components/Button/index.scss: -------------------------------------------------------------------------------- 1 | @import '../styles/themes/default'; 2 | 3 | .snake-button { 4 | line-height: 2; 5 | cursor: pointer; 6 | outline: none; 7 | 8 | &-small { 9 | font-size: 12px; 10 | padding: 0 15px; 11 | border-radius: 2px; 12 | } 13 | 14 | &-default { 15 | font-size: 14px; 16 | padding: 0 20px; 17 | border-radius: 3px; 18 | } 19 | 20 | &-large { 21 | font-size: 16px; 22 | padding: 0 30px; 23 | border-radius: 4px; 24 | } 25 | 26 | &-btn-primary { 27 | color: $white; 28 | background-color: $brand; 29 | border: 1px solid $brand; 30 | 31 | &:hover { 32 | opacity: 0.8; 33 | } 34 | } 35 | 36 | &-btn-gray { 37 | color: $textTitle; 38 | border: 1px solid $lineNormalColor; 39 | 40 | &:hover { 41 | border: 1px solid $brand; 42 | color: $brand; 43 | } 44 | } 45 | 46 | &-btn-warn { 47 | color: $errorDeep; 48 | border: 1px solid $errorNormal; 49 | 50 | &:hover { 51 | background-color: $errorDeep; 52 | border: 1px solid $errorDeep; 53 | color: $white; 54 | opacity: 0.8; 55 | } 56 | } 57 | 58 | &-text-primary { 59 | color: $brand; 60 | 61 | &:hover { 62 | color: $brandDeep; 63 | } 64 | } 65 | 66 | &-text-gray { 67 | color: $textTitle; 68 | 69 | &:hover { 70 | color: $brand; 71 | } 72 | } 73 | 74 | &-text-warn { 75 | color: $errorDeep; 76 | 77 | &:hover { 78 | color: $errorWeight; 79 | } 80 | } 81 | 82 | &-text-disabled { 83 | color: $textHint; 84 | cursor: not-allowed; 85 | 86 | &:hover { 87 | color: $textHint; 88 | } 89 | } 90 | 91 | &-text-loading, 92 | &-btn-loading { 93 | opacity: 0.8; 94 | cursor: progress; 95 | } 96 | 97 | &-text-loading { 98 | &:hover { 99 | color: $brand; 100 | opacity: 0.8; 101 | } 102 | } 103 | 104 | &[disabled] { 105 | color: $textHint; 106 | background-color: #f7f7f7; 107 | border-color: $lineDeepColor; 108 | cursor: not-allowed; 109 | 110 | &:hover { 111 | opacity: 1; 112 | } 113 | } 114 | 115 | &-icon.snake-icon { 116 | font-size: 1em; 117 | margin-right: 4px; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /components/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import cx from 'classnames' 3 | import { ButtonProps } from 'types/button.d' 4 | import Icon from '../Icon' 5 | import './index.scss' 6 | 7 | const defaultProps = { 8 | prefixCls: 'snake-button', 9 | size: 'default', 10 | disabled: false, 11 | loading: false, 12 | type: 'primary', 13 | text: false, 14 | className: '', 15 | onClick: () => {} 16 | } 17 | 18 | const Button = (userProps: ButtonProps, ref: any) => { 19 | const props = { 20 | ...defaultProps, 21 | ...userProps 22 | } 23 | const { 24 | prefixCls, 25 | style, 26 | size, 27 | onClick, 28 | loading, 29 | disabled, 30 | className, 31 | children, 32 | type, 33 | text, 34 | icon, 35 | iconStyle 36 | } = props 37 | 38 | const handleClick = (e: React.MouseEvent) => { 39 | if (loading) return 40 | if (disabled) return 41 | onClick(e) 42 | } 43 | 44 | const iconClass = cx({ [`${prefixCls}-icon`]: true }) 45 | 46 | if (!!text) { 47 | const classStr = cx(prefixCls, { 48 | [`${prefixCls}-text-${type}`]: type, 49 | [`${prefixCls}-text-disabled`]: disabled, 50 | [`${prefixCls}-text-loading`]: loading, 51 | className 52 | }) 53 | return ( 54 | 55 | {loading ? ( 56 | 57 | ) : null} 58 | {icon ? : null} 59 | {children} 60 | 61 | ) 62 | } 63 | const classStr = cx(prefixCls, { 64 | [`${prefixCls}-${size}`]: size, 65 | [`${prefixCls}-btn-${type}`]: type, 66 | [`${prefixCls}-btn-loading`]: loading, 67 | className 68 | }) 69 | return ( 70 | 82 | ) 83 | } 84 | 85 | export default React.forwardRef(Button) 86 | -------------------------------------------------------------------------------- /components/Checkbox/__test__/__snapshots__/index.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`checkbox test snapshot 1`] = `""`; 4 | 5 | exports[`checkbox test snapshot 2`] = `""`; 6 | 7 | exports[`checkbox test snapshot 3`] = `""`; 8 | 9 | exports[`checkbox test snapshot 4`] = `""`; 10 | -------------------------------------------------------------------------------- /components/Checkbox/__test__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { render, fireEvent } from 'react-testing-library' 3 | import Checkbox from '../index' 4 | 5 | describe('checkbox test', () => { 6 | const CheckboxItem = Checkbox.item 7 | const options = [ 8 | { 9 | label: '篮球', 10 | value: 'basketball' 11 | }, 12 | { 13 | label: '足球', 14 | value: 'football' 15 | }, 16 | { 17 | label: '滑板', 18 | value: 'skateboard' 19 | }, 20 | { 21 | label: '跳伞', 22 | value: 'parachute' 23 | } 24 | ] 25 | 26 | it('snapshot', () => { 27 | const checked = render(选中) 28 | const notChecked = render(未选中) 29 | const disabled = render(不可用) 30 | const indent = render(半选) 31 | expect(checked.container.innerHTML).toMatchSnapshot() 32 | expect(notChecked.container.innerHTML).toMatchSnapshot() 33 | expect(disabled.container.innerHTML).toMatchSnapshot() 34 | expect(indent.container.innerHTML).toMatchSnapshot() 35 | }) 36 | 37 | it('fire change', () => { 38 | function CheckDemo() { 39 | const [checked, setChecked] = React.useState(false) 40 | return ( 41 | setChecked(checked)}> 42 | 测试demo 43 | 44 | ) 45 | } 46 | const { getByText } = render() 47 | const inputNode = getByText(/测试demo/i).previousSibling.firstChild as HTMLInputElement 48 | expect(inputNode.checked).toBe(false) 49 | fireEvent.click(inputNode) 50 | expect(inputNode.checked).toBe(true) 51 | }) 52 | 53 | it('checkbox group', () => { 54 | const { container } = render() 55 | expect(container.firstChild.childNodes.length).toBe(4) 56 | }) 57 | 58 | it('fire group change', () => { 59 | function GroupDemo() { 60 | const [value, setValue] = React.useState([]) 61 | return setValue(value)} options={options} /> 62 | } 63 | const { getByText } = render() 64 | const baskNode = getByText(/篮球/i).previousSibling.firstChild as HTMLInputElement 65 | fireEvent.click(baskNode) 66 | expect(baskNode.checked).toBe(true) 67 | fireEvent.click(baskNode) 68 | expect(baskNode.checked).toBe(false) 69 | }) 70 | 71 | it('disabled checkbox', () => { 72 | const fn = jest.fn() 73 | const { getByText } = render( 74 | 75 | 测试 76 | 77 | ) 78 | const inputNode = getByText(/测试/i).previousSibling.firstChild as HTMLInputElement 79 | fireEvent.click(inputNode) 80 | expect(fn).not.toBeCalled() 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /components/Checkbox/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { CheckboxItemProps, CheckboxProps } from '../../types/checkbox' 3 | import CheckboxItem from './checkboxItem' 4 | import { noop } from '../utils/tool' 5 | 6 | const prefixCls = 'snake-checkbox' 7 | 8 | const Checkbox: React.SFC & { 9 | item: React.ForwardRefExoticComponent> 10 | } = ({ onChange = noop, disabled = false, options = [], value = [] }) => { 11 | const handleChange = (checked: boolean, checkedValue: string | number) => { 12 | const cloneValue = value.slice() 13 | if (checked) { 14 | cloneValue.push(checkedValue) 15 | } else { 16 | cloneValue.splice(cloneValue.indexOf(checkedValue), 1) 17 | } 18 | onChange(cloneValue) 19 | } 20 | 21 | return ( 22 |
23 | {options.map(p => { 24 | const checked = value.includes(p.value) 25 | return ( 26 | handleChange(checked, p.value)} 32 | > 33 | {p.label} 34 | 35 | ) 36 | })} 37 |
38 | ) 39 | } 40 | 41 | Checkbox.item = CheckboxItem 42 | 43 | export default Checkbox 44 | -------------------------------------------------------------------------------- /components/Checkbox/checkboxItem.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import cx from 'classnames' 3 | import { CheckboxItemProps } from '../../types/checkbox' 4 | import { noop, omit } from '../utils/tool' 5 | 6 | const defaultProps = { 7 | checked: false, 8 | onChange: noop, 9 | autoFocus: false, 10 | disabled: false, 11 | indeterminate: false, 12 | onBlur: noop, 13 | onFocus: noop, 14 | prefixCls: 'snake-checkbox-item' 15 | } 16 | 17 | function getClassName({ 18 | disabled, 19 | indeterminate, 20 | checked, 21 | prefixCls, 22 | className 23 | }: CheckboxItemProps) { 24 | return cx( 25 | prefixCls, 26 | { 27 | [`${prefixCls}-disabled`]: disabled, 28 | [`${prefixCls}-checked`]: checked, 29 | [`${prefixCls}-indeterminate`]: indeterminate 30 | }, 31 | className 32 | ) 33 | } 34 | 35 | function handleChange( 36 | e: React.ChangeEvent, 37 | { onChange, disabled }: CheckboxItemProps 38 | ) { 39 | const checked = e.target.checked 40 | if (disabled) return 41 | onChange(checked) 42 | } 43 | 44 | function getOtherProps(props: CheckboxItemProps) { 45 | const omitStr = ['onChange', 'prefixCls', 'classNames', 'children', 'indeterminate', 'checked'] 46 | let omitProps = omit(props, omitStr) 47 | return omitProps 48 | } 49 | 50 | function CheckboxItem(checkboxItemProps: CheckboxItemProps, ref: any) { 51 | const props = { ...defaultProps, ...checkboxItemProps } 52 | const { prefixCls, children, checked, indeterminate } = props 53 | const inputRef = React.useRef() 54 | const otherProps = getOtherProps(props) 55 | 56 | React.useImperativeHandle(ref, () => ({ 57 | focus: () => { 58 | inputRef.current.focus() 59 | }, 60 | blur: () => { 61 | inputRef.current.blur() 62 | } 63 | })) 64 | 65 | return ( 66 | 79 | ) 80 | } 81 | 82 | export default React.forwardRef(CheckboxItem) 83 | -------------------------------------------------------------------------------- /components/Checkbox/index.tsx: -------------------------------------------------------------------------------- 1 | import Checkbox from './checkbox' 2 | 3 | export default Checkbox 4 | -------------------------------------------------------------------------------- /components/Dropdown/__test__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { render } from 'react-testing-library' 3 | import Dropdown from '../index' 4 | import { Placement } from 'types/dropdown' 5 | 6 | describe('Dropdown Test', () => { 7 | function DropdownDemo({ 8 | placement = 'bottomLeft', 9 | visible = true 10 | }: { 11 | placement?: Placement 12 | visible?: boolean 13 | }) { 14 | return ( 15 | 内容区}> 16 | click me 17 | 18 | ) 19 | } 20 | 21 | it('snapshot when visible is true', () => { 22 | const { baseElement } = render() 23 | expect(baseElement).toMatchSnapshot() 24 | }) 25 | 26 | it('snapshot when visible is false', () => { 27 | const { baseElement } = render() 28 | expect(baseElement).toMatchSnapshot() 29 | }) 30 | 31 | it('snapshot when placement is bottom', () => { 32 | const { baseElement } = render() 33 | expect(baseElement).toMatchSnapshot() 34 | }) 35 | 36 | it('snapshot when placement is bottomRight', () => { 37 | const { baseElement } = render() 38 | expect(baseElement).toMatchSnapshot() 39 | }) 40 | 41 | it('snapshot when placement is top', () => { 42 | const { baseElement } = render() 43 | expect(baseElement).toMatchSnapshot() 44 | }) 45 | 46 | it('snapshot when placement is topLeft', () => { 47 | const { baseElement } = render() 48 | expect(baseElement).toMatchSnapshot() 49 | }) 50 | 51 | it('snapshot when placement is topRight', () => { 52 | const { baseElement } = render() 53 | expect(baseElement).toMatchSnapshot() 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /components/Dropdown/dropdown.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { DropdownProps } from 'types/dropdown' 3 | import Portal from '../Portal' 4 | 5 | import './index.scss' 6 | 7 | const defaultProps: Partial = { 8 | disabled: false, 9 | visible: false, 10 | trigger: 'hover', 11 | placement: 'bottomLeft', 12 | destroy: true 13 | } 14 | 15 | const Dropdown: React.FC = dropdown => { 16 | const props = { ...defaultProps, ...dropdown } 17 | return 18 | } 19 | 20 | export default Dropdown 21 | -------------------------------------------------------------------------------- /components/Dropdown/index.tsx: -------------------------------------------------------------------------------- 1 | import Dropdown from './dropdown' 2 | 3 | export default Dropdown 4 | -------------------------------------------------------------------------------- /components/Icon/__test__/__snapshots__/index.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`icon test snapshot 1`] = `""`; 4 | 5 | exports[`icon test snapshot 2`] = `""`; 6 | 7 | exports[`icon test snapshot 3`] = `""`; 8 | -------------------------------------------------------------------------------- /components/Icon/__test__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { render, fireEvent } from 'react-testing-library' 3 | import Icon from '../index' 4 | 5 | describe('icon test', () => { 6 | it('snapshot', () => { 7 | const { container } = render() 8 | const { container: contaner1 } = render( 9 | 10 | ) 11 | const { container: contaner2 } = render() 12 | expect(container.innerHTML).toMatchSnapshot() 13 | expect(contaner1.innerHTML).toMatchSnapshot() 14 | expect(contaner2.innerHTML).toMatchSnapshot() 15 | }) 16 | it('fire click', () => { 17 | const fn = jest.fn() 18 | const { container } = render() 19 | fireEvent.click(container.firstChild as HTMLElement) 20 | expect(fn).toBeCalled() 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /components/Icon/index.scss: -------------------------------------------------------------------------------- 1 | @import '../styles/themes/default'; 2 | $icon: 'snake-icon'; 3 | 4 | .#{$icon} { 5 | width: 1em; 6 | height: 1em; 7 | vertical-align: -0.15em; 8 | fill: currentColor; 9 | overflow: hidden; 10 | font-size: 24px; 11 | 12 | &-spin { 13 | animation: loadingSpin 1s infinite linear; 14 | } 15 | } 16 | 17 | @keyframes loadingSpin { 18 | 100% { 19 | transform: rotate(360deg); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /components/Icon/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import cx from 'classnames' 3 | import * as warning from 'warning' 4 | import { IconProps } from 'types/icon.d' 5 | 6 | const { useCallback, useEffect } = React 7 | const cacheScript = new Set() 8 | const url = 'https://at.alicdn.com/t/font_1127944_82mztmm5t8t.js' 9 | 10 | function Icon( 11 | { spin = false, prefixCls = 'snake-icon', ...rest }: IconProps, 12 | ref: React.RefObject 13 | ) { 14 | const { className, size, type, color, rotate, style, ...other } = rest 15 | const classStr = cx( 16 | prefixCls, 17 | { 18 | [`${prefixCls}-${type}`]: type, 19 | [`${prefixCls}-spin`]: spin 20 | }, 21 | className 22 | ) 23 | 24 | const getStyle = useCallback(() => { 25 | const cloneStyle: React.CSSProperties = { ...style } 26 | if (size) cloneStyle.fontSize = size 27 | if (color) cloneStyle.color = color 28 | if (rotate) cloneStyle.rotate = `${rotate}deg` 29 | return cloneStyle 30 | }, [size, color, rotate, style]) 31 | 32 | useEffect(() => { 33 | if (!cacheScript.has(url)) { 34 | const script = document.createElement('script') 35 | script.src = url 36 | cacheScript.add(url) 37 | document.body.appendChild(script) 38 | } 39 | }, []) 40 | 41 | warning(!!type, 'Icon', 'Should have `type` prop.') 42 | 43 | return ( 44 | 45 | 46 | 47 | ) 48 | } 49 | 50 | export default React.forwardRef(Icon) 51 | -------------------------------------------------------------------------------- /components/Layout/Col.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { ColProps } from 'types/layout.d' 3 | import './index.scss' 4 | 5 | export const colDefaultProps: Partial = { 6 | prefixCls: 'snake-col' 7 | } 8 | 9 | const Col = (userProps: ColProps, ref: any) => { 10 | const { children, size, offset, margin, padding, prefixCls, className, style, ...rest } = { 11 | ...colDefaultProps, 12 | ...userProps 13 | } 14 | const cls = 15 | prefixCls + (size ? ` ${prefixCls}-size-${size}` : '') + (className ? ` ${className}` : '') 16 | return ( 17 |
27 | {children} 28 |
29 | ) 30 | } 31 | 32 | export default React.forwardRef(Col) 33 | -------------------------------------------------------------------------------- /components/Layout/Row.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { getType } from 'components/utils/tool' 3 | import { RowProps } from 'types/layout.d' 4 | import './index.scss' 5 | 6 | export const rowDefaultProps: Partial = { 7 | prefixCls: 'snake-row', 8 | total: 36, 9 | justify: 'start', 10 | direction: 'horizontal', 11 | align: 'stretch', 12 | gutter: 0 13 | } 14 | 15 | const Row = (userProps: RowProps, ref: any) => { 16 | const { 17 | children, 18 | gutter, 19 | padding, 20 | margin, 21 | justify, 22 | align, 23 | direction, 24 | colProps, 25 | prefixCls, 26 | className, 27 | style 28 | } = { 29 | ...rowDefaultProps, 30 | ...userProps 31 | } 32 | const cls = 33 | `${prefixCls} ${prefixCls}-${direction} ${prefixCls}-${justify} ${prefixCls}-${align} ${prefixCls}-gutter-${gutter}` + 34 | (className ? ` ${className}` : '') 35 | const child = (getType(children) === 'array' ? children : [children]) as React.ReactElement[] 36 | const childNodes = child.map((item: React.ReactElement, index: number) => { 37 | let marginStyle: any = {} 38 | if (gutter && !(item.props.marginLeft || item.props.padding)) { 39 | marginStyle.marginLeft = gutter / 2 40 | } 41 | if (gutter && !(item.props.marginRight || item.props.padding)) { 42 | marginStyle.marginRight = gutter / 2 43 | } 44 | return React.cloneElement(item, { 45 | key: index, 46 | ...colProps, 47 | className: [(colProps && colProps.className) || '', item.props.className || ''].join(' '), 48 | style: { 49 | ...((colProps && colProps.style) || {}), 50 | ...(item.props.style || {}), 51 | ...(gutter ? marginStyle : {}) 52 | } 53 | }) 54 | }) 55 | return ( 56 |
65 | {childNodes} 66 |
67 | ) 68 | } 69 | 70 | export default React.forwardRef(Row) 71 | -------------------------------------------------------------------------------- /components/Layout/index.scss: -------------------------------------------------------------------------------- 1 | @import '../styles/themes/default'; 2 | 3 | $sizes: 2 3 4 6 8 12; // 24, 12 8 6 4 3 2 4 | @mixin snakeColSize($attr) { 5 | @each $size in $sizes { 6 | .snake-col-size-#{$size} { 7 | #{$attr}: #{$size / 24 * 100 + '%'}; 8 | } 9 | } 10 | } 11 | 12 | .snake-row { 13 | display: flex; 14 | 15 | // direction 16 | &-horizontal { 17 | flex-direction: row; 18 | @include snakeColSize('width'); 19 | } 20 | 21 | &-vertical { 22 | flex-direction: column; 23 | @include snakeColSize('height'); 24 | } 25 | 26 | // justify-content 27 | &-start { 28 | justify-content: flex-start; 29 | } 30 | 31 | &-center { 32 | justify-content: center; 33 | } 34 | 35 | &-end { 36 | justify-content: flex-end; 37 | } 38 | 39 | &-space-between { 40 | justify-content: space-between; 41 | } 42 | 43 | &-space-around { 44 | justify-content: space-around; 45 | } 46 | 47 | // align-items 48 | &-top { 49 | align-items: flex-start; 50 | } 51 | 52 | &-middle { 53 | align-items: center; 54 | } 55 | 56 | &-bottom { 57 | align-items: flex-end; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /components/Layout/index.tsx: -------------------------------------------------------------------------------- 1 | import Row from './Row' 2 | import Col from './Col' 3 | 4 | const Layout = { 5 | Row, 6 | Col 7 | } 8 | 9 | export { Row, Col } 10 | 11 | export default Layout 12 | -------------------------------------------------------------------------------- /components/Modal/__test__/__snapshots__/index.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Modal Test modal visible = false 1`] = ` 4 | 5 | 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /docs/layout/block/index.scss: -------------------------------------------------------------------------------- 1 | .snake-design-block { 2 | &-header { 3 | border-bottom: 1px solid #e2ecf4; 4 | padding: 0 20px 20px; 5 | } 6 | 7 | &-description { 8 | padding: 10px; 9 | color: #314659; 10 | position: relative; 11 | span { 12 | position: absolute; 13 | right: 20px; 14 | cursor: pointer; 15 | } 16 | &.dash { 17 | border-bottom: 1px dashed #e2ecf4; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /docs/layout/block/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Code from '../prism' 3 | import { Icon } from 'components' 4 | import './index.scss' 5 | 6 | export default function Block({ children, des, code }) { 7 | 8 | const [open, setOpen] = React.useState(false) 9 | 10 | return ( 11 |
12 |
13 | { children } 14 |
15 |
16 | { des } 17 | setOpen(!open)}/> 18 |
19 | { 20 | open &&
21 | {code} 22 |
23 | } 24 |
25 | ) 26 | } -------------------------------------------------------------------------------- /docs/layout/docsNav.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { NavLink } from 'react-router-dom' 3 | import { Affix } from 'components/index' 4 | 5 | interface ComponentProps { 6 | name: string 7 | path: string 8 | isDevelopmenting?: boolean 9 | } 10 | 11 | interface NavProps { 12 | basicCList: Array 13 | aboutSnakeDesignList: Array 14 | } 15 | 16 | export default function DocsNav(props: NavProps) { 17 | const { basicCList, aboutSnakeDesignList } = props 18 | 19 | // 渲染左侧导航 20 | const renderNavList = (list: Array) => { 21 | return list.map((item, index) => { 22 | if (item.isDevelopmenting && process.env.NODE_ENV !== 'localdev') { 23 | return null 24 | } 25 | return ( 26 | 27 | {item.name} 28 | 29 | ) 30 | }) 31 | } 32 | 33 | return ( 34 |
35 | 36 |
37 |
38 |
快速开始
39 |
40 | {renderNavList(aboutSnakeDesignList.filter(item => item.path.match(/\/guide\//)))} 41 |
42 |
43 |
44 |
基础组件
45 |
{renderNavList(basicCList)}
46 |
47 |
48 |
49 |
50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /docs/layout/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Link } from 'react-router-dom' 3 | import DocsNav from './docsNav' 4 | import './docs.scss' 5 | 6 | export default class App extends React.Component { 7 | // 渲染PC端页面 8 | renderPCContainer() { 9 | const { children } = this.props 10 | return [ 11 |
12 |
13 | 14 |
snake-design
15 |
16 | 17 | 指南 18 | Guide 19 | 20 |
21 |
22 |
, 23 |
24 |
25 | 29 |
{children}
30 |
31 |
32 | ] 33 | } 34 | 35 | render() { 36 | return this.renderPCContainer() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /docs/layout/prism/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { PrismCode } from 'react-prism' 3 | import './prism' 4 | import './prism.scss' 5 | 6 | class Code extends React.Component { 7 | render() { 8 | const { children } = this.props 9 | 10 | return ( 11 |
12 |
13 |           {children}
14 |         
15 |
16 | ) 17 | } 18 | } 19 | 20 | export default Code 21 | -------------------------------------------------------------------------------- /docs/routes/about.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | key: 'install', 4 | path: '/guide/install', 5 | name: '安装', 6 | component: require('../about/install').default 7 | }, 8 | { 9 | key: 'usage', 10 | path: '/guide/usage', 11 | name: '使用', 12 | component: require('../about/usage').default 13 | }, 14 | { 15 | key: 'changelog', 16 | path: '/guide/changelog', 17 | name: '更新日志', 18 | component: require('../about/changeLog').default 19 | }, 20 | { 21 | key: 'contribution', 22 | path: '/guide/contribution', 23 | name: '贡献指南', 24 | component: require('../about/contribution').default 25 | } 26 | ] 27 | -------------------------------------------------------------------------------- /docs/routes/components.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | key: 'button', 4 | name: 'Button 按钮', 5 | path: '/app/basic/button', 6 | component: require('../components/Button').default 7 | }, 8 | { 9 | key: 'breadcrumb', 10 | name: 'Breadcrumb 面包屑', 11 | path: '/app/basic/breadcrumb', 12 | component: require('../components/Breadcrumb').default 13 | }, 14 | { 15 | key: 'tabs', 16 | name: 'Tabs 标签页', 17 | path: '/app/basic/tabs', 18 | component: require('../components/Tabs').default 19 | }, 20 | { 21 | key: 'icon', 22 | name: 'Icon 图标', 23 | path: '/app/basic/icon', 24 | component: require('../components/Icon').default 25 | }, 26 | { 27 | key: 'checkbox', 28 | name: 'Checkbox 复选框', 29 | path: '/app/basic/checkbox', 30 | component: require('../components/Checkbox').default 31 | }, 32 | { 33 | key: 'radio', 34 | name: 'Radio 单选框', 35 | path: '/app/basic/radio', 36 | component: require('../components/Radio').default 37 | }, 38 | { 39 | key: 'overlay', 40 | name: 'overlay 弹出层', 41 | path: '/app/basic/overlay', 42 | component: require('../components/Overlay').default 43 | }, 44 | { 45 | key: 'layout', 46 | name: 'Layout 布局', 47 | path: '/app/basic/layout', 48 | component: require('../components/Layout').default 49 | }, 50 | { 51 | key: 'affix', 52 | name: 'Affix 固钉', 53 | path: '/app/basic/affix', 54 | component: require('../components/Affix').default 55 | }, 56 | { 57 | key: 'modal', 58 | name: 'Modal 模态框', 59 | path: '/app/basic/modal', 60 | component: require('../components/Modal').default 61 | }, 62 | { 63 | key: 'dropdown', 64 | name: 'Dropdown 下拉框', 65 | path: '/app/basic/dropdown', 66 | component: require('../components/Dropdown').default 67 | }, 68 | { 69 | key: 'backTop', 70 | name: 'BackTop 回到顶部', 71 | path: '/app/basic/backTop', 72 | component: require('../components/BackTop').default 73 | }, 74 | { 75 | key: 'popover', 76 | name: 'Popover 气泡卡片', 77 | path: '/app/basic/popover', 78 | component: require('../components/Popover').default 79 | }, 80 | { 81 | key: 'tooltip', 82 | name: 'Tooltip 文字提示', 83 | path: '/app/basic/tooltip', 84 | component: require('../components/Tooltip').default 85 | }, 86 | { 87 | key: 'spin', 88 | name: 'Spin 文字提示', 89 | path: '/app/basic/spin', 90 | component: require('../components/Spin').default 91 | }, 92 | { 93 | key: 'progress', 94 | name: 'Progress 进度条', 95 | path: '/app/basic/progress', 96 | component: require('../components/Progress').default 97 | } 98 | ] 99 | -------------------------------------------------------------------------------- /docs/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { HashRouter as Router, Route, Redirect } from 'react-router-dom' 3 | import Layout from '../layout' 4 | 5 | /** 组件介绍页 */ 6 | import about from './about' 7 | /** 组件说明页展示路由 */ 8 | import components from './components' 9 | 10 | interface RouterProps { 11 | key: string 12 | name: string 13 | path: string 14 | component: React.ComponentType 15 | isDevelopmenting?: boolean 16 | } 17 | // 渲染一条路由,隐藏开发中的组件 18 | function OneRoute(route: RouterProps) { 19 | if (route.isDevelopmenting && process.env.NODE_ENV !== 'localdev') { 20 | return null 21 | } 22 | return 23 | } 24 | 25 | export default function(props: any) { 26 | return ( 27 | { 30 | if (params.location.pathname === '/') { 31 | return 32 | } 33 | return ( 34 | 40 | {components.map(OneRoute)} 41 | {about.map(OneRoute)} 42 | )} 43 | } 44 | /> 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /docs/types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.md" 2 | declare module "react-prism" -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | '.(ts|tsx)': 'ts-jest' 4 | }, 5 | moduleNameMapper: { 6 | '\\.(css|less|scss)$': 'identity-obj-proxy', 7 | '^components$': '/components/index.tsx', 8 | '^components(.*)$': '/components/$1' 9 | }, 10 | testPathIgnorePatterns: ['/node_modules/', '/lib/', '/es/'], 11 | testRegex: '(/test/.*|\\.(test|spec))\\.(ts|tsx|js)$', 12 | moduleFileExtensions: ['ts', 'tsx', 'js', 'json'], 13 | setupFilesAfterEnv: ['react-testing-library/cleanup-after-each', 'jest-dom/extend-expect'], 14 | watchPlugins: ['jest-watch-typeahead/filename', 'jest-watch-typeahead/testname'], 15 | setupFiles: ['/setup.js'] 16 | } 17 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | module.exports = { 3 | plugins: [ 4 | require('postcss-cssnext')({ browsers:['Android >= 4.0', 'ios >= 6', 'last 2 versions'] }), 5 | ], 6 | }; 7 | /* eslint-enable */ 8 | -------------------------------------------------------------------------------- /scripts/publishdoc.js: -------------------------------------------------------------------------------- 1 | var ghpages = require('gh-pages') 2 | 3 | ghpages.publish( 4 | 'docsDist', 5 | { 6 | repo: 'git@github.com:ming-cult/snake-design.git' 7 | }, 8 | err => { 9 | if (err) { 10 | console.log(err) 11 | } 12 | } 13 | ) 14 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | if [[ -z $1 ]]; then 4 | echo "Enter new version: " 5 | read VERSION 6 | else 7 | VERSION=$1 8 | fi 9 | 10 | read -p "Releasing $VERSION - are you sure? (y/n) " -n 1 -r 11 | echo 12 | if [[ $REPLY =~ ^[Yy]$ ]]; then 13 | echo "Releasing $VERSION ..." 14 | 15 | # npm run lint 16 | # npm run test 17 | 18 | # build 19 | VERSION=$VERSION npm run build 20 | 21 | # commit 22 | git add -A 23 | git commit -m "feat(build): $VERSION" 24 | npm version $VERSION --message "feat: $VERSION" 25 | 26 | # publish 27 | git push origin refs/tags/v$VERSION 28 | git push 29 | npm publish 30 | fi 31 | -------------------------------------------------------------------------------- /setup.js: -------------------------------------------------------------------------------- 1 | window.alert = msg => { 2 | console.log(msg) 3 | } 4 | window.matchMedia = () => ({}) 5 | window.scrollTo = () => {} 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "module": "commonjs", 5 | "outDir": "./build/", 6 | "preserveConstEnums": true, 7 | "removeComments": true, 8 | "target": "ES5", 9 | "allowJs": true, 10 | "experimentalDecorators": true, 11 | "noImplicitReturns": true, 12 | "noUnusedParameters": true, 13 | "noUnusedLocals": true, 14 | "baseUrl": ".", 15 | "typeRoots": [ 16 | "./types", 17 | "node_modules/@types" 18 | ], 19 | "lib": [ 20 | "es2015", 21 | "es2016", 22 | "es2017", 23 | "dom" 24 | ], 25 | "paths": { 26 | "components/*": [ 27 | "./components/*" 28 | ], 29 | "types/*": [ 30 | "./types/*" 31 | ], 32 | "layout/*": [ 33 | "./layout/*" 34 | ], 35 | "routes/*": [ 36 | "./routes/*" 37 | ], 38 | "static/*": [ 39 | "./static/*" 40 | ] 41 | } 42 | }, 43 | "files": [ 44 | "types/index.d.ts" 45 | ], 46 | "exclude": [ 47 | "node_modules" 48 | ], 49 | "include": [ 50 | "docs", 51 | "components" 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint-config-prettier"], 3 | "defaultSeverity": "error", 4 | "rules": { 5 | "adjacent-overload-signatures": true, 6 | "class-name": true, 7 | "eofline": true, 8 | "forin": true, 9 | "interface-over-type-literal": true, 10 | "new-parens": true, 11 | "no-angle-bracket-type-assertion": true, 12 | "no-arg": true, 13 | "no-conditional-assignment": true, 14 | "no-consecutive-blank-lines": true, 15 | "no-construct": true, 16 | "no-duplicate-super": true, 17 | "no-empty": false, 18 | "no-empty-interface": true, 19 | "no-eval": true, 20 | "no-internal-module": true, 21 | "no-invalid-this": false, 22 | "no-string-literal": true, 23 | "no-string-throw": true, 24 | "no-switch-case-fall-through": false, 25 | "no-trailing-whitespace": true, 26 | "no-unsafe-finally": true, 27 | "no-use-before-declare": false, 28 | "no-var-keyword": true, 29 | "no-var-requires": true, 30 | "no-console": [true, "debug", "info", "log", "time", "timeEnd", "trace"], 31 | "no-unused-expression": false, 32 | "radix": true, 33 | "use-isnan": true, 34 | "variable-name": [ 35 | true, 36 | "allow-leading-underscore", 37 | "ban-keywords", 38 | "check-format", 39 | "allow-pascal-case" 40 | ] 41 | } 42 | } -------------------------------------------------------------------------------- /types/affix.d.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | 3 | export interface AffixProps { 4 | // 距离窗口顶部达到指定偏移量后触发 5 | offsetTop?: number 6 | // 距离窗口底部达到指定偏移量后触发 7 | offsetBottom?: number 8 | // 子元素 9 | children?: ReactNode | String 10 | // 容器 11 | target?: () => HTMLElement 12 | // 固定状态改变时触发的回调函数 13 | onChange?: (affixed: boolean) => void 14 | /* 样式相关 */ 15 | className?: string 16 | // 样式 17 | style?: React.CSSProperties 18 | } 19 | -------------------------------------------------------------------------------- /types/backtop.d.ts: -------------------------------------------------------------------------------- 1 | export interface BacktopProps { 2 | /* 滚动高度达到此参数值才出现 BackTop */ 3 | visibilityHeight?: number 4 | /* 传入子节点 */ 5 | children?: React.ReactNode 6 | /* 样式相关 */ 7 | className?: string 8 | /* 样式 */ 9 | style?: React.CSSProperties 10 | } 11 | -------------------------------------------------------------------------------- /types/breadcrumb.d.ts: -------------------------------------------------------------------------------- 1 | export interface BreadcrumbItemProps { 2 | /** 该级的展示内容 **/ 3 | content?: React.ReactNode 4 | /** 该级的链接 **/ 5 | link: string 6 | } 7 | 8 | export interface BreadcrumbProps { 9 | /** 组件className前缀 **/ 10 | prefixCls?: string 11 | /** 样式相关 **/ 12 | className?: string 13 | /** 样式 **/ 14 | style?: React.CSSProperties 15 | /** 点击的回调 **/ 16 | onClick?: (index: number, url: string) => void 17 | /** 尺寸 **/ 18 | size?: 'default' | 'small' | 'large' 19 | /** 数据源 **/ 20 | dataSource: BreadcrumbItemProps[] 21 | /** 分隔符 **/ 22 | separator?: React.ReactNode 23 | /** 最大显示多少级 **/ 24 | expandMax?: number 25 | } 26 | -------------------------------------------------------------------------------- /types/button.d.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface ButtonProps { 3 | /** 组件className前缀 **/ 4 | prefixCls?: string 5 | 6 | /* 样式相关 */ 7 | // 按钮类名 8 | className?: string 9 | // 样式 10 | style?: React.CSSProperties 11 | // 按钮尺寸 12 | size?: 'default' | 'small' | 'large' 13 | 14 | /* 功能相关 */ 15 | // 是否禁止按钮 16 | disabled?: boolean 17 | // 是否正在加载 18 | loading?: boolean 19 | // 点击的回调 20 | onClick?: (e: React.MouseEvent) => void 21 | // 按钮种类 22 | type?: 'primary' | 'gray' | 'warn' 23 | // 是不是文字按钮 24 | text?: boolean 25 | 26 | /* icon相关 */ 27 | // icon类名 28 | icon?: string 29 | // icon样式 30 | iconStyle?: React.CSSProperties 31 | 32 | children?: React.ReactNode 33 | } 34 | -------------------------------------------------------------------------------- /types/checkbox.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * checkbox interface 3 | */ 4 | export interface CheckboxItemProps { 5 | // 是否被选中 默认为false 6 | checked?: boolean 7 | // onChange 变化时候发生的回调 8 | onChange?: (checked: boolean) => void 9 | // autofocus 默认聚焦状态 10 | autoFocus?: boolean 11 | // disabled 默认为false 12 | disabled?: boolean 13 | // className 14 | className?: string 15 | // 半选状态 默认为false 16 | indeterminate?: boolean 17 | // onBlur 18 | onBlur?: (e: React.ChangeEvent) => void 19 | // onFocus 20 | onFocus?: (e: React.ChangeEvent) => void 21 | // prefixCls 22 | prefixCls?: string 23 | children?: React.ReactNode 24 | } 25 | 26 | interface OptionValue { 27 | // 文本信息 28 | label: React.ReactNode 29 | // 选中的值 30 | value: string | number 31 | // 不可用 32 | disabled?: boolean 33 | // autoFocus 34 | autoFocus?: boolean 35 | } 36 | 37 | /** 38 | * checkbox 39 | */ 40 | export interface CheckboxProps { 41 | // onChange 默认noop 42 | onChange?: (checkedValue: Array) => void 43 | // disabled 失效 全部不可用 默认false 44 | disabled?: boolean 45 | // 配置的选项 默认为空数组 46 | options?: OptionValue[] 47 | // value 指定选中 的项 48 | value?: Array 49 | } -------------------------------------------------------------------------------- /types/dropdown.d.ts: -------------------------------------------------------------------------------- 1 | export type Trigger = 'hover' | 'click' 2 | // dropdown 使用 top, topLeft, topRight, bottomLeft, bottomRight, bottom 3 | 4 | // popover and tooltip 使用全部属性 5 | export type Placement = 'top' | 'bottom' | 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight' 6 | 7 | export interface DropdownProps { 8 | /** 是否禁用 */ 9 | disabled?: boolean 10 | // visible 是否可见 默认为 false 11 | visible?: boolean 12 | // content dropdown 的内容区 13 | content?: React.ReactNode 14 | // wrapperComponent 默认为 span 15 | wrapperComponent?: any 16 | // wrapperStyle 包裹层的样式 17 | wrapperStyle?: React.CSSProperties 18 | // 触发 dropdown 的方式 19 | trigger?: Trigger 20 | // placement 菜单的弹出位置 默认 bottom 21 | placement?: Placement 22 | // onVisibleChange 显隐状态变化的回调 23 | onVisibleChange?: (visible: boolean) => void 24 | // destroy 是否消失的时候销毁 默认销毁 25 | destroy?: boolean 26 | // className 弹出层的 class 27 | className?: string 28 | // style dropdown 样式 29 | style?: React.CSSProperties 30 | // getPopupContainer 31 | getPopupContainer?: () => HTMLElement 32 | } 33 | -------------------------------------------------------------------------------- /types/icon.d.ts: -------------------------------------------------------------------------------- 1 | import { HTMLAttributes } from "react"; 2 | 3 | export interface IconProps { 4 | // 图标的名称 5 | type: string 6 | // size 大小 7 | size?: number 8 | // color 图标的颜色 9 | color?: string 10 | // rotate 渲染角度 11 | rotate?: number 12 | // spin 是否有旋转动画, 默认为false 13 | spin?: boolean 14 | // prefixCls 类名的前缀 15 | prefixCls?: string 16 | // className 17 | className?: string 18 | // style 19 | style?: React.CSSProperties 20 | // onClick 21 | onClick?: (e: React.MouseEvent) => void 22 | } -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | /// 6 | /// 7 | /// 8 | /// 9 | /// 10 | /// 11 | -------------------------------------------------------------------------------- /types/layout.d.tsx: -------------------------------------------------------------------------------- 1 | import { tuple } from 'components/utils/tool' 2 | 3 | export const directions = tuple('horizontal', 'vertical') 4 | export const justifies = tuple('start', 'center', 'end', 'space-around', 'space-between') 5 | export const aligns = tuple('stretch', 'top', 'middle', 'bottom') 6 | 7 | export type DirectionType = (typeof directions)[number] 8 | export type JustifyType = (typeof justifies)[number] 9 | export type AlignType = (typeof aligns)[number] 10 | 11 | export interface RowProps { 12 | /** 组件className前缀 **/ 13 | prefixCls?: string 14 | /** 栅格总数,一般为12/24/36 */ 15 | total?: number 16 | /** 子节点 */ 17 | children: React.ReactElement | React.ReactElement[] 18 | /** 排列方向,row表示横着摆放,column表示纵向摆放 */ 19 | direction?: DirectionType 20 | /** 横向对齐方式 */ 21 | justify?: JustifyType 22 | /** 纵向对齐方式 */ 23 | align?: AlignType 24 | /** 每个子元素之间的距离,会给Col加上`gutter/2`的左右padding */ 25 | gutter?: number 26 | /** Row的内边距,number形式的单位是px */ 27 | padding?: number | string 28 | /** Row的外边距,number形式的单位是px */ 29 | margin?: number | string 30 | /** 追加行内样式 */ 31 | style?: React.CSSProperties 32 | /** 追加ClassName */ 33 | className?: string 34 | /** 给col统一添加props,除了style和className是追加方式,其他都是被Col自己的props覆盖 */ 35 | colProps?: Partial 36 | } 37 | 38 | export interface ColProps { 39 | /** 组件className前缀 **/ 40 | prefixCls?: string 41 | /** 子节点 */ 42 | children: React.ReactNode 43 | /** 占多少格 */ 44 | size?: number 45 | /** 占多少格 */ 46 | offset?: number 47 | /** Col的内边距,number形式的单位是px */ 48 | padding?: number | string 49 | /** Col的外边距,number形式的单位是px */ 50 | margin?: number | string 51 | /** 追加行内样式 */ 52 | style?: React.CSSProperties 53 | /** 追加ClassName */ 54 | className?: string 55 | } 56 | -------------------------------------------------------------------------------- /types/modal.d.ts: -------------------------------------------------------------------------------- 1 | import { ButtonProps } from './button' 2 | import { OmitType } from '../components/utils/type' 3 | 4 | export interface ModalProps { 5 | /** 取消文本 默认为 `取消` */ 6 | cancelText?: string | React.ReactNode 7 | /** okText 确认文本 默认为 `确定` */ 8 | okText?: string | React.ReactNode 9 | /** closable 是否显示右上角的叉号 默认 `true` */ 10 | closable?: boolean 11 | /** destroy 是否关闭弹窗销毁modal 默认为 `true` */ 12 | destroy?: boolean 13 | /** onOk 点击确定的回调 */ 14 | onOk?: () => void 15 | /** onCancel 点击取消或遮罩层或者右上角按钮的回调 */ 16 | onCancel?: () => void 17 | /** title 标题 */ 18 | title?: React.ReactNode | null 19 | /** visible 是否可见 默认为 `false` */ 20 | visible?: boolean 21 | /** maskClosable 点击蒙层是否关闭弹窗 默认为 `true` */ 22 | maskClosable?: boolean 23 | /** className 包裹层的 className */ 24 | className?: string 25 | /** style 包裹层的 style 可以用来调整 modal 的位置 */ 26 | style?: React.CSSProperties 27 | /** zIndex 设置 modal 的 z-index */ 28 | zIndex?: number 29 | /** footer 底部 当不需要时设置 null */ 30 | footer?: React.ReactNode | null 31 | /** width 可主动设置内容区的宽度 */ 32 | width?: number 33 | /** okButtonProps */ 34 | okButtonProps?: ButtonProps 35 | /** 取消按钮的 props */ 36 | cancelButtonProps?: ButtonProps 37 | /** esc 是否支持键盘 esc 关闭 modal 默认为 `true` */ 38 | esc?: boolean 39 | /** center 是否居中显示 modal 默认为 false */ 40 | center?: boolean 41 | /** children */ 42 | children?: React.ReactNode 43 | /** afterClose 弹窗完全关闭的回调 */ 44 | afterClose?: () => void 45 | } 46 | 47 | export interface AlertProps 48 | extends OmitType { 49 | /** icon 类型 可自定义 默认与 快捷弹窗的名称一致 */ 50 | icon?: React.ReactNode 51 | /** content 内容 */ 52 | content?: string | React.ReactNode 53 | /** hasCancelBtn 是否需要取消按钮 默认为 true */ 54 | hasCancelBtn?: boolean 55 | } 56 | -------------------------------------------------------------------------------- /types/overlay.d.ts: -------------------------------------------------------------------------------- 1 | export interface OverlayProps { 2 | /** visible 是否可见 */ 3 | visible?: boolean 4 | /** 是否有遮罩层 默认为true */ 5 | hasMask?: boolean 6 | /** onClose 关闭弹层的回调 */ 7 | onClose?: (e: React.MouseEvent) => void 8 | /** 是否点击遮罩层 关闭 默认为 true */ 9 | maskClosable?: boolean 10 | /** destroy 隐藏时, 是否销毁 默认为 true */ 11 | destroy?: boolean 12 | /** children 弹层的内容 */ 13 | children?: React.ReactNode 14 | /** wrapperClassName */ 15 | wrapperClassName?: string 16 | /** wrapperStyle */ 17 | wrapperStyle?: React.CSSProperties 18 | /** maskAnimation 弹层的动画 默认为 fade */ 19 | maskAnimation?: string 20 | /** contentAnimation 内容区的动画 默认为 zoom */ 21 | contentAnimation?: string 22 | /** zIndex */ 23 | zIndex?: number 24 | /** prefixCls */ 25 | prefixCls?: string 26 | /** maskTimeout 动画时间 自定义动画的时候需要与自定义动画的时间保持一致 默认 300ms */ 27 | maskTimeout?: number | object 28 | /** contentTimeout 内容区域动画时间 */ 29 | contentTimeout?: number | object 30 | /** header 自定义 header 区域 */ 31 | header?: React.ReactNode 32 | /** footer 自定义 footer 区域 */ 33 | footer?: React.ReactNode 34 | /** closable 是否显示右上角的叉号 */ 35 | closable?: boolean 36 | /** esc 是否支持键盘 esc 关闭 默认为 true */ 37 | esc?: boolean 38 | /** afterClose 弹窗完全关闭后的回调 */ 39 | afterClose?: () => void 40 | } 41 | -------------------------------------------------------------------------------- /types/panel.d.ts: -------------------------------------------------------------------------------- 1 | export interface PanelProps { 2 | children: any 3 | [propName: string]: any 4 | } -------------------------------------------------------------------------------- /types/popover.d.ts: -------------------------------------------------------------------------------- 1 | import { Placement, Trigger } from './portal' 2 | 3 | export interface PopoverProps { 4 | // 内容区 5 | content?: React.ReactNode 6 | // popover 的方向 7 | placement?: Placement 8 | // 是否可见 9 | visible?: boolean 10 | // 显示隐藏的回调 11 | onVisibleChange?: (visible: boolean) => void 12 | // getPopupContainer 是否渲染到父亲节点 默认为 document.body 13 | getPopupContainer?: () => HTMLElement 14 | // contentClass 内容区的 class 15 | contentClass?: string 16 | // contentStyle 内容区的样式 17 | contentStyle?: React.CSSProperties 18 | // trigger 触发的行为 19 | trigger?: Trigger 20 | // autoAdjustOverflow 是否自动调整位置 默认为 `true` 21 | autoAdjustOverflow?: boolean 22 | // title 标题 23 | title?: React.ReactNode 24 | children?: React.ReactNode 25 | // wrapperComponent 默认为 span 26 | wrapperComponent?: any 27 | // wrapperStyle 包裹层的样式 28 | wrapperStyle?: React.CSSProperties 29 | } 30 | -------------------------------------------------------------------------------- /types/portal.d.ts: -------------------------------------------------------------------------------- 1 | /** 使用该组件 默认给孩子节点加一个标签, 标签的样式可以自定义, 并不是所有的内部组件都会暴露出 ref */ 2 | export interface Portal { 3 | // 触发 dropdown 的方式 4 | trigger?: Trigger 5 | // placement 菜单的弹出位置 默认 bottom 6 | placement?: Placement 7 | // onVisibleChange 显隐状态变化的回调 8 | onVisibleChange?: (visible: boolean) => void 9 | // className 10 | className?: string 11 | // disabled 是否禁用 12 | disabled?: boolean 13 | // style dropdown 样式 14 | style?: React.CSSProperties 15 | // wrapperComponent 默认为 span 16 | wrapperComponent?: any 17 | // wrapperStyle 包裹层的样式 18 | wrapperStyle?: React.CSSProperties 19 | // content dropdown 的内容区 20 | content?: React.ReactNode 21 | // prefixCls 22 | prefixCls?: string 23 | // animationName 动画的类名 24 | animationName?: string 25 | // hasTriangle 是否有三角形 默认为 false 26 | hasTriangle?: boolean 27 | // timeout 动画时间默认 3000ms 28 | timeout?: number 29 | // offset 偏移距离 默认为4 30 | offset?: number 31 | // mode 类型 dropdown 类型的时候有默认最小值 32 | mode?: Mode 33 | // destroy 是否消失的时候销毁 默认销毁 34 | destroy?: boolean 35 | // visible 是否可见 36 | visible?: boolean 37 | // 获取当前的容器 38 | getPopupContainer?: () => HTMLElement 39 | // 默认为 true 40 | autoAdjustOverflow?: boolean 41 | } 42 | 43 | export type Trigger = 'hover' | 'click' 44 | // dropdown 使用 top, topLeft, topRight, bottomLeft, bottomRight, bottom 45 | // popover and tooltip 使用全部属性 46 | export type Placement = 47 | | 'top' 48 | | 'bottom' 49 | | 'topLeft' 50 | | 'topRight' 51 | | 'bottomLeft' 52 | | 'bottomRight' 53 | | 'left' 54 | | 'leftTop' 55 | | 'leftBottom' 56 | | 'right' 57 | | 'rightTop' 58 | | 'rightBottom' 59 | 60 | export type Mode = 'dropdown' | 'tooltip' | 'popover' 61 | -------------------------------------------------------------------------------- /types/progress.d.ts: -------------------------------------------------------------------------------- 1 | export type SizeType = 'small' | 'medium' | 'large' 2 | 3 | export type Type = 'line' | 'circle' 4 | 5 | export type StatusType = 'normal' | 'success' | 'error' 6 | 7 | export interface ProgressProps { 8 | // 尺寸 默认为normal 9 | size?: SizeType 10 | // type 类型 默认为line 11 | type?: Type 12 | // percent 当前的进度 默认为 0 13 | percent?: number 14 | // showInfo 是否显示进度条的数值, 默认为true 15 | showInfo?: boolean 16 | // status 状态 错误的时候是红色 17 | status?: StatusType 18 | // width 进度条的宽度 19 | width?: number 20 | // activeColor 21 | activeColor?: string 22 | // 圆形的时候的文本渲染函数 23 | textRender?: (percent: number) => React.ReactNode 24 | } 25 | -------------------------------------------------------------------------------- /types/radio.d.ts: -------------------------------------------------------------------------------- 1 | export interface RadioItemProps { 2 | // 自动获取焦点 默认false 3 | autoFocus?: boolean 4 | // checked 指定当前是否选中 默认false 5 | checked?: boolean 6 | // disabled 不可用状态 默认为false 7 | disabled?: boolean 8 | // onChange 9 | onChange?: (checked: boolean) => void 10 | // className 11 | className?: string 12 | // prefixCls 前缀 13 | prefixCls?: string 14 | children?: React.ReactNode 15 | } 16 | 17 | export interface OptionTypes { 18 | label: React.ReactNode 19 | value: string | number 20 | disabled?: boolean 21 | } 22 | 23 | export type SizeTypes = 'small' | 'normal' | 'large' 24 | 25 | export type ShapeTypes = 'default' | 'button' 26 | 27 | export interface RadioProps { 28 | // options 数据源 默认[] 29 | options?: OptionTypes[] 30 | // onChange 31 | onChange?: (value: string | number) => void 32 | // size 33 | size?: SizeTypes 34 | // value 当前选中的值 必选 35 | value: string | number 36 | //disabled 禁用所有 默认为false 37 | disabled?: boolean 38 | // className 39 | className?: string 40 | // 形状 默认为default 41 | shape?: ShapeTypes 42 | // buttonStyle 填充或者描边 43 | buttonStyle?: 'outline' | 'solid' 44 | } -------------------------------------------------------------------------------- /types/spin.d.ts: -------------------------------------------------------------------------------- 1 | export type SizeType = 'small' | 'normal' 2 | 3 | export interface SpinProps { 4 | // 自定义的指示符 5 | indicator?: React.ReactNode 6 | // 尺寸 7 | size?: SizeType 8 | // tip 当有children的时候自定义文案 9 | tip?: React.ReactNode 10 | // children 11 | children?: React.ReactNode 12 | } 13 | -------------------------------------------------------------------------------- /types/tabs.d.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | 3 | export interface TabsProps { 4 | style?: object 5 | className?: string 6 | tabBarPosition?: string 7 | onChange?: (index: number, e: any) => void 8 | activeTab?: number 9 | options: TabsItemProps[] 10 | tabBarUnderlineColor?: string 11 | children?: any 12 | prefixCls?: string 13 | type?: string 14 | } 15 | 16 | export interface TabsItemProps { 17 | title: String | Number | ReactNode 18 | disabled?: boolean 19 | content: ReactNode 20 | } 21 | -------------------------------------------------------------------------------- /types/tooltip.d.ts: -------------------------------------------------------------------------------- 1 | import { PopoverProps } from './popover' 2 | import { OmitType } from '../components/utils/type' 3 | 4 | export interface TooltipProps extends OmitType {} 5 | -------------------------------------------------------------------------------- /webpack/webpack.dll.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin') 4 | const CleanPlugin = require('clean-webpack-plugin') 5 | const projectRootPath = path.resolve(__dirname, '../') 6 | // const dllPath = path.resolve(projectRootPath, './build/vendor'); 7 | 8 | const dllPath = 9 | process.env.NODE_ENV === 'production' 10 | ? path.join(__dirname, '../server/static/build/vendor') 11 | : path.join(__dirname, '../build/vendor') 12 | 13 | const plugins = [ 14 | new webpack.DllPlugin({ 15 | /** 16 | * path 17 | * 定义 manifest 文件生成的位置 18 | * [name]的部分由entry的名字替换 19 | */ 20 | path: path.join(dllPath, '[name]-manifest.json'), 21 | /** 22 | * name 23 | * dll bundle 输出到那个全局变量上 24 | * 和 output.library 一样即可。 25 | */ 26 | context: __dirname, 27 | name: '[name]_library' 28 | }) 29 | ] 30 | const outputFileName = '[name].dll.js' 31 | if (process.env.NODE_ENV === 'production') { 32 | plugins.push( 33 | new CleanPlugin([dllPath], { root: projectRootPath }), 34 | new webpack.DefinePlugin({ 35 | 'process.env': { 36 | NODE_ENV: '"production"' 37 | } 38 | }), 39 | new webpack.optimize.OccurrenceOrderPlugin(), 40 | new ParallelUglifyPlugin({ 41 | cacheDir: 'node_modules/.cache/uglifyjs_cache', 42 | sourceMap: true, 43 | uglifyJS: { 44 | output: { 45 | comments: false 46 | }, 47 | compress: { 48 | warnings: false 49 | } 50 | } 51 | }) 52 | ) 53 | } 54 | 55 | module.exports = { 56 | mode: 'development', 57 | entry: { 58 | vendor: ['react', 'react-dom', 'react-router-dom'] 59 | }, 60 | output: { 61 | path: dllPath, 62 | filename: outputFileName, 63 | library: '[name]_library' 64 | }, 65 | plugins: plugins 66 | } 67 | --------------------------------------------------------------------------------