├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── custom.md │ └── feature_request.md └── workflows │ ├── publish.yml │ └── test.yml ├── .gitignore ├── .prettierrc ├── .storybook ├── main.js └── netlify.toml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTION.md ├── LICENSE ├── README.md ├── SUMMARY.md ├── changelog.md ├── package.json ├── src ├── assets │ └── icons │ │ ├── loaders │ │ ├── Circular │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ └── Spinner │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ └── vant-icons │ │ ├── config.js │ │ ├── encode.scss │ │ ├── index.scss │ │ ├── vant-icon-db1de1.ttf │ │ ├── vant-icon-db1de1.woff │ │ └── vant-icon-db1de1.woff2 ├── components │ ├── Button │ │ ├── helper.tsx │ │ ├── index.scss │ │ ├── index.stories.tsx │ │ ├── index.tsx │ │ └── types.ts │ ├── Cell │ │ ├── index.scss │ │ ├── index.stories.tsx │ │ ├── index.tsx │ │ └── types.ts │ ├── Checkbox │ │ ├── index.scss │ │ ├── index.stories.tsx │ │ └── index.tsx │ ├── Divider │ │ ├── index.scss │ │ ├── index.stories.tsx │ │ └── index.tsx │ ├── Field │ │ ├── index.scss │ │ ├── index.stories.tsx │ │ ├── index.tsx │ │ └── types.ts │ ├── Icons │ │ ├── index.scss │ │ ├── index.stories.js │ │ └── index.tsx │ ├── Image │ │ ├── index.scss │ │ ├── index.stories.tsx │ │ └── index.tsx │ ├── Loading │ │ ├── index.scss │ │ ├── index.stories.tsx │ │ ├── index.tsx │ │ └── types.ts │ ├── Navbar │ │ ├── index.scss │ │ ├── index.stories.tsx │ │ └── index.tsx │ ├── Popup │ │ ├── index.scss │ │ ├── index.stories.tsx │ │ ├── index.tsx │ │ └── types.ts │ ├── Radio │ │ ├── index.scss │ │ ├── index.stories.tsx │ │ ├── index.tsx │ │ └── types.ts │ ├── Rate │ │ ├── index.scss │ │ ├── index.stories.tsx │ │ ├── index.tsx │ │ ├── subcomponents │ │ │ └── rate-icon.tsx │ │ └── types.ts │ ├── Search │ │ ├── index.scss │ │ ├── index.stories.tsx │ │ ├── index.tsx │ │ └── types.ts │ ├── Slider │ │ ├── index.scss │ │ ├── index.stories.tsx │ │ ├── index.tsx │ │ └── types.ts │ ├── Stepper │ │ ├── index.scss │ │ ├── index.stories.tsx │ │ └── index.tsx │ ├── Switch │ │ ├── index.scss │ │ ├── index.stories.tsx │ │ ├── index.tsx │ │ └── types.ts │ ├── Tag │ │ ├── index.scss │ │ ├── index.stories.tsx │ │ └── index.tsx │ ├── Toast │ │ ├── CreateToast.tsx │ │ ├── Toast.tsx │ │ ├── ToastContainer.tsx │ │ ├── index.scss │ │ ├── index.stories.tsx │ │ ├── index.ts │ │ └── types.ts │ └── template │ │ ├── index.scss │ │ ├── index.stories.tsx │ │ └── index.tsx ├── index.tsx ├── react-app-env.d.ts ├── styles │ ├── animation.scss │ ├── colors.scss │ ├── global.scss │ ├── opacity.scss │ ├── spacing.scss │ ├── stories.scss │ ├── style.scss │ ├── typography.scss │ └── variables.scss ├── types │ ├── positions.ts │ └── shapes.ts ├── typings.d.ts └── utils │ ├── base.ts │ ├── classNames.ts │ ├── format │ └── unit.ts │ ├── index.ts │ └── validate │ └── number.ts ├── tsconfig.json └── tsconfig.test.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | node_modules/ 4 | .snapshots/ 5 | *.min.js -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "standard", 5 | "standard-react", 6 | "plugin:prettier/recommended", 7 | "prettier/standard", 8 | "prettier/react", 9 | "plugin:@typescript-eslint/eslint-recommended" 10 | ], 11 | "overrides": [ 12 | { 13 | "files": ["**/*.ts", "**/*.tsx"], 14 | "rules": { 15 | "no-unused-vars": ["off"], 16 | "no-undef": ["off"] 17 | } 18 | } 19 | ], 20 | "env": { 21 | "node": true 22 | }, 23 | "parserOptions": { 24 | "ecmaVersion": 2020, 25 | "ecmaFeatures": { 26 | "legacyDecorators": true, 27 | "jsx": true 28 | } 29 | }, 30 | "settings": { 31 | "react": { 32 | "version": "16" 33 | } 34 | }, 35 | "rules": { 36 | "space-before-function-paren": 0, 37 | "react/prop-types": 0, 38 | "react/jsx-handler-names": 0, 39 | "react/jsx-fragments": 0, 40 | "react/no-unused-prop-types": 0, 41 | "import/export": 2, 42 | "no-extra-boolean-cast": 0 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish CI 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: 12 15 | registry-url: https://registry.npmjs.org/ 16 | - run: yarn install 17 | - run: npm publish --access public 18 | env: 19 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 20 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v1 10 | - uses: actions/setup-node@v1 11 | with: 12 | node-version: 12 13 | registry-url: https://registry.npmjs.org/ 14 | - run: yarn install 15 | - run: yarn test 16 | build: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v1 20 | - uses: actions/setup-node@v1 21 | with: 22 | node-version: 12 23 | registry-url: https://registry.npmjs.org/ 24 | - run: yarn install 25 | - run: yarn run build-storybook 26 | - run: yarn run build 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | demo/ 4 | node_modules/ 5 | **/node_modules/ 6 | .snapshots/ 7 | *.min.js 8 | .idea/ 9 | .vscode/ 10 | .vs/ 11 | **/.DS_Store 12 | 13 | # testing 14 | /coverage 15 | /storybook-static 16 | 17 | # production 18 | /build 19 | 20 | # misc 21 | .DS_Store 22 | .env.local 23 | .env.development.local 24 | .env.test.local 25 | .env.production.local 26 | 27 | package-lock.json 28 | yarn.lock 29 | 30 | npm-debug.log* 31 | yarn-debug.log* 32 | yarn-error.log* 33 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": true, 4 | "semi": true, 5 | "tabWidth": 2, 6 | "bracketSpacing": true, 7 | "jsxBracketSameLine": false, 8 | "arrowParens": "always", 9 | "trailingComma": "none", 10 | "endOfLine":"auto" 11 | } 12 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: ['../src/**/*.stories.js', '../src/**/*.stories.tsx'], 3 | addons: [ 4 | '@storybook/preset-create-react-app', 5 | '@storybook/addon-actions', 6 | '@storybook/addon-links' 7 | ] 8 | }; 9 | -------------------------------------------------------------------------------- /.storybook/netlify.toml: -------------------------------------------------------------------------------- 1 | 2 | # COMMENT: This a rule for Single Page Applications 3 | [[redirects]] 4 | from = "/*" 5 | to = "/index.html" 6 | status = 200 -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at peter.zheng88228@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTION.md: -------------------------------------------------------------------------------- 1 | 9 | # Contributing to Vant React 10 | 11 | The following is a set of guidelines for contributing to Vant. Please spend several minutes in reading these guidelines before you create an issue or pull request. 12 | 13 | Anyway, these are just guidelines, not rules, use your best judgment and feel free to propose changes to this document in a pull request. 14 | 15 | ## Opening an Issue 16 | 17 | If you think you have found a bug, or have a new feature idea, please start by making sure it hasn't already been reported or fixed. You can search through existing issues and PRs to see if someone has reported one similar to yours. 18 | 19 | Next, create a new issue that briefly explains the problem, and provides a bit of background as to the circumstances that triggered it, and steps to reproduce it. 20 | 21 | ## Submitting a Pull Request 22 | 23 | It's welcomed to pull request, And there are some tips about that: 24 | 25 | - Before working on a large change, it is best to open an issue first to discuss it with the maintainers. 26 | 27 | - When in doubt, keep your pull requests small. To give a PR the best chance of getting accepted, don't bundle more than one feature or bug fix per pull request. It's always best to create two smaller PRs than one big one. 28 | 29 | - When adding new features or modifying existing, please attempt to include tests to confirm the new behavior. 30 | 31 | - Rebase before creating a PR to keep commit history clear. 32 | 33 | - Create a brunch name as the standard “contributer firstname/component name or feature name. 34 | 35 | - Add your branch name as PR title. 36 | 37 | - Add some descriptions and refer relative issues for your PR. 38 | 39 | ## Coding suggestion 40 | 41 | It's the suggestions for your coding 42 | 43 | - To define a prop as `string | ReactNode` if possible, developers usually need to support putting a DOM element in the place where the string is placed. 44 | 45 | ## Getting started 46 | 47 | ``` 48 | git clone https://github.com/mxdi9i7/vant-react.git 49 | 50 | cd vant-react 51 | 52 | npm install 53 | 54 | npm run storybook 55 | # open http://localhost:9009 56 | ``` 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Vant React Open Source Software. and its affiliates. 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 | # **Vant React** 2 | 3 | [![npm version](https://badge.fury.io/js/vant-react.svg)](https://badge.fury.io/js/vant-react) 4 | [![NPM](https://img.shields.io/npm/l/vant-react)](LICENSE) 5 | ![Test CI](https://github.com/mxdi9i7/vant-react/workflows/Test%20CI/badge.svg) 6 | [![Netlify Status](https://api.netlify.com/api/v1/badges/30ddabc0-3eb6-4530-ab08-58db247a2b48/deploy-status)](https://vant.bctc.io) 7 | [![Storybook](https://cdn.jsdelivr.net/gh/storybookjs/brand@master/badge/badge-storybook.svg)](https://vant.bctc.io) 8 | 9 | Lightweight Mobile UI Components built on Typescript and React in under 2kb! 10 | 11 | ## **Features** 12 | 13 | - Support Typescript 14 | - 60+ Reusable components 15 | - 100% Storybook coverage: [https://vant.bctc.io](https://vant.bctc.io) 16 | - Extensive documentation and demos 17 | 18 | ## Install 19 | 20 | ```text 21 | # Using npm 22 | npm i vant-react -S 23 | 24 | # Using yarn 25 | yarn add vant-react 26 | ``` 27 | 28 | ## Quickstart 29 | 30 | ```text 31 | import React from 'react'; 32 | import { Button } from 'vant-react'; 33 | import 'vant-react/dist/index.css'; 34 | 35 | const App = () => { 36 | return ( 37 | 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | 20 | export const PlainButtons = () => ( 21 |
22 | 25 | 28 |
29 | ); 30 | 31 | export const HairlineButtons = () => ( 32 |
33 | 36 | 39 |
40 | ); 41 | 42 | export const DisabledButtons = () => ( 43 |
44 | 47 | 50 |
51 | ); 52 | 53 | export const LoadingButtons = () => ( 54 |
55 | 58 | 61 | 64 | 67 |
68 | ); 69 | 70 | export const ButtonShapes = () => ( 71 |
72 | 75 | 78 |
79 | ); 80 | 81 | export const ButtonSize = () => ( 82 |
83 | 86 | 89 | 92 |
93 | ); 94 | 95 | export const ButtonColor = () => ( 96 |
97 | 98 | 101 | 104 |
105 | ); 106 | 107 | export const ButtonTags = () => ( 108 |
109 | 110 | 111 |
112 | ); 113 | 114 | export const ButtonNativeTypes = () => ( 115 |
116 | 117 | 118 | 119 |
120 | ); 121 | 122 | export const BlockButtons = () => ( 123 |
124 | 125 | 126 |
127 | ); 128 | 129 | export const IconButton = () => ( 130 |
131 | 135 | 138 |
139 | ); 140 | 141 | export const ButtonURL = () => ( 142 |
143 | 146 | 149 | 152 |
153 | ); 154 | 155 | export const ButtonAction = () => ( 156 |
157 | 158 | 159 | 162 |
170 | ); 171 | -------------------------------------------------------------------------------- /src/components/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { renderLoadingIcon, getContrastTextColor, colorType } from './helper'; 4 | import classnames from '../../utils/classNames'; 5 | 6 | import { Props } from './types'; 7 | import Icon from '../Icons'; 8 | 9 | import './index.scss'; 10 | 11 | const baseClass = 'vant-button'; 12 | 13 | export default function Button({ 14 | text, 15 | children, 16 | type = 'default', 17 | plain, 18 | disabled, 19 | loading, 20 | loadingType, 21 | loadingText, 22 | loadingSize, 23 | round, 24 | square, 25 | color, 26 | fontColor, 27 | tag, 28 | nativeType, 29 | block, 30 | url, 31 | replace, 32 | onClick, 33 | onTouchStart, 34 | icon, 35 | hairline, 36 | size = 'normal' 37 | }: Props) { 38 | const CustomTag = tag || 'button'; 39 | const props = { 40 | className: classnames(baseClass, [ 41 | { type }, 42 | { plain: plain || hairline }, 43 | { disabled }, 44 | { loading }, 45 | { round }, 46 | { square }, 47 | { block }, 48 | { hairline }, 49 | { [size]: size }, 50 | { onlyIcon: !text && !children } 51 | ]), 52 | style: {} 53 | }; 54 | 55 | if (nativeType) Object.assign(props, { nativeType }); 56 | 57 | if (loadingSize) 58 | Object.assign(props, { style: { ...props.style, height: loadingSize } }); 59 | 60 | if (fontColor) 61 | Object.assign(props, { style: { ...props.style, color: fontColor } }); 62 | 63 | if (color) { 64 | if (color.indexOf('linear-gradient') === -1) { 65 | Object.assign(props, { 66 | style: { 67 | ...props.style, 68 | color: fontColor || getContrastTextColor(color), 69 | backgroundColor: colorType(color), 70 | borderColor: colorType(color) 71 | } 72 | }); 73 | } else { 74 | Object.assign(props, { 75 | style: { 76 | ...props.style, 77 | color: fontColor || getContrastTextColor(color), 78 | background: color 79 | } 80 | }); 81 | } 82 | } 83 | 84 | if (disabled) 85 | Object.assign(props, { 86 | disabled 87 | }); 88 | 89 | if (url && tag === 'a') { 90 | Object.assign(props, { 91 | href: url 92 | }); 93 | if (replace) { 94 | Object.assign(props, { 95 | target: '_self' 96 | }); 97 | } else { 98 | Object.assign(props, { 99 | target: '_blank' 100 | }); 101 | } 102 | } 103 | 104 | if (onClick) { 105 | Object.assign(props, { 106 | onClick 107 | }); 108 | } 109 | 110 | if (onClick && loading) { 111 | Object.assign(props, { 112 | onClick: () => {} 113 | }); 114 | } 115 | 116 | if (onTouchStart) { 117 | Object.assign(props, { 118 | onTouchStart 119 | }); 120 | } 121 | 122 | if (onTouchStart && loading) { 123 | Object.assign(props, { 124 | onTouchStart: () => {} 125 | }); 126 | } 127 | 128 | const NAV_ICON_SIZE = '16px'; 129 | 130 | return ( 131 | 132 | {icon?.includes('.') || icon?.includes('http') 133 | ? icon && button icon 134 | : icon && } 135 | {loading 136 | ? renderLoadingIcon({ 137 | className: loadingType 138 | ? `${baseClass}__${loadingType}` 139 | : `${baseClass}__circular`, 140 | loadingType, 141 | loadingText, 142 | loadingSize 143 | }) 144 | : text || children} 145 | 146 | ); 147 | } 148 | -------------------------------------------------------------------------------- /src/components/Button/types.ts: -------------------------------------------------------------------------------- 1 | export interface Props { 2 | text?: string; 3 | color?: string; 4 | fontColor?: string; 5 | children?: string; 6 | loadingText?: string; 7 | loadingSize?: string; 8 | size?: 'large' | 'small' | 'mini' | 'normal'; 9 | icon?: string; 10 | hairline?: boolean; 11 | url?: string; 12 | plain?: boolean; 13 | loading?: boolean; 14 | disabled?: boolean; 15 | round?: boolean; 16 | square?: boolean; 17 | replace?: boolean; 18 | block?: boolean; 19 | tag?: 'button' | 'a'; 20 | nativeType?: 'button' | 'submit' | 'reset'; 21 | type?: ButtonTypes; 22 | loadingType?: LoadingTypes; 23 | onClick?: Function; 24 | onTouchStart?: Function; 25 | } 26 | 27 | export interface LoadingIconProps { 28 | className: string; 29 | loadingType: LoadingTypes; 30 | loadingText?: string; 31 | loadingSize?: string; 32 | } 33 | 34 | export type LoadingTypes = 'spinner' | 'circular' | undefined; 35 | 36 | export type ButtonTypes = 'default' | 'primary' | 'warning' | 'info' | 'danger'; 37 | -------------------------------------------------------------------------------- /src/components/Cell/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/colors.scss'; 2 | 3 | $baseClass: 'vant-cell'; 4 | 5 | * { 6 | margin: 0; 7 | padding: 0; 8 | } 9 | 10 | .#{$baseClass}__container { 11 | display: flex; 12 | flex-direction: column; 13 | justify-content: center; 14 | background-color: $default; 15 | width: 100%; 16 | padding: 12px; 17 | margin: 5px 0; 18 | cursor: pointer; 19 | text-decoration: none; 20 | 21 | .#{$baseClass}__block { 22 | display: flex; 23 | justify-content: space-between; 24 | cursor: pointer; 25 | } 26 | 27 | .#{$baseClass}__title, 28 | .#{$baseClass}__content { 29 | display: flex; 30 | align-items: center; 31 | p { 32 | margin: 0 5px; 33 | } 34 | } 35 | 36 | p { 37 | color: $grey; 38 | margin-top: 6px; 39 | } 40 | 41 | i, 42 | span { 43 | color: $black; 44 | margin-right: 6px; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/components/Cell/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Cell from '.'; 4 | import Tag from '../Tag'; 5 | import '../../styles/stories.scss'; 6 | 7 | export default { 8 | title: 'Cell', 9 | component: Cell 10 | }; 11 | 12 | export const BasicUsage = () => ( 13 |
14 | 18 | 23 |
24 | ); 25 | 26 | export const cellIcon = () => ( 27 |
28 | 34 |
35 | ); 36 | 37 | export const cellTag = () => ( 38 |
39 | } 42 | content={{ text: 'Content', fontSize: '12px' }} 43 | /> 44 |
45 | ); 46 | 47 | export const roundCell = () => ( 48 |
49 | 50 |
51 | ); 52 | 53 | export const valueOnly = () => ( 54 |
55 | 56 |
57 | ); 58 | 59 | export const URL = () => ( 60 |
61 | 66 |
67 | ); 68 | 69 | export const checkbox = () => ( 70 |
71 | 77 |
78 | ); 79 | 80 | export const OnClick = () => ( 81 |
82 | { 85 | alert(e); 86 | }} 87 | /> 88 |
89 | ); 90 | -------------------------------------------------------------------------------- /src/components/Cell/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import classnames from '../../utils/classNames'; 4 | 5 | import './index.scss'; 6 | import Icon from '../Icons'; 7 | import Checkbox from '../Checkbox'; 8 | import { IProps } from './types'; 9 | import Radio from '../Radio'; 10 | 11 | const baseClass = 'vant-cell'; 12 | 13 | const Cell = ({ 14 | url, 15 | onClick, 16 | title, 17 | titleIcon, 18 | content, 19 | contentIcon = url || onClick ? { name: 'arrow', size: '12px' } : null, 20 | description, 21 | checkbox, 22 | radio, 23 | tag, 24 | replace, 25 | round 26 | }: IProps) => { 27 | const [isActive, setActive] = useState(false); 28 | 29 | const CustomTag = url ? 'a' : 'div'; 30 | const containerProps = { 31 | className: classnames(`${baseClass}__container`, []), 32 | style: {} 33 | }; 34 | const titleProps = { 35 | className: classnames(`${baseClass}__title`, []) 36 | }; 37 | const contentProps = { 38 | className: classnames(`${baseClass}__content`, []) 39 | }; 40 | 41 | if (round) 42 | Object.assign(containerProps, { 43 | style: { ...containerProps.style, borderRadius: '16px' } 44 | }); 45 | 46 | if (url) { 47 | Object.assign(containerProps, { 48 | href: url 49 | }); 50 | if (replace) { 51 | Object.assign(containerProps, { 52 | target: '_self' 53 | }); 54 | } else { 55 | Object.assign(containerProps, { 56 | target: '_blank' 57 | }); 58 | } 59 | } 60 | 61 | if (onClick) { 62 | Object.assign(containerProps, { 63 | onClick 64 | }); 65 | } 66 | 67 | if (checkbox) { 68 | Object.assign(containerProps, { 69 | onClick: () => { 70 | setActive(!isActive); 71 | } 72 | }); 73 | } 74 | 75 | const renderCustomContent = () => { 76 | if (checkbox) { 77 | return ( 78 | 83 | ); 84 | } else if (radio) { 85 | return ; 86 | } else { 87 | return ( 88 |
89 | {content && ( 90 |

{content.text}

91 | )} 92 | {contentIcon && ( 93 | 94 | )} 95 |
96 | ); 97 | } 98 | }; 99 | 100 | return ( 101 | 102 |
103 |
104 | {titleIcon && } 105 | {title && ( 106 | {title.text} 107 | )} 108 | {tag && tag} 109 |
110 | {renderCustomContent()} 111 |
112 | {description && ( 113 |

{description.text}

114 | )} 115 |
116 | ); 117 | }; 118 | 119 | export default Cell; 120 | -------------------------------------------------------------------------------- /src/components/Cell/types.ts: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react'; 2 | import { IProps as RadioProps } from '../Radio/types'; 3 | import { IProps as CheckboxProps } from '../Checkbox/index'; 4 | 5 | export interface IProps { 6 | title?: { text: string; fontSize: string }; 7 | titleIcon?: { name: string; size: string }; 8 | content?: { text: string; fontSize: string }; 9 | contentIcon?: { name: string; size: string } | null; 10 | description?: { text: string; fontSize: string }; 11 | checkbox?: CheckboxProps; 12 | radio?: RadioProps; 13 | tag?: ReactElement; 14 | url?: string; 15 | replace?: boolean; 16 | round?: boolean; 17 | onClick?: Function; 18 | } 19 | -------------------------------------------------------------------------------- /src/components/Checkbox/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/colors.scss'; 2 | @import '../../styles/spacing.scss'; 3 | @import '../../styles/typography.scss'; 4 | 5 | $baseClass: 'vant-checkbox'; 6 | 7 | .#{$baseClass} { 8 | display: flex; 9 | align-items: center; 10 | @include form-label; 11 | 12 | label { 13 | margin-left: $space-md; 14 | user-select: none; 15 | cursor: pointer; 16 | } 17 | 18 | &:hover { 19 | cursor: pointer; 20 | } 21 | 22 | &__icon-container { 23 | display: inline-flex; 24 | align-items: center; 25 | } 26 | 27 | &__disabled { 28 | color: $placeholder; 29 | user-select: none; 30 | 31 | &:hover { 32 | cursor: not-allowed; 33 | } 34 | 35 | label { 36 | cursor: not-allowed; 37 | } 38 | 39 | i { 40 | color: $placeholder !important; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/components/Checkbox/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Checkbox from '.'; 3 | 4 | import '../../styles/stories.scss'; 5 | 6 | export default { 7 | title: 'Checkbox', 8 | component: Checkbox 9 | }; 10 | 11 | export const BasicUsage = () => ( 12 |
13 | 14 |
15 | ); 16 | 17 | export const Disabled = () => ( 18 |
19 | 20 |
21 | ); 22 | 23 | export const CustomColor = () => ( 24 |
25 | 26 |
27 | ); 28 | 29 | export const LabelDisable = () => ( 30 |
31 | 32 |
33 | ); 34 | 35 | export const OnChange = () => ( 36 |
37 | alert(`Checkbox is checked: ${checked}`)} 40 | /> 41 |
42 | ); 43 | 44 | export const OnClick = () => ( 45 |
46 | alert('clicked')} /> 47 |
48 | ); 49 | -------------------------------------------------------------------------------- /src/components/Checkbox/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | import classnames from '../../utils/classNames'; 4 | 5 | import './index.scss'; 6 | import Icon from '../Icons'; 7 | 8 | export interface IProps { 9 | checked?: boolean; 10 | name?: string; 11 | activeIcon?: string; 12 | inactiveIcon?: string; 13 | checkedColor?: string; 14 | labelText?: string; 15 | disabled?: boolean; 16 | labelDisabled?: boolean; 17 | onChange?: Function; 18 | onClicked?: Function; 19 | } 20 | 21 | const baseClass = 'vant-checkbox'; 22 | 23 | // TODO: Round/Square checkbox 24 | // TODO: Checkbox groups 25 | 26 | const Checkbox = ({ 27 | checked = false, 28 | onChange, 29 | onClicked, 30 | name, 31 | activeIcon = 'checked', 32 | checkedColor = '#1989fa', 33 | labelText, 34 | inactiveIcon = 'passed', 35 | disabled, 36 | labelDisabled 37 | }: IProps) => { 38 | const [isChecked, handleCheck] = useState(checked); 39 | 40 | const handleClick = (e) => { 41 | return onClicked && onClicked(e); 42 | }; 43 | 44 | useEffect(() => { 45 | return onChange && onChange(isChecked); 46 | }, [isChecked]); 47 | 48 | const handleContainerClick = (e) => { 49 | e.preventDefault(); 50 | if (!disabled && !labelDisabled) { 51 | handleCheck(!isChecked); 52 | handleClick(e); 53 | } 54 | }; 55 | 56 | const handleIconClick = (e) => { 57 | e.preventDefault(); 58 | if (!disabled) { 59 | handleCheck(!isChecked); 60 | handleClick(e); 61 | } 62 | }; 63 | 64 | const iconName = isChecked ? activeIcon : inactiveIcon; 65 | const iconColor = disabled ? '#c8c9cc' : checkedColor; 66 | 67 | return ( 68 |
76 |
77 | 78 |
79 | 80 |
81 | ); 82 | }; 83 | 84 | export default Checkbox; 85 | -------------------------------------------------------------------------------- /src/components/Divider/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/variables.scss'; 2 | $baseClass: 'vant-divider'; 3 | 4 | .#{$baseClass} { 5 | display: flex; 6 | align-items: center; 7 | margin: $divider-margin; 8 | color: $divider-text-color; 9 | font-size: $divider-font-size; 10 | line-height: $divider-line-height; 11 | border-color: $divider-border-color; 12 | border-style: solid; 13 | border-width: 0; 14 | 15 | &::before, 16 | &::after { 17 | display: block; 18 | flex: 1; 19 | box-sizing: border-box; 20 | height: 1px; 21 | border-color: inherit; 22 | border-style: inherit; 23 | border-width: $border-width-base 0 0; 24 | } 25 | 26 | &::before { 27 | content: ''; 28 | } 29 | 30 | &__hairline { 31 | &::before, 32 | &::after { 33 | transform: scaleY(0.5); 34 | } 35 | } 36 | 37 | &__dashed { 38 | border-style: dashed; 39 | } 40 | 41 | &__content-center, 42 | &__content-left, 43 | &__content-right { 44 | &::before { 45 | margin-right: $divider-content-padding; 46 | } 47 | 48 | &::after { 49 | margin-left: $divider-content-padding; 50 | content: ''; 51 | } 52 | } 53 | 54 | &__content-left { 55 | &::before { 56 | max-width: $divider-content-left-width; 57 | } 58 | } 59 | 60 | &__content-right { 61 | &::after { 62 | max-width: $divider-content-right-width; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/components/Divider/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Divider from './index'; 3 | 4 | export default { 5 | title: 'Divider', 6 | component: Divider 7 | }; 8 | 9 | export const BasicWithContent = () => { 10 | return 312; 11 | }; 12 | export const BasicWithOutContent = () => { 13 | return ; 14 | }; 15 | 16 | export const leftContent = () => { 17 | return 312; 18 | }; 19 | 20 | export const rightContent = () => { 21 | return 312; 22 | }; 23 | export const dashed = () => { 24 | return 312; 25 | }; 26 | 27 | export const hairline = () => { 28 | return 312; 29 | }; 30 | -------------------------------------------------------------------------------- /src/components/Divider/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import classnames from '../../utils/classNames'; 3 | 4 | import './index.scss'; 5 | 6 | export type contentPosition = 'center' | 'left' | 'right'; 7 | export interface DividerProps { 8 | dashed?: boolean; 9 | hairline?: boolean; 10 | contentPosition?: contentPosition; 11 | className?: string; 12 | } 13 | const baseClass = 'vant-divider'; 14 | const Divider: FC = ({ 15 | dashed = false, 16 | hairline = true, 17 | contentPosition = 'center', 18 | children, 19 | className, 20 | ...restProps 21 | }) => { 22 | if (children) { 23 | className = classnames(baseClass, [ 24 | { dashed }, 25 | { hairline }, 26 | { type: 'content-' + contentPosition } 27 | ]); 28 | } else { 29 | className = classnames(baseClass, [{ dashed }, { hairline }]); 30 | } 31 | return ( 32 |
33 | {children || ''} 34 |
35 | ); 36 | }; 37 | export default Divider; 38 | -------------------------------------------------------------------------------- /src/components/Field/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/colors.scss'; 2 | @import '../../styles/spacing.scss'; 3 | @import '../../styles/variables.scss'; 4 | @import '../../styles/typography.scss'; 5 | 6 | $baseClass: 'vant-field'; 7 | 8 | .#{$baseClass} { 9 | @include field-label; 10 | width: 100%; 11 | background-color: $default; 12 | padding: 10px $space-lg; 13 | display: flex; 14 | overflow: hidden; 15 | position: relative; 16 | 17 | .#{$baseClass}__label { 18 | display: flex; 19 | align-items: center; 20 | width: 90px; 21 | 22 | .vant-icon__container { 23 | margin-right: 5px; 24 | } 25 | } 26 | 27 | .#{$baseClass}__input { 28 | width: 100%; 29 | display: flex; 30 | flex-direction: column; 31 | overflow: visible; 32 | color: $grey; 33 | word-wrap: break-word; 34 | vertical-align: middle; 35 | position: relative; 36 | 37 | .#{$baseClass}__field { 38 | display: flex; 39 | width: inherit; 40 | } 41 | 42 | .vant-icon__container { 43 | padding: 0 5px; 44 | } 45 | 46 | button { 47 | position: absolute; 48 | right: 0; 49 | top: 50%; 50 | transform: translateY(-50%); 51 | } 52 | } 53 | 54 | .#{$baseClass}__error { 55 | color: $danger; 56 | font-size: 12px; 57 | text-align: left; 58 | } 59 | 60 | .#{$baseClass}__word-limit { 61 | margin-top: $space-sm; 62 | font-size: 12px; 63 | color: $word-limit; 64 | line-height: 16px; 65 | text-align: right; 66 | } 67 | 68 | input { 69 | @include field-label; 70 | width: inherit; 71 | outline: none; 72 | display: block; 73 | text-align: left; 74 | line-height: inherit; 75 | border: 0; 76 | resize: none; 77 | padding: 0; 78 | 79 | &::placeholder { 80 | color: $placeholder; 81 | } 82 | } 83 | 84 | &__disabled { 85 | input { 86 | color: $grey; 87 | } 88 | } 89 | 90 | &__error, 91 | &__showWordLimit { 92 | .full { 93 | color: $danger; 94 | } 95 | 96 | .#{$baseClass}__label { 97 | align-items: flex-start; 98 | } 99 | } 100 | 101 | &__input-center { 102 | input { 103 | text-align: center; 104 | } 105 | } 106 | &__input-right { 107 | input { 108 | text-align: right; 109 | } 110 | } 111 | 112 | &__label-center { 113 | .#{$baseClass}__label { 114 | justify-content: center; 115 | } 116 | } 117 | &__label-right { 118 | .#{$baseClass}__label { 119 | justify-content: flex-end; 120 | } 121 | } 122 | 123 | &__error-right { 124 | .#{$baseClass}__error { 125 | text-align: right; 126 | } 127 | } 128 | &__error-center { 129 | .#{$baseClass}__error { 130 | text-align: center; 131 | } 132 | } 133 | 134 | &__required { 135 | .#{$baseClass}__label { 136 | label { 137 | &::before { 138 | position: absolute; 139 | left: $space-md; 140 | color: $danger; 141 | font-size: 14px; 142 | content: '*'; 143 | } 144 | } 145 | } 146 | } 147 | 148 | &__border { 149 | &:not(:last-child)::after { 150 | position: absolute; 151 | box-sizing: border-box; 152 | content: ' '; 153 | pointer-events: none; 154 | right: 0; 155 | bottom: 0; 156 | left: $space-lg; 157 | border-bottom: 1px solid $grey-text; 158 | transform: scaleY(0.5); 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/components/Field/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Field from '.'; 3 | import '../../styles/stories.scss'; 4 | import Button from '../Button'; 5 | 6 | export default { 7 | title: 'Field', 8 | component: Field 9 | }; 10 | 11 | export const BasicUsage = () => ( 12 |
13 | 14 |
15 | ); 16 | 17 | export const RequiredField = () => ( 18 |
19 | 20 |
21 | ); 22 | 23 | export const CustomTypes = () => ( 24 |
25 | 26 | 27 | 28 | 29 | 30 |
31 | ); 32 | 33 | export const Disabled = () => ( 34 |
35 | 36 | 37 |
38 | ); 39 | 40 | export const Colon = () => ( 41 |
42 | 43 |
44 | ); 45 | 46 | export const ShowIcon = () => { 47 | return ( 48 |
49 | 50 | 51 | 52 |
53 | ); 54 | }; 55 | 56 | export const FieldEvents = () => { 57 | const [value, setValue] = useState(''); 58 | const [isFocus, setFocus] = useState(false); 59 | return ( 60 |
61 |

Value: {value}

62 | setValue(e.target.value)} 68 | onClear={() => setValue('')} 69 | clearable 70 | /> 71 | alert('Click event')} 77 | /> 78 | setFocus(true)} 83 | onBlur={() => setFocus(false)} 84 | /> 85 | alert('Input clicked')} 90 | /> 91 | alert('Left Icon Clicked')} 97 | onClickRightIcon={() => alert('Right Icon Clicked')} 98 | /> 99 |
100 | ); 101 | }; 102 | 103 | export const FieldRef = () => { 104 | const [containerRef, setContainerRef] = useState(null); 105 | const [fieldRef, setFieldRef] = useState(null); 106 | const [clickOutside, setClickOutside] = useState(false); 107 | 108 | window.addEventListener('click', (e) => { 109 | if ( 110 | containerRef !== undefined && 111 | // @ts-ignore: Object is possibly 'null'. 112 | containerRef.current && 113 | // @ts-ignore: Object is possibly 'null'. 114 | !containerRef.current.contains(e.target) 115 | ) { 116 | setClickOutside(true); 117 | } else { 118 | setClickOutside(false); 119 | } 120 | }); 121 | 122 | return ( 123 |
124 |

125 | Container Ref element name: 126 | { 127 | // @ts-ignore: Object is possibly 'null'. 128 | containerRef && containerRef.current.localName 129 | } 130 |

131 |

132 | Field Ref element name:{' '} 133 | { 134 | // @ts-ignore: Object is possibly 'null'. 135 | fieldRef && fieldRef.current.localName 136 | } 137 |

138 | setContainerRef(ref)} 143 | getFieldRef={(ref) => setFieldRef(ref)} 144 | /> 145 |
146 | ); 147 | }; 148 | 149 | export const AutoFocus = () => { 150 | return ( 151 |
152 | 153 |
154 | ); 155 | }; 156 | 157 | export const ErrorInfo = () => { 158 | return ( 159 |
160 | 161 |
162 | ); 163 | }; 164 | 165 | export const MaxLengthWordLimit = () => { 166 | const [value, setValue] = useState(''); 167 | return ( 168 |
169 | setValue(e.target.value)} 172 | label='Max length' 173 | maxLength={5} 174 | showWordLimit 175 | /> 176 |
177 | ); 178 | }; 179 | 180 | export const FieldWithButton = () => { 181 | return ( 182 |
183 | alert('Message sent!')} 190 | text='Send SMS' 191 | type='primary' 192 | /> 193 | } 194 | /> 195 |
196 | ); 197 | }; 198 | 199 | const pattern = new RegExp(/^[a-zA-Z]*$/); 200 | 201 | export const Formatter = () => { 202 | const [value, setValue] = useState(''); 203 | return ( 204 |
205 | setValue(e.target.value)} 210 | formatter={(value) => pattern.test(value)} 211 | /> 212 |
213 | ); 214 | }; 215 | 216 | export const LabelUtilities = () => ( 217 |
218 | 219 |
220 | ); 221 | 222 | export const LabelInputAlignment = () => ( 223 |
224 | 231 | 238 | 248 | 258 |
259 | ); 260 | 261 | export const AutoResize = () => ( 262 |
263 | 264 |
265 | ); 266 | -------------------------------------------------------------------------------- /src/components/Field/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect, useState } from 'react'; 2 | 3 | import classnames from '../../utils/classNames'; 4 | 5 | import './index.scss'; 6 | import Icon from '../Icons'; 7 | import { IProps } from './types'; 8 | 9 | const ICON_SIZE = '16px'; 10 | 11 | const baseClass = 'vant-field'; 12 | 13 | // TODO: Resize inputs 14 | 15 | const Field = ({ 16 | value, 17 | type = 'text', 18 | label, 19 | name, 20 | placeholder, 21 | readonly, 22 | disabled, 23 | colon, 24 | labelIcon, 25 | leftIcon, 26 | rightIcon, 27 | clearable, 28 | clickable, 29 | onChange, 30 | onClear, 31 | onClick, 32 | onFocus, 33 | onBlur, 34 | onClickInput, 35 | onClickLeftIcon, 36 | onClickRightIcon, 37 | getContainerRef, 38 | getFieldRef, 39 | autofocus, 40 | error, 41 | errorMessage, 42 | maxLength, 43 | showWordLimit, 44 | button, 45 | formatter = () => true, 46 | labelClass, 47 | labelWidth, 48 | labelAlign = 'left', 49 | inputAlign = 'left', 50 | errorAlign = 'left', 51 | required, 52 | border = true 53 | }: IProps) => { 54 | const [containerFocus, setContainerFocus] = useState(false); 55 | 56 | const handleChange = (e) => { 57 | const inputValue = e.target.value; 58 | if (formatter(inputValue)) { 59 | if (onChange) { 60 | if (!maxLength) { 61 | return onChange(e); 62 | } else { 63 | if ( 64 | (value && value.length < maxLength) || 65 | inputValue.length < maxLength 66 | ) { 67 | return onChange(e); 68 | } 69 | } 70 | } 71 | } 72 | }; 73 | 74 | const handleClick = (e) => { 75 | if (clickable && onClick) { 76 | return onClick(e); 77 | } 78 | }; 79 | 80 | const handleClickInput = (e) => { 81 | if (clickable && onClickInput) { 82 | return onClickInput(e); 83 | } 84 | }; 85 | 86 | const handleFocus = (e) => { 87 | if (onFocus) return onFocus(e); 88 | }; 89 | 90 | const handleBlur = (e) => { 91 | if (onBlur) return onBlur(e); 92 | }; 93 | 94 | const handleClickLeftIcon = (e) => { 95 | if (onClickLeftIcon && clickable) return onClickLeftIcon(e); 96 | }; 97 | 98 | const handleClickRightIcon = (e) => { 99 | if (onClickRightIcon && clickable) return onClickRightIcon(e); 100 | }; 101 | 102 | const fieldContainerRef = useRef(null); 103 | const fieldRef = useRef(null); 104 | 105 | useEffect(() => { 106 | if (getContainerRef) getContainerRef(fieldContainerRef); 107 | if (getFieldRef) getFieldRef(fieldRef); 108 | }, [getContainerRef, getFieldRef]); 109 | 110 | useEffect(() => { 111 | window.addEventListener('click', (e) => { 112 | // @ts-ignore: Object is possibly 'null'. 113 | if (fieldContainerRef?.current?.contains(e.target)) { 114 | setContainerFocus(true); 115 | } else { 116 | setContainerFocus(false); 117 | } 118 | }); 119 | return () => window.removeEventListener('click', () => {}); 120 | }, []); 121 | 122 | const containerProps = { 123 | className: classnames(baseClass, [ 124 | { disabled }, 125 | { readonly }, 126 | { error }, 127 | { showWordLimit }, 128 | { [`input-${inputAlign}`]: inputAlign }, 129 | { [`label-${labelAlign}`]: labelAlign }, 130 | { [`error-${errorAlign}`]: errorAlign }, 131 | { border }, 132 | { required } 133 | ]), 134 | onClick: handleClick, 135 | ref: fieldContainerRef 136 | }; 137 | 138 | const inputProps = { 139 | value, 140 | type, 141 | name, 142 | placeholder: placeholder || label, 143 | disabled, 144 | readOnly: readonly, 145 | ref: fieldRef, 146 | autoFocus: autofocus, 147 | onChange: handleChange, 148 | onBlur: handleBlur, 149 | onFocus: handleFocus, 150 | onClick: handleClickInput 151 | }; 152 | 153 | const labelProps = { 154 | htmlFor: name, 155 | className: labelClass 156 | }; 157 | 158 | const labelContainerProps = { 159 | style: {}, 160 | className: `${baseClass}__label ${labelClass || ''}` 161 | }; 162 | 163 | if (type === 'digit') 164 | Object.assign(inputProps, { inputMode: 'numeric', type: 'tel' }); 165 | 166 | if (labelWidth) 167 | Object.assign(labelContainerProps, { style: { width: labelWidth } }); 168 | 169 | return ( 170 |
171 | {label && ( 172 |
173 | {labelIcon && } 174 | 178 |
179 | )} 180 |
181 |
182 | {leftIcon && ( 183 | 189 | )} 190 | 191 | {clearable && value && containerFocus && ( 192 | 193 | )} 194 | {rightIcon && !clearable && ( 195 | 200 | )} 201 | {button && button} 202 |
203 | {error && errorMessage && ( 204 |
{errorMessage}
205 | )} 206 | {showWordLimit && ( 207 |
212 | {value ? value.length : 0}/{maxLength} 213 |
214 | )} 215 |
216 |
217 | ); 218 | }; 219 | 220 | export default Field; 221 | -------------------------------------------------------------------------------- /src/components/Field/types.ts: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react'; 2 | export type TAlignment = 'center' | 'right' | 'left'; 3 | 4 | export interface IProps { 5 | value?: string; 6 | type?: string; 7 | name?: string; 8 | label?: string; 9 | placeholder?: string; 10 | errorMessage?: string; 11 | labelClass?: string; 12 | labelWidth?: string; 13 | labelAlign?: TAlignment; 14 | inputAlign?: TAlignment; 15 | errorAlign?: TAlignment; 16 | maxLength?: number; 17 | showWordLimit?: boolean; 18 | disabled?: boolean; 19 | readonly?: boolean; 20 | clearable?: boolean; 21 | colon?: boolean; 22 | clickable?: boolean; 23 | autofocus?: boolean; 24 | required?: boolean; 25 | border?: boolean; 26 | error?: boolean; 27 | labelIcon?: string; 28 | leftIcon?: string; 29 | rightIcon?: string; 30 | onChange?: Function; 31 | onClear?: Function; 32 | onClick?: Function; 33 | onFocus?: Function; 34 | onBlur?: Function; 35 | onClickInput?: Function; 36 | onClickLeftIcon?: Function; 37 | onClickRightIcon?: Function; 38 | getContainerRef?: Function; 39 | getFieldRef?: Function; 40 | formatter?: Function; 41 | button?: ReactElement; 42 | } 43 | -------------------------------------------------------------------------------- /src/components/Icons/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../assets/icons/vant-icons/index.scss'; 2 | @import '../../styles/colors.scss'; 3 | @import '../../styles/variables.scss'; 4 | @import '../../styles/global.scss'; 5 | 6 | $baseContainerClass: 'vant-icon__container'; 7 | $baseClass: 'vant-icon'; 8 | 9 | .#{$baseContainerClass} { 10 | display: flex; 11 | justify-content: center; 12 | align-items: center; 13 | position: relative; 14 | 15 | &__dot { 16 | .#{$baseClass}--dot { 17 | position: absolute; 18 | top: 0; 19 | right: 5px; 20 | height: $icon-dot-size; 21 | width: $icon-dot-size; 22 | border-radius: 50%; 23 | background-color: $danger; 24 | font-size: 12px; 25 | z-index: 1; 26 | } 27 | 28 | .#{$baseClass}--badge { 29 | position: absolute; 30 | top: 0; 31 | right: 1; 32 | min-width: 16px; 33 | padding: 0 3px; 34 | color: $default; 35 | font-weight: 500; 36 | font-size: 12px; 37 | line-height: 14px; 38 | text-align: center; 39 | background-color: $danger; 40 | border-radius: 16px; 41 | -webkit-transform: translate(50%, -50%); 42 | transform: translate(50%, -50%); 43 | -webkit-transform-origin: 100%; 44 | transform-origin: 100%; 45 | z-index: 1; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/components/Icons/index.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon from './'; 3 | import IconsConfig from '../../assets/icons/vant-icons/config'; 4 | import '../../styles/stories.scss'; 5 | 6 | export default { 7 | title: 'Icons', 8 | component: Icon 9 | }; 10 | 11 | export const AllIcons = () => ( 12 |
13 |

{IconsConfig.basic.length} Basic Icons

14 |
15 | {IconsConfig.basic.map((v, i) => ( 16 |
17 | 18 | {v} 19 |
20 | ))} 21 |
22 |

{IconsConfig.outline.length} Outline Icons

23 |
24 | {IconsConfig.outline.map((v, i) => ( 25 |
26 | 27 | {v} 28 |
29 | ))} 30 |
31 |

{IconsConfig.filled.length} Filled Icons

32 |
33 | {IconsConfig.filled.map((v, i) => ( 34 |
35 | 36 | {v} 37 |
38 | ))} 39 |
40 |
41 | ); 42 | 43 | export const IconColor = () => ( 44 |
45 | 46 | 47 | 48 | 49 |
50 | ); 51 | 52 | export const IconSize = () => ( 53 |
54 | 55 | 56 | 57 | 58 |
59 | ); 60 | 61 | export const IconDotsAndBadges = () => ( 62 |
63 | 64 | 65 | 66 | 67 | 68 |
69 | ); 70 | export const IconTags = () => ( 71 |
72 | 73 | 74 |
75 | ); 76 | 77 | export const IconAction = () => ( 78 |
79 | window.alert(e.target)} /> 80 |
81 | ); 82 | -------------------------------------------------------------------------------- /src/components/Icons/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './index.scss'; 4 | import classnames from '../../utils/classNames'; 5 | 6 | interface IProps { 7 | name: string; 8 | dot?: boolean; 9 | badge?: string; 10 | color?: string; 11 | size?: string; 12 | classPrefix?: string; 13 | tag?: 'i' | 'span'; 14 | onClick?: Function; 15 | } 16 | 17 | const baseClass = 'vant-icon'; 18 | 19 | export default function Icon({ 20 | name, 21 | dot, 22 | badge, 23 | color, 24 | size, 25 | classPrefix = baseClass, 26 | tag, 27 | onClick 28 | }: IProps) { 29 | const CustomTag = tag || 'i' || 'span'; 30 | const containerProps = { 31 | className: classnames(`${classPrefix}__container`, [ 32 | { 33 | dot: dot || badge 34 | } 35 | ]) 36 | }; 37 | const iconProps = { 38 | className: `${classPrefix} ${classPrefix}-${name}`, 39 | style: { 40 | fontSize: '28px' 41 | } 42 | }; 43 | 44 | if (color) 45 | Object.assign(iconProps, { 46 | style: { 47 | ...iconProps.style, 48 | color 49 | } 50 | }); 51 | 52 | if (size) { 53 | Object.assign(iconProps, { 54 | style: { 55 | ...iconProps.style, 56 | fontSize: size 57 | } 58 | }); 59 | } 60 | if (onClick) { 61 | Object.assign(iconProps, { 62 | onClick 63 | }); 64 | } 65 | 66 | return ( 67 |
68 | {dot && !badge && } 69 | {badge && {badge}} 70 | 71 |
72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /src/components/Image/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/colors.scss'; 2 | @import '../../styles/spacing.scss'; 3 | @import '../../styles/variables.scss'; 4 | @import '../../styles/typography.scss'; 5 | 6 | $baseClass: 'vant-image'; 7 | 8 | .#{$baseClass} { 9 | background-color: #f7f8fa; 10 | &__contain { 11 | img { 12 | object-fit: contain; 13 | } 14 | } 15 | 16 | &__cover { 17 | img { 18 | object-fit: cover; 19 | } 20 | } 21 | 22 | &__fill { 23 | img { 24 | object-fit: fill; 25 | } 26 | } 27 | 28 | &__none { 29 | img { 30 | object-fit: none; 31 | } 32 | } 33 | 34 | &__scale-down { 35 | img { 36 | object-fit: scale-down; 37 | } 38 | } 39 | 40 | &__round { 41 | img { 42 | border-radius: 50%; 43 | } 44 | } 45 | 46 | &__empty { 47 | height: 100px; 48 | width: 100px; 49 | display: flex; 50 | justify-content: center; 51 | align-items: center; 52 | color: $grey; 53 | 54 | circle { 55 | stroke: $grey; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/components/Image/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Image from '.'; 3 | import '../../styles/stories.scss'; 4 | 5 | export default { 6 | title: 'Image', 7 | component: Image 8 | }; 9 | 10 | const dummyImage = 'https://img.yzcdn.cn/vant/cat.jpeg'; 11 | const nonExistentImage = 'https://img.yzcdn.cn/vant/cat123.jpeg'; 12 | 13 | export const BasicUsage = () => ( 14 |
15 | 16 |
17 | ); 18 | 19 | export const FillMode = () => ( 20 |
21 |
22 | 23 | contain 24 |
25 |
26 | 27 | cover 28 |
29 |
30 | 31 | fill 32 |
33 |
34 | 35 | none 36 |
37 |
38 | 39 | scale-down 40 |
41 |
42 | ); 43 | 44 | export const RoundImage = () => ( 45 |
46 |
47 | 48 | contain 49 |
50 |
51 | 52 | cover 53 |
54 |
55 | 56 | fill 57 |
58 |
59 | 60 | none 61 |
62 |
63 | 64 | scale-down 65 |
66 |
67 | ); 68 | 69 | export const Loading = () => ( 70 |
71 | 72 | 73 |
74 | ); 75 | 76 | export const Error = () => ( 77 |
78 | 79 |
80 | ); 81 | -------------------------------------------------------------------------------- /src/components/Image/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Icon from '../Icons'; 3 | import classnames from '../../utils/classNames'; 4 | 5 | import './index.scss'; 6 | import CircularLoading from '../../assets/icons/loaders/Circular'; 7 | 8 | export interface IProps { 9 | src?: string; 10 | fit?: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down'; 11 | alt?: string; 12 | width?: number | string; 13 | height?: number | string; 14 | radius?: number | string; 15 | round?: boolean; 16 | showError?: boolean; 17 | showLoading?: boolean; 18 | errorIcon?: string; 19 | loadingIcon?: string; 20 | loadingSpinner?: boolean; 21 | } 22 | 23 | // TODO: LazyLoad, need lazyLoad component 24 | 25 | const baseClass = 'vant-image'; 26 | 27 | const Image = ({ 28 | src, 29 | fit = 'fill', 30 | alt, 31 | width, 32 | height, 33 | radius = 0, 34 | round = false, 35 | showError = true, 36 | showLoading = true, 37 | errorIcon = 'warning-o', 38 | loadingIcon = 'photo-o', 39 | loadingSpinner = false 40 | }: IProps) => { 41 | const [isError, setError] = useState(false); 42 | const [isLoading, setLoading] = useState(true); 43 | 44 | const className = classnames(baseClass, [ 45 | { 46 | contain: fit === 'contain' 47 | }, 48 | { 49 | cover: fit === 'cover' 50 | }, 51 | { 52 | fill: fit === 'fill' 53 | }, 54 | { 55 | 'scale-down': fit === 'scale-down' 56 | }, 57 | { 58 | none: fit === 'none' 59 | }, 60 | { 61 | round 62 | }, 63 | { 64 | empty: isError || isLoading 65 | } 66 | ]); 67 | 68 | const renderIcon = () => { 69 | if (isLoading && showLoading) { 70 | if (loadingSpinner) return ; 71 | if (!src) return ; 72 | if (isError && showError) { 73 | return ; 74 | } 75 | if (loadingIcon) return ; 76 | } 77 | if (isError && showError) return ; 78 | return null; 79 | }; 80 | 81 | return ( 82 |
83 | {renderIcon()} 84 | {alt} setError(true)} 94 | onLoad={() => setLoading(false)} 95 | /> 96 |
97 | ); 98 | }; 99 | 100 | export default Image; 101 | -------------------------------------------------------------------------------- /src/components/Loading/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/colors.scss'; 2 | @import '../../styles/spacing.scss'; 3 | @import '../../styles/animation.scss'; 4 | 5 | $baseClass: vant-loading; 6 | 7 | @mixin generate-spinner($n, $i: 1) { 8 | @if $i <= $n { 9 | i:nth-of-type(#{$i}) { 10 | transform: rotate($i * 30deg); 11 | opacity: 1 - (0.75 / 12) * ($i - 1); 12 | } 13 | @include generate-spinner($n, ($i + 1)); 14 | } 15 | } 16 | 17 | .#{$baseClass} { 18 | position: relative; 19 | color: #c8c9cc; 20 | font-size: 0; 21 | vertical-align: middle; 22 | 23 | &--spinner { 24 | position: relative; 25 | display: inline-block; 26 | width: 30px; 27 | // compatible for 1.x, users may set width or height in root element 28 | max-width: 100%; 29 | height: 30px; 30 | max-height: 100%; 31 | vertical-align: middle; 32 | animation: vant-rotate 0.8s linear infinite; 33 | 34 | @include generate-spinner(12); 35 | 36 | &__spinner { 37 | animation-timing-function: steps(12); 38 | 39 | i { 40 | position: absolute; 41 | top: 0; 42 | left: 0; 43 | width: 100%; 44 | height: 100%; 45 | 46 | &::before { 47 | display: block; 48 | width: 2px; 49 | height: 25%; 50 | margin: 0 auto; 51 | background-color: currentColor; 52 | border-radius: 40%; 53 | content: ' '; 54 | } 55 | } 56 | } 57 | 58 | &__circular { 59 | animation-duration: 2s; 60 | } 61 | } 62 | 63 | &--circular { 64 | display: block; 65 | width: 100%; 66 | height: 100%; 67 | 68 | circle { 69 | animation: vant-circular 1.5s ease-in-out infinite; 70 | stroke: currentColor; 71 | stroke-width: 3; 72 | stroke-linecap: round; 73 | } 74 | } 75 | 76 | &--text { 77 | display: inline-block; 78 | margin-left: $padding-xs; 79 | color: #c8c9cc; 80 | font-size: 14px; 81 | vertical-align: middle; 82 | } 83 | 84 | &__vertical { 85 | display: flex; 86 | flex-direction: column; 87 | align-items: center; 88 | 89 | .#{$baseClass}--text { 90 | margin: $padding-xs 0 0; 91 | } 92 | } 93 | } 94 | 95 | @keyframes vant-circular { 96 | 0% { 97 | stroke-dasharray: 1, 200; 98 | stroke-dashoffset: 0; 99 | } 100 | 101 | 50% { 102 | stroke-dasharray: 90, 150; 103 | stroke-dashoffset: -40; 104 | } 105 | 106 | 100% { 107 | stroke-dasharray: 90, 150; 108 | stroke-dashoffset: -120; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/components/Loading/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Loading from './index'; 3 | 4 | import '../../styles/stories.scss'; 5 | 6 | export default { 7 | title: 'Loading', 8 | component: Loading 9 | }; 10 | 11 | export const BasicUsage = () => { 12 | return ( 13 |
14 | 15 | 16 |
17 | ); 18 | }; 19 | 20 | export const LoadingText = () => { 21 | return ( 22 |
23 | 24 | 25 |
26 | ); 27 | }; 28 | 29 | export const LoadingColor = () => { 30 | return ( 31 |
32 | 33 | 34 | 40 |
41 | ); 42 | }; 43 | 44 | export const LoadingSize = () => { 45 | return ( 46 |
47 | 48 | 49 | 50 |
51 | ); 52 | }; 53 | 54 | export const LoadingVertical = () => { 55 | return ( 56 |
57 | 58 | 59 | 60 |
61 | ); 62 | }; 63 | -------------------------------------------------------------------------------- /src/components/Loading/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { IProps } from './types'; 3 | import { addUnit, classnames, getSizeStyle } from '../../utils'; 4 | 5 | import './index.scss'; 6 | 7 | const baseClass = 'vant-loading'; 8 | 9 | const SpinIcon = () => { 10 | const arr: JSX.Element[] = []; 11 | for (let i = 0; i < 12; i++) { 12 | arr.push(); 13 | } 14 | return arr; 15 | }; 16 | 17 | const CircularIcon = ( 18 | 22 | 23 | 24 | ); 25 | 26 | const Loading: FC = ({ 27 | size = '30px', 28 | type = 'circular', 29 | color = '#c9c9c9', 30 | text, 31 | textSize = '14px', 32 | textColor = '#c9c9c9', 33 | vertical 34 | }) => { 35 | console.log('loading'); 36 | const contentProps = { 37 | className: classnames(`${baseClass}`, [{ vertical }]), 38 | style: {} 39 | }; 40 | 41 | const iconProps = { 42 | className: classnames(`${baseClass}--spinner`, [{ [type]: type }]), 43 | style: { 44 | color, 45 | ...getSizeStyle(size) 46 | } 47 | }; 48 | 49 | const textProps = { 50 | className: classnames(`${baseClass}--text`, []), 51 | style: { 52 | fontSize: addUnit(textSize), 53 | color: textColor ?? color 54 | } 55 | }; 56 | 57 | return ( 58 |
59 | 60 | {type === 'spinner' ? SpinIcon() : CircularIcon} 61 | 62 | {text && {text}} 63 |
64 | ); 65 | }; 66 | 67 | export default Loading; 68 | -------------------------------------------------------------------------------- /src/components/Loading/types.ts: -------------------------------------------------------------------------------- 1 | export type LoadingType = 'circular' | 'spinner'; 2 | 3 | export interface IProps { 4 | color?: string; 5 | type?: LoadingType; 6 | size?: number | string; 7 | text?: string; 8 | textSize?: number | string; 9 | textColor?: string; 10 | vertical?: boolean; 11 | } 12 | -------------------------------------------------------------------------------- /src/components/Navbar/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/colors.scss'; 2 | @import '../../styles/spacing.scss'; 3 | @import '../../styles/variables.scss'; 4 | @import '../../styles/typography.scss'; 5 | 6 | $baseClass: 'vant-navbar'; 7 | 8 | nav.#{$baseClass} { 9 | position: relative; 10 | display: flex; 11 | justify-content: space-between; 12 | align-items: center; 13 | background-color: $default; 14 | height: $navbar-height; 15 | width: 100%; 16 | border-bottom: 1px solid transparent; 17 | 18 | .#{$baseClass}__title { 19 | @include nav-title; 20 | position: absolute; 21 | left: 50%; 22 | top: 50%; 23 | transform: translate(-50%, -50%); 24 | } 25 | 26 | .#{$baseClass}__left, 27 | .#{$baseClass}__right { 28 | @include nav-link; 29 | display: flex; 30 | align-items: center; 31 | padding: 0 $space-lg; 32 | cursor: pointer; 33 | 34 | .vant-icon__container { 35 | height: auto; 36 | width: auto; 37 | .vant-icon { 38 | color: $info; 39 | } 40 | } 41 | } 42 | 43 | .#{$baseClass}__left { 44 | .vant-icon { 45 | margin-right: $space-sm; 46 | } 47 | } 48 | .#{$baseClass}__right { 49 | .vant-icon { 50 | margin-left: $space-sm; 51 | } 52 | } 53 | 54 | &__fixed { 55 | position: fixed; 56 | top: 0; 57 | } 58 | 59 | &__border { 60 | border-color: $grey-text; 61 | } 62 | 63 | .#{$baseClass}__text--left, 64 | .#{$baseClass}__text--right { 65 | font-weight: 300; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/components/Navbar/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Navbar from '.'; 3 | import '../../styles/stories.scss'; 4 | 5 | export default { 6 | title: 'Navbar', 7 | component: Navbar 8 | }; 9 | 10 | export const NavbarTitle = () => ( 11 |
12 | 13 |
14 | ); 15 | 16 | export const NavbarLeftAndRightText = () => ( 17 |
18 | 19 | 26 | 27 |
28 | ); 29 | 30 | export const NavbarFixed = () => ( 31 |
32 | 39 |
40 | ); 41 | export const NavbarBorder = () => ( 42 |
43 | 50 |
51 | ); 52 | 53 | export const NavbarWithLongTitle = () => ( 54 |
55 | 61 |
62 | ); 63 | 64 | export const NavbarClickHandler = () => ( 65 |
66 | alert(e.target.innerHTML + ' Left Click')} 72 | onClickRight={(e) => alert(e.target.innerHTML + ' Right Click')} 73 | /> 74 |
75 | ); 76 | -------------------------------------------------------------------------------- /src/components/Navbar/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react'; 2 | 3 | import Icon from '../Icons'; 4 | 5 | import classnames from '../../utils/classNames'; 6 | 7 | import './index.scss'; 8 | 9 | interface Props { 10 | title?: string; 11 | leftText?: string; 12 | rightText?: string; 13 | border?: boolean; 14 | fixed?: boolean; 15 | leftIcon?: string; 16 | rightIcon?: string; 17 | onClickLeft?: Function; 18 | onClickRight?: Function; 19 | zIndex?: number; 20 | } 21 | 22 | const baseClass = 'vant-navbar'; 23 | 24 | // TODO: Enable placeholder: Whether to generate a placeholder element when fixed 25 | 26 | export default function Navbar({ 27 | title, 28 | leftText, 29 | rightText, 30 | leftIcon, 31 | rightIcon, 32 | border, 33 | fixed, 34 | zIndex = 1, 35 | onClickLeft = () => {}, 36 | onClickRight = () => {} 37 | }: Props): ReactElement { 38 | const navProps = { 39 | style: { 40 | zIndex 41 | }, 42 | className: classnames(baseClass, [{ border }, { fixed }]) 43 | }; 44 | 45 | const NAV_ICON_SIZE = '16px'; 46 | 47 | return ( 48 | 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /src/components/Popup/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/colors.scss'; 2 | @import '../../styles/variables.scss'; 3 | 4 | $baseClass: 'vant-popup'; 5 | $baseContainerClass: 'vant-popup__container'; 6 | $baseContentClass: 'vant-popup__content'; 7 | 8 | .#{$baseContainerClass} { 9 | visibility: hidden; 10 | position: fixed; 11 | top: 0; 12 | left: 0; 13 | height: 100vh; 14 | width: 100vw; 15 | z-index: 10; 16 | transition: 0.6s; 17 | 18 | &__isActive { 19 | visibility: visible; 20 | background-color: rgba( 21 | $color: $popup-background-color, 22 | $alpha: $popup-alpha 23 | ); 24 | } 25 | 26 | &__center { 27 | display: flex; 28 | justify-content: center; 29 | align-items: center; 30 | } 31 | } 32 | 33 | .#{$baseClass} { 34 | overflow: scroll; 35 | background-color: $default; 36 | display: flex; 37 | justify-content: center; 38 | align-items: center; 39 | transition: 0.5s ease-in-out; 40 | position: fixed; 41 | 42 | &__closeable { 43 | .closeIcon { 44 | position: absolute; 45 | z-index: 2; 46 | 47 | i { 48 | cursor: pointer; 49 | } 50 | } 51 | } 52 | 53 | &__isActive { 54 | transform: translateX(0px) !important; 55 | transform: translateY(0px) !important; 56 | transition: 0.5s ease-in-out; 57 | } 58 | 59 | &__center { 60 | display: none; 61 | } 62 | 63 | &__left { 64 | height: 100vh; 65 | left: 0; 66 | transform: translateX(-100%); 67 | } 68 | 69 | &__right { 70 | height: 100vh; 71 | right: 0; 72 | transform: translateX(100%); 73 | } 74 | 75 | &__top { 76 | width: 100vw; 77 | top: 0; 78 | transform: translateY(-100%); 79 | } 80 | 81 | &__bottom { 82 | width: 100vw; 83 | bottom: 0; 84 | transform: translateY(100%); 85 | } 86 | } 87 | 88 | .#{$baseClass}::-webkit-scrollbar { 89 | width: 0 !important; //chrome and Safari 90 | } 91 | 92 | .#{$baseClass} { 93 | -ms-overflow-style: none; //IE 10+ 94 | overflow: -moz-scrollbars-none; //Firefox 95 | } 96 | 97 | .vant-popup__center.vant-popup__isActive { 98 | display: block; 99 | } 100 | 101 | .#{$baseContentClass} { 102 | width: -moz-fit-content; 103 | width: -webkit-fit-content; 104 | width: fit-content; 105 | height: -moz-fit-content; 106 | height: -webkit-fit-content; 107 | height: fit-content; 108 | display: none; 109 | 110 | &__isActive { 111 | display: block; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/components/Popup/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Popup from './'; 3 | import Button from '../Button'; 4 | import { AllIcons } from '../Icons/index.stories'; 5 | import { Types } from '../Tag/index.stories'; 6 | 7 | import '../../styles/stories.scss'; 8 | 9 | export default { 10 | title: 'Popup', 11 | component: Popup 12 | }; 13 | 14 | export const DefaultPopup = () => { 15 | const [centerPopup, setCenterPopup] = useState(false); 16 | 17 | return ( 18 |
19 |
32 | ); 33 | }; 34 | 35 | export const PopupTypes = () => { 36 | const [leftPopup, setLeftPopup] = useState(false); 37 | const [rightPopup, setRightPopup] = useState(false); 38 | const [topPopup, setTopPopup] = useState(false); 39 | const [bottomPopup, setBottomPopup] = useState(false); 40 | 41 | return ( 42 |
43 |
96 | ); 97 | }; 98 | 99 | export const PopupSize = () => { 100 | const [centerPopupA, setCenterPopupA] = useState(false); 101 | const [centerPopupB, setCenterPopupB] = useState(false); 102 | 103 | return ( 104 |
105 |
130 | ); 131 | }; 132 | 133 | export const PopupContent = () => { 134 | const [centerPopupA, setCenterPopupA] = useState(false); 135 | const [centerPopupB, setCenterPopupB] = useState(false); 136 | 137 | return ( 138 |
139 |
171 | ); 172 | }; 173 | 174 | export const CloseIcon = () => { 175 | const [centerPopupA, setCenterPopupA] = useState(false); 176 | const [centerPopupB, setCenterPopupB] = useState(false); 177 | const [centerPopupC, setCenterPopupC] = useState(false); 178 | 179 | return ( 180 |
181 |
224 | ); 225 | }; 226 | 227 | export const RoundPopup = () => { 228 | const [leftPopup, setLeftPopup] = useState(false); 229 | const [topPopup, setTopPopup] = useState(false); 230 | 231 | return ( 232 |
233 |
265 | ); 266 | }; 267 | 268 | export const PopupColor = () => { 269 | const [centerPopupA, setCenterPopupA] = useState(false); 270 | const [centerPopupB, setCenterPopupB] = useState(false); 271 | 272 | return ( 273 |
274 |
301 | ); 302 | }; 303 | 304 | export const PopupAction = () => { 305 | const [centerPopupA, setCenterPopupA] = useState(false); 306 | 307 | return ( 308 |
309 |
330 | ); 331 | }; 332 | -------------------------------------------------------------------------------- /src/components/Popup/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect } from 'react'; 2 | 3 | import classnames from '../../utils/classNames'; 4 | import Icon from '../Icons'; 5 | import { IProps } from './types'; 6 | 7 | import './index.scss'; 8 | 9 | const baseClass = 'vant-popup'; 10 | 11 | const Popup = ({ 12 | closeable, 13 | text, 14 | content, 15 | borderRadius, 16 | type = 'center', 17 | color, 18 | size, 19 | padding, 20 | isActive, 21 | onSetActive, 22 | onClick, 23 | closeIcon = { name: 'cross', size: '20px' }, 24 | closeIconPosition = { top: '10px', right: '10px' } 25 | }: IProps) => { 26 | const popupRef = useRef(null) || { current: {} }; 27 | 28 | useEffect(() => { 29 | document.addEventListener('click', handleClickOutside, true); 30 | return () => { 31 | document.removeEventListener('click', handleClickOutside, true); 32 | }; 33 | }); 34 | 35 | const containerProps = { 36 | className: classnames(`${baseClass}__container`, [{ isActive }, { type }]), 37 | style: {} 38 | }; 39 | 40 | const popupProps = { 41 | className: classnames(baseClass, [{ closeable }, { isActive }, { type }]), 42 | style: {} 43 | }; 44 | 45 | const contentProps = { 46 | className: classnames(`${baseClass}__content`, [{ isActive }]), 47 | style: {} 48 | }; 49 | 50 | const handleClickOutside = (e) => { 51 | if (popupRef.current && !(popupRef as any).current.contains(e.target)) { 52 | onSetActive(false); 53 | } 54 | }; 55 | 56 | if (size) 57 | Object.assign(popupProps, { 58 | style: { 59 | ...popupProps.style, 60 | width: size.width && size.width, 61 | height: size.height && size.height 62 | } 63 | }); 64 | 65 | if (size) 66 | Object.assign(contentProps, { 67 | style: { 68 | ...contentProps.style, 69 | width: 'inherit', 70 | height: 'inherit' 71 | } 72 | }); 73 | 74 | if (padding) 75 | Object.assign(contentProps, { 76 | style: { 77 | ...contentProps.style, 78 | padding 79 | } 80 | }); 81 | 82 | if (onClick) { 83 | Object.assign(contentProps, { 84 | onClick 85 | }); 86 | } 87 | 88 | if (color) 89 | Object.assign(popupProps, { 90 | style: { 91 | ...popupProps.style, 92 | backgroundColor: color, 93 | borderColor: color 94 | } 95 | }); 96 | 97 | const isInclude: Function = (data: string) => { 98 | return popupProps.className.includes(data); 99 | }; 100 | 101 | if (borderRadius) 102 | Object.assign(popupProps, { 103 | style: { 104 | ...popupProps.style, 105 | borderTopLeftRadius: 106 | (isInclude('right') || isInclude('center') || isInclude('bottom')) && 107 | borderRadius, 108 | borderTopRightRadius: 109 | (isInclude('left') || isInclude('center') || isInclude('bottom')) && 110 | borderRadius, 111 | borderBottomLeftRadius: 112 | (isInclude('right') || isInclude('center') || isInclude('top')) && 113 | borderRadius, 114 | borderBottomRightRadius: 115 | (isInclude('left') || isInclude('center') || isInclude('top')) && 116 | borderRadius 117 | } 118 | }); 119 | 120 | return ( 121 |
122 |
123 | {closeable && ( 124 | { 127 | onSetActive(false); 128 | }} 129 | style={closeIconPosition} 130 | > 131 | 132 | 133 | )} 134 |
135 | {text && ( 136 |

144 | {text.text} 145 |

146 | )} 147 | {content && content} 148 |
149 |
150 |
151 | ); 152 | }; 153 | 154 | export default Popup; 155 | -------------------------------------------------------------------------------- /src/components/Popup/types.ts: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react'; 2 | import { TAlignment } from '../Field/types'; 3 | 4 | export interface IProps { 5 | isActive: boolean; 6 | borderRadius?: string; 7 | size?: { width: string; height: string }; 8 | text?: { 9 | text: string; 10 | color: string; 11 | fontSize: string; 12 | textAlign: TAlignment; 13 | }; 14 | content?: ReactElement; 15 | type?: PopupTypes; 16 | color?: string; 17 | children?: string; 18 | padding?: string; 19 | closeable?: boolean; 20 | closeIcon?: { name: string; size: string }; 21 | closeIconPosition?: object; 22 | onSetActive: Function; 23 | onClick?: Function; 24 | } 25 | 26 | export type PopupTypes = 'center' | 'top' | 'bottom' | 'left' | 'right'; 27 | -------------------------------------------------------------------------------- /src/components/Radio/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/colors.scss'; 2 | 3 | $baseClass: 'vant-radio'; 4 | 5 | .#{$baseClass} { 6 | display: flex; 7 | align-items: center; 8 | justify-content: space-between; 9 | width: 100%; 10 | 11 | label { 12 | margin-left: 5px; 13 | } 14 | 15 | input { 16 | visibility: hidden; 17 | } 18 | 19 | *:hover { 20 | cursor: pointer; 21 | } 22 | 23 | &.#{$baseClass}__disabled { 24 | &:hover, 25 | *:hover { 26 | cursor: not-allowed; 27 | } 28 | label { 29 | color: $placeholder; 30 | } 31 | } 32 | 33 | &.#{$baseClass}__rtl { 34 | flex-direction: row-reverse; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/components/Radio/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Radio from '.'; 3 | 4 | import '../../styles/stories.scss'; 5 | import Cell from '../Cell'; 6 | 7 | export default { 8 | title: 'Radio', 9 | component: Radio 10 | }; 11 | 12 | const FormContainer = ({ children }) => { 13 | return
{children}
; 14 | }; 15 | 16 | export const BasicUsage = () => { 17 | const [checked, setChecked] = useState(false); 18 | return ( 19 |
20 | 21 | setChecked(!checked)} /> 22 | 23 |
24 | ); 25 | }; 26 | 27 | export const RadioDisabled = () => { 28 | const [checked, setChecked] = useState(false); 29 | 30 | return ( 31 |
32 | 33 | setChecked(!checked)} 37 | /> 38 | 39 |
40 | ); 41 | }; 42 | 43 | export const LabelDisabled = () => { 44 | const [checked, setChecked] = useState(false); 45 | 46 | return ( 47 |
48 | 49 | setChecked(!checked)} 53 | /> 54 | 55 |
56 | ); 57 | }; 58 | 59 | export const RadioColor = () => { 60 | const [checked, setChecked] = useState(false); 61 | 62 | return ( 63 |
64 | 65 | setChecked(!checked)} 69 | /> 70 | 71 |
72 | ); 73 | }; 74 | 75 | export const OnClick = () => { 76 | const [checked, setChecked] = useState(false); 77 | 78 | return ( 79 |
80 | 81 | { 84 | setChecked(!checked); 85 | alert(checked); 86 | }} 87 | /> 88 | 89 |
90 | ); 91 | }; 92 | 93 | export const RadioCell = () => { 94 | const [checked, setChecked] = useState(false); 95 | 96 | return ( 97 | <> 98 |
99 | setChecked(!checked) 104 | }} 105 | /> 106 |
107 | 108 | ); 109 | }; 110 | 111 | export const RadioCellRTL = () => { 112 | const [checked, setChecked] = useState(false); 113 | 114 | return ( 115 | <> 116 |
117 | setChecked(!checked) 123 | }} 124 | /> 125 |
126 | 127 | ); 128 | }; 129 | -------------------------------------------------------------------------------- /src/components/Radio/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { MouseEvent } from 'react'; 2 | 3 | import classnames from '../../utils/classNames'; 4 | import Icon from '../Icons'; 5 | 6 | import './index.scss'; 7 | import { IProps } from './types'; 8 | 9 | const baseClass = 'vant-radio'; 10 | 11 | const Radio = ({ 12 | name, 13 | disabled, 14 | checked, 15 | labelDisabled, 16 | checkedColor, 17 | onClick, 18 | rtl, 19 | label = 'radio button' 20 | }: IProps) => { 21 | const handleClick = (event: MouseEvent): void => { 22 | if (!labelDisabled) { 23 | onClick && onClick(event); 24 | } 25 | }; 26 | 27 | const handleRadioClick = (event: MouseEvent): void => { 28 | if (labelDisabled) { 29 | onClick && onClick(event); 30 | } 31 | }; 32 | 33 | const iconName = checked ? 'checked' : 'circle'; 34 | const iconColor = disabled ? '#c8c9cc' : checked ? checkedColor : '#000'; 35 | 36 | // TODO: Add form related inputs here when working on form element 37 | return ( 38 |
46 |
50 | 51 |
52 | 53 |
54 | ); 55 | }; 56 | 57 | export default Radio; 58 | -------------------------------------------------------------------------------- /src/components/Radio/types.ts: -------------------------------------------------------------------------------- 1 | export interface IProps { 2 | name?: string; 3 | disabled?: boolean; 4 | checked: boolean; 5 | labelDisabled?: boolean; 6 | rtl?: boolean; 7 | iconSize?: string; 8 | checkedColor?: string; 9 | onClick: Function; 10 | label?: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/components/Rate/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/colors.scss'; 2 | @import '../../styles/spacing.scss'; 3 | @import '../../styles/typography.scss'; 4 | @import '../../styles/opacity.scss'; 5 | @import '../../styles/variables.scss'; 6 | 7 | $baseClass: 'vant-rate'; 8 | 9 | .#{$baseClass} { 10 | display: flex; 11 | cursor: pointer; 12 | 13 | .#{$baseClass}__icon { 14 | &:last-of-type { 15 | margin-right: 0 !important; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/components/Rate/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Rate from '../Rate'; 3 | import '../../styles/stories.scss'; 4 | 5 | export default { 6 | title: 'Rate', 7 | component: Rate 8 | }; 9 | 10 | export const BasicUsage = () => ( 11 |
12 | 13 |
14 | ); 15 | 16 | export const CustomIcon = () => ( 17 |
18 | 19 |
20 | ); 21 | 22 | export const CustomColor = () => ( 23 |
24 | 25 |
26 | ); 27 | 28 | export const CustomCount = () => ( 29 |
30 | 37 |
38 | ); 39 | 40 | export const Disabled = () => ( 41 |
42 | 49 |
50 | ); 51 | 52 | export const ReadOnly = () => ( 53 |
54 | 61 |
62 | ); 63 | 64 | export const CustomGutter = () => ( 65 |
66 | 73 |
74 | ); 75 | 76 | export const ListenOnChange = () => { 77 | const [currentRate, setRate] = useState(4); 78 | 79 | return ( 80 |
81 |

{currentRate}

82 | setRate(rate)} 84 | currentRate={currentRate} 85 | icon='like' 86 | voidIcon='like-o' 87 | color='#1989fa' 88 | /> 89 |
90 | ); 91 | }; 92 | -------------------------------------------------------------------------------- /src/components/Rate/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import shortid from 'shortid'; 3 | 4 | import classnames from '../../utils/classNames'; 5 | 6 | import './index.scss'; 7 | import Icon from '../Icons'; 8 | import { IProps } from './types'; 9 | import RateIcon from './subcomponents/rate-icon'; 10 | 11 | const baseClass = 'vant-rate'; 12 | 13 | const renderIcon = ( 14 | color, 15 | size, 16 | icon, 17 | numberOfIcons, 18 | handleClick, 19 | isActive, 20 | activeCount, 21 | gutter 22 | ) => { 23 | const icons = new Array(numberOfIcons); 24 | for (let i = 0; i < numberOfIcons; i++) { 25 | icons.push( 26 | 30 | handleClick(isActive ? index : index + activeCount) 31 | } 32 | key={shortid.generate()} 33 | icon={} 34 | className={`${baseClass}__icon`} 35 | /> 36 | ); 37 | } 38 | return icons; 39 | }; 40 | 41 | const Rate = ({ 42 | currentRate = 5, 43 | count = 5, 44 | size = '20px', 45 | icon = 'star', 46 | voidIcon = 'star-o', 47 | gutter = '4px', 48 | color = '#ffd21e', 49 | voidColor = '#c8c9cc', 50 | disabledColor = '#c8c9cc', 51 | allowHalf, 52 | disabled, 53 | readonly, 54 | onChange 55 | }: IProps) => { 56 | const [activeCount, setActiveCount] = useState(currentRate || count); 57 | 58 | const rateProps = { 59 | className: classnames(baseClass, [ 60 | { 61 | allowHalf, 62 | disabled, 63 | readonly 64 | } 65 | ]) 66 | }; 67 | 68 | // TODO: Add half star feature 69 | // TODO: Add touchable feature 70 | 71 | const handleClick = (index) => { 72 | if (!disabled && !readonly) { 73 | const nextRate = index + 1; 74 | setActiveCount(nextRate); 75 | if (!!onChange) onChange(nextRate); 76 | } 77 | }; 78 | 79 | return ( 80 |
81 | {renderIcon( 82 | disabled ? disabledColor : color, 83 | size, 84 | icon, 85 | activeCount, 86 | handleClick, 87 | true, 88 | activeCount, 89 | gutter 90 | )} 91 | {renderIcon( 92 | disabled ? disabledColor : voidColor, 93 | size, 94 | voidIcon, 95 | count - activeCount, 96 | handleClick, 97 | false, 98 | activeCount, 99 | gutter 100 | )} 101 |
102 | ); 103 | }; 104 | 105 | export default Rate; 106 | -------------------------------------------------------------------------------- /src/components/Rate/subcomponents/rate-icon.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react'; 2 | 3 | interface Props { 4 | icon: ReactElement; 5 | className: string; 6 | handleClick: Function; 7 | index: number; 8 | gutter: string; 9 | } 10 | 11 | const RateIcon = ({ handleClick, index, gutter, icon, className }: Props) => { 12 | return ( 13 | handleClick(index)} 16 | className={className} 17 | > 18 | {icon} 19 | 20 | ); 21 | }; 22 | 23 | export default RateIcon; 24 | -------------------------------------------------------------------------------- /src/components/Rate/types.ts: -------------------------------------------------------------------------------- 1 | export interface IProps { 2 | currentRate?: number; 3 | count?: number; 4 | size?: string; 5 | icon?: string; 6 | gutter?: string; 7 | voidIcon?: string; 8 | allowHalf?: boolean; 9 | disabled?: boolean; 10 | readonly?: boolean; 11 | color?: string; 12 | voidColor?: string; 13 | disabledColor?: string; 14 | touchable?: boolean; 15 | onChange?: Function; 16 | } 17 | -------------------------------------------------------------------------------- /src/components/Search/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/colors.scss'; 2 | @import '../../styles/spacing.scss'; 3 | @import '../../styles/variables.scss'; 4 | @import '../../styles/typography.scss'; 5 | 6 | $baseClass: 'vant-search'; 7 | 8 | .#{$baseClass} { 9 | background-color: $default; 10 | width: 100%; 11 | display: flex; 12 | padding: 10px 12px; 13 | position: relative; 14 | align-items: center; 15 | 16 | &:first-of-type(i) { 17 | position: absolute; 18 | left: 8px; 19 | } 20 | 21 | &__round { 22 | .vant-field { 23 | border-radius: 999px; 24 | } 25 | } 26 | 27 | &__showAction { 28 | padding-right: 0; 29 | } 30 | 31 | &__leftIcon { 32 | .vant-field__error { 33 | padding-left: 24px; 34 | } 35 | } 36 | 37 | .vant-field { 38 | background-color: $grey-background; 39 | padding: 0 0 0 8px; 40 | 41 | .vant-field__input { 42 | padding: 5px 8px 5px 0; 43 | 44 | .vant-icon__container { 45 | padding: 0; 46 | } 47 | 48 | input { 49 | width: 100%; 50 | background-color: $grey-background; 51 | border: none; 52 | border-radius: 2px; 53 | padding-left: $space-md; 54 | } 55 | } 56 | } 57 | 58 | &__action { 59 | padding: 0 $space-md; 60 | cursor: pointer; 61 | 62 | button { 63 | min-height: 34px; 64 | } 65 | 66 | .#{$baseClass}__cancel { 67 | @include search-action; 68 | cursor: pointer; 69 | background-color: $default; 70 | border: 0; 71 | padding: 0; 72 | } 73 | } 74 | 75 | &__disabled { 76 | input { 77 | cursor: not-allowed; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/components/Search/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Search from '.'; 3 | import '../../styles/stories.scss'; 4 | import Button from '../Button'; 5 | 6 | export default { 7 | title: 'Search', 8 | component: Search 9 | }; 10 | 11 | export const BasicUsage = () => ( 12 |
13 | 14 | 15 |
16 | ); 17 | 18 | export const CustomLabel = () => ( 19 |
20 | 25 | 26 | 27 |
28 | ); 29 | 30 | export const BackgroundColor = () => ( 31 |
32 | 33 |
34 | ); 35 | 36 | export const MaxLength = () => ( 37 |
38 | 39 |
40 | ); 41 | 42 | export const PlaceholderAutoFocus = () => ( 43 |
44 | 45 | 46 |
47 | ); 48 | 49 | export const SearchActions = () => { 50 | const handleClick = (e) => { 51 | e.preventDefault(); 52 | alert('Action clicked'); 53 | }; 54 | const [value, setValue] = useState(''); 55 | const [focus, setFocus] = useState(false); 56 | return ( 57 |
58 |

Value: {value}

59 | 60 | 61 | 71 | } 72 | /> 73 | alert('Searched')} 76 | /> 77 | 78 | setValue(e.target.value)} 83 | onFocus={() => setFocus(true)} 84 | onBlur={() => setFocus(false)} 85 | /> 86 |
87 | ); 88 | }; 89 | 90 | export const DisabledReadonlyError = () => ( 91 |
92 | 93 | 94 | 95 |
96 | ); 97 | 98 | export const AlignmentAndIcon = () => ( 99 |
100 | 101 | 102 | 103 | 104 |
105 | ); 106 | -------------------------------------------------------------------------------- /src/components/Search/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import classnames from '../../utils/classNames'; 4 | 5 | import './index.scss'; 6 | import { IProps } from './types'; 7 | import Field from '../Field'; 8 | 9 | const baseClass = 'vant-search'; 10 | 11 | const Search = ({ 12 | label, 13 | shape = 'square', 14 | background, 15 | maxLength, 16 | placeholder, 17 | clearable = true, 18 | autofocus, 19 | showAction, 20 | disabled, 21 | readonly, 22 | error, 23 | inputAlign = 'left', 24 | leftIcon = 'search', 25 | rightIcon, 26 | actionText = 'Cancel', 27 | onSearch, 28 | onChange, 29 | onFocus, 30 | onBlur, 31 | onClear, 32 | onCancel, 33 | action, 34 | errorMessage, 35 | labelAlign, 36 | labelWidth 37 | }: IProps) => { 38 | const [value, setValue] = useState(''); 39 | 40 | const handleSearch = (e) => { 41 | e.preventDefault(); 42 | if (onSearch) onSearch(e); 43 | }; 44 | const handleActionClick = (e) => { 45 | e.preventDefault(); 46 | if (onCancel) onCancel(e); 47 | }; 48 | 49 | const handleInput = (e) => { 50 | if (onChange) onChange(e); 51 | setValue(e.target.value); 52 | }; 53 | 54 | const handleFocus = (e) => { 55 | if (onFocus) onFocus(e); 56 | }; 57 | 58 | const handleBlur = (e) => { 59 | if (onBlur) onBlur(e); 60 | }; 61 | 62 | const handleClear = (e) => { 63 | e.preventDefault(); 64 | if (onClear) { 65 | onClear(e); 66 | } 67 | setValue(''); 68 | }; 69 | 70 | const searchProps = { 71 | className: classnames(baseClass, [ 72 | { label }, 73 | { [shape]: shape }, 74 | { disabled }, 75 | { showAction }, 76 | { leftIcon } 77 | ]), 78 | style: {}, 79 | onSubmit: handleSearch 80 | }; 81 | 82 | if (background) 83 | Object.assign(searchProps, { style: { backgroundColor: background } }); 84 | 85 | return ( 86 |
87 | 108 | {showAction && ( 109 |
110 | {action || ( 111 | 117 | )} 118 |
119 | )} 120 | 121 | ); 122 | }; 123 | 124 | export default Search; 125 | -------------------------------------------------------------------------------- /src/components/Search/types.ts: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react'; 2 | import TShape from '../../types/shapes'; 3 | import { TAlignment } from '../Field/types'; 4 | 5 | export interface IProps { 6 | label?: string; 7 | labelWidth?: string; 8 | labelAlign?: TAlignment; 9 | shape?: TShape; 10 | background?: string; 11 | maxLength?: number; 12 | placeholder?: string; 13 | errorMessage?: string; 14 | clearable?: boolean; 15 | autofocus?: boolean; 16 | showAction?: boolean; 17 | disabled?: boolean; 18 | readonly?: boolean; 19 | error?: boolean; 20 | inputAlign?: TAlignment; 21 | leftIcon?: string; 22 | rightIcon?: string; 23 | actionText?: string; 24 | onSearch?: Function; 25 | onChange?: Function; 26 | onFocus?: Function; 27 | onBlur?: Function; 28 | onClear?: Function; 29 | onCancel?: Function; 30 | action?: ReactElement; 31 | } 32 | -------------------------------------------------------------------------------- /src/components/Slider/index.scss: -------------------------------------------------------------------------------- 1 | $baseClass: 'vant-slider'; 2 | $baseWrapperClass: 'vant-slider__wrapper'; 3 | $baseFillClass: 'vant-slider__fill'; 4 | $baseSliderClass: 'vant-slider__slider'; 5 | 6 | .#{$baseWrapperClass} { 7 | position: relative; 8 | display: flex; 9 | align-items: center; 10 | border-radius: 4px; 11 | cursor: pointer; 12 | -webkit-user-select: none; 13 | -moz-user-select: none; 14 | -ms-user-select: none; 15 | user-select: none; 16 | 17 | &__disabled { 18 | opacity: 0.6; 19 | cursor: not-allowed; 20 | } 21 | } 22 | 23 | .#{$baseFillClass} { 24 | display: flex; 25 | justify-content: flex-start; 26 | align-items: center; 27 | border-radius: 4px; 28 | cursor: pointer; 29 | 30 | &__disabled { 31 | cursor: not-allowed; 32 | } 33 | } 34 | 35 | .#{$baseSliderClass} { 36 | position: absolute; 37 | background-color: #fff; 38 | cursor: grab; 39 | border-radius: 50%; 40 | display: flex; 41 | justify-content: center; 42 | align-items: center; 43 | 44 | &__disabled { 45 | cursor: not-allowed; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/components/Slider/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Slider from './'; 3 | 4 | import '../../styles/stories.scss'; 5 | 6 | export default { 7 | title: 'Slider', 8 | component: Slider 9 | }; 10 | 11 | export const BasicUsage = () => { 12 | const [value, setValue] = useState(0); 13 | 14 | return ( 15 |
16 |
17 |

{`Current Value : ${value}`}

18 | 19 |
20 |
21 | ); 22 | }; 23 | 24 | export const SlideRange = () => { 25 | const [value, setValue] = useState(0); 26 | 27 | return ( 28 |
29 |
30 |

{`Current Value : ${value}`}

31 | 36 |
37 |
38 | ); 39 | }; 40 | 41 | export const SlideStep = () => { 42 | const [value, setValue] = useState(0); 43 | 44 | return ( 45 |
46 |
47 |

{`Current Value : ${value}`}

48 | 49 |
50 |
51 | ); 52 | }; 53 | 54 | export const Disabled = () => { 55 | const [value, setValue] = useState(30); 56 | 57 | return ( 58 |
59 |
60 |

{`Current Value : ${value}`}

61 | 62 |
63 |
64 | ); 65 | }; 66 | 67 | export const ShowValue = () => { 68 | const [value, setValue] = useState(30); 69 | 70 | return ( 71 |
72 | 85 |
86 | ); 87 | }; 88 | 89 | export const CustomSize = () => { 90 | const [valueA, setValueA] = useState(80); 91 | const [valueB, setValueB] = useState(-50); 92 | 93 | return ( 94 |
95 |
96 |

{`Current Value : ${valueA}`}

97 | 103 |
104 |
105 |

{`Current Value : ${valueB}`}

106 | 112 |
113 |
114 | ); 115 | }; 116 | 117 | export const CustomStyle = () => { 118 | const [valueA, setValueA] = useState(30); 119 | const [valueB, setValueB] = useState(-50); 120 | 121 | return ( 122 |
123 |
124 |

{`Current Value : ${valueA}`}

125 | 132 |
133 |
134 |

{`Current Value : ${valueB}`}

135 | 142 |
143 |
144 | ); 145 | }; 146 | -------------------------------------------------------------------------------- /src/components/Slider/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | 3 | import classnames from '../../utils/classNames'; 4 | import { IProps } from './types'; 5 | 6 | import './index.scss'; 7 | 8 | const baseClass = 'vant-slider'; 9 | 10 | const Slider = ({ 11 | range = { min: '0px', max: '100px' }, 12 | size = { width: '400px', height: '5px' }, 13 | sliderSize = { width: '20px', height: '20px' }, 14 | sliderStyle = { 15 | color: '#000', 16 | fontSize: '10px', 17 | backgroundColor: '#fff', 18 | borderRadius: '50%', 19 | borderColor: '#000' 20 | }, 21 | disabled, 22 | hasValue, 23 | activeColor = '#4169e1', 24 | inactiveColor = '#d3d3d3', 25 | id, 26 | step = 1, 27 | value = parseInt(range.min), 28 | onSetValue 29 | }: IProps) => { 30 | const { color, fontSize, backgroundColor, borderRadius, borderColor } = 31 | sliderStyle; 32 | const slideRange = parseInt(range.max) - parseInt(range.min); 33 | const sliderOffset = parseInt(sliderSize.width) / 2; 34 | const initialPosition = 35 | (Math.abs(value - parseInt(range.min)) / slideRange) * 36 | parseInt(size.width) - 37 | sliderOffset; 38 | const HIDE_ROUND = 4; 39 | 40 | const wrapperProps = { 41 | className: classnames(`${baseClass}__wrapper`, [{ disabled }]), 42 | style: {} 43 | }; 44 | const fillProps = { 45 | className: classnames(`${baseClass}__fill`, [{ disabled }]), 46 | style: {} 47 | }; 48 | const sliderProps = { 49 | className: classnames(`${baseClass}__slider`, [{ disabled }]), 50 | style: {} 51 | }; 52 | 53 | const handleStep = (value) => { 54 | return ( 55 | Math.round( 56 | Math.max(parseInt(range.min), Math.min(value, parseInt(range.max))) / 57 | step 58 | ) * step 59 | ); 60 | }; 61 | 62 | useEffect(() => { 63 | !disabled && handleSlide(); 64 | }, [value]); 65 | 66 | const handleSlide = () => { 67 | const wrapper = document.getElementById(`wrapper${id}`); 68 | const fill = document.getElementById(`fill${id}`); 69 | const slider = document.getElementById(`slider${id}`); 70 | move(wrapper, slider, fill); 71 | }; 72 | 73 | const getValue = (e, dom1) => { 74 | const result = Math.round( 75 | ((e.pageX - dom1.offsetLeft) / parseInt(size.width)) * slideRange + 76 | parseInt(range.min) 77 | ); 78 | return handleStep(result); 79 | }; 80 | 81 | const move = (dom1, dom2, dom3) => { 82 | const CLICKABLE = 1; 83 | const NON_CLICK = 0; 84 | let drag = NON_CLICK; 85 | dom1.addEventListener('click', function (e) { 86 | if (e.target === dom2) { 87 | } else { 88 | if (e.offsetX > parseInt(size.width)) { 89 | dom2.style.left = size.width; 90 | dom3.style.width = size.width; 91 | } else if (e.offsetX < sliderOffset) { 92 | dom2.style.left = '0px'; 93 | dom3.style.width = '0px'; 94 | } else { 95 | dom2.style.left = `${e.offsetX - sliderOffset}px`; 96 | dom3.style.width = `${e.offsetX - sliderOffset + HIDE_ROUND}px`; 97 | } 98 | } 99 | onSetValue(getValue(e, dom1)); 100 | }); 101 | dom2.addEventListener('mousedown', function () { 102 | drag = CLICKABLE; 103 | }); 104 | document.addEventListener('mouseup', function (e) { 105 | drag = NON_CLICK; 106 | if (e.target === dom1 && e.target === dom3) { 107 | onSetValue(getValue(e, dom1)); 108 | } 109 | }); 110 | document.addEventListener('mousemove', function (e) { 111 | if (e.offsetX && drag === CLICKABLE) { 112 | if (e.pageX > dom1.offsetLeft + parseInt(size.width)) { 113 | dom2.style.left = `${parseInt(size.width) - sliderOffset}px`; 114 | dom3.style.width = `${ 115 | parseInt(size.width) - sliderOffset + HIDE_ROUND 116 | }px`; 117 | onSetValue(handleStep(parseInt(range.max))); 118 | } else if (e.pageX < dom1.offsetLeft) { 119 | dom2.style.left = `${0 - sliderOffset}px`; 120 | dom3.style.width = '0px'; 121 | onSetValue(handleStep(parseInt(range.min))); 122 | } else { 123 | dom2.style.left = `${e.pageX - dom1.offsetLeft - sliderOffset}px`; 124 | dom3.style.width = `${ 125 | e.pageX - dom1.offsetLeft - sliderOffset + HIDE_ROUND 126 | }px`; 127 | onSetValue(getValue(e, dom1)); 128 | } 129 | } 130 | }); 131 | }; 132 | 133 | if (disabled) 134 | Object.assign(wrapperProps, { 135 | disabled 136 | }); 137 | 138 | return ( 139 |
148 |
157 |
171 | {hasValue && ( 172 | 178 | {value} 179 | 180 | )} 181 |
182 |
183 |
184 | ); 185 | }; 186 | 187 | export default Slider; 188 | -------------------------------------------------------------------------------- /src/components/Slider/types.ts: -------------------------------------------------------------------------------- 1 | export interface IProps { 2 | disabled?: boolean; 3 | hasValue?: boolean; 4 | activeColor?: string; 5 | inactiveColor?: string; 6 | size?: { width: string; height: string }; 7 | sliderSize?: { width: string; height: string }; 8 | sliderStyle?: { 9 | color?: string; 10 | fontSize?: string; 11 | backgroundColor?: string; 12 | borderRadius?: string; 13 | borderColor?: string; 14 | }; 15 | range?: { min: string; max: string }; 16 | id?: string; 17 | step?: number; 18 | value: number; 19 | onSetValue: Function; 20 | } 21 | -------------------------------------------------------------------------------- /src/components/Stepper/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/colors.scss'; 2 | @import '../../styles/spacing.scss'; 3 | @import '../../styles/typography.scss'; 4 | @import '../../styles/opacity.scss'; 5 | @import '../../styles/variables.scss'; 6 | @keyframes donut-spin { 7 | 0% { 8 | transform: rotate(0deg); 9 | } 10 | 11 | 100% { 12 | transform: rotate(360deg); 13 | } 14 | } 15 | .stepper { 16 | display: flex; 17 | position: absolute; 18 | left: 50%; 19 | top: 50%; 20 | transform: translate(-50%, -50%); 21 | } 22 | 23 | $baseClass: 'vant-stepper'; 24 | .vant-stepper-container { 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | margin: 0px; 29 | padding: 0px; 30 | 31 | input { 32 | margin: 0px 2px; 33 | border-radius: 5px; 34 | font-size: 10px; 35 | border: none; 36 | display: flex; 37 | justify-content: center; 38 | align-items: center; 39 | text-align: center; 40 | background-color: rgba(128, 128, 128, 0.137); 41 | } 42 | button { 43 | border-radius: 5px; 44 | font-size: 20px; 45 | outline: none; 46 | background-color: rgba(128, 128, 128, 0.137); 47 | border: none; 48 | font-weight: 100; 49 | text-align: center; 50 | display: flex; 51 | justify-content: center; 52 | align-items: center; 53 | padding: 0px; 54 | margin: 0; 55 | position: relative; 56 | } 57 | button:first-child { 58 | ::before { 59 | content: ''; 60 | position: absolute; 61 | border-top: 1px solid black; 62 | 63 | width: 10px; 64 | left: 50%; 65 | top: 50%; 66 | transform: translate(-50%, -50%); 67 | } 68 | } 69 | button:last-child { 70 | ::before { 71 | content: ''; 72 | position: absolute; 73 | border-top: 1px solid black; 74 | width: 10px; 75 | left: 50%; 76 | top: 50%; 77 | transform: translate(-50%, -50%); 78 | } 79 | ::after { 80 | content: ''; 81 | position: absolute; 82 | left: 50%; 83 | top: 50%; 84 | transform: translate(-50%, -50%); 85 | height: 10px; 86 | border-left: 1px solid black; 87 | } 88 | } 89 | } 90 | 91 | button.#{$baseClass} { 92 | width: 28px; 93 | height: 28px; 94 | text-align: center; 95 | display: flex; 96 | justify-content: center; 97 | align-items: center; 98 | 99 | &__disabled { 100 | cursor: not-allowed; 101 | opacity: $button-disabled-opacity; 102 | } 103 | &__theme { 104 | border-radius: 50%; 105 | outline: none; 106 | width: 25px; 107 | height: 25px; 108 | background-color: red; 109 | border-color: white; 110 | .minus::before { 111 | content: ''; 112 | position: absolute; 113 | border-top: 1px solid white; 114 | 115 | width: 10px; 116 | left: 50%; 117 | top: 50%; 118 | transform: translate(-50%, -50%); 119 | } 120 | .add::before { 121 | content: ''; 122 | position: absolute; 123 | border-top: 1px solid white; 124 | width: 10px; 125 | left: 50%; 126 | top: 50%; 127 | transform: translate(-50%, -50%); 128 | } 129 | .add::after { 130 | content: ''; 131 | position: absolute; 132 | left: 50%; 133 | top: 50%; 134 | transform: translate(-50%, -50%); 135 | height: 10px; 136 | border-left: 1px solid white; 137 | } 138 | } 139 | } 140 | input.#{$baseClass} { 141 | width: 32px; 142 | height: 28px; 143 | 144 | &__disableInput { 145 | cursor: not-allowed; 146 | opacity: $button-disabled-opacity; 147 | } 148 | &__theme { 149 | border-radius: 50%; 150 | width: 25px; 151 | height: 25px; 152 | background-color: white; 153 | } 154 | } 155 | .load-background { 156 | display: inline-block; 157 | border: 4px solid rgba(0, 0, 0, 0.1); 158 | border-left-color: white; 159 | border-radius: 50%; 160 | width: 28px; 161 | height: 28px; 162 | animation: donut-spin 1.2s linear infinite; 163 | position: relative; 164 | opacity: 0; 165 | } 166 | .load { 167 | width: 85px; 168 | height: 85px; 169 | position: absolute; 170 | top: 30%; 171 | background-color: rgba(0, 0, 0, 0.836); 172 | display: flex; 173 | justify-content: center; 174 | align-items: center; 175 | border-radius: 10px; 176 | opacity: 0; 177 | pointer-events: none; 178 | } 179 | .load { 180 | position: absolute; 181 | bottom: 150px; 182 | } 183 | -------------------------------------------------------------------------------- /src/components/Stepper/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Stepper from '.'; 3 | import '../../styles/stories.scss'; 4 | 5 | export default { 6 | title: 'Stepper', 7 | component: Stepper 8 | }; 9 | 10 | export const BasicStepper = () => ( 11 |
12 | console.log(value)} /> 13 |
14 | ); 15 | 16 | export const DisableStepper = () => ( 17 |
18 | console.log(value)} /> 19 |
20 | ); 21 | 22 | export const StepStepper = () => ( 23 |
24 | console.log(value)} /> 25 |
26 | ); 27 | 28 | export const RangeStepper = () => ( 29 |
30 | console.log(value)} /> 31 |
32 | ); 33 | 34 | export const SizeStepper = () => ( 35 |
36 | console.log(value)} /> 37 |
38 | ); 39 | 40 | export const RoundStepper = () => ( 41 |
42 | console.log(value)} /> 43 |
44 | ); 45 | 46 | export const DisableInputStepper = () => ( 47 |
48 | console.log(value)} /> 49 |
50 | ); 51 | 52 | export const AsyncStepper = () => ( 53 |
54 | {}} 57 | onChange={(value) => console.log(value)} 58 | /> 59 |
60 | ); 61 | -------------------------------------------------------------------------------- /src/components/Stepper/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, ReactElement, createRef } from 'react'; 2 | 3 | import classnames from '../../utils/classNames'; 4 | 5 | import './index.scss'; 6 | 7 | const baseClass = 'vant-stepper'; 8 | 9 | export interface IProps { 10 | value?: number; 11 | theme?: String | any; 12 | disabled?: Boolean; 13 | disableInput?: Boolean; 14 | min?: number | any; 15 | max?: number | any; 16 | step?: number | any; 17 | longPress?: Boolean; 18 | plus?: Boolean; 19 | minus?: Boolean; 20 | size?: number; 21 | loading?: Boolean; 22 | tag?: ReactElement; 23 | onChange: Function; 24 | onAsyncChange?: Function | any; 25 | } 26 | 27 | export default function Stepper({ 28 | disabled, 29 | step, 30 | min, 31 | max, 32 | disableInput, 33 | size, 34 | theme, 35 | loading, 36 | onChange, 37 | onAsyncChange 38 | }: IProps) { 39 | const [value, setValue] = useState(0); 40 | const [isMinus, setIsMinus] = useState(false); 41 | const [isPlus, setIsPlus] = useState(false); 42 | const [isInput, setIsInput] = useState(false); 43 | 44 | const [minusBt, setMinusBt] = useState({}); 45 | const [plusBt, setPlusBt] = useState({}); 46 | const [inputBt, setInputBt] = useState({}); 47 | const animationDiv = createRef(); 48 | const animationBackgroundDiv = createRef(); 49 | 50 | const handleIncrementBtProps = { 51 | className: classnames(baseClass, [{ disabled }, { theme }]), 52 | style: {} 53 | }; 54 | const handleDecrementProps = { 55 | className: classnames(baseClass, [{ disabled }, { theme }]), 56 | style: {} 57 | }; 58 | const inputProps = { 59 | className: classnames(baseClass, [{ disableInput }, { theme }]), 60 | style: {} 61 | }; 62 | 63 | const handleIncrement = () => { 64 | if (loading) { 65 | const aniNode = animationDiv.current; 66 | const aniBgNode = animationBackgroundDiv.current; 67 | if (aniNode && aniBgNode) { 68 | aniNode.style.opacity = '1'; 69 | aniBgNode.style.opacity = '1'; 70 | } 71 | 72 | const handlePlus = () => { 73 | const nextValue = value + (step || 1); 74 | setValue(nextValue); 75 | 76 | if (aniNode && aniBgNode) { 77 | aniNode.style.opacity = '0'; 78 | aniBgNode.style.opacity = '0'; 79 | } 80 | onChange(nextValue); 81 | onAsyncChange(); 82 | }; 83 | setTimeout(handlePlus, 1000); 84 | } else { 85 | const nextValue = value + (step || 1); 86 | setValue(nextValue); 87 | onChange(nextValue); 88 | } 89 | }; 90 | 91 | const handleDecrement = () => { 92 | setIsPlus(false); 93 | if (loading) { 94 | const aniNode = animationDiv.current; 95 | const aniBgNode = animationBackgroundDiv.current; 96 | if (aniNode && aniBgNode) { 97 | aniNode.style.opacity = '1'; 98 | aniBgNode.style.opacity = '1'; 99 | } 100 | 101 | const decrement = () => { 102 | const nextValue = value - (step || 1); 103 | setValue(nextValue); 104 | onChange(nextValue); 105 | if (aniNode && aniBgNode) { 106 | aniNode.style.opacity = '0'; 107 | aniBgNode.style.opacity = '0'; 108 | } 109 | }; 110 | setTimeout(decrement, 1000); 111 | } else { 112 | const nextValue = value - (step || 1); 113 | if (nextValue >= 0) { 114 | setValue(nextValue); 115 | onChange(nextValue); 116 | } 117 | } 118 | }; 119 | 120 | const handleInputChange = (e) => { 121 | const result = e.target.value; 122 | if (loading) { 123 | const aniNode = animationDiv.current; 124 | const aniBgNode = animationBackgroundDiv.current; 125 | if (aniNode && aniBgNode) { 126 | aniNode.style.opacity = '1'; 127 | aniBgNode.style.opacity = '1'; 128 | } 129 | const changeInput = () => { 130 | setValue(Number(result)); 131 | onChange(Number(result)); 132 | if (aniNode && aniBgNode) { 133 | aniNode.style.opacity = '0'; 134 | aniBgNode.style.opacity = '0'; 135 | } 136 | }; 137 | setTimeout(changeInput, 2000); 138 | } else { 139 | setValue(Number(e.target.value)); 140 | onChange(Number(e.target.value)); 141 | } 142 | }; 143 | 144 | useEffect(() => { 145 | if (disabled) { 146 | const btStyle = { 147 | cursor: 'not-allowed', 148 | opacity: '0.2' 149 | }; 150 | setMinusBt(btStyle); 151 | setPlusBt(btStyle); 152 | setIsPlus(true); 153 | setIsMinus(true); 154 | Object.assign(handleDecrementProps, { disabled }); 155 | Object.assign(handleIncrementBtProps, { disabled }); 156 | } else if (size) { 157 | const Size = `${size}px`; 158 | setMinusBt({ width: Size, height: Size }); 159 | setInputBt({ width: Size, height: Size }); 160 | setPlusBt({ cursor: 'pointer' }); 161 | 162 | if (value === 0 || value === min) { 163 | const btStyle = { 164 | cursor: 'not-allowed', 165 | width: Size, 166 | height: Size, 167 | opacity: '0.2' 168 | }; 169 | const btnStyle = { 170 | cursor: 'pointer', 171 | width: Size, 172 | height: Size 173 | }; 174 | setMinusBt(btStyle); 175 | setPlusBt(btnStyle); 176 | } else if (value === max) { 177 | const btStyle = { 178 | cursor: 'not-allowed', 179 | width: Size, 180 | height: Size, 181 | opacity: '0.2' 182 | }; 183 | setPlusBt(btStyle); 184 | setIsPlus(true); 185 | } else { 186 | const btnStyle = { 187 | cursor: 'pointer', 188 | width: Size, 189 | height: Size 190 | }; 191 | setMinusBt(btnStyle); 192 | setInputBt(btnStyle); 193 | setPlusBt(btnStyle); 194 | } 195 | } else { 196 | if (value === 0 || value === min) { 197 | const btStyle = { cursor: 'not-allowed', opacity: '0.2' }; 198 | const btnStyle = { cursor: 'pointer' }; 199 | setMinusBt(btStyle); 200 | setPlusBt(btnStyle); 201 | } else if (value === max) { 202 | const btStyle = { cursor: 'not-allowed', opacity: '0.2' }; 203 | setPlusBt(btStyle); 204 | setIsPlus(true); 205 | } else { 206 | const btnStyle = { 207 | cursor: 'pointer' 208 | }; 209 | setMinusBt(btnStyle); 210 | setPlusBt(btnStyle); 211 | } 212 | } 213 | }, [value]); 214 | 215 | useEffect(() => { 216 | if (disableInput) { 217 | setIsInput(true); 218 | Object.assign(inputProps, { disabled }); 219 | } 220 | }, [disableInput]); 221 | return ( 222 |
223 | 232 | 239 | {loading && ( 240 |
241 |
242 |
243 | )} 244 | 252 |
253 | ); 254 | } 255 | -------------------------------------------------------------------------------- /src/components/Switch/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/colors.scss'; 2 | 3 | @import '../../styles/variables.scss'; 4 | 5 | $baseClass: 'vant-switch'; 6 | 7 | .#{$baseClass} { 8 | width: 2em; 9 | height: 1em; 10 | border-radius: 1em; 11 | cursor: pointer; 12 | position: relative; 13 | box-sizing: content-box; 14 | display: inline-block; 15 | transition: background-color 0.3s cubic-bezier(0.3, 1.05, 0.4, 1.05); 16 | border: 1px solid rgba(0, 0, 0, 0.1); 17 | 18 | // 开关节点 switch node 19 | &__node { 20 | position: absolute; 21 | width: 1em; 22 | height: 1em; 23 | top: 0; 24 | border-radius: 100%; 25 | background: white; 26 | box-shadow: 0 3px 1px 0 #00000005, 0 2px 2px 0 #00000010, 0 3px 3px 0 #00000005; 27 | transition: transform 0.3s cubic-bezier(0.3, 1.05, 0.4, 1.05); 28 | .circular-loading { 29 | position: relative; 30 | font-size: 0; 31 | vertical-align: middle; 32 | top: 25%; 33 | left: 25%; 34 | width: 50%!important; 35 | height: 50%!important; 36 | line-height: 1; 37 | circle { 38 | stroke: $info 39 | } 40 | } 41 | } 42 | 43 | //开关loading 44 | &__loading { 45 | cursor: not-allowed; 46 | } 47 | 48 | // 开关禁用 switch disable 49 | &__disabled { 50 | cursor: not-allowed; 51 | opacity: .6; 52 | } 53 | 54 | // 开关开启 Switch on 55 | &__checked { 56 | .#{$baseClass}__node { 57 | transform: translateX(1em); 58 | } 59 | } 60 | 61 | } 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/components/Switch/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Switch from '.'; 3 | 4 | import '../../styles/stories.scss'; 5 | 6 | export default { 7 | title: 'Switch', 8 | component: Switch 9 | }; 10 | 11 | export const BasicUsage = () => { 12 | const [checked, setChecked] = useState(false); 13 | const handleChange = (value) => { 14 | console.log(value); 15 | setChecked(!checked); 16 | }; 17 | 18 | const handleClick = (e) => { 19 | console.log(e); 20 | }; 21 | 22 | return ( 23 |
24 | 25 |
26 | ); 27 | }; 28 | 29 | export const DisabledUsage = () => { 30 | return ( 31 |
32 | 33 |
34 | ); 35 | }; 36 | 37 | export const LoadingUsage = () => { 38 | return ( 39 |
40 | 41 | 42 |
43 | ); 44 | }; 45 | 46 | export const SizeUsage = () => { 47 | const [checked, setChecked] = useState(false); 48 | const handleChange = (value) => { 49 | console.log(value); 50 | setChecked(!checked); 51 | }; 52 | return ( 53 |
54 | 55 | 56 | 57 |
58 | ); 59 | }; 60 | 61 | export const ColorUsage = () => { 62 | const [checked, setChecked] = useState(false); 63 | const handleChange = (value) => { 64 | console.log(value); 65 | setChecked(!checked); 66 | }; 67 | return ( 68 |
69 | 77 | 83 | 89 |
90 | ); 91 | }; 92 | -------------------------------------------------------------------------------- /src/components/Switch/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classnames from '../../utils/classNames'; 3 | import './index.scss'; 4 | import { IProps } from './types'; 5 | import CircularLoading from '../../assets/icons/loaders/Circular'; 6 | const baseClass = 'vant-switch'; 7 | 8 | const Switch = ({ 9 | checked = false, 10 | disabled = false, 11 | size = '30px', 12 | activeColor = '#1989fa', 13 | inactiveColor = 'gray', 14 | activeValue = true, 15 | inactiveValue = false, 16 | loading, 17 | onClick, 18 | onChange 19 | }: IProps) => { 20 | const handleClick = (e) => { 21 | if (!disabled && !loading) { 22 | const value = !checked ? activeValue : inactiveValue; 23 | onChange && onChange(value); 24 | onClick && onClick(e); 25 | } 26 | }; 27 | 28 | const containerProps = { 29 | onClick: (e) => handleClick(e), 30 | className: classnames(baseClass, [{ checked }, { disabled }, { loading }]), 31 | style: { 32 | fontSize: typeof size === 'number' ? size + 'px' : size, 33 | backgroundColor: checked ? activeColor : inactiveColor 34 | } 35 | }; 36 | 37 | const renderLoading = () => { 38 | if (loading) { 39 | const color = checked ? activeColor : inactiveColor; 40 | return ( 41 | 42 | ); 43 | } 44 | return ''; 45 | }; 46 | 47 | return ( 48 |
49 |
{renderLoading()}
50 |
51 | ); 52 | }; 53 | 54 | export default Switch; 55 | -------------------------------------------------------------------------------- /src/components/Switch/types.ts: -------------------------------------------------------------------------------- 1 | export interface IProps { 2 | checked?: boolean; 3 | disabled?: boolean; 4 | size?: number | string; 5 | activeColor?: string; 6 | inactiveColor?: string; 7 | activeValue?: any; 8 | inactiveValue?: any; 9 | onClick?: Function; 10 | onChange?: Function; 11 | loading?: boolean; 12 | } 13 | 14 | -------------------------------------------------------------------------------- /src/components/Tag/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/colors.scss'; 2 | 3 | $baseClass: 'vant-tag'; 4 | 5 | .#{$baseClass} { 6 | display: inline-flex; 7 | justify-content: center; 8 | align-items: center; 9 | padding: 0.2em 0.5em; 10 | font-size: 10px; 11 | color: $default; 12 | line-height: normal; 13 | border-radius: 0.2em; 14 | border: 1px solid $grey; 15 | background-color: $grey; 16 | 17 | &__primary { 18 | background-color: $primary; 19 | border-color: $primary; 20 | } 21 | &__info { 22 | background-color: $info; 23 | border-color: $info; 24 | } 25 | &__warning { 26 | background-color: $warning; 27 | border-color: $warning; 28 | } 29 | &__danger { 30 | background-color: $danger; 31 | border-color: $danger; 32 | } 33 | 34 | &__medium { 35 | font-size: 12px; 36 | } 37 | 38 | &__large { 39 | font-size: 14px; 40 | } 41 | 42 | &__plain { 43 | background-color: transparent; 44 | color: $grey; 45 | 46 | &.#{$baseClass}__primary { 47 | color: $primary; 48 | } 49 | &.#{$baseClass}__info { 50 | color: $info; 51 | } 52 | &.#{$baseClass}__warning { 53 | color: $warning; 54 | } 55 | &.#{$baseClass}__danger { 56 | color: $danger; 57 | } 58 | } 59 | 60 | &__round { 61 | border-radius: 999px; 62 | } 63 | 64 | &__mark { 65 | border-top-right-radius: 999px; 66 | border-bottom-right-radius: 999px; 67 | } 68 | 69 | &__closeable { 70 | span { 71 | margin-right: 0; 72 | 73 | .vant-icon { 74 | margin-left: 5px; 75 | cursor: pointer; 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/components/Tag/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Tag from './'; 3 | 4 | import '../../styles/stories.scss'; 5 | 6 | export default { 7 | title: 'Tag', 8 | component: Tag 9 | }; 10 | 11 | export const Types = () => ( 12 |
13 | 14 | 15 | 16 | 17 | 18 |
19 | ); 20 | 21 | export const Plain = () => ( 22 |
23 | 24 | 25 | 26 | 27 | 28 |
29 | ); 30 | 31 | export const Sizes = () => ( 32 |
33 | 34 | 35 | 36 |
37 | ); 38 | 39 | export const CustomColors = () => ( 40 |
41 | 42 | 43 | 44 |
45 | ); 46 | 47 | export const Round = () => ( 48 |
49 | 50 | 51 | 52 | 53 | 54 |
55 | ); 56 | 57 | export const Mark = () => ( 58 |
59 | 60 | 61 | 62 | 63 | 64 |
65 | ); 66 | 67 | export const Closeable = () => ( 68 |
69 | 70 | 71 |
72 | ); 73 | -------------------------------------------------------------------------------- /src/components/Tag/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | 3 | import Icon from '../Icons'; 4 | 5 | import classnames from '../../utils/classNames'; 6 | 7 | import './index.scss'; 8 | import { getContrastTextColor } from '../Button/helper'; 9 | 10 | export interface IProps { 11 | type?: 'default' | 'primary' | 'info' | 'danger' | 'warning'; 12 | text: string; 13 | size?: 'small' | 'medium' | 'large'; 14 | color?: string; 15 | children?: string; 16 | plain?: boolean; 17 | mark?: boolean; 18 | round?: boolean; 19 | closeable?: boolean; 20 | } 21 | 22 | const baseClass = 'vant-tag'; 23 | 24 | // TODO: Fix closeable error 25 | // TODO: Fix tag padding when closeable is true 26 | 27 | const Tag = ({ 28 | type, 29 | closeable, 30 | text, 31 | children, 32 | size = 'small', 33 | color, 34 | plain, 35 | round, 36 | mark 37 | }: IProps) => { 38 | const tagRef = useRef(null) || { current: {} }; 39 | const contrastingColor = color ? getContrastTextColor(color) : 'ffffff'; 40 | const props = { 41 | className: classnames(baseClass, [ 42 | { type }, 43 | { plain }, 44 | { round }, 45 | { mark }, 46 | { closeable }, 47 | { 48 | [size]: size 49 | } 50 | ]), 51 | style: {} 52 | }; 53 | 54 | if (color) 55 | Object.assign(props, { 56 | style: { 57 | ...props.style, 58 | color: contrastingColor, 59 | backgroundColor: `#${color}`, 60 | borderColor: `#${color}` 61 | } 62 | }); 63 | 64 | return ( 65 | 66 | {children || text} 67 | {closeable && ( 68 | { 70 | if (tagRef !== null) { 71 | const current = tagRef.current; 72 | if (current) { 73 | const style = (current as any).style; 74 | style.display = 'none'; 75 | } 76 | } 77 | }} 78 | > 79 | 80 | 81 | )} 82 | 83 | ); 84 | }; 85 | 86 | export default Tag; 87 | -------------------------------------------------------------------------------- /src/components/Toast/CreateToast.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: zhaohui 3 | * @Date: 2021-05-17 14:50:33 4 | * @LastEditTime: 2021-05-26 11:21:13 5 | * @LastEditors: zhaohui 6 | * @Description: 7 | * @FilePath: /vant-react/src/components/Toast/CreateToast.tsx 8 | */ 9 | import ReactDom from 'react-dom'; 10 | import React from 'react'; 11 | import ToastContainer from './ToastContainer'; 12 | import { ToastItemProps, ToastProps, LoadingOption } from './types'; 13 | 14 | const createToast = () => { 15 | const div = document.createElement('div'); 16 | document.body.appendChild(div); 17 | const toast = ReactDom.render(, div); 18 | let defaultProps: ToastProps = { 19 | type: 'message', 20 | position: 'top', 21 | duration: 2000 22 | }; 23 | return { 24 | info: (info: ToastItemProps) => { 25 | toast.pushToastItem(Object.assign({}, defaultProps, info)); 26 | }, 27 | desdroy: () => { 28 | ReactDom.unmountComponentAtNode(div); 29 | }, 30 | setDefaultOptions(info: ToastProps) { 31 | defaultProps = Object.assign({}, defaultProps, info); 32 | }, 33 | Loading: (option: LoadingOption | string) => { 34 | if (typeof option === 'string') { 35 | return toast.pushToastItem( 36 | Object.assign({}, defaultProps, { 37 | message: option, 38 | type: 'loading', 39 | loadingType: 'circular' 40 | }) 41 | ); 42 | } else { 43 | return toast.pushToastItem( 44 | Object.assign({}, defaultProps, { 45 | type: 'loading', 46 | message: option.message, 47 | duration: option.duration, 48 | loadingType: option.type 49 | }) 50 | ); 51 | } 52 | } 53 | }; 54 | }; 55 | export default createToast(); 56 | -------------------------------------------------------------------------------- /src/components/Toast/Toast.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: zhaohui 3 | * @Date: 2021-05-14 09:30:56 4 | * @LastEditTime: 2021-05-26 11:30:38 5 | * @LastEditors: zhaohui 6 | * @Description: 7 | * @FilePath: /vant-react/src/components/Toast/Toast.tsx 8 | */ 9 | import React from 'react'; 10 | import classnames from '../../utils/classNames'; 11 | import { baseClass, ToastProps } from './types'; 12 | import Icon from '../Icons'; 13 | import { renderLoadingIcon } from '../Button/helper'; 14 | 15 | const Toast = ({ 16 | message = '', 17 | position = 'center', 18 | type, 19 | icon, 20 | loadingType = 'spinner' 21 | }: ToastProps) => { 22 | const toastItem = { 23 | className: classnames(`${baseClass}`, [ 24 | { 25 | toastItem: 'toastItem' 26 | }, 27 | { 28 | [`position`]: 'position' 29 | }, 30 | { 31 | [`position__${position}`]: `position__${position}` 32 | }, 33 | { extra: type !== 'message' }, 34 | { 35 | [type === 'message' && icon ? 'user__type' : '']: 36 | type === 'message' && icon ? 'user__type' : '' 37 | } 38 | ]), 39 | style: {} 40 | }; 41 | switch (type) { 42 | case 'checked': 43 | case 'fail': 44 | icon = ; 45 | break; 46 | case 'loading': 47 | icon = renderLoadingIcon({ 48 | loadingType, 49 | className: '', 50 | loadingSize: 'large' 51 | }); 52 | break; 53 | default: 54 | break; 55 | } 56 | const contentStyle = { 57 | className: classnames(`${baseClass}__text`, []), 58 | style: {} 59 | }; 60 | return ( 61 |
62 | {icon} 63 |
{message}
64 |
65 | ); 66 | }; 67 | export default Toast; 68 | -------------------------------------------------------------------------------- /src/components/Toast/ToastContainer.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: zhaohui 3 | * @Date: 2021-05-17 18:50:48 4 | * @LastEditTime: 2021-05-26 11:27:45 5 | * @LastEditors: zhaohui 6 | * @Description: 7 | * @FilePath: /vant-react/src/components/Toast/ToastContainer.tsx 8 | */ 9 | import React, { Component } from 'react'; 10 | import classnames from '../../utils/classNames'; 11 | import Toast from './Toast'; 12 | import './index.scss'; 13 | import { ToastProps, baseClass, ToastItemProps } from './types'; 14 | 15 | interface ToastContainerState { 16 | toastList: ToastProps[]; 17 | } 18 | let timer; 19 | class ToastContainer extends Component { 20 | constructor(props) { 21 | super(props); 22 | this.state = { 23 | toastList: [] 24 | }; 25 | } 26 | 27 | pushToastItem = (info: ToastItemProps): void | Promise => { 28 | const toastItem = Object.assign({}, info, { id: getUid() }); 29 | const { duration } = toastItem; 30 | clearTimeout(timer); 31 | if (toastItem.type === 'loading') { 32 | return new Promise((resolve) => { 33 | this.setState({ toastList: [toastItem] }, () => { 34 | timer = setTimeout(() => { 35 | this.popToast(toastItem.id); 36 | clearTimeout(timer); 37 | resolve(); 38 | }, duration || 2000); 39 | }); 40 | }); 41 | } else { 42 | this.setState({ toastList: [toastItem] }, () => { 43 | timer = setTimeout(() => { 44 | this.popToast(toastItem.id); 45 | }, duration || 2000); 46 | }); 47 | } 48 | }; 49 | 50 | popToast = (id: string) => { 51 | const { toastList } = this.state; 52 | const newToastList: ToastItemProps[] = toastList.filter( 53 | (item: ToastItemProps) => item.id !== id 54 | ); 55 | this.setState({ 56 | toastList: newToastList 57 | }); 58 | }; 59 | 60 | render() { 61 | const toastContainerStyle = { 62 | className: classnames(`${baseClass}__container`, []), 63 | style: {} 64 | }; 65 | const toastMaskStyle = { 66 | className: classnames(`${baseClass}__mask`, []), 67 | style: {} 68 | }; 69 | return ( 70 |
71 | {this.state.toastList.map((item: ToastItemProps) => ( 72 |
73 | {item.overlay ?
: ''} 74 | 75 |
76 | ))} 77 |
78 | ); 79 | } 80 | } 81 | 82 | let toastCount = 0; 83 | const getUid = () => { 84 | return `${baseClass}__container__${new Date().getTime()}__${toastCount++}`; 85 | }; 86 | export default ToastContainer; 87 | -------------------------------------------------------------------------------- /src/components/Toast/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/colors.scss'; 2 | 3 | $baseClass: 'vant-toast'; 4 | 5 | * { 6 | margin: 0; 7 | padding: 0; 8 | } 9 | .#{$baseClass}__container { 10 | position: relative; 11 | z-index: 10000; 12 | .#{$baseClass}__mask { 13 | position: fixed; 14 | left: 0; 15 | top: 0; 16 | width: 100%; 17 | height: 100%; 18 | z-index: 1; 19 | background-color: transparent; 20 | } 21 | .#{$baseClass}__toastItem { 22 | position: fixed; 23 | z-index: 2; 24 | background-color: rgba(0, 0, 0, 0.7); 25 | border-radius: 10px; 26 | color: white; 27 | } 28 | .#{$baseClass}__position { 29 | } 30 | .#{$baseClass}__position__center { 31 | text-align: center; 32 | top: 50%; 33 | left: 50%; 34 | transform: translate3d(-50%, -50%, 0); 35 | z-index: 2; 36 | } 37 | .#{$baseClass}__position__top { 38 | position: fixed; 39 | top: 10%; 40 | left: 50%; 41 | transform: translate3d(-50%, 0, 0); 42 | z-index: 2; 43 | } 44 | .#{$baseClass}__position__bottom { 45 | position: fixed; 46 | bottom: 10%; 47 | left: 50%; 48 | transform: translate3d(-50%, 0, 0); 49 | z-index: 2; 50 | } 51 | .#{$baseClass}__text { 52 | width: fit-content; 53 | min-width: 96px; 54 | min-height: 0; 55 | text-align: center; 56 | font-size: 14px; 57 | line-height: 20px; 58 | white-space: pre-wrap; 59 | color: white; 60 | word-wrap: break-word; 61 | border-radius: 8px; 62 | padding: 8px 12px; 63 | } 64 | .#{$baseClass}__extra { 65 | display: flex; 66 | flex-direction: column; 67 | justify-content: center; 68 | align-items: center; 69 | width: 120px; 70 | min-width: 120px; 71 | min-height: 120px; 72 | text-align: center; 73 | max-width: 70%; 74 | font-size: 14px; 75 | line-height: 20px; 76 | padding: 16px; 77 | white-space: pre-wrap; 78 | color: white; 79 | word-wrap: break-word; 80 | border-radius: 8px; 81 | .#{$baseClass}__text { 82 | width: 100%; 83 | min-width: 0; 84 | margin-top: 8px; 85 | padding: 0; 86 | } 87 | } 88 | .#{$baseClass}__user__type { 89 | display: flex; 90 | flex-direction: column; 91 | justify-content: center; 92 | align-items: center; 93 | width: 120px; 94 | min-width: 120px; 95 | min-height: 120px; 96 | text-align: center; 97 | max-width: 70%; 98 | font-size: 14px; 99 | line-height: 20px; 100 | padding: 16px; 101 | white-space: pre-wrap; 102 | color: white; 103 | word-wrap: break-word; 104 | border-radius: 8px; 105 | .#{$baseClass}__text { 106 | width: 100%; 107 | min-width: 0; 108 | margin-top: 8px; 109 | padding: 0; 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/components/Toast/index.stories.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: zhaohui 3 | * @Date: 2021-05-17 14:21:43 4 | * @LastEditTime: 2021-05-26 11:25:59 5 | * @LastEditors: zhaohui 6 | * @Description: 7 | * @FilePath: /vant-react/src/components/Toast/index.stories.tsx 8 | */ 9 | import React from 'react'; 10 | import Toast from '.'; 11 | import Cell from '../Cell'; 12 | import Icon from '../Icons'; 13 | 14 | import '../../styles/stories.scss'; 15 | 16 | export default { 17 | title: 'Toast', 18 | component: Toast 19 | }; 20 | export const BasicUsage = () => { 21 | return ( 22 |
23 | Toast.info({ message: 'Base Usage' }) 30 | }} 31 | /> 32 | 39 | Toast.info({ message: 'Toast bottom', position: 'bottom' }) 40 | }} 41 | /> 42 | 49 | Toast.info({ message: 'Toast center', position: 'center' }) 50 | }} 51 | /> 52 |
53 | ); 54 | }; 55 | 56 | export const ToastStatus = () => { 57 | return ( 58 |
59 | 66 | Toast.info({ message: 'ToastSuccess', type: 'checked' }) 67 | }} 68 | /> 69 | Toast.info({ message: 'ToastFail', type: 'fail' }) 76 | }} 77 | /> 78 |
79 | ); 80 | }; 81 | export const ToastLoading = () => { 82 | return ( 83 |
84 | Toast.Loading('Loading') 91 | }} 92 | /> 93 | 100 | Toast.Loading({ 101 | type: 'spinner', 102 | message: 'ToastLoadingWithSpinner' 103 | }) 104 | }} 105 | /> 106 | 113 | Toast.Loading({ 114 | type: 'circular', 115 | message: 'ToastLoadingWithCpinner' 116 | }) 117 | }} 118 | /> 119 | toastSync() 126 | }} 127 | /> 128 |
129 | ); 130 | }; 131 | 132 | export const ToastUserSet = () => { 133 | return ( 134 |
135 | 142 | Toast.info({ 143 | message: 'Toast user set icon', 144 | icon: 145 | }) 146 | }} 147 | /> 148 | 155 | Toast.info({ 156 | message: 'Toast user set img', 157 | icon: 158 | }) 159 | }} 160 | /> 161 |
162 | ); 163 | }; 164 | 165 | export const ToastSetDefaultOptionGlobal = () => { 166 | return ( 167 |
168 | 175 | Toast.setDefaultOptions({ 176 | duration: 5000 177 | }) 178 | }} 179 | /> 180 | 187 | Toast.setDefaultOptions({ 188 | duration: 1000 189 | }) 190 | }} 191 | /> 192 |
193 | ); 194 | }; 195 | 196 | const toastSync = () => { 197 | Toast.Loading({ 198 | type: 'circular', 199 | message: 'ToastLoadingBackSync' 200 | }).then(() => { 201 | alert('finished'); 202 | }); 203 | }; 204 | -------------------------------------------------------------------------------- /src/components/Toast/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: zhaohui 3 | * @Date: 2021-05-14 11:42:17 4 | * @LastEditTime: 2021-05-17 15:17:13 5 | * @LastEditors: zhaohui 6 | * @Description: 7 | * @FilePath: /vant-react/src/components/Toast/index.ts 8 | */ 9 | import Toast from './CreateToast'; 10 | 11 | export default Toast; 12 | -------------------------------------------------------------------------------- /src/components/Toast/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: zhaohui 3 | * @Date: 2021-05-14 09:32:24 4 | * @LastEditTime: 2021-05-26 11:17:15 5 | * @LastEditors: zhaohui 6 | * @Description: 7 | * @FilePath: /vant-react/src/components/Toast/types.ts 8 | */ 9 | import React from 'react'; 10 | import { LoadingTypes } from '../Button/types'; 11 | export interface ToastProps { 12 | overlay?: boolean; 13 | message?: React.ReactNode; 14 | type?: 'message' | 'loading' | 'fail' | 'checked'; 15 | position?: 'center' | 'top' | 'bottom'; 16 | icon?: React.ReactNode; 17 | duration?: number; 18 | loadingType?: LoadingTypes; 19 | } 20 | export interface ToastItemProps extends ToastProps { 21 | id?: string; 22 | } 23 | export interface LoadingOption { 24 | type?: LoadingTypes; 25 | duration?: number; 26 | message?: string; 27 | } 28 | export const baseClass = 'vant-toast'; 29 | -------------------------------------------------------------------------------- /src/components/template/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/colors.scss'; 2 | 3 | $baseClass: 'vant-template'; 4 | 5 | .#{$baseClass} { 6 | } 7 | -------------------------------------------------------------------------------- /src/components/template/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Template from './'; 3 | 4 | import '../../styles/stories.scss'; 5 | 6 | export default { 7 | title: 'Component Template', 8 | component: Template 9 | }; 10 | 11 | export const BasicUsage = () => ( 12 |
13 |