├── .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 | [](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 |
13 | `;
14 |
15 | exports[`BackTop Test snapshot without children 1`] = `
16 |
17 |
20 |
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()
9 | expect(wrapper.container).toMatchSnapshot()
10 | })
11 | it('render button with children', () => {
12 | const childrenBtn = 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 |
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 |
8 |
9 |
10 |
11 |
12 | `;
13 |
14 | exports[`Modal Test modal visible snapshot 1`] = `
15 |
16 |
17 |
22 |
23 |
27 |
30 |
33 |
40 |
41 |
44 | 内容区
45 |
46 |
68 |
69 |
70 |
71 |
74 |
75 | `;
76 |
77 | exports[`Modal Test modal without footer 1`] = `
78 |
79 |
82 |
83 |
88 |
89 |
93 |
96 |
99 |
106 |
107 |
110 | 内容区
111 |
112 |
113 |
114 |
115 |
116 | `;
117 |
--------------------------------------------------------------------------------
/components/Modal/index.tsx:
--------------------------------------------------------------------------------
1 | import Modal from './modal'
2 |
3 | export default Modal
4 |
--------------------------------------------------------------------------------
/components/Modal/modal.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import cx from 'classnames'
3 | import { ModalProps, AlertProps } from 'types/modal'
4 | import Overlay from '../Overlay'
5 | import Button from '../Button'
6 | import confirm, { info, success, error, warning, Close } from './confirm'
7 | import { noop } from '../utils/tool'
8 |
9 | import './index.scss'
10 |
11 | type Confirm = (props: AlertProps) => Close
12 |
13 | const defaultProps = {
14 | cancelText: '取消',
15 | okText: '确定',
16 | closable: true,
17 | destroy: true,
18 | onOk: noop,
19 | onCancel: noop,
20 | visible: false,
21 | maskClosable: true,
22 | esc: true,
23 | center: false,
24 | afterClose: noop
25 | }
26 |
27 | const prefixCls = 'snake-modal'
28 |
29 | const renderHeader = ({ title }: ModalProps) => {
30 | if (title) {
31 | return {title}
32 | }
33 | return null
34 | }
35 |
36 | const renderFooter = ({
37 | cancelText,
38 | okText,
39 | onOk,
40 | okButtonProps,
41 | cancelButtonProps,
42 | footer,
43 | onCancel
44 | }: ModalProps) => {
45 | if (footer === null) return null
46 | return (
47 |
48 | {footer || (
49 | <>
50 |
53 |
56 | >
57 | )}
58 |
59 | )
60 | }
61 |
62 | const Modal: React.FC & {
63 | confirm: Confirm
64 | info: Confirm
65 | success: Confirm
66 | error: Confirm
67 | warning: Confirm
68 | } = modalProps => {
69 | const props = { ...defaultProps, ...modalProps }
70 | const {
71 | children,
72 | className,
73 | center,
74 | esc,
75 | destroy,
76 | onCancel,
77 | visible,
78 | style,
79 | maskClosable,
80 | closable,
81 | zIndex,
82 | width,
83 | afterClose
84 | } = props
85 |
86 | const getClassStr = React.useCallback(() => {
87 | return cx(
88 | {
89 | [`${prefixCls}-center`]: center
90 | },
91 | className
92 | )
93 | }, [className, center])
94 |
95 | const getStyle = React.useCallback(() => {
96 | const cloneStyle: React.CSSProperties = {}
97 | if (width) {
98 | cloneStyle.width = width
99 | }
100 | return { ...style, ...cloneStyle }
101 | }, [width, style])
102 |
103 | return (
104 |
119 | {children}
120 |
121 | )
122 | }
123 |
124 | Modal.confirm = confirm
125 | Modal.success = success
126 | Modal.error = error
127 | Modal.warning = warning
128 | Modal.info = info
129 |
130 | export default Modal
131 |
--------------------------------------------------------------------------------
/components/Overlay/index.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ming-cult/snake-design/2ae6723911ef670c4e141108a99508be8597299d/components/Overlay/index.scss
--------------------------------------------------------------------------------
/components/Overlay/index.tsx:
--------------------------------------------------------------------------------
1 | import Overlay from './overlay'
2 |
3 | export default Overlay
4 |
--------------------------------------------------------------------------------
/components/Panel/index.scss:
--------------------------------------------------------------------------------
1 | @import '../styles/mixins/onePxBorder';
2 | $_pddTop: 12px;
3 | $_pddLeft: 15px;
4 |
5 | .snake-design-panel {
6 | color: red;
7 |
8 | &:not(:last-child) {
9 | margin-bottom: 10px;
10 | }
11 |
12 | &-header {
13 | @include one-px-border(top);
14 |
15 | padding: $_pddTop $_pddLeft;
16 | background: #fff;
17 | }
18 |
19 | &-body {
20 | @include one-px-border(top);
21 | @include one-px-border(bottom);
22 |
23 | background: #fff;
24 | }
25 |
26 | &-footer {
27 | @include one-px-border(bottom);
28 |
29 | padding: $_pddTop $_pddLeft;
30 | background: #fff;
31 | }
32 |
33 | &-addons {
34 | font-size: 12px;
35 | padding: $_pddTop $_pddLeft $_pddTop - 2px;
36 | }
37 |
38 | &-item {
39 | padding: $_pddTop $_pddLeft;
40 | display: flex;
41 | align-items: center;
42 | justify-content: space-between;
43 |
44 | &:not(:last-child) {
45 | @include one-px-border(bottom);
46 |
47 | &::after {
48 | left: $_pddLeft;
49 | }
50 | }
51 |
52 | &-inner {
53 | display: inline-flex;
54 | flex: 1;
55 | width: 100%;
56 |
57 | &-row {
58 | flex-direction: row;
59 | }
60 |
61 | &-column {
62 | flex-direction: column;
63 | }
64 | }
65 |
66 | &-leftIcon,
67 | &-leftAddon {
68 | margin-right: 10px;
69 | }
70 |
71 | &-rightAddon {
72 | margin: 0 6px;
73 | }
74 |
75 | &-rightIcon {
76 | line-height: 1;
77 | }
78 | }
79 | // 触摸反馈
80 | &-feedback:active {
81 | background: #f5f5f5;
82 | }
83 |
84 | // 对齐方式
85 | &-align__ {
86 | &stretch {
87 | align-items: stretch;
88 | }
89 |
90 | ¢er {
91 | align-items: center;
92 | }
93 |
94 | &end {
95 | align-items: flex-end;
96 | }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/components/Panel/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as cx from 'classnames'
3 |
4 | // 通用Panel组件
5 | const getCom = (name?: string) => (props: any) => {
6 | const { children, className, alignItems, ...ps } = props
7 | const classNames = cx(className, {
8 | 'snake-design-panel': !name,
9 | [`snake-design-panel-${name}`]: !!name,
10 | })
11 | return
12 | {children}
13 |
14 | }
15 |
16 | export default class Panel extends React.Component {
17 |
18 | static Header: any = getCom('header')
19 | static Footer: any = getCom('footer')
20 | static Body: any = getCom('body')
21 | static Addons: any = getCom('addons') // 附带内容
22 |
23 | // A item
24 | static Item = function(props: any) {
25 | const {
26 | leftIcon,
27 | leftAddon,
28 | rightIcon,
29 | rightAddon,
30 | feedback,
31 | alignItems,
32 | children,
33 | className,
34 | innerStyle,
35 | innerClass,
36 | innerDirection,
37 | ...ps
38 | } = props
39 | const classNames = cx({
40 | 'snake-design-panel-feedback': feedback,
41 | }, 'snake-design-panel-item', `snake-design-panel-align__${alignItems || 'center'}`, className)
42 | return
43 | {leftIcon &&
{leftIcon}
}
44 | {leftAddon &&
{leftAddon}
}
45 |
51 | {children}
52 |
53 | {rightAddon &&
{rightAddon}
}
54 | {rightIcon &&
{rightIcon}
}
55 |
56 | }
57 |
58 | render() {
59 | return getCom()(this.props)
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/components/Popover/__test__/index.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { render } from 'react-testing-library'
3 | import Popover from '../index'
4 | import { Placement } from 'types/portal'
5 |
6 | jest.mock('react-transition-group', () => {
7 | return {
8 | CSSTransition: props => {
9 | return {props.in || !props.unmountOnExit ? props.children : null}
10 | }
11 | }
12 | })
13 |
14 | describe('Popover Test', () => {
15 | function PopoverDemo({
16 | placement = 'top',
17 | visible = true
18 | }: {
19 | placement?: Placement
20 | visible?: boolean
21 | }) {
22 | return (
23 | 内容区}>
24 | click me
25 |
26 | )
27 | }
28 |
29 | it('snapshot when visible is false', () => {
30 | const { baseElement } = render()
31 | expect(baseElement).toMatchSnapshot()
32 | })
33 |
34 | it('snapshot when visible is true', () => {
35 | const { baseElement } = render()
36 | expect(baseElement).toMatchSnapshot()
37 | })
38 |
39 | it('snapshot with placement is topLeft', () => {
40 | const { baseElement } = render()
41 | expect(baseElement).toMatchSnapshot()
42 | })
43 |
44 | it('snapshot with placement is topRight', () => {
45 | const { baseElement } = render()
46 | expect(baseElement).toMatchSnapshot()
47 | })
48 |
49 | it('snapshot with placement is bottom', () => {
50 | const { baseElement } = render()
51 | expect(baseElement).toMatchSnapshot()
52 | })
53 |
54 | it('snapshot with placement is bottomLeft', () => {
55 | const { baseElement } = render()
56 | expect(baseElement).toMatchSnapshot()
57 | })
58 |
59 | it('snapshot with placement is bottomRight', () => {
60 | const { baseElement } = render()
61 | expect(baseElement).toMatchSnapshot()
62 | })
63 |
64 | it('snapshot with placement is left', () => {
65 | const { baseElement } = render()
66 | expect(baseElement).toMatchSnapshot()
67 | })
68 |
69 | it('snapshot with placement is leftTop', () => {
70 | const { baseElement } = render()
71 | expect(baseElement).toMatchSnapshot()
72 | })
73 |
74 | it('snapshot with placement is leftBottom', () => {
75 | const { baseElement } = render()
76 | expect(baseElement).toMatchSnapshot()
77 | })
78 |
79 | it('snapshot with placement is right', () => {
80 | const { baseElement } = render()
81 | expect(baseElement).toMatchSnapshot()
82 | })
83 |
84 | it('snapshot with placement is rightTop', () => {
85 | const { baseElement } = render()
86 | expect(baseElement).toMatchSnapshot()
87 | })
88 |
89 | it('snapshot with placement is rightBottom', () => {
90 | const { baseElement } = render()
91 | expect(baseElement).toMatchSnapshot()
92 | })
93 | })
94 |
--------------------------------------------------------------------------------
/components/Popover/index.tsx:
--------------------------------------------------------------------------------
1 | import Popover from './popover'
2 |
3 | export default Popover
4 |
--------------------------------------------------------------------------------
/components/Popover/popover.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { PopoverProps } from 'types/popover'
3 | import Portal from '../Portal'
4 | import { noop } from '../utils/tool'
5 |
6 | import './index.scss'
7 |
8 | const defaultProps: Partial = {
9 | placement: 'top',
10 | visible: false,
11 | onVisibleChange: noop,
12 | trigger: 'hover',
13 | autoAdjustOverflow: true
14 | }
15 |
16 | const prefixCls = 'snake-popover'
17 |
18 | export default function Popover(popover: PopoverProps) {
19 | const props = { ...defaultProps, ...popover }
20 | const { content, title, contentClass, contentStyle, ...rest } = props
21 | const renderContent = () => {
22 | return (
23 |
24 | {title ?
{title}
: null}
25 | {content ?
{content}
: null}
26 |
27 | )
28 | }
29 | return (
30 |
40 | )
41 | }
42 |
--------------------------------------------------------------------------------
/components/Portal/index.tsx:
--------------------------------------------------------------------------------
1 | import Portal from './portal'
2 |
3 | export default Portal
4 |
--------------------------------------------------------------------------------
/components/Progress/__test__/__snapshots__/index.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Progress Test snapshot with circle progress 1`] = `""`;
4 |
5 | exports[`Progress Test snapshot with color progress 1`] = `""`;
6 |
7 | exports[`Progress Test snapshot with error progress 1`] = `""`;
8 |
9 | exports[`Progress Test snapshot with progress normal 1`] = `""`;
10 |
11 | exports[`Progress Test snapshot with size large progress 1`] = `""`;
12 |
13 | exports[`Progress Test snapshot with size small progress 1`] = `""`;
14 |
15 | exports[`Progress Test snapshot with success progress 1`] = `""`;
16 |
--------------------------------------------------------------------------------
/components/Progress/__test__/index.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { render, fireEvent } from 'react-testing-library'
3 | import Progress from '../index'
4 |
5 | const { useState } = React
6 |
7 | describe('Progress Test', () => {
8 | function Demo(props) {
9 | const [percent, setPercent] = useState(0)
10 |
11 | const handleClick = () => {
12 | let p = percent
13 | p = percent >= 100 ? 0 : p + 10
14 | setPercent(p)
15 | }
16 |
17 | return (
18 |
22 | )
23 | }
24 | it('snapshot with progress normal', () => {
25 | const { container } = render()
26 | expect(container.innerHTML).toMatchSnapshot()
27 | })
28 |
29 | it('snapshot with color progress', () => {
30 | const { container } = render()
31 | expect(container.innerHTML).toMatchSnapshot()
32 | })
33 |
34 | it('snapshot with size large progress', () => {
35 | const { container } = render()
36 | expect(container.innerHTML).toMatchSnapshot()
37 | })
38 |
39 | it('snapshot with size small progress', () => {
40 | const { container } = render()
41 | expect(container.innerHTML).toMatchSnapshot()
42 | })
43 |
44 | it('snapshot with circle progress', () => {
45 | const { container } = render()
46 | expect(container.innerHTML).toMatchSnapshot()
47 | })
48 |
49 | it('snapshot with success progress', () => {
50 | const { container } = render()
51 | expect(container.innerHTML).toMatchSnapshot()
52 | })
53 |
54 | it('snapshot with error progress', () => {
55 | const { container } = render()
56 | expect(container.innerHTML).toMatchSnapshot()
57 | })
58 |
59 | it('get correct progress', () => {
60 | const { getByText } = render()
61 | expect(getByText('0%')).not.toBeNull()
62 | const btn = getByText(/点击增加进度/i)
63 | fireEvent.click(btn)
64 | expect(getByText('10%')).not.toBeNull()
65 | })
66 |
67 | it('with text render', () => {
68 | const { getByText } = render(