├── src ├── react-app-env.d.ts ├── components │ ├── Icon │ │ ├── index.tsx │ │ ├── _style.scss │ │ └── icon.tsx │ ├── Alert │ │ ├── index.tsx │ │ ├── _style.scss │ │ ├── alert.stories.tsx │ │ ├── alert.test.tsx │ │ └── alert.tsx │ ├── Input │ │ ├── index.tsx │ │ ├── input.stories.tsx │ │ ├── input.test.tsx │ │ ├── input.tsx │ │ └── _style.scss │ ├── Button │ │ ├── index.tsx │ │ ├── Button.stories.tsx │ │ ├── _style.scss │ │ ├── button.tsx │ │ └── button.test.tsx │ ├── Upload │ │ ├── index.tsx │ │ ├── dragger.tsx │ │ ├── uploadList.tsx │ │ ├── _style.scss │ │ ├── upload.stories.tsx │ │ └── upload.test.tsx │ ├── Progress │ │ ├── index.tsx │ │ ├── progress.stories.tsx │ │ ├── _style.scss │ │ ├── progress.test.tsx │ │ └── progress.tsx │ ├── Transition │ │ ├── index.tsx │ │ ├── transition.test.tsx │ │ ├── transition.tsx │ │ └── Transition.stories.tsx │ ├── AutoComplete │ │ ├── index.tsx │ │ ├── _style.scss │ │ ├── autoComplete.stories.tsx │ │ └── autoComplete.tsx │ ├── Form │ │ ├── index.tsx │ │ ├── _style.scss │ │ ├── form.tsx │ │ ├── formItem.tsx │ │ └── form.test.tsx │ ├── Tabs │ │ ├── index.tsx │ │ ├── tabItem.tsx │ │ ├── _style.scss │ │ ├── tabs.stories.tsx │ │ ├── tabs.test.tsx │ │ └── tabs.tsx │ ├── Select │ │ ├── index.tsx │ │ ├── option.tsx │ │ ├── select.stories.tsx │ │ ├── _style.scss │ │ └── select.test.tsx │ └── Menu │ │ ├── index.tsx │ │ ├── menuItem.tsx │ │ ├── menu.stories.tsx │ │ ├── _style.scss │ │ ├── menu.tsx │ │ ├── subMenu.tsx │ │ └── menu.test.tsx ├── setupTests.ts ├── styles │ ├── _animation.scss │ ├── index.scss │ └── _mixin.scss ├── index.css ├── hooks │ ├── useDebounce.tsx │ └── useClickOutside.tsx ├── reportWebVitals.ts ├── stories │ ├── header.css │ ├── Header.stories.tsx │ ├── button.css │ ├── Page.stories.tsx │ ├── Button.tsx │ ├── assets │ │ ├── direction.svg │ │ ├── flow.svg │ │ ├── code-brackets.svg │ │ ├── comments.svg │ │ ├── repo.svg │ │ ├── plugin.svg │ │ └── stackalt.svg │ ├── Button.stories.tsx │ ├── page.css │ ├── Header.tsx │ └── Page.tsx ├── index.tsx ├── App.tsx └── welcome.stories.tsx ├── public ├── favicon.ico ├── logo192.png ├── logo512.png ├── robots.txt ├── manifest.json └── index.html ├── dist ├── components │ ├── Icon │ │ ├── index.d.ts │ │ └── icon.d.ts │ ├── Alert │ │ ├── index.d.ts │ │ └── alert.d.ts │ ├── Input │ │ ├── index.d.ts │ │ └── input.d.ts │ ├── Button │ │ ├── index.d.ts │ │ └── button.d.ts │ ├── Upload │ │ ├── index.d.ts │ │ ├── dragger.d.ts │ │ ├── uploadList.d.ts │ │ └── upload.d.ts │ ├── Progress │ │ ├── index.d.ts │ │ └── progress.d.ts │ ├── Transition │ │ ├── index.d.ts │ │ └── transition.d.ts │ ├── AutoComplete │ │ ├── index.d.ts │ │ └── autoComplete.d.ts │ ├── Form │ │ ├── index.d.ts │ │ ├── formItem.d.ts │ │ ├── form.d.ts │ │ └── useStore.d.ts │ ├── Tabs │ │ ├── index.d.ts │ │ ├── tabItem.d.ts │ │ └── tabs.d.ts │ ├── Select │ │ ├── index.d.ts │ │ ├── option.d.ts │ │ └── select.d.ts │ └── Menu │ │ ├── index.d.ts │ │ ├── subMenu.d.ts │ │ ├── menuItem.d.ts │ │ └── menu.d.ts ├── App.d.ts ├── stories │ ├── Page.d.ts │ ├── Header.d.ts │ └── Button.d.ts ├── hooks │ ├── useDebounce.d.ts │ └── useClickOutside.d.ts ├── reportWebVitals.d.ts └── index.d.ts ├── storybook-static ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── 448.621a75c9.iframe.bundle.js.LICENSE.txt ├── 448.aa4e675d93ecbccd4331.manager.bundle.js.LICENSE.txt ├── 720.3b49ac39.iframe.bundle.js.LICENSE.txt ├── 720.ea17b63f36a0cad758c9.manager.bundle.js.LICENSE.txt ├── 338.39166dc4.iframe.bundle.js ├── main.81834fac6f9fd283e63c.manager.bundle.js ├── manifest.json ├── project.json ├── 794.0d1e3911deee4eefd1f9.manager.bundle.js ├── 463.c7cae972.iframe.bundle.js.LICENSE.txt ├── 463.5fe71decd3ffa06e60bb.manager.bundle.js.LICENSE.txt ├── static │ └── media │ │ ├── direction.b770f9af5f20abac0352e73b4676bba2.svg │ │ ├── flow.edad2ac1b0bb28e0ce513d5b7a65f8fe.svg │ │ ├── code-brackets.2e1112d71f1a3ba28d2461481dce689b.svg │ │ ├── comments.a38590896b951b65e7ada9af32d6915d.svg │ │ ├── repo.6d4963229d067828d1326ea3f60f5136.svg │ │ ├── plugin.d494b22808806ebe8ff4c5b276819e72.svg │ │ └── stackalt.dba9fbb33e1e5daf57e0cf575f818e65.svg ├── index.html ├── 824.a95daad82d70a59c564b.manager.bundle.js.LICENSE.txt ├── 320.8d8728bb.iframe.bundle.js.LICENSE.txt └── 192.b37a6581.iframe.bundle.js ├── .storybook ├── preview.js └── main.js ├── .gitignore ├── tsconfig.build.json ├── rollup ├── rollup.esm.config.js ├── rollup.config.js └── rollup.umd.config.js ├── tsconfig.json ├── test.html ├── .github └── workflows │ └── action.yml ├── README.md └── package.json /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coder-XiaoZhuang/betterui/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coder-XiaoZhuang/betterui/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coder-XiaoZhuang/betterui/HEAD/public/logo512.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/components/Icon/index.tsx: -------------------------------------------------------------------------------- 1 | import BetterIcon from "./icon"; 2 | 3 | export default BetterIcon; -------------------------------------------------------------------------------- /dist/components/Icon/index.d.ts: -------------------------------------------------------------------------------- 1 | import BetterIcon from "./icon"; 2 | export default BetterIcon; 3 | -------------------------------------------------------------------------------- /src/components/Alert/index.tsx: -------------------------------------------------------------------------------- 1 | import BetterAlert from './alert'; 2 | 3 | export default BetterAlert; -------------------------------------------------------------------------------- /src/components/Input/index.tsx: -------------------------------------------------------------------------------- 1 | import BetterInput from './input'; 2 | 3 | export default BetterInput; -------------------------------------------------------------------------------- /dist/App.d.ts: -------------------------------------------------------------------------------- 1 | declare function App(): import("react/jsx-runtime").JSX.Element; 2 | export default App; 3 | -------------------------------------------------------------------------------- /dist/components/Alert/index.d.ts: -------------------------------------------------------------------------------- 1 | import BetterAlert from './alert'; 2 | export default BetterAlert; 3 | -------------------------------------------------------------------------------- /dist/components/Input/index.d.ts: -------------------------------------------------------------------------------- 1 | import BetterInput from './input'; 2 | export default BetterInput; 3 | -------------------------------------------------------------------------------- /src/components/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import BetterButton from './button'; 2 | 3 | export default BetterButton; -------------------------------------------------------------------------------- /src/components/Upload/index.tsx: -------------------------------------------------------------------------------- 1 | import BetterUpload from './upload'; 2 | 3 | export default BetterUpload; -------------------------------------------------------------------------------- /storybook-static/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /dist/components/Button/index.d.ts: -------------------------------------------------------------------------------- 1 | import BetterButton from './button'; 2 | export default BetterButton; 3 | -------------------------------------------------------------------------------- /dist/components/Upload/index.d.ts: -------------------------------------------------------------------------------- 1 | import BetterUpload from './upload'; 2 | export default BetterUpload; 3 | -------------------------------------------------------------------------------- /src/components/Progress/index.tsx: -------------------------------------------------------------------------------- 1 | import BetterProgress from './progress'; 2 | 3 | export default BetterProgress; -------------------------------------------------------------------------------- /dist/components/Progress/index.d.ts: -------------------------------------------------------------------------------- 1 | import BetterProgress from './progress'; 2 | export default BetterProgress; 3 | -------------------------------------------------------------------------------- /storybook-static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coder-XiaoZhuang/betterui/HEAD/storybook-static/favicon.ico -------------------------------------------------------------------------------- /storybook-static/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coder-XiaoZhuang/betterui/HEAD/storybook-static/logo192.png -------------------------------------------------------------------------------- /storybook-static/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coder-XiaoZhuang/betterui/HEAD/storybook-static/logo512.png -------------------------------------------------------------------------------- /dist/stories/Page.d.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './page.css'; 3 | export declare const Page: React.VFC; 4 | -------------------------------------------------------------------------------- /src/components/Transition/index.tsx: -------------------------------------------------------------------------------- 1 | import BetterTransition from './transition'; 2 | 3 | export default BetterTransition; -------------------------------------------------------------------------------- /dist/components/Transition/index.d.ts: -------------------------------------------------------------------------------- 1 | import BetterTransition from './transition'; 2 | export default BetterTransition; 3 | -------------------------------------------------------------------------------- /dist/hooks/useDebounce.d.ts: -------------------------------------------------------------------------------- 1 | declare function useDebounce(value: any, delay?: number): any; 2 | export default useDebounce; 3 | -------------------------------------------------------------------------------- /dist/components/AutoComplete/index.d.ts: -------------------------------------------------------------------------------- 1 | import BetterAutoComplete from './autoComplete'; 2 | export default BetterAutoComplete; 3 | -------------------------------------------------------------------------------- /src/components/AutoComplete/index.tsx: -------------------------------------------------------------------------------- 1 | import BetterAutoComplete from './autoComplete'; 2 | 3 | export default BetterAutoComplete; -------------------------------------------------------------------------------- /dist/reportWebVitals.d.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | declare const reportWebVitals: (onPerfEntry?: ReportHandler) => void; 3 | export default reportWebVitals; 4 | -------------------------------------------------------------------------------- /dist/hooks/useClickOutside.d.ts: -------------------------------------------------------------------------------- 1 | import { RefObject } from "react"; 2 | declare function useClickOutside(ref: RefObject, handler: Function): void; 3 | export default useClickOutside; 4 | -------------------------------------------------------------------------------- /dist/components/Upload/dragger.d.ts: -------------------------------------------------------------------------------- 1 | import { FC, ReactNode } from 'react'; 2 | interface DraggerProps { 3 | onFile: (files: FileList) => void; 4 | children?: ReactNode; 5 | } 6 | export declare const Dragger: FC; 7 | export default Dragger; 8 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /storybook-static/448.621a75c9.iframe.bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * Prism: Lightweight, robust, elegant syntax highlighting 3 | * 4 | * @license MIT 5 | * @author Lea Verou 6 | * @namespace 7 | * @public 8 | */ 9 | -------------------------------------------------------------------------------- /src/components/Icon/_style.scss: -------------------------------------------------------------------------------- 1 | .better-icon { 2 | margin: 5px 5px 0 0; 3 | } 4 | .combine-icon { 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | } 9 | 10 | @each $key, $val in $theme-colors { 11 | .icon-#{$key} { 12 | color: $val; 13 | } 14 | } -------------------------------------------------------------------------------- /storybook-static/448.aa4e675d93ecbccd4331.manager.bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * Prism: Lightweight, robust, elegant syntax highlighting 3 | * 4 | * @license MIT 5 | * @author Lea Verou 6 | * @namespace 7 | * @public 8 | */ 9 | -------------------------------------------------------------------------------- /dist/components/Form/index.d.ts: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import Form from './form'; 3 | import { FormItemProps } from './formItem'; 4 | export type IFormComponent = typeof Form & { 5 | Item: FC; 6 | }; 7 | declare const BetterForm: IFormComponent; 8 | export default BetterForm; 9 | -------------------------------------------------------------------------------- /dist/components/Tabs/index.d.ts: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { TabsProps } from './tabs'; 3 | import { TabItemProps } from './tabItem'; 4 | export type ITabsComponent = FC & { 5 | Item: FC; 6 | }; 7 | declare const BetterTabs: ITabsComponent; 8 | export default BetterTabs; 9 | -------------------------------------------------------------------------------- /dist/components/Upload/uploadList.d.ts: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { UploadFile } from './upload'; 3 | interface UploadListProps { 4 | fileList: UploadFile[]; 5 | onRemove: (file: UploadFile) => void; 6 | } 7 | export declare const UploadList: FC; 8 | export default UploadList; 9 | -------------------------------------------------------------------------------- /src/styles/_animation.scss: -------------------------------------------------------------------------------- 1 | @include zoom-animation('top', scaleY(0), scaleY(1), center top); 2 | @include zoom-animation('left', scale(.45, .45), scale(1, 1), top left); 3 | @include zoom-animation('right', scale(.45, .45), scale(1, 1), top right); 4 | @include zoom-animation('bottom', scaleY(0), scaleY(1), center bottom); 5 | -------------------------------------------------------------------------------- /storybook-static/720.3b49ac39.iframe.bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * OverlayScrollbars 3 | * https://github.com/KingSora/OverlayScrollbars 4 | * 5 | * Version: 1.13.0 6 | * 7 | * Copyright KingSora | Rene Haas. 8 | * https://github.com/KingSora 9 | * 10 | * Released under the MIT license. 11 | * Date: 02.08.2020 12 | */ 13 | -------------------------------------------------------------------------------- /dist/components/Select/index.d.ts: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { SelectProps } from './select'; 3 | import { SelectOptionProps } from './option'; 4 | export type ISelectComponent = FC & { 5 | Option: FC; 6 | }; 7 | declare const BetterSelect: ISelectComponent; 8 | export default BetterSelect; 9 | -------------------------------------------------------------------------------- /storybook-static/720.ea17b63f36a0cad758c9.manager.bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * OverlayScrollbars 3 | * https://github.com/KingSora/OverlayScrollbars 4 | * 5 | * Version: 1.13.0 6 | * 7 | * Copyright KingSora | Rene Haas. 8 | * https://github.com/KingSora 9 | * 10 | * Released under the MIT license. 11 | * Date: 02.08.2020 12 | */ 13 | -------------------------------------------------------------------------------- /src/components/Form/index.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import Form from './form'; 3 | import Item, { FormItemProps } from './formItem'; 4 | 5 | export type IFormComponent = typeof Form & { 6 | Item: FC; 7 | }; 8 | 9 | const BetterForm: IFormComponent = Form as IFormComponent; 10 | BetterForm.Item = Item; 11 | 12 | export default BetterForm; -------------------------------------------------------------------------------- /src/components/Tabs/index.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import Tabs, { TabsProps } from './tabs'; 3 | import TabItem, { TabItemProps } from './tabItem'; 4 | 5 | export type ITabsComponent = FC & { 6 | Item: FC; 7 | }; 8 | const BetterTabs = Tabs as ITabsComponent; 9 | BetterTabs.Item = TabItem; 10 | 11 | export default BetterTabs; -------------------------------------------------------------------------------- /storybook-static/338.39166dc4.iframe.bundle.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunk_zhuangjiaqing_betterui=self.webpackChunk_zhuangjiaqing_betterui||[]).push([[338],{"./node_modules/react-dom/client.js":(__unused_webpack_module,exports,__webpack_require__)=>{var m=__webpack_require__("./node_modules/react-dom/index.js");exports.createRoot=m.createRoot,exports.hydrateRoot=m.hydrateRoot}}]); -------------------------------------------------------------------------------- /src/components/Select/index.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import Select, { SelectProps } from './select'; 3 | import Option, { SelectOptionProps } from './option'; 4 | 5 | export type ISelectComponent = FC & { 6 | Option: FC, 7 | }; 8 | 9 | const BetterSelect = Select as ISelectComponent; 10 | BetterSelect.Option = Option; 11 | 12 | export default BetterSelect; -------------------------------------------------------------------------------- /dist/components/Menu/index.d.ts: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { MenuProps } from './menu'; 3 | import { SubMenuProps } from './subMenu'; 4 | import { MenuItemProps } from './menuItem'; 5 | export type IMenuComponent = FC & { 6 | Item: FC; 7 | SubMenu: FC; 8 | }; 9 | declare const BetterMenu: IMenuComponent; 10 | export default BetterMenu; 11 | -------------------------------------------------------------------------------- /dist/components/Tabs/tabItem.d.ts: -------------------------------------------------------------------------------- 1 | import React, { FC, ReactNode } from 'react'; 2 | export interface TabItemProps { 3 | /**必填,设置 Tab 选项上面的文字 */ 4 | label: string | React.ReactElement; 5 | /**选填,设置 Tab 选项是否被禁用 */ 6 | disabled?: boolean; 7 | /**选填,设置 Tab 的子元素 */ 8 | children?: ReactNode; 9 | } 10 | export declare const TabItem: FC; 11 | export default TabItem; 12 | -------------------------------------------------------------------------------- /dist/stories/Header.d.ts: -------------------------------------------------------------------------------- 1 | import './header.css'; 2 | type User = { 3 | name: string; 4 | }; 5 | interface HeaderProps { 6 | user?: User; 7 | onLogin: () => void; 8 | onLogout: () => void; 9 | onCreateAccount: () => void; 10 | } 11 | export declare const Header: ({ user, onLogin, onLogout, onCreateAccount }: HeaderProps) => import("react/jsx-runtime").JSX.Element; 12 | export {}; 13 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import { library } from '@fortawesome/fontawesome-svg-core'; 2 | import { fas } from '@fortawesome/free-solid-svg-icons'; 3 | import "../src/styles/index.scss"; 4 | library.add(fas); 5 | 6 | export const parameters = { 7 | actions: { argTypesRegex: "^on[A-Z].*" }, 8 | controls: { 9 | matchers: { 10 | color: /(background|color)$/i, 11 | date: /Date$/, 12 | }, 13 | }, 14 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "esnext", 5 | "target": "es5", 6 | "declaration": true, 7 | "jsx": "react", 8 | "moduleResolution":"Node", 9 | "allowSyntheticDefaultImports": true, 10 | }, 11 | "include": [ 12 | "src" 13 | ], 14 | "exclude": [ 15 | "src/**/*.test.tsx", 16 | "src/**/*.stories.tsx", 17 | "src/setupTests.ts", 18 | ] 19 | } -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "stories": [ 3 | "../src/**/*.stories.mdx", 4 | "../src/**/*.stories.@(js|jsx|ts|tsx)" 5 | ], 6 | "addons": [ 7 | "@storybook/addon-links", 8 | "@storybook/addon-essentials", 9 | "@storybook/addon-interactions", 10 | "@storybook/preset-create-react-app" 11 | ], 12 | "framework": "@storybook/react", 13 | "core": { 14 | "builder": "@storybook/builder-webpack5" 15 | } 16 | } -------------------------------------------------------------------------------- /dist/components/Menu/subMenu.d.ts: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from "react"; 2 | export interface SubMenuProps { 3 | /**选填,设置 SubMenu 的索引值 */ 4 | index?: string; 5 | /**必填,设置 SubMenu 的标题文字 */ 6 | title: string; 7 | /**选填,设置 SubMenu 的自定义类名 */ 8 | className?: string; 9 | /**选填,设置 SubMenu 的子元素 */ 10 | children?: ReactNode; 11 | } 12 | declare const SubMenu: React.FC; 13 | export default SubMenu; 14 | -------------------------------------------------------------------------------- /rollup/rollup.esm.config.js: -------------------------------------------------------------------------------- 1 | import basicConfig from './rollup.config'; 2 | import excludeDependenciesFromBundle from "rollup-plugin-exclude-dependencies-from-bundle"; 3 | 4 | const config = { 5 | ...basicConfig, 6 | output: [ 7 | { 8 | file: 'dist/index.js', 9 | format: 'es' 10 | } 11 | ], 12 | plugins: [ 13 | ...basicConfig.plugins, 14 | excludeDependenciesFromBundle(), 15 | ] 16 | } 17 | 18 | export default config -------------------------------------------------------------------------------- /dist/components/Select/option.d.ts: -------------------------------------------------------------------------------- 1 | import { FC, ReactNode } from 'react'; 2 | export interface SelectOptionProps { 3 | /** 选填,标记选项的索引下标*/ 4 | index?: string; 5 | /** 必填,默认根据此属性值进行筛选,该值不能相同*/ 6 | value: string; 7 | /** 选填,选项的标签,若不设置则默认与 value 相同*/ 8 | label?: string; 9 | /** 选填,是否禁用该选项*/ 10 | disabled?: boolean; 11 | children?: ReactNode; 12 | } 13 | export declare const Option: FC; 14 | export default Option; 15 | -------------------------------------------------------------------------------- /src/hooks/useDebounce.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | function useDebounce(value: any, delay = 300) { 4 | const [debouncedValue, setDebouncedValue] = useState(value); 5 | useEffect(() => { 6 | const handler = window.setTimeout(() => { 7 | setDebouncedValue(value); 8 | }, delay); 9 | return () => { 10 | clearTimeout(handler); 11 | }; 12 | }, [value, delay]); 13 | return debouncedValue; 14 | } 15 | 16 | export default useDebounce; -------------------------------------------------------------------------------- /src/components/Menu/index.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import Menu, { MenuProps } from './menu'; 3 | import SubMenu, { SubMenuProps } from './subMenu'; 4 | import MenuItem, { MenuItemProps } from './menuItem'; 5 | 6 | export type IMenuComponent = FC & { 7 | Item: FC; 8 | SubMenu: FC; 9 | }; 10 | 11 | const BetterMenu = Menu as IMenuComponent; 12 | BetterMenu.Item = MenuItem; 13 | BetterMenu.SubMenu = SubMenu; 14 | 15 | export default BetterMenu; -------------------------------------------------------------------------------- /src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /src/components/Tabs/tabItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, ReactNode } from 'react'; 2 | 3 | export interface TabItemProps { 4 | /**必填,设置 Tab 选项上面的文字 */ 5 | label: string | React.ReactElement; 6 | /**选填,设置 Tab 选项是否被禁用 */ 7 | disabled?: boolean; 8 | /**选填,设置 Tab 的子元素 */ 9 | children?: ReactNode; 10 | }; 11 | 12 | export const TabItem: FC = ({ children }) => { 13 | return ( 14 |
15 | { children } 16 |
17 | ); 18 | }; 19 | 20 | export default TabItem; -------------------------------------------------------------------------------- /dist/components/Menu/menuItem.d.ts: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from "react"; 2 | export interface MenuItemProps { 3 | /**选填,设置 MenuItem 的索引值 */ 4 | index?: string; 5 | /**选填,设置 MenuItem 的自定义类名 */ 6 | className?: string; 7 | /**选填,设置 MenuItem 的禁用 */ 8 | disabled?: boolean; 9 | /**选填,设置 MenuItem 的自定义样式 */ 10 | style?: React.CSSProperties; 11 | /**选填,设置 MenuItem 的子元素 */ 12 | children?: ReactNode; 13 | } 14 | declare const MenuItem: React.FC; 15 | export default MenuItem; 16 | -------------------------------------------------------------------------------- /src/hooks/useClickOutside.tsx: -------------------------------------------------------------------------------- 1 | import { RefObject, useEffect } from "react"; 2 | 3 | function useClickOutside(ref: RefObject, handler: Function) { 4 | useEffect(() => { 5 | const listener = (event: MouseEvent) => { 6 | if (!ref.current || ref.current.contains(event.target as HTMLElement)) { 7 | return; 8 | } 9 | handler(event); 10 | }; 11 | document.addEventListener('click', listener); 12 | return () => { 13 | document.removeEventListener('click', listener); 14 | }; 15 | }, [ref, handler]); 16 | } 17 | 18 | export default useClickOutside; -------------------------------------------------------------------------------- /storybook-static/main.81834fac6f9fd283e63c.manager.bundle.js: -------------------------------------------------------------------------------- 1 | (self.webpackChunk_zhuangjiaqing_betterui=self.webpackChunk_zhuangjiaqing_betterui||[]).push([[792],{42634:()=>{}},__webpack_require__=>{var __webpack_exec__=moduleId=>__webpack_require__(__webpack_require__.s=moduleId);__webpack_require__.O(0,[824],(()=>(__webpack_exec__(17835),__webpack_exec__(60535),__webpack_exec__(7626),__webpack_exec__(51807),__webpack_exec__(12282),__webpack_exec__(55563),__webpack_exec__(27675),__webpack_exec__(18296),__webpack_exec__(57483),__webpack_exec__(11389),__webpack_exec__(4121),__webpack_exec__(667))));__webpack_require__.O()}]); -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /storybook-static/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/stories/header.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); 4 | padding: 15px 20px; 5 | display: flex; 6 | align-items: center; 7 | justify-content: space-between; 8 | } 9 | 10 | svg { 11 | display: inline-block; 12 | vertical-align: top; 13 | } 14 | 15 | h1 { 16 | font-weight: 900; 17 | font-size: 20px; 18 | line-height: 1; 19 | margin: 6px 0 6px 10px; 20 | display: inline-block; 21 | vertical-align: top; 22 | } 23 | 24 | button + button { 25 | margin-left: 10px; 26 | } 27 | 28 | .welcome { 29 | color: #333; 30 | font-size: 14px; 31 | margin-right: 10px; 32 | } 33 | -------------------------------------------------------------------------------- /dist/components/Alert/alert.d.ts: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | export type AlertType = 'success' | 'default' | 'danger' | 'warning'; 3 | export interface AlertProps { 4 | /**必填,设置 Alert 的标题 */ 5 | title: string; 6 | /**选填,设置 Alert 的描述 */ 7 | description?: string; 8 | /**选填,设置 Alert 的类型 */ 9 | type?: AlertType; 10 | /**选填,设置 Alert 关闭时触发的事件 */ 11 | onClose?: () => void; 12 | /**选填,设置 Alert 是否显示关闭图标*/ 13 | closable?: boolean; 14 | } 15 | /** 16 | * 用来展现需要重点关注的信息。 17 | * 18 | * ~~~js 19 | * // 这样引用 20 | * import { BetterAlert } from 'betterui'; 21 | * ~~~ 22 | * 23 | */ 24 | export declare const Alert: FC; 25 | export default Alert; 26 | -------------------------------------------------------------------------------- /src/components/Progress/progress.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ComponentStory, ComponentMeta } from '@storybook/react'; 3 | import BetterProgress from './progress'; 4 | 5 | export default { 6 | title: 'Progress 进度条', 7 | id: 'BetterProgress', 8 | component: BetterProgress, 9 | parameters: { 10 | docs: { 11 | source: { 12 | type: "code", 13 | }, 14 | }, 15 | }, 16 | } as ComponentMeta; 17 | 18 | const Template: ComponentStory = (args) => ; 19 | 20 | export const DefaultProgress = Template.bind({}); 21 | DefaultProgress.args = { 22 | percent: 50, 23 | }; 24 | DefaultProgress.storyName = '默认的进度条'; -------------------------------------------------------------------------------- /rollup/rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2'; 2 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | import json from '@rollup/plugin-json'; 5 | import sass from 'rollup-plugin-sass'; 6 | 7 | const overrides = { 8 | compilerOptions: { declaration: true }, 9 | exclude: ["src/**/*.test.tsx", "src/**/*.stories.tsx", "src/**/*.stories.mdx", "src/setupTests.ts"] 10 | } 11 | 12 | const config = { 13 | input: 'src/index.tsx', 14 | plugins: [ 15 | nodeResolve(), 16 | commonjs(), 17 | json(), 18 | typescript({ tsconfigOverride: overrides }), 19 | sass({ output: 'dist/index.css' }) 20 | ], 21 | } 22 | 23 | export default config; 24 | 25 | -------------------------------------------------------------------------------- /src/stories/Header.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ComponentStory, ComponentMeta } from '@storybook/react'; 3 | 4 | import { Header } from './Header'; 5 | 6 | export default { 7 | title: 'Example/Header', 8 | component: Header, 9 | parameters: { 10 | // More on Story layout: https://storybook.js.org/docs/react/configure/story-layout 11 | layout: 'fullscreen', 12 | }, 13 | } as ComponentMeta; 14 | 15 | const Template: ComponentStory = (args) =>
; 16 | 17 | export const LoggedIn = Template.bind({}); 18 | LoggedIn.args = { 19 | user: { 20 | name: 'Jane Doe', 21 | }, 22 | }; 23 | 24 | export const LoggedOut = Template.bind({}); 25 | LoggedOut.args = {}; 26 | -------------------------------------------------------------------------------- /dist/components/Tabs/tabs.d.ts: -------------------------------------------------------------------------------- 1 | import { FC, ReactNode } from 'react'; 2 | export interface TabsProps { 3 | /**选填,设置 Tabs 当前激活的index,默认为0 */ 4 | defaultIndex?: number; 5 | /**选填,设置 Tabs 可以扩展的类名 */ 6 | className?: string; 7 | /**选填,点击 Tab 触发的回调函数 */ 8 | onSelect?: (selectedIndex: number) => void; 9 | /**选填,设置 Tabs 的类型,两种可选,默认为 line */ 10 | type?: 'line' | 'card'; 11 | /**选填,设置 Tabs 的子元素 */ 12 | children?: ReactNode; 13 | } 14 | /** 15 | * 用于承载同一层级下不同页面或类别的组件,方便用户在同一个页面框架下进行快速切换。 16 | * 17 | * ~~~js 18 | * // 这样引用,再分别使用 19 | * import { BetterTabs } from 'betterui'; 20 | * ~~~ 21 | * 22 | */ 23 | export declare const Tabs: FC; 24 | export default Tabs; 25 | -------------------------------------------------------------------------------- /dist/components/Progress/progress.d.ts: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { ThemeProps } from '../Icon/icon'; 3 | export interface ProgressProps { 4 | /**必填,设置进度条的进度百分比 */ 5 | percent: number; 6 | /**选填,设置进度条的高度,默认为15px */ 7 | strokeHeight?: number; 8 | /**选填,是否显示进度条的百分比,默认展示 */ 9 | showText?: boolean; 10 | /**选填,设置进度条的样式 */ 11 | styles?: React.CSSProperties; 12 | /**选填,设置进度条的主题,仅支持以下9种主题 */ 13 | theme?: ThemeProps; 14 | } 15 | /** 16 | * 进度条,给予用户当前系统执行中任务运行状态的反馈,多用于运行一段时间的场景,有效减轻用户在等待中产生的焦虑感。 17 | * 18 | * ~~~js 19 | * // 这样引用 20 | * import { BetterProgress } from 'betterui'; 21 | * ~~~ 22 | * 23 | */ 24 | export declare const Progress: FC; 25 | export default Progress; 26 | -------------------------------------------------------------------------------- /src/stories/button.css: -------------------------------------------------------------------------------- 1 | .storybook-button { 2 | font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | font-weight: 700; 4 | border: 0; 5 | border-radius: 3em; 6 | cursor: pointer; 7 | display: inline-block; 8 | line-height: 1; 9 | } 10 | .storybook-button--primary { 11 | color: white; 12 | background-color: #1ea7fd; 13 | } 14 | .storybook-button--secondary { 15 | color: #333; 16 | background-color: transparent; 17 | box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset; 18 | } 19 | .storybook-button--small { 20 | font-size: 12px; 21 | padding: 10px 16px; 22 | } 23 | .storybook-button--medium { 24 | font-size: 14px; 25 | padding: 11px 20px; 26 | } 27 | .storybook-button--large { 28 | font-size: 16px; 29 | padding: 12px 24px; 30 | } 31 | -------------------------------------------------------------------------------- /rollup/rollup.umd.config.js: -------------------------------------------------------------------------------- 1 | import basicConfig from './rollup.config'; 2 | import { terser } from "rollup-plugin-terser"; 3 | import replace from '@rollup/plugin-replace'; 4 | 5 | const config = { 6 | ...basicConfig, 7 | output: [ 8 | { 9 | name: 'Betterui', 10 | file: 'dist/index.umd.js', 11 | format: 'umd', 12 | exports: 'named', 13 | globals: { 14 | 'react': 'React', 15 | 'react-dom': 'ReactDOM', 16 | 'axios': 'Axios' 17 | }, 18 | plugins: [ 19 | terser() 20 | ], 21 | }, 22 | ], 23 | plugins: [ 24 | replace({ 25 | 'process.env.NODE_ENV': JSON.stringify('production'), 26 | }), 27 | ...basicConfig.plugins 28 | ], 29 | external: ['react', 'react-dom', 'axios'] 30 | } 31 | 32 | export default config; -------------------------------------------------------------------------------- /dist/stories/Button.d.ts: -------------------------------------------------------------------------------- 1 | import './button.css'; 2 | interface ButtonProps { 3 | /** 4 | * Is this the principal call to action on the page? 5 | */ 6 | primary?: boolean; 7 | /** 8 | * What background color to use 9 | */ 10 | backgroundColor?: string; 11 | /** 12 | * How large should the button be? 13 | */ 14 | size?: 'small' | 'medium' | 'large'; 15 | /** 16 | * Button contents 17 | */ 18 | label: string; 19 | /** 20 | * Optional click handler 21 | */ 22 | onClick?: () => void; 23 | } 24 | /** 25 | * Primary UI component for user interaction 26 | */ 27 | export declare const Button: ({ primary, size, backgroundColor, label, ...props }: ButtonProps) => import("react/jsx-runtime").JSX.Element; 28 | export {}; 29 | -------------------------------------------------------------------------------- /dist/components/Form/formItem.d.ts: -------------------------------------------------------------------------------- 1 | import { FC, ReactNode } from 'react'; 2 | import { CustomRule } from './useStore'; 3 | export type SomeRequired = T & Required> & Omit; 4 | export interface FormItemProps { 5 | /**必填,设置表单项字段名 */ 6 | name: string; 7 | /**选填,设置表单项标签文本 */ 8 | label?: string; 9 | /**选填,设置表单项子元素 */ 10 | children?: ReactNode; 11 | /**选填,设置表单项值的属性,例如 checkbox 的是 'checked' */ 12 | valuePropName?: string; 13 | /**选填,设置表单项值的更新触发事件 */ 14 | trigger?: string; 15 | /**选填,设置如何将 event 的值转换成字段值 */ 16 | getValueFromEvent?: (...args: any) => any; 17 | /**选填,设置校验规则 */ 18 | rules?: CustomRule[]; 19 | /**选填,设置校验触发事件 */ 20 | validateTrigger?: string; 21 | } 22 | export declare const FormItem: FC; 23 | export default FormItem; 24 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | import './styles/index.scss'; 2 | export { default as BetterAlert } from './components/Alert'; 3 | export { default as BetterAutoComplete } from './components/AutoComplete'; 4 | export { default as BetterButton } from './components/Button'; 5 | export { default as BetterForm } from './components/Form'; 6 | export { default as BetterIcon } from './components/Icon'; 7 | export { default as BetterInput } from './components/Input'; 8 | export { default as BetterMenu } from './components/Menu'; 9 | export { default as BetterProgress } from './components/Progress'; 10 | export { default as BetterSelect } from './components/Select'; 11 | export { default as BetterTabs } from './components/Tabs'; 12 | export { default as BetterTransition } from './components/Transition'; 13 | export { default as BetterUpload } from './components/Upload'; 14 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | // config 2 | @import "variables"; 3 | 4 | //layout 5 | @import "reboot"; 6 | 7 | // mixin 8 | @import "mixin"; 9 | 10 | // animation 11 | @import "animation"; 12 | 13 | // Button 14 | @import "../components/Button/style"; 15 | 16 | // Menu 17 | @import "../components/Menu/style"; 18 | 19 | // Icon 20 | @import "../components/Icon/style"; 21 | 22 | //input 23 | @import "../components/Input/style"; 24 | 25 | // AutoComplete 26 | @import "../components/AutoComplete/style"; 27 | 28 | // Upload 29 | @import "../components/Upload/style"; 30 | 31 | // Progress 32 | @import "../components/Progress/style"; 33 | 34 | // Form 35 | @import "../components/Form/style"; 36 | 37 | // Select 38 | @import "../components/Select/style"; 39 | 40 | // Alert 41 | @import "../components/Alert/style"; 42 | 43 | // Tabs 44 | @import "../components/Tabs/style"; -------------------------------------------------------------------------------- /dist/components/Icon/icon.d.ts: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { FontAwesomeIconProps } from '@fortawesome/react-fontawesome'; 3 | export type ThemeProps = 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'danger' | 'light' | 'dark'; 4 | export interface IconProps extends FontAwesomeIconProps { 5 | /** 选填,根据不同主题显示不同的颜色 */ 6 | theme?: ThemeProps; 7 | } 8 | /** 9 | * 提供了一套常用的图标集合,基于 react-fontawesome 实现 10 | * 11 | * 支持 react-fontawesome 的所有属性,可以在这里查询: 12 | * https://github.com/FortAwesome/react-fontawesome#basic 13 | * 14 | * 支持 react-fontawesome 所有 free-solid-icons,可以在这里查看所有图标: 15 | * https://fontawesome.com/icons?d=gallery&s=solid&m=free 16 | * 17 | * ~~~js 18 | * // 这样引用 19 | * import { BetterIcon } from 'betterui'; 20 | * ~~~ 21 | * 22 | */ 23 | export declare const Icon: FC; 24 | export default Icon; 25 | -------------------------------------------------------------------------------- /src/components/Progress/_style.scss: -------------------------------------------------------------------------------- 1 | .better-progress-bar { 2 | width: 100%; 3 | box-sizing: border-box; 4 | .better-progress-bar-outer { 5 | height: 10px !important; 6 | border-radius: $progress-border-radius; 7 | background-color: $progress-bg; 8 | overflow: hidden; 9 | position: relative; 10 | } 11 | .better-progress-bar-inner { 12 | position: absolute; 13 | left: 0; 14 | top: 0; 15 | display: flex; 16 | justify-content: flex-end; 17 | align-items: center; 18 | height: 100%; 19 | border-radius: $progress-border-radius; 20 | line-height: 1; 21 | transition: $progress-bar-transition; 22 | .inner-text { 23 | color: $progress-bar-color; 24 | font-size: $progress-font-size; 25 | margin: 0 5px; 26 | } 27 | } 28 | @each $key, $val in $theme-colors { 29 | .color-#{$key} { 30 | background-color: $val; 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /storybook-static/project.json: -------------------------------------------------------------------------------- 1 | {"generatedAt":1726129431162,"builder":{"name":"@storybook/builder-webpack5"},"hasCustomBabel":false,"hasCustomWebpack":false,"hasStaticDirs":false,"hasStorybookEslint":false,"refCount":0,"metaFramework":{"name":"CRA","packageName":"react-scripts","version":"5.0.1"},"packageManager":{"type":"npm","version":"8.19.4"},"storybookVersion":"6.5.16","language":"typescript","storybookPackages":{"@storybook/addon-actions":{"version":"6.5.16"},"@storybook/builder-webpack5":{"version":"6.5.16"},"@storybook/manager-webpack5":{"version":"6.5.16"},"@storybook/node-logger":{"version":"6.5.16"},"@storybook/react":{"version":"6.5.16"},"@storybook/testing-library":{"version":"0.0.11"}},"framework":{"name":"react"},"addons":{"@storybook/addon-links":{"version":"6.5.16"},"@storybook/addon-essentials":{"version":"6.5.16"},"@storybook/addon-interactions":{"version":"6.5.16"},"@storybook/preset-create-react-app":{"version":"4.1.2"}}} 2 | -------------------------------------------------------------------------------- /dist/components/Transition/transition.d.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CSSTransitionProps } from 'react-transition-group/CSSTransition'; 3 | type AnimationName = 'zoom-in-top' | 'zoom-in-left' | 'zoom-in-bottom' | 'zoom-in-right'; 4 | export type TransitionProps = { 5 | /**选填,设置动画的过渡方向 */ 6 | animation?: AnimationName; 7 | /** 8 | * 选填,设置 Transition 的子元素是否有根元素进行包裹, 9 | * 当wrapper为false时,Transition组件的子元素必须包裹一个根元素 10 | * */ 11 | wrapper?: boolean; 12 | /**选填,设置 Transition 的子元素
*/ 13 | children?: React.ReactNode; 14 | /**选填,设置 Transition 的自定义类名
*/ 15 | classNames?: unknown; 16 | } & CSSTransitionProps; 17 | /** 18 | * 页面中常用的内置组件,可以帮助你制作基于状态变化的过渡和动画效果 19 | * 20 | * ~~~js 21 | * // 这样引用 22 | * import { BetterTransition } from 'betterui'; 23 | * ~~~ 24 | * 25 | */ 26 | export declare const Transition: React.FC; 27 | export default Transition; 28 | -------------------------------------------------------------------------------- /src/components/AutoComplete/_style.scss: -------------------------------------------------------------------------------- 1 | .better-auto-complete { 2 | position: relative; 3 | width: 350px; 4 | .loading-icon { 5 | width: 100%; 6 | display: flex; 7 | align-items: center; 8 | justify-content: center; 9 | } 10 | } 11 | .better-suggestion-list { 12 | position: absolute; 13 | width: auto; 14 | min-width: 350px; 15 | list-style:none; 16 | padding-left: 0; 17 | white-space: nowrap; 18 | background: $white; 19 | z-index: 100; 20 | border: $menu-border-width solid $menu-border-color; 21 | box-shadow: $submenu-box-shadow; 22 | .suggestion-item { 23 | padding: $menu-item-padding-y $menu-item-padding-x; 24 | cursor: pointer; 25 | transition: $menu-transition; 26 | color: $body-color; 27 | &.is-active { 28 | background: $menu-item-active-color !important; 29 | color: $white !important; 30 | } 31 | &:hover { 32 | color: $menu-item-active-color !important; 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /dist/components/AutoComplete/autoComplete.d.ts: -------------------------------------------------------------------------------- 1 | import { FC, ReactElement } from 'react'; 2 | import { InputProps } from '../Input/input'; 3 | interface DataSourceObject { 4 | value: string; 5 | } 6 | export type DataSourceType = T & DataSourceObject; 7 | export interface AutoCompleteProps extends Omit { 8 | /** 必填,可以拿到当前的输入,然后返回同步的数组或者是异步的 Promise */ 9 | fetchSuggestions: (str: string) => DataSourceType[] | Promise; 10 | /** 选填,选中后执行的回调函数 */ 11 | onSelect?: (item: DataSourceType) => void; 12 | /** 选填,支持自定义渲染下拉列表 */ 13 | renderOption?: (item: DataSourceType) => ReactElement; 14 | } 15 | /** 16 | * 联想搜索,通过鼠标或键盘输入内容进行自动联想,支持同步和异步两种方式。 17 | * 支持 Input 组件的所有属性,支持键盘事件选择 18 | * 19 | * ~~~js 20 | * // 这样引用 21 | * import { BetterAutoComplete } from 'betterui'; 22 | * ~~~ 23 | * 24 | */ 25 | export declare const AutoComplete: FC; 26 | export default AutoComplete; 27 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { library } from '@fortawesome/fontawesome-svg-core'; 2 | import { fas } from '@fortawesome/free-solid-svg-icons'; 3 | import './styles/index.scss'; 4 | 5 | library.add(fas); 6 | 7 | export { default as BetterAlert } from './components/Alert'; 8 | export { default as BetterAutoComplete } from './components/AutoComplete'; 9 | export { default as BetterButton } from './components/Button'; 10 | export { default as BetterForm } from './components/Form'; 11 | export { default as BetterIcon } from './components/Icon'; 12 | export { default as BetterInput } from './components/Input'; 13 | export { default as BetterMenu } from './components/Menu'; 14 | export { default as BetterProgress } from './components/Progress'; 15 | export { default as BetterSelect } from './components/Select'; 16 | export { default as BetterTabs } from './components/Tabs'; 17 | export { default as BetterTransition } from './components/Transition'; 18 | export { default as BetterUpload } from './components/Upload'; 19 | -------------------------------------------------------------------------------- /src/components/Alert/_style.scss: -------------------------------------------------------------------------------- 1 | $alert-colors: 2 | ( 3 | "default": $light-blue, 4 | "success": $light-green, 5 | "warning": $light-orange, 6 | "danger": $light-red, 7 | ); 8 | .better-alert { 9 | position: relative; 10 | padding: $alert-padding-y $alert-padding-x; 11 | margin-bottom: $alert-margin-bottom; 12 | border: $alert-border-width solid transparent; 13 | border-radius: $alert-border-radius; 14 | .better-alert-close { 15 | position: absolute; 16 | top: 0; 17 | right: 0; 18 | padding: $alert-padding-y $alert-padding-x; 19 | color: inherit; 20 | cursor: pointer; 21 | } 22 | .bold-title { 23 | font-weight: $font-weight-bold; 24 | } 25 | .better-alert-desc { 26 | font-size: $alert-description-font-size; 27 | margin: $alert-description-top-margin 0 0; 28 | } 29 | } 30 | @each $color, $value in $alert-colors { 31 | .better-alert-#{$color} { 32 | @include alert-style($value, darken($value, 10%), $gray-700); 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /src/stories/Page.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ComponentStory, ComponentMeta } from '@storybook/react'; 3 | import { within, userEvent } from '@storybook/testing-library'; 4 | import { Page } from './Page'; 5 | 6 | export default { 7 | title: 'Example/Page', 8 | component: Page, 9 | parameters: { 10 | // More on Story layout: https://storybook.js.org/docs/react/configure/story-layout 11 | layout: 'fullscreen', 12 | }, 13 | } as ComponentMeta; 14 | 15 | const Template: ComponentStory = (args) => ; 16 | 17 | export const LoggedOut = Template.bind({}); 18 | 19 | export const LoggedIn = Template.bind({}); 20 | 21 | // More on interaction testing: https://storybook.js.org/docs/react/writing-tests/interaction-testing 22 | LoggedIn.play = async ({ canvasElement }) => { 23 | const canvas = within(canvasElement); 24 | const loginButton = await canvas.getByRole('button', { name: /Log in/i }); 25 | await userEvent.click(loginButton); 26 | }; 27 | -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 24 | -------------------------------------------------------------------------------- /storybook-static/794.0d1e3911deee4eefd1f9.manager.bundle.js: -------------------------------------------------------------------------------- 1 | (self.webpackChunk_zhuangjiaqing_betterui=self.webpackChunk_zhuangjiaqing_betterui||[]).push([[794],{83794:module=>{module.exports=function(e,n){return n=n||{},new Promise((function(t,r){var s=new XMLHttpRequest,o=[],u=[],i={},a=function(){return{ok:2==(s.status/100|0),statusText:s.statusText,status:s.status,url:s.responseURL,text:function(){return Promise.resolve(s.responseText)},json:function(){return Promise.resolve(s.responseText).then(JSON.parse)},blob:function(){return Promise.resolve(new Blob([s.response]))},clone:a,headers:{keys:function(){return o},entries:function(){return u},get:function(e){return i[e.toLowerCase()]},has:function(e){return e.toLowerCase()in i}}}};for(var l in s.open(n.method||"get",e,!0),s.onload=function(){s.getAllResponseHeaders().replace(/^(.*?):[^\S\n]*([\s\S]*?)$/gm,(function(e,n,t){o.push(n=n.toLowerCase()),u.push([n,t]),i[n]=i[n]?i[n]+","+t:t})),t(a())},s.onerror=r,s.withCredentials="include"==n.credentials,n.headers)s.setRequestHeader(l,n.headers[l]);s.send(n.body||null)}))}}}]); -------------------------------------------------------------------------------- /src/components/Upload/dragger.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState, DragEvent, ReactNode } from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | interface DraggerProps { 5 | onFile: (files: FileList) => void; 6 | children?: ReactNode; 7 | }; 8 | export const Dragger: FC = (props) => { 9 | const { onFile, children } = props; 10 | const [ dragOver, setDragOver ] = useState(false); 11 | const betterClass = classNames('better-uploader-dragger', { 12 | 'is-dragover': dragOver, 13 | }); 14 | const handleDrop = (e: DragEvent) => { 15 | e.preventDefault(); 16 | setDragOver(false); 17 | onFile(e.dataTransfer.files); 18 | }; 19 | const handleDrag = (e: DragEvent, over: boolean) => { 20 | e.preventDefault(); 21 | setDragOver(over); 22 | }; 23 | return ( 24 |
{ handleDrag(e, true) }} 27 | onDragLeave={ e => { handleDrag(e, false) }} 28 | onDrop={ handleDrop } 29 | > 30 | { children } 31 |
32 | ) 33 | }; 34 | 35 | export default Dragger; -------------------------------------------------------------------------------- /storybook-static/463.c7cae972.iframe.bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | 9 | /** 10 | * @license 11 | * Copyright Google Inc. All Rights Reserved. 12 | * 13 | * Use of this source code is governed by an MIT-style license that can be 14 | * found in the LICENSE file at https://angular.io/license 15 | */ 16 | 17 | /** 18 | * @license 19 | * Copyright Google Inc. All Rights Reserved. 20 | * 21 | * Use of this source code is governed by an MIT-style license that can be 22 | * found in the LICENSE file at https://angular.io/license 23 | */ 24 | 25 | /** 26 | * @license 27 | * Copyright Google Inc. All Rights Reserved. 28 | * 29 | * Use of this source code is governed by an MIT-style license that can be 30 | * found in the LICENSE file at https://angular.io/license 31 | */ 32 | -------------------------------------------------------------------------------- /src/components/Progress/progress.test.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable testing-library/no-node-access */ 2 | /* eslint-disable testing-library/prefer-screen-queries */ 3 | import React from 'react'; 4 | import { render } from '@testing-library/react'; 5 | import Progress from './progress'; 6 | 7 | describe('Progress Component', () => { 8 | it('renders without error', () => { 9 | render(); 10 | }); 11 | 12 | it('displays the correct progress percentage', () => { 13 | const { getByText } = render(); 14 | expect(getByText('75%')).toBeInTheDocument(); 15 | }); 16 | 17 | it('applies custom styles correctly', () => { 18 | const customStyles = { color: 'red' }; 19 | const { container } = render(); 20 | expect(container.firstChild).toHaveStyle('color: red'); 21 | }); 22 | 23 | it('does not display the progress text if showText is set to false', () => { 24 | const { queryByText } = render(); 25 | expect(queryByText('60%')).toBeNull(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /storybook-static/463.5fe71decd3ffa06e60bb.manager.bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | 9 | /** 10 | * @license 11 | * Copyright Google Inc. All Rights Reserved. 12 | * 13 | * Use of this source code is governed by an MIT-style license that can be 14 | * found in the LICENSE file at https://angular.io/license 15 | */ 16 | 17 | /** 18 | * @license 19 | * Copyright Google Inc. All Rights Reserved. 20 | * 21 | * Use of this source code is governed by an MIT-style license that can be 22 | * found in the LICENSE file at https://angular.io/license 23 | */ 24 | 25 | /** 26 | * @license 27 | * Copyright Google Inc. All Rights Reserved. 28 | * 29 | * Use of this source code is governed by an MIT-style license that can be 30 | * found in the LICENSE file at https://angular.io/license 31 | */ 32 | -------------------------------------------------------------------------------- /dist/components/Button/button.d.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | export type ButtonSize = 'lg' | 'sm'; 3 | export type ButtonType = 'primary' | 'default' | 'danger' | 'link'; 4 | interface BaseButtonProps { 5 | /**选填,设置 Button 的自定义类名 */ 6 | className?: string; 7 | /**选填,设置 Button 的禁用 */ 8 | disabled?: boolean; 9 | /**选填,设置 Button 的尺寸 */ 10 | size?: ButtonSize; 11 | /**选填,设置 Button 的类型 */ 12 | btnType?: ButtonType; 13 | /**选填,设置 Button 的子元素 */ 14 | children: React.ReactNode; 15 | /**选填,设置 Button 的超链接目标,仅在btnType属性为link时有效 */ 16 | href?: string; 17 | } 18 | type NativeButtonProps = BaseButtonProps & React.ButtonHTMLAttributes; 19 | type AnchorButtonProps = BaseButtonProps & React.AnchorHTMLAttributes; 20 | export type ButtonProps = Partial; 21 | /** 22 | * 页面中最常用的的按钮元素,适合于完成特定的交互,支持 HTML button 和 a 链接 的所有属性 23 | * 24 | * ~~~js 25 | * // 这样引用 26 | * import { BetterButton } from 'betterui'; 27 | * ~~~ 28 | * 29 | */ 30 | export declare const Button: React.FC; 31 | export default Button; 32 | -------------------------------------------------------------------------------- /src/components/Transition/transition.test.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable testing-library/prefer-screen-queries */ 2 | import React from 'react'; 3 | import { render, screen } from '@testing-library/react'; 4 | import { Transition } from './transition'; 5 | 6 | describe('Transition', () => { 7 | it('renders children correctly', () => { 8 | const { getByText } = render( 9 | 14 |
Child Component
15 |
16 | ); 17 | expect(getByText('Child Component')).toBeInTheDocument(); 18 | }); 19 | 20 | it('applies custom classNames correctly', () => { 21 | render( 22 | 28 |
Child Component
29 |
30 | ); 31 | const childComponent = screen.getByText('Child Component'); 32 | expect(childComponent).toHaveClass('custom-transition-appear custom-transition-appear-active'); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/stories/Button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './button.css'; 3 | 4 | interface ButtonProps { 5 | /** 6 | * Is this the principal call to action on the page? 7 | */ 8 | primary?: boolean; 9 | /** 10 | * What background color to use 11 | */ 12 | backgroundColor?: string; 13 | /** 14 | * How large should the button be? 15 | */ 16 | size?: 'small' | 'medium' | 'large'; 17 | /** 18 | * Button contents 19 | */ 20 | label: string; 21 | /** 22 | * Optional click handler 23 | */ 24 | onClick?: () => void; 25 | } 26 | 27 | /** 28 | * Primary UI component for user interaction 29 | */ 30 | export const Button = ({ 31 | primary = false, 32 | size = 'medium', 33 | backgroundColor, 34 | label, 35 | ...props 36 | }: ButtonProps) => { 37 | const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary'; 38 | return ( 39 | 47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /dist/components/Input/input.d.ts: -------------------------------------------------------------------------------- 1 | import React, { ReactElement, InputHTMLAttributes, ChangeEvent } from 'react'; 2 | import { IconProp } from '@fortawesome/fontawesome-svg-core'; 3 | type InputSize = 'lg' | 'sm'; 4 | export interface InputProps extends Omit, 'size'> { 5 | /**选填,设置 Input 的禁用 */ 6 | disabled?: boolean; 7 | /**选填,设置 Input 大小,支持 lg 或者是 sm */ 8 | size?: InputSize; 9 | /**选填,设置 Input 右侧悬浮的图标,用于提示 */ 10 | icon?: IconProp; 11 | /**选填,设置 Input 前缀,用于配置一些固定组合 */ 12 | prepend?: string | ReactElement; 13 | /**选填,设置 Input 后缀,用于配置一些固定组合 */ 14 | append?: string | ReactElement; 15 | /**选填,设置 Input 的占位符 */ 16 | placeholder?: string; 17 | /**选填,设置 Input 的 change 事件 */ 18 | onChange?: (e: ChangeEvent) => void; 19 | } 20 | /** 21 | * Input 输入框,通过鼠标或键盘输入内容,是最基础的表单域的包装,支持 HTMLInput 的所有基本属性 22 | * 23 | * ~~~js 24 | * // 这样引用 25 | * import { BetterInput } from 'betterui'; 26 | * ~~~ 27 | * 28 | */ 29 | export declare const Input: React.ForwardRefExoticComponent>; 30 | export default Input; 31 | -------------------------------------------------------------------------------- /src/components/Icon/icon.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import classNames from 'classnames'; 3 | import { FontAwesomeIcon, FontAwesomeIconProps } from '@fortawesome/react-fontawesome'; 4 | 5 | export type ThemeProps = 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'danger' | 'light' | 'dark'; 6 | 7 | export interface IconProps extends FontAwesomeIconProps { 8 | /** 选填,根据不同主题显示不同的颜色 */ 9 | theme? : ThemeProps, 10 | }; 11 | 12 | /** 13 | * 提供了一套常用的图标集合,基于 react-fontawesome 实现 14 | * 15 | * 支持 react-fontawesome 的所有属性,可以在这里查询: 16 | * https://github.com/FortAwesome/react-fontawesome#basic 17 | * 18 | * 支持 react-fontawesome 所有 free-solid-icons,可以在这里查看所有图标: 19 | * https://fontawesome.com/icons?d=gallery&s=solid&m=free 20 | * 21 | * ~~~js 22 | * // 这样引用 23 | * import { BetterIcon } from 'betterui'; 24 | * ~~~ 25 | * 26 | */ 27 | export const Icon: FC = (props) => { 28 | const { className, theme, ...restProps } = props; 29 | const classes = classNames('better-icon', className, { 30 | [`icon-${theme}`]: theme, 31 | }); 32 | return ( 33 | 34 | ); 35 | }; 36 | 37 | export default Icon; -------------------------------------------------------------------------------- /.github/workflows/action.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: betterui CI 5 | 6 | # 触发条件:在 push 到 master 分支后 7 | on: 8 | push: 9 | branches: 10 | - master 11 | 12 | # 任务 13 | jobs: 14 | build: 15 | 16 | runs-on: ubuntu-latest 17 | 18 | strategy: 19 | matrix: 20 | node-version: [14.x] 21 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 22 | 23 | steps: 24 | # 拉取代码 25 | - name: Checkout 26 | uses: actions/checkout@v2 27 | with: 28 | persist-credentials: false 29 | 30 | # 1、生成静态文件 31 | - name: Build-Storybook 32 | run: npm ci && npm run build-storybook 33 | 34 | # 2、部署到 GitHub Pages 35 | - name: Deploy 36 | uses: JamesIves/github-pages-deploy-action@releases/v3 37 | with: 38 | github_token: ${{ secrets.GITHUB_TOKEN }} 39 | BRANCH: master 40 | FOLDER: storybook-static 41 | -------------------------------------------------------------------------------- /dist/components/Select/select.d.ts: -------------------------------------------------------------------------------- 1 | import React, { FC, ReactNode } from 'react'; 2 | export interface SelectProps { 3 | /**指定默认选中的条目 可以是是字符串或者字符串数组*/ 4 | defaultValue?: string | string[]; 5 | /** 选择框默认文字*/ 6 | placeholder?: string; 7 | /** 是否禁用*/ 8 | disabled?: boolean; 9 | /** 是否支持多选*/ 10 | multiple?: boolean; 11 | /** select input 的 name 属性 */ 12 | name?: string; 13 | /**选中值发生变化时触发 */ 14 | onChange?: (selectedValue: string, selectedValues: string[]) => void; 15 | /**下拉框出现/隐藏时触发 */ 16 | onVisibleChange?: (visible: boolean) => void; 17 | children?: ReactNode; 18 | } 19 | export interface ISelectContext { 20 | onSelect?: (value: string, isSelected?: boolean) => void; 21 | selectedValues: string[]; 22 | multiple?: boolean; 23 | } 24 | export declare const SelectContext: React.Context; 25 | /** 26 | * 下拉选择器。 27 | * 弹出一个下拉菜单给用户选择操作,用于代替原生的选择器,或者需要一个更优雅的多选器时。 28 | * ### 引用方法 29 | * 30 | * ~~~js 31 | * // 这样引用,再分别使用 32 | * import { BetterSelect } from 'betterui'; 33 | * ~~~ 34 | */ 35 | export declare const Select: FC; 36 | export default Select; 37 | -------------------------------------------------------------------------------- /src/components/Alert/alert.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ComponentMeta } from '@storybook/react'; 3 | import BetterAlert from './alert'; 4 | 5 | export default { 6 | title: 'Alert 警告提示', 7 | id: 'BetterAlert', 8 | component: BetterAlert, 9 | parameters: { 10 | docs: { 11 | source: { 12 | type: "code", 13 | }, 14 | }, 15 | }, 16 | } as ComponentMeta; 17 | 18 | export const DefaultAlert = () => { 19 | return ( 20 | <> 21 | 22 | 23 | ); 24 | }; 25 | DefaultAlert.storyName = '基本版'; 26 | 27 | export const StylesAlert = () => { 28 | return ( 29 | <> 30 | 31 | 32 | 33 | 34 | 35 | ); 36 | }; 37 | StylesAlert.storyName = '标准版'; 38 | 39 | export const DescAlert = () => { 40 | return ( 41 | <> 42 | 43 | 44 | ); 45 | }; 46 | DescAlert.storyName = '带有辅助性文字介绍'; 47 | 48 | -------------------------------------------------------------------------------- /src/stories/assets/direction.svg: -------------------------------------------------------------------------------- 1 | illustration/direction -------------------------------------------------------------------------------- /dist/components/Menu/menu.d.ts: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from "react"; 2 | type MenuMode = "horizontal" | "vertical"; 3 | type SelectCallback = (selectedIndex: string) => void; 4 | export interface MenuProps { 5 | /**选填,设置 active 菜单项的索引值 */ 6 | defaultIndex?: string; 7 | /**选填,设置 Menu 的自定义类名 */ 8 | className?: string; 9 | /**选填,设置 Menu 的展示类型,分为横向模式(horizontal)和纵向模式(vertical) */ 10 | mode?: MenuMode; 11 | /**选填,设置 Menu 的自定义样式 */ 12 | style?: React.CSSProperties; 13 | /**选填,设置 Menu 的子元素 */ 14 | children?: ReactNode; 15 | /**选填,点击菜单项触发的回调函数 */ 16 | onSelect?: SelectCallback; 17 | /**选填,设置默认展开的子菜单数组,仅当 mode 为纵向模式(vertical)时生效 */ 18 | defaultOpenSubMenus?: string[]; 19 | } 20 | interface IMenuContext { 21 | index: string; 22 | onSelect?: SelectCallback; 23 | mode?: MenuMode; 24 | defaultOpenSubMenus?: string[]; 25 | } 26 | export declare const MenuContext: React.Context; 27 | /** 28 | * 为网站提供导航功能的菜单。支持横向纵向两种模式,支持下拉菜单 29 | * 30 | * ~~~js 31 | * // 这样引用,再分别使用 32 | * import { BetterMenu } from 'betterui'; 33 | * ~~~ 34 | * 35 | */ 36 | export declare const Menu: React.FC; 37 | export default Menu; 38 | -------------------------------------------------------------------------------- /src/components/Button/Button.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ComponentStory, ComponentMeta } from '@storybook/react'; 3 | import BetterButton from './button'; 4 | 5 | export default { 6 | title: 'Button 按钮', 7 | id: 'BetterButton', 8 | component: BetterButton, 9 | parameters: { 10 | docs: { 11 | source: { 12 | type: "code", 13 | }, 14 | }, 15 | }, 16 | } as ComponentMeta; 17 | 18 | const Template: ComponentStory = (args) => ; 19 | 20 | export const DefaultButton = Template.bind({}); 21 | DefaultButton.args = { 22 | children: 'Default Button', 23 | }; 24 | DefaultButton.storyName = '默认的按钮'; 25 | 26 | export const ButtonWithSize = () => ( 27 | <> 28 | Large Button 29 | Small Button 30 | 31 | ); 32 | ButtonWithSize.storyName = '不同尺寸的按钮'; 33 | 34 | export const ButtonWithType = () => ( 35 | <> 36 | Primary Button 37 | Danger Button 38 | Link Button 39 | 40 | ); 41 | ButtonWithType.storyName = '不同类型的按钮'; -------------------------------------------------------------------------------- /src/stories/Button.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ComponentStory, ComponentMeta } from '@storybook/react'; 3 | 4 | import { Button } from './Button'; 5 | 6 | // More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export 7 | export default { 8 | title: 'Example/Button', 9 | component: Button, 10 | // More on argTypes: https://storybook.js.org/docs/react/api/argtypes 11 | argTypes: { 12 | backgroundColor: { control: 'color' }, 13 | }, 14 | } as ComponentMeta; 15 | 16 | // More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args 17 | const Template: ComponentStory = (args) =>
56 | ); 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 引言 2 | 3 | 欢迎来到 betterui 组件库,这是一个基于 React 的组件库,用于快速搭建页面。 4 | 5 | [点击查看组件库文档](https://coder-xiaozhuang.github.io/betterui) 6 | 7 | 8 | ## 快速上手 9 | node版本推荐:14.x.x 10 | 11 | ### 如何安装 12 | 13 | #### NPM 14 | 推荐使用 npm 的方式安装,它能更好地和 webpack 打包工具配合使用。 15 | 16 | ~~~javascript 17 | npm install @zhuangjiaqing/betterui --save 18 | ~~~ 19 | 20 | #### CDN 21 | 目前可以通过 `unpkg.com/@zhuangjiaqing/betterui` 获取到最新版本的资源,在页面上引入 css 和 js 文件即可开始使用。 22 | 23 | ~~~javascript 24 | 25 | 26 | 27 | 28 | 29 | ~~~ 30 | > 注意:我们建议使用 CDN 引入 betterui 的用户在链接地址上锁定版本,以免将来 betterui 升级时受到非兼容性更新的影响。锁定版本的方法请查看 unpkg.com。 31 | 32 | ### 如何使用 33 | 34 | #### NPM 35 | 36 | ~~~javascript 37 | import React from 'react'; 38 | import '@zhuangjiaqing/betterui/dist/index.css'; 39 | import { BetterButton } from '@zhuangjiaqing/betterui'; 40 | 41 | function App() { 42 | return ( 43 |
44 | Hello World 45 |
46 | ); 47 | } 48 | 49 | export default App; 50 | ~~~ 51 | 好了,现在你应该能看到页面上已经有了 betterui 的蓝色按钮组件,接下来就可以继续选用其他组件开发应用了。[示例源码](https://github.com/Coder-XiaoZhuang/betterui_test_app/blob/main/src/App.tsx) 52 | 53 | #### CDN 54 | 55 | 通过 CDN 的方式我们可以很容易地使用 betterui 写出一个 Hello world 页面。 56 | [示例源码](https://github.com/Coder-XiaoZhuang/betterui/blob/main/test.html) 57 | [在线演示](https://unpkg.com/@zhuangjiaqing/betterui@1.0.3/test.html) 58 | 59 | 60 | ### 一些本地开发命令 61 | 62 | ~~~bash 63 | //启动本地环境 64 | npm run storybook 65 | 66 | //跑单元测试 67 | npm run test 68 | 69 | //build可发布静态文件 70 | npm run build 71 | 72 | //发布到 npm 73 | npm run publish 74 | ~~~ -------------------------------------------------------------------------------- /src/components/Alert/alert.test.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable testing-library/no-node-access */ 2 | /* eslint-disable testing-library/no-container */ 3 | /* eslint-disable testing-library/prefer-presence-queries */ 4 | /* eslint-disable testing-library/prefer-screen-queries */ 5 | import React from 'react'; 6 | import { config } from 'react-transition-group'; 7 | import { render, fireEvent } from '@testing-library/react'; 8 | import Alert, { AlertProps } from './alert'; 9 | 10 | config.disabled = true; 11 | 12 | jest.mock('../Icon', () => ((props: any) => { props.icon })); 13 | 14 | const testProps: AlertProps = { 15 | title: 'title', 16 | onClose: jest.fn(), 17 | }; 18 | 19 | const typeProps: AlertProps = { 20 | ...testProps, 21 | type: 'success', 22 | description: 'hello', 23 | closable: false 24 | }; 25 | 26 | describe('test Alert Component', () => { 27 | it('should render the correct default Alert', () => { 28 | const { getByText, container, queryByText } = render(); 29 | expect(queryByText('title')).toBeInTheDocument(); 30 | expect(container.querySelector('.better-alert')).toHaveClass('better-alert-default'); 31 | fireEvent.click(getByText('times')); 32 | expect(testProps.onClose).toHaveBeenCalled(); 33 | expect(queryByText('title')).not.toBeInTheDocument(); 34 | }); 35 | it('should render the correct Alert based on different type and description', () => { 36 | const { container, queryByText } = render(); 37 | expect(queryByText('title')).toHaveClass('bold-title'); 38 | expect(container.querySelector('.better-alert')).toHaveClass('better-alert-success'); 39 | expect(queryByText('hello')).toBeInTheDocument(); 40 | expect(queryByText('times')).not.toBeInTheDocument(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/components/Button/_style.scss: -------------------------------------------------------------------------------- 1 | .btn { 2 | position: relative; 3 | display: inline-block; 4 | font-weight: $btn-font-weight; 5 | line-height: $btn-line-height; 6 | color: $body-color; 7 | white-space: nowrap; 8 | text-align: center; 9 | vertical-align: middle; 10 | background-image: none; 11 | border: $btn-border-width solid transparent; 12 | @include button-size( $btn-padding-y, $btn-padding-x, $btn-font-size, $border-radius); 13 | box-shadow: $btn-box-shadow; 14 | cursor: pointer; 15 | transition: $btn-transition; 16 | &.disabled, 17 | &[disabled] { 18 | cursor: not-allowed; 19 | opacity: $btn-disabled-opacity; 20 | box-shadow: none; 21 | > * { 22 | pointer-events: none; 23 | } 24 | } 25 | } 26 | 27 | .btn-lg { 28 | @include button-size($btn-padding-y-lg, $btn-padding-x-lg, $btn-font-size-lg, $btn-border-radius-lg); 29 | } 30 | .btn-sm { 31 | @include button-size($btn-padding-y-sm, $btn-padding-x-sm, $btn-font-size-sm, $btn-border-radius-sm); 32 | } 33 | 34 | .btn-primary { 35 | @include button-style($primary, $primary, $white) 36 | } 37 | .btn-danger { 38 | @include button-style($danger, $danger, $white) 39 | } 40 | 41 | .btn-default { 42 | @include button-style($white, $gray-400, $body-color, $white, $primary, $primary) 43 | } 44 | 45 | .btn-link { 46 | font-weight: $font-weight-normal; 47 | color: $btn-link-color; 48 | text-decoration: $link-decoration; 49 | box-shadow: none; 50 | &:hover { 51 | color: $btn-link-hover-color; 52 | text-decoration: $link-hover-decoration; 53 | } 54 | &:focus, 55 | &.focus { 56 | text-decoration: $link-hover-decoration; 57 | box-shadow: none; 58 | } 59 | &:disabled, 60 | &.disabled { 61 | color: $btn-link-disabled-color; 62 | pointer-events: none; 63 | } 64 | } -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/components/Alert/alert.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState } from 'react'; 2 | import classNames from 'classnames'; 3 | import Icon from '../Icon'; 4 | import Transition from '../Transition'; 5 | 6 | export type AlertType = 'success' | 'default' | 'danger' | 'warning'; 7 | export interface AlertProps { 8 | /**必填,设置 Alert 的标题 */ 9 | title: string; 10 | /**选填,设置 Alert 的描述 */ 11 | description?: string; 12 | /**选填,设置 Alert 的类型 */ 13 | type?: AlertType; 14 | /**选填,设置 Alert 关闭时触发的事件 */ 15 | onClose?: () => void; 16 | /**选填,设置 Alert 是否显示关闭图标*/ 17 | closable?: boolean; 18 | }; 19 | 20 | /** 21 | * 用来展现需要重点关注的信息。 22 | * 23 | * ~~~js 24 | * // 这样引用 25 | * import { BetterAlert } from 'betterui'; 26 | * ~~~ 27 | * 28 | */ 29 | export const Alert: FC = (props) => { 30 | const [ hide, setHide ] = useState(false); 31 | const { 32 | title, 33 | description, 34 | type, 35 | onClose, 36 | closable, 37 | } = props; 38 | const classes = classNames('better-alert', { 39 | [`better-alert-${type}`]: type, 40 | }); 41 | const titleClass = classNames('better-alert-title', { 42 | 'bold-title': description, 43 | }); 44 | const handleClose = (e: React.MouseEvent) => { 45 | if (onClose) { 46 | onClose(); 47 | } 48 | setHide(true); 49 | }; 50 | return ( 51 | 56 |
57 | { title } 58 | { description &&

{ description }

} 59 | { closable && } 60 |
61 |
62 | ); 63 | }; 64 | 65 | Alert.defaultProps = { 66 | type: 'default', 67 | closable: true, 68 | }; 69 | export default Alert; -------------------------------------------------------------------------------- /src/components/Select/select.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ComponentMeta } from '@storybook/react'; 3 | import { JSX } from 'react/jsx-runtime'; 4 | import { SelectProps } from './select'; 5 | import BetterSelect from './index'; 6 | 7 | const selectMeta: ComponentMeta = { 8 | title: 'Select 选择器', 9 | id: 'BetterSelect', 10 | component: BetterSelect, 11 | subcomponents: { 12 | 'Option': BetterSelect.Option, 13 | }, 14 | parameters: { 15 | docs: { 16 | source: { 17 | type: "code", 18 | }, 19 | }, 20 | }, 21 | }; 22 | export default selectMeta; 23 | 24 | export const DefaultSelect = (args: JSX.IntrinsicAttributes & SelectProps) => ( 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | DefaultSelect.storyName = '默认的选择器'; 34 | 35 | export const MultipleSelect = (args: JSX.IntrinsicAttributes & SelectProps) => ( 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | ); 44 | MultipleSelect.storyName = '支持多选'; 45 | 46 | export const DisabledSelect = (args: JSX.IntrinsicAttributes & SelectProps) => ( 47 | 48 | 49 | 50 | 51 | 52 | ); 53 | DisabledSelect.storyName = '禁用状态'; -------------------------------------------------------------------------------- /dist/components/Upload/upload.d.ts: -------------------------------------------------------------------------------- 1 | import { FC, ReactNode } from 'react'; 2 | export type UploadFileStatus = 'ready' | 'uploading' | 'success' | 'error'; 3 | export interface UploadFile { 4 | uid: string; 5 | size: number; 6 | name: string; 7 | status?: UploadFileStatus; 8 | percent: number; 9 | raw?: File; 10 | response?: any; 11 | error?: any; 12 | } 13 | export interface UploadProps { 14 | /**必填, 上传的地址 */ 15 | action: string; 16 | /**选填,上传的文件列表 */ 17 | defaultFileList?: UploadFile[]; 18 | /**选填,上传文件之前的钩子,参数为上传的文件,若返回 false 或者 Promise 则停止上传 */ 19 | beforeUpload?: (file: File) => boolean | Promise; 20 | /**选填,文件上传时的钩子 */ 21 | onProgress?: (percentage: number, file: UploadFile) => void; 22 | /**选填,文件上传成功时的钩子 */ 23 | onSuccess?: (data: any, file: UploadFile) => void; 24 | /**选填,文件上传失败时的钩子 */ 25 | onError?: (err: any, file: UploadFile) => void; 26 | /**选填,文件状态改变时的钩子,上传成功或者失败时都会被调用 */ 27 | onChange?: (file: UploadFile) => void; 28 | /**选填,文件列表移除文件时的钩子 */ 29 | onRemove?: (file: UploadFile) => void; 30 | /**选填,设置上传的请求头部 */ 31 | headers?: { 32 | [key: string]: any; 33 | }; 34 | /**选填,上传的文件字段名 */ 35 | name?: string; 36 | /**选填,上传时附带的额外参数 */ 37 | data?: { 38 | [key: string]: any; 39 | }; 40 | /**选填,是否支持发送 cookie 凭证信息 */ 41 | withCredentials?: boolean; 42 | /**选填,可选参数, 接受上传的文件类型 */ 43 | accept?: string; 44 | /**选填,是否支持多选文件,默认不支持 */ 45 | multiple?: boolean; 46 | /**设置 Upload 的子元素 */ 47 | children?: ReactNode; 48 | /**选填,是否支持拖拽上传,默认为否 */ 49 | drag?: boolean; 50 | } 51 | /** 52 | * 通过点击或者拖拽上传文件。 53 | * 54 | * ~~~js 55 | * // 这样引用 56 | * import { BetterUpload } from 'betterui'; 57 | * ~~~ 58 | * 59 | */ 60 | export declare const Upload: FC; 61 | export default Upload; 62 | -------------------------------------------------------------------------------- /src/components/Upload/upload.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ComponentMeta } from '@storybook/react'; 3 | import BetterUpload, { UploadProps } from './upload'; 4 | import BetterButton from '../Button'; 5 | import BetterIcon from '../Icon'; 6 | import { JSX } from 'react/jsx-runtime'; 7 | 8 | export default { 9 | title: 'Upload 上传', 10 | id: 'BetterUpload', 11 | component: BetterUpload, 12 | parameters: { 13 | docs: { 14 | source: { 15 | type: "code", 16 | }, 17 | }, 18 | }, 19 | } as ComponentMeta; 20 | 21 | export const SimpleUpload = (args: JSX.IntrinsicAttributes & UploadProps) => ( 22 | 26 | 点击上传 27 | 28 | ); 29 | SimpleUpload.storyName = '基本上传'; 30 | 31 | export const CheckUpload = (args: JSX.IntrinsicAttributes & UploadProps) => { 32 | const checkFileSize = (file: File) => { 33 | if (Math.round(file.size / 1024) > 50) { 34 | alert('上传文件不能大于50Kb!'); 35 | return false; 36 | } 37 | return true; 38 | }; 39 | return ( 40 | 45 | 不能传大于50Kb! 46 | 47 | ) 48 | } 49 | CheckUpload.storyName = '限制上传'; 50 | 51 | export const DragUpload = (args: JSX.IntrinsicAttributes & UploadProps) => ( 52 | 59 | 60 |
61 |

点击上传 / 拖拽文件到此区域上传

62 |
63 | ); 64 | DragUpload.storyName = '拖拽上传'; -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { library } from '@fortawesome/fontawesome-svg-core'; 3 | import { fas } from '@fortawesome/free-solid-svg-icons'; 4 | import BetterButton from './components/Button'; 5 | import BetterMenu from './components/Menu'; 6 | import BetterIcon from './components/Icon'; 7 | import BetterTransition from './components/Transition'; 8 | 9 | library.add(fas); 10 | 11 | function App() { 12 | const [ show, setShow ] = useState(false); 13 | 14 | return ( 15 |
16 |
17 | 18 | alert(index)} mode="horizontal"> 19 | 20 | cool link 21 | 22 | 23 | cool link 2 24 | 25 | 26 | 27 | dropdown1 28 | 29 | 30 | dropdown2 31 | 32 | 33 | 34 | cool link 3 35 | 36 | 37 | setShow(!show) }>{ show ? 'close' : 'open' } 38 | 43 |
hi, I am better
44 |
45 | 51 |
better btn
52 |
53 |
54 |
55 | ); 56 | } 57 | 58 | export default App; 59 | -------------------------------------------------------------------------------- /src/components/Button/button.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import classNames from "classnames"; 3 | 4 | export type ButtonSize = 'lg' | 'sm'; 5 | export type ButtonType = 'primary' | 'default' | 'danger' | 'link'; 6 | interface BaseButtonProps { 7 | /**选填,设置 Button 的自定义类名 */ 8 | className?: string; 9 | /**选填,设置 Button 的禁用 */ 10 | disabled?: boolean; 11 | /**选填,设置 Button 的尺寸 */ 12 | size?: ButtonSize; 13 | /**选填,设置 Button 的类型 */ 14 | btnType?: ButtonType; 15 | /**选填,设置 Button 的子元素 */ 16 | children: React.ReactNode; 17 | /**选填,设置 Button 的超链接目标,仅在btnType属性为link时有效 */ 18 | href?: string; 19 | }; 20 | 21 | type NativeButtonProps = BaseButtonProps & React.ButtonHTMLAttributes; 22 | type AnchorButtonProps = BaseButtonProps & React.AnchorHTMLAttributes; 23 | export type ButtonProps = Partial; 24 | 25 | /** 26 | * 页面中最常用的的按钮元素,适合于完成特定的交互,支持 HTML button 和 a 链接 的所有属性 27 | * 28 | * ~~~js 29 | * // 这样引用 30 | * import { BetterButton } from 'betterui'; 31 | * ~~~ 32 | * 33 | */ 34 | export const Button: React.FC = (props) => { 35 | const { 36 | btnType, 37 | className, 38 | disabled, 39 | size, 40 | children, 41 | href, 42 | ...restProps 43 | } = props; 44 | 45 | const classes = classNames("btn", className, { 46 | [`btn-${btnType}`]: btnType, 47 | [`btn-${size}`]: size, 48 | "disabled": (btnType === "link") && disabled, 49 | }); 50 | 51 | // 当按钮类型为 Link 时,返回 a 标签 52 | if (btnType === "link" && href) { 53 | return ( 54 | 59 | { children } 60 | 61 | ); 62 | } else { 63 | return ( 64 | 71 | ); 72 | } 73 | } 74 | 75 | Button.defaultProps = { 76 | disabled: false, 77 | btnType: "default", 78 | }; 79 | 80 | export default Button; -------------------------------------------------------------------------------- /src/stories/assets/plugin.svg: -------------------------------------------------------------------------------- 1 | illustration/plugin -------------------------------------------------------------------------------- /storybook-static/static/media/plugin.d494b22808806ebe8ff4c5b276819e72.svg: -------------------------------------------------------------------------------- 1 | illustration/plugin -------------------------------------------------------------------------------- /src/components/Button/button.test.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable testing-library/prefer-screen-queries */ 2 | import React from 'react'; 3 | import { render, fireEvent } from '@testing-library/react'; 4 | import Button, { ButtonProps } from './button'; 5 | 6 | const defaultProps = { 7 | onClick: jest.fn(), 8 | }; 9 | 10 | const testProps: ButtonProps = { 11 | btnType: 'primary', 12 | size: 'lg', 13 | className: 'klass' 14 | } 15 | 16 | const disabledProps: ButtonProps = { 17 | disabled: true, 18 | onClick: jest.fn(), 19 | }; 20 | 21 | describe('test Button component', () => { 22 | it('should render the correct default button', () => { 23 | const view = render(); 24 | const element = view.getByText('Nice') as HTMLButtonElement; 25 | expect(element).toBeInTheDocument(); 26 | expect(element.tagName).toEqual('BUTTON'); 27 | expect(element).toHaveClass('btn btn-default'); 28 | expect(element.disabled).toBeFalsy(); 29 | fireEvent.click(element); 30 | expect(defaultProps.onClick).toHaveBeenCalled(); 31 | }); 32 | it('should render the correct component based on different props', () => { 33 | const view = render(); 34 | const element = view.getByText('Nice'); 35 | expect(element).toBeInTheDocument(); 36 | expect(element).toHaveClass('btn klass btn-primary btn-lg'); 37 | }); 38 | it('should render a link when btnType equals link and href is provided', () => { 39 | const view = render(); 40 | const element = view.getByText('Link'); 41 | expect(element).toBeInTheDocument(); 42 | expect(element.tagName).toEqual('A'); 43 | expect(element).toHaveClass('btn btn-link'); 44 | }); 45 | it('should render disabled button when disabled set to true', () => { 46 | const view = render(); 47 | const element = view.getByText('Nice') as HTMLButtonElement; 48 | expect(element).toBeInTheDocument(); 49 | expect(element.disabled).toBeTruthy(); 50 | fireEvent.click(element); 51 | expect(disabledProps.onClick).not.toHaveBeenCalled(); 52 | }); 53 | }); -------------------------------------------------------------------------------- /src/components/Input/input.test.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable testing-library/prefer-presence-queries */ 2 | /* eslint-disable testing-library/no-container */ 3 | /* eslint-disable testing-library/no-node-access */ 4 | /* eslint-disable testing-library/prefer-screen-queries */ 5 | /* eslint-disable testing-library/render-result-naming-convention */ 6 | import React from 'react'; 7 | import { render, fireEvent } from '@testing-library/react'; 8 | import { Input, InputProps } from './input'; 9 | 10 | const defaultProps: InputProps = { 11 | onChange: jest.fn(), 12 | placeholder: 'test-input', 13 | }; 14 | 15 | describe('test Input component', () => { 16 | it('should render the correct default Input', () => { 17 | const wrapper = render(); 18 | const testNode = wrapper.getByPlaceholderText('test-input') as HTMLInputElement; 19 | expect(testNode).toBeInTheDocument(); 20 | expect(testNode).toHaveClass('better-input-inner'); 21 | fireEvent.change(testNode, { target: { value: '23' } }); 22 | expect(defaultProps.onChange).toHaveBeenCalled(); 23 | expect(testNode.value).toEqual('23'); 24 | }); 25 | it('should render the disabled Input on disabled property', () => { 26 | const wrapper = render(); 27 | const testNode = wrapper.getByPlaceholderText('disabled') as HTMLInputElement; 28 | expect(testNode.disabled).toBeTruthy(); 29 | }); 30 | it('should render different input sizes on size property', () => { 31 | const wrapper = render(); 32 | const testContainer = wrapper.container.querySelector('.better-input-wrapper'); 33 | expect(testContainer).toHaveClass('input-size-lg'); 34 | }); 35 | it('should render prepand and append element on prepand/append property', () => { 36 | const {queryByText, container } = render(); 37 | const testContainer = container.querySelector('.better-input-wrapper'); 38 | expect(testContainer).toHaveClass('input-group input-group-append input-group-prepend'); 39 | expect(queryByText('https://')).toBeInTheDocument(); 40 | expect(queryByText('.com')).toBeInTheDocument(); 41 | }); 42 | }); -------------------------------------------------------------------------------- /src/components/Transition/Transition.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { ComponentStory, ComponentMeta } from '@storybook/react'; 3 | import BetterTransition from './transition'; 4 | import BetterButton from '../Button'; 5 | 6 | export default { 7 | title: 'Transition 过渡效果', 8 | id: 'BetterTransition', 9 | component: BetterTransition, 10 | parameters: { 11 | docs: { 12 | source: { 13 | type: "code", 14 | }, 15 | }, 16 | }, 17 | } as ComponentMeta; 18 | 19 | export const DefaultTransition: ComponentStory = (args) => { 20 | const [ show, setShow ] = useState(false); 21 | return ( 22 | <> 23 | setShow(!show) } 27 | > 28 | { show ? 'close' : 'open' } 29 | 30 | 31 | {/* 当wrapper为true时,Transition组件的子元素无须包裹一个根元素 */} 32 | 33 | hi, I am betterui! 34 | 35 | 36 | {/* 当wrapper为false时,Transition组件的子元素必须包裹一个根元素 */} 37 | 38 |
hi, I am betterui!
39 |
40 | 41 | ); 42 | }; 43 | 44 | DefaultTransition.args = { 45 | animation: 'zoom-in-left', 46 | wrapper: false, 47 | }; 48 | DefaultTransition.storyName = '默认的过渡效果'; 49 | 50 | export const CombineTransition = () => { 51 | const [ show, setShow ] = useState(false); 52 | return ( 53 | <> 54 | setShow(!show) } 59 | > 60 | { show ? 'close' : 'open' } 61 | 62 | 63 | 69 | Default BetterButton 70 | 71 | 72 | ); 73 | }; 74 | CombineTransition.storyName = '其他组件和过渡效果结合使用'; -------------------------------------------------------------------------------- /src/styles/_mixin.scss: -------------------------------------------------------------------------------- 1 | @mixin button-size($padding-y, $padding-x, $font-size, $border-raduis) { 2 | padding: $padding-y $padding-x; 3 | font-size: $font-size; 4 | border-radius: $border-raduis; 5 | } 6 | 7 | @mixin button-style( 8 | $background, 9 | $border, 10 | $color, 11 | $hover-background: lighten($background, 7.5%), 12 | $hover-border: lighten($border, 10%), 13 | $hover-color: $color, 14 | ) { 15 | color: $color; 16 | background: $background; 17 | border-color: $border; 18 | &:hover { 19 | color: $hover-color; 20 | background: $hover-background; 21 | border-color: $hover-border; 22 | } 23 | &:focus, 24 | &.focus { 25 | color: $hover-color; 26 | background: $hover-background; 27 | border-color: $hover-border; 28 | } 29 | &:disabled, 30 | &.disabled { 31 | color: $color; 32 | background: $background; 33 | border-color: $border; 34 | } 35 | } 36 | 37 | @mixin alert-style($background, $border, $color) { 38 | color: $color; 39 | background: $background; 40 | border-color: $border; 41 | } 42 | 43 | @mixin zoom-animation( 44 | $direction: 'top', 45 | $scaleStart: scaleY(0), 46 | $scaleEnd: scaleY(1), 47 | $origin: center top, 48 | ) { 49 | .zoom-in-#{$direction}-enter { 50 | opacity: 0; 51 | transform: $scaleStart; 52 | } 53 | .zoom-in-#{$direction}-enter-active { 54 | opacity: 1; 55 | transform: $scaleEnd; 56 | transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1) 100ms, opacity 300ms cubic-bezier(0.23, 1, 0.32, 1) 100ms; 57 | transform-origin: $origin 58 | } 59 | .zoom-in-#{$direction}-exit { 60 | opacity: 1; 61 | } 62 | .zoom-in-#{$direction}-exit-active { 63 | opacity: 0; 64 | transform: $scaleStart; 65 | transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1) 100ms, opacity 300ms cubic-bezier(0.23, 1, 0.32, 1) 100ms; 66 | transform-origin: $origin; 67 | } 68 | } 69 | 70 | @mixin border-right-radius($raduis) { 71 | border-top-right-radius: $raduis; 72 | border-bottom-right-radius: $raduis; 73 | } 74 | 75 | @mixin border-left-radius($raduis) { 76 | border-top-left-radius: $raduis; 77 | border-bottom-left-radius: $raduis; 78 | } 79 | 80 | @mixin border-top-radius($raduis) { 81 | border-top-left-radius: $raduis; 82 | border-top-right-radius: $raduis; 83 | } -------------------------------------------------------------------------------- /src/components/Input/input.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement, InputHTMLAttributes, ChangeEvent, forwardRef } from 'react'; 2 | import classNames from 'classnames'; 3 | import { IconProp } from '@fortawesome/fontawesome-svg-core'; 4 | import Icon from '../Icon'; 5 | 6 | type InputSize = 'lg' | 'sm'; 7 | export interface InputProps extends Omit, 'size'> { 8 | /**选填,设置 Input 的禁用 */ 9 | disabled?: boolean; 10 | /**选填,设置 Input 大小,支持 lg 或者是 sm */ 11 | size?: InputSize; 12 | /**选填,设置 Input 右侧悬浮的图标,用于提示 */ 13 | icon?: IconProp; 14 | /**选填,设置 Input 前缀,用于配置一些固定组合 */ 15 | prepend?: string | ReactElement; 16 | /**选填,设置 Input 后缀,用于配置一些固定组合 */ 17 | append?: string | ReactElement; 18 | /**选填,设置 Input 的占位符 */ 19 | placeholder?: string; 20 | /**选填,设置 Input 的 change 事件 */ 21 | onChange? : (e: ChangeEvent) => void; 22 | }; 23 | 24 | /** 25 | * Input 输入框,通过鼠标或键盘输入内容,是最基础的表单域的包装,支持 HTMLInput 的所有基本属性 26 | * 27 | * ~~~js 28 | * // 这样引用 29 | * import { BetterInput } from 'betterui'; 30 | * ~~~ 31 | * 32 | */ 33 | export const Input = forwardRef((props, ref) => { 34 | const { 35 | disabled, 36 | size, 37 | icon, 38 | prepend, 39 | append, 40 | style, 41 | ...restProps 42 | } = props; 43 | 44 | const cnames = classNames('better-input-wrapper', { 45 | [`input-size-${size}`]: size, 46 | 'is-disabled': disabled, 47 | 'input-group': prepend || append, 48 | 'input-group-append': !!append, 49 | 'input-group-prepend': !!prepend, 50 | }); 51 | const fixControlledValue = (value: any) => { 52 | if (typeof value === 'undefined' || value === null) { 53 | return ''; 54 | } 55 | return value; 56 | }; 57 | if('value' in props) { 58 | delete restProps.defaultValue; 59 | restProps.value = fixControlledValue(props.value); 60 | } 61 | return ( 62 |
63 | { prepend &&
{ prepend }
} 64 | { icon &&
} 65 | 71 | { append &&
{ append }
} 72 |
73 | ); 74 | }); 75 | 76 | export default Input; -------------------------------------------------------------------------------- /src/components/Tabs/tabs.test.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable testing-library/prefer-presence-queries */ 2 | /* eslint-disable testing-library/prefer-screen-queries */ 3 | /* eslint-disable testing-library/no-node-access */ 4 | /* eslint-disable testing-library/no-render-in-setup */ 5 | import React from 'react'; 6 | import { render, fireEvent, RenderResult } from '@testing-library/react'; 7 | import '@testing-library/jest-dom/extend-expect'; 8 | import Tabs, { TabsProps } from './tabs'; 9 | import TabItem from './tabItem'; 10 | 11 | const testProps: TabsProps = { 12 | defaultIndex: 1, 13 | onSelect: jest.fn(), 14 | }; 15 | let wrapper: RenderResult; 16 | 17 | describe('test Tabs Component', () => { 18 | beforeEach(() => { 19 | wrapper = render( 20 | 21 | content1 22 | content2 23 | content3 24 | 25 | ); 26 | }); 27 | afterEach(() => { 28 | jest.clearAllMocks(); 29 | }); 30 | it('should render the correct default Tabs', () => { 31 | const { queryByText, container } = wrapper; 32 | expect(container.querySelector('.better-tabs-nav')).toHaveClass('nav-line'); 33 | const activeElement = queryByText('tab2'); 34 | expect(activeElement).toBeInTheDocument(); 35 | expect(activeElement).toHaveClass('is-active'); 36 | expect(queryByText('tab1')).not.toHaveClass('is-active'); 37 | expect(queryByText('content2')).toBeInTheDocument(); 38 | expect(queryByText('content1')).not.toBeInTheDocument(); 39 | }); 40 | it('click tabItem should switch to content', () => { 41 | const { queryByText, getByText } = wrapper; 42 | const clickedElement = getByText('tab1'); 43 | fireEvent.click(clickedElement); 44 | expect(clickedElement).toHaveClass('is-active'); 45 | expect(queryByText('tab2')).not.toHaveClass('is-active'); 46 | expect(queryByText('content1')).toBeInTheDocument(); 47 | expect(queryByText('content2')).not.toBeInTheDocument(); 48 | expect(testProps.onSelect).toHaveBeenCalledWith(0); 49 | }); 50 | it('click disabled tabItem should not works', () => { 51 | const { getByText } = wrapper; 52 | const disableElement = getByText('disabled'); 53 | expect(disableElement).toHaveClass('disabled'); 54 | fireEvent.click(disableElement); 55 | expect(disableElement).not.toHaveClass('is-active'); 56 | expect(testProps.onSelect).not.toHaveBeenCalled(); 57 | }); 58 | }); -------------------------------------------------------------------------------- /src/stories/assets/stackalt.svg: -------------------------------------------------------------------------------- 1 | illustration/stackalt -------------------------------------------------------------------------------- /src/components/Form/form.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, createContext, forwardRef, useImperativeHandle } from 'react'; 2 | import { ValidateError } from 'async-validator'; 3 | import useStore, { FormState } from './useStore'; 4 | 5 | export type RenderProps = (form: FormState) => ReactNode; 6 | export interface FormProps { 7 | /**选填,设置表单名称,会作为表单字段 id 前缀使用 */ 8 | name?: string; 9 | /**选填,设置表单的初始值 */ 10 | initialValues?: Record; 11 | /**选填,设置表单的子元素 */ 12 | children?: ReactNode | RenderProps; 13 | /**选填,设置表单提交成功时的回调函数 */ 14 | onSuccessfulSubmit?: (values: Record) => void; 15 | /**选填,设置表单提交失败时的回调函数 */ 16 | onFailedSubmit?: (values: Record, errors: Record) => void; 17 | }; 18 | export type IFormContext = Pick, 'dispatch' | 'fields' | 'validateField'> & Pick; 19 | export type IFormRef = Omit, 'dispatch' | 'fields' | 'form'>; 20 | export const FormContext = createContext({} as IFormContext); 21 | 22 | /** 23 | * Form 表单,用以收集、校验和提交数据,一般由输入框、单选框、复选框、选择器等控件组成。 24 | * 25 | * ~~~js 26 | * // 这样引用,再分别使用 27 | * import { BetterForm } from 'betterui'; 28 | * ~~~ 29 | * 30 | */ 31 | export const Form = forwardRef((props, ref) => { 32 | const { name, children, initialValues, onSuccessfulSubmit, onFailedSubmit } = props; 33 | const { form, fields, dispatch, ...restProps } = useStore(initialValues); 34 | const { validateField, validateAllFields } = restProps; 35 | useImperativeHandle(ref, () => ({ ...restProps, })); 36 | const passedContext: IFormContext = { dispatch, fields, initialValues, validateField, }; 37 | const submitForm = async (e: React.FormEvent) => { 38 | e.preventDefault(); 39 | e.stopPropagation(); 40 | const { isValid, errors, values, } = await validateAllFields(); 41 | if (isValid) { 42 | onSuccessfulSubmit && onSuccessfulSubmit(values); 43 | } else { 44 | onFailedSubmit && onFailedSubmit(values, errors); 45 | } 46 | }; 47 | return ( 48 | <> 49 |
50 | 51 | { typeof children === 'function' ? children(form) : children } 52 | 53 |
54 | 55 | ); 56 | }); 57 | 58 | Form.defaultProps = { 59 | name: 'better-form', 60 | }; 61 | 62 | export default Form; -------------------------------------------------------------------------------- /storybook-static/index.html: -------------------------------------------------------------------------------- 1 | Webpack App
-------------------------------------------------------------------------------- /storybook-static/static/media/stackalt.dba9fbb33e1e5daf57e0cf575f818e65.svg: -------------------------------------------------------------------------------- 1 | illustration/stackalt -------------------------------------------------------------------------------- /src/components/Tabs/tabs.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState, FunctionComponentElement, ReactNode } from 'react'; 2 | import classNames from 'classnames'; 3 | import { TabItemProps } from './tabItem'; 4 | 5 | export interface TabsProps { 6 | /**选填,设置 Tabs 当前激活的index,默认为0 */ 7 | defaultIndex?: number; 8 | /**选填,设置 Tabs 可以扩展的类名 */ 9 | className?: string; 10 | /**选填,点击 Tab 触发的回调函数 */ 11 | onSelect?: (selectedIndex: number) => void; 12 | /**选填,设置 Tabs 的类型,两种可选,默认为 line */ 13 | type?: 'line' | 'card'; 14 | /**选填,设置 Tabs 的子元素 */ 15 | children?: ReactNode; 16 | }; 17 | 18 | /** 19 | * 用于承载同一层级下不同页面或类别的组件,方便用户在同一个页面框架下进行快速切换。 20 | * 21 | * ~~~js 22 | * // 这样引用,再分别使用 23 | * import { BetterTabs } from 'betterui'; 24 | * ~~~ 25 | * 26 | */ 27 | export const Tabs: FC = (props) => { 28 | const { 29 | defaultIndex, 30 | className, 31 | onSelect, 32 | children, 33 | type, 34 | } = props; 35 | const [ activeIndex, setActiveIndex ] = useState(defaultIndex); 36 | const handleClick = (e: React.MouseEvent, index: number, disabled: boolean | undefined) => { 37 | if (!disabled) { 38 | setActiveIndex(index); 39 | if (onSelect) { 40 | onSelect(index); 41 | } 42 | } 43 | }; 44 | const navClass = classNames('better-tabs-nav', { 45 | 'nav-line': type === 'line', 46 | 'nav-card': type === 'card', 47 | }); 48 | const renderNavLinks = () => { 49 | return React.Children.map(children, (child, index) => { 50 | const childElement = child as FunctionComponentElement; 51 | const { label, disabled } = childElement.props; 52 | const classes = classNames('better-tabs-nav-item', { 53 | 'is-active': activeIndex === index, 54 | 'disabled': disabled, 55 | }); 56 | return ( 57 |
  • { handleClick(e, index, disabled) }} 61 | > 62 | { label } 63 |
  • 64 | ); 65 | }); 66 | }; 67 | const renderContent = () => { 68 | return React.Children.map(children, (child, index) => { 69 | if (index === activeIndex) { 70 | return child; 71 | } 72 | }); 73 | }; 74 | return ( 75 |
    76 |
      77 | { renderNavLinks() } 78 |
    79 |
    80 | { renderContent() } 81 |
    82 |
    83 | ); 84 | }; 85 | 86 | Tabs.defaultProps = { 87 | defaultIndex: 0, 88 | type: 'line', 89 | }; 90 | export default Tabs; -------------------------------------------------------------------------------- /src/components/Select/_style.scss: -------------------------------------------------------------------------------- 1 | .better-select { 2 | width: 350px; 3 | position: relative; 4 | .better-input-wrapper { 5 | cursor: pointer; 6 | &:hover { 7 | input { 8 | border-color: $primary !important; 9 | } 10 | } 11 | } 12 | input { 13 | &[readonly] { 14 | background-color: $input-bg; 15 | border-color: $input-border-color; 16 | cursor: pointer; 17 | opacity: 1; 18 | } 19 | &:disabled { 20 | background-color: $input-disabled-bg; 21 | border-color: $input-disabled-border-color; 22 | opacity: 1; 23 | cursor: not-allowed; 24 | } 25 | } 26 | .icon-wrapper { 27 | transition: transform .25s ease-in-out; 28 | transform: rotate(0deg) !important; 29 | } 30 | } 31 | 32 | .better-select.menu-is-open { 33 | .icon-wrapper { 34 | transform: rotate(180deg) !important; 35 | } 36 | } 37 | 38 | .better-select-dropdown { 39 | list-style:none; 40 | padding-left: 0; 41 | white-space: nowrap; 42 | position: absolute; 43 | background: $white; 44 | z-index: 100; 45 | top: calc(100% + 8px); 46 | left: 0; 47 | border: $menu-border-width solid $menu-border-color; 48 | box-shadow: $submenu-box-shadow; 49 | width: auto; 50 | min-width: 350px; 51 | .better-select-item { 52 | padding: $menu-item-padding-y $menu-item-padding-x; 53 | cursor: pointer; 54 | transition: $menu-transition; 55 | color: $body-color; 56 | display: flex; 57 | align-items: center; 58 | justify-content: space-between; 59 | &.is-selected { 60 | color: $menu-item-active-color ; 61 | font-weight: $font-weight-bold; 62 | } 63 | &.is-disabled { 64 | color: $menu-item-disabled-color; 65 | // pointer-events: none; 66 | cursor: not-allowed; 67 | } 68 | &:hover { 69 | background-color: rgba($primary, .1); 70 | } 71 | } 72 | } 73 | 74 | .better-selected-tags { 75 | position: absolute; 76 | z-index: 100; 77 | top: 0; 78 | left: 0; 79 | height: 100%; 80 | max-width: 100%; 81 | display: flex; 82 | justify-content: center; 83 | align-items: center; 84 | flex-wrap: wrap; 85 | .better-tag { 86 | height: auto; 87 | padding: 2px 5px; 88 | box-sizing: border-box; 89 | border: 1px solid rgba($primary, .2); 90 | margin: 2px 3px 0px 3px; 91 | border-radius: 3px; 92 | color: $primary; 93 | background-color: rgba($primary, .1); 94 | } 95 | .better-icon { 96 | margin-left: 3px; 97 | cursor: pointer; 98 | &:hover { 99 | color: darken($primary, 10%) 100 | } 101 | } 102 | } -------------------------------------------------------------------------------- /src/components/Menu/_style.scss: -------------------------------------------------------------------------------- 1 | .better-menu { 2 | display: flex; 3 | flex-wrap: wrap; 4 | padding-left: 0; 5 | margin-bottom: 30px; 6 | list-style: none; 7 | border-bottom: $menu-border-width solid $menu-border-color; 8 | box-shadow: $menu-box-shadow; 9 | >.menu-item { 10 | padding: $menu-item-padding-y $menu-item-padding-x; 11 | cursor: pointer; 12 | transition: $menu-transition; 13 | &:hover, &:focus { 14 | text-decoration: none; 15 | } 16 | &.is-disabled { 17 | color: $menu-item-disabled-color; 18 | // pointer-events: none; 19 | cursor: not-allowed; 20 | } 21 | &.is-active { 22 | color: $menu-item-active-color; 23 | border-bottom: $menu-item-active-border-width solid $menu-item-active-color; 24 | } 25 | } 26 | .submenu-item { 27 | position: relative; 28 | .submenu-title { 29 | display: flex; 30 | // align-items: center; 31 | } 32 | .arrow-icon { 33 | transition: transform .25s ease-in-out; 34 | margin-left: 3px; 35 | } 36 | &:hover { 37 | .arrow-icon { 38 | transform: rotate(180deg); 39 | } 40 | } 41 | } 42 | .is-vertical { 43 | .arrow-icon { 44 | transform: rotate(0deg) !important; 45 | } 46 | } 47 | .is-vertical.is-opened { 48 | .arrow-icon { 49 | transform: rotate(180deg) !important; 50 | } 51 | } 52 | .better-submenu { 53 | // display: none; 54 | list-style:none; 55 | padding-left: 0; 56 | white-space: nowrap; 57 | //transition: $menu-transition; 58 | .menu-item { 59 | padding: $menu-item-padding-y $menu-item-padding-x; 60 | cursor: pointer; 61 | transition: $menu-transition; 62 | color: $body-color; 63 | &.is-active, &:hover { 64 | color: $menu-item-active-color !important; 65 | } 66 | } 67 | } 68 | .better-submenu.menu-opened { 69 | // display: block; 70 | } 71 | } 72 | .menu-horizontal { 73 | >.menu-item { 74 | border-bottom: $menu-item-active-border-width solid transparent; 75 | } 76 | .better-submenu { 77 | position: absolute; 78 | background: $white; 79 | z-index: 100; 80 | top: calc(100% + 8px); 81 | left: 0; 82 | border: $menu-border-width solid $menu-border-color; 83 | box-shadow: $submenu-box-shadow; 84 | } 85 | } 86 | .menu-vertical { 87 | flex-direction: column; 88 | border-bottom: 0px; 89 | margin: 10px 20px; 90 | border-right: $menu-border-width solid $menu-border-color; 91 | >.menu-item { 92 | border-left: $menu-item-active-border-width solid transparent; 93 | &.is-active { 94 | border-bottom: 0px; 95 | border-left: $menu-item-active-border-width solid $menu-item-active-color; 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /src/welcome.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | 4 | storiesOf('Welcome 首页', module) 5 | .add('安装', () => { 6 | return ( 7 | <> 8 |

    欢迎来到 betterui 组件库

    9 |

    这是一个基于 React 的组件库,用于快速搭建页面。

    10 |

    安装

    11 |
    npm 安装
    12 |

    推荐使用 npm 的方式安装,它能更好地和 webpack 打包工具配合使用。

    13 | 14 | npm install @zhuangjiaqing/betterui --save 15 | 16 | 17 |

    18 | 19 |
    CDN
    20 |

    目前可以通过 unpkg.com/@zhuangjiaqing/betterui 获取到最新版本的资源,在页面上引入 css 和 js 文件即可开始使用。

    21 | 22 | {'<'}!-- 引入样式 --{'>'} 23 |

    <link rel="stylesheet" href="https://unpkg.com/@zhuangjiaqing/betterui@1.0.3/dist/index.css" />

    24 | {'<'}!-- 引入组件库 --{'>'} 25 |

    <script src="https://unpkg.com/@zhuangjiaqing/betterui@1.0.3/dist/index.umd.js"></script>

    26 |
    27 |

    注意:我们建议使用 CDN 引入 betterui 的用户在链接地址上锁定版本,以免将来 betterui 升级时受到非兼容性更新的影响。锁定版本的方法请查看 unpkg.com

    28 | 29 | ); 30 | }, { info : { disable: true } }) 31 | .add('快速上手', () => { 32 | return ( 33 | <> 34 |

    这里我们提供两个基础的例子来快速上手 betterui。

    35 |

    CDN

    36 |

    37 | 通过 CDN 的方式我们可以很容易地使用 betterui 写出一个 Hello world 页面。 38 | 示例源码 39 |   40 | 在线演示 41 |

    42 |

    npm安装

    43 |

    44 | 45 | npm install @zhuangjiaqing/betterui --save 46 | 47 |

    48 |

    49 | 修改 src/App.tsx,引入 betterui 的按钮组件。 50 |

    51 |

    52 | 53 |

    import React from 'react';

    54 |

    import '@zhuangjiaqing/betterui/dist/index.css';

    55 |

    import { '{ BetterButton }' } from '@zhuangjiaqing/betterui';

    56 |

    57 | {`function App() { 58 | return ( 59 |

    60 | Hello World 61 |
    62 | ); 63 | }`} 64 |

    65 |

    export default App;

    66 |
    67 |

    68 | 69 |

    70 | 好了,现在你应该能看到页面上已经有了 betterui 的蓝色按钮组件,接下来就可以继续选用其他组件开发应用了。 71 | 示例源码 72 |

    73 | 74 | ); 75 | }, { info : { disable: true } }); 76 | -------------------------------------------------------------------------------- /src/stories/Page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Header } from './Header'; 4 | import './page.css'; 5 | 6 | type User = { 7 | name: string; 8 | }; 9 | 10 | export const Page: React.VFC = () => { 11 | const [user, setUser] = React.useState(); 12 | 13 | return ( 14 |
    15 |
    setUser({ name: 'Jane Doe' })} 18 | onLogout={() => setUser(undefined)} 19 | onCreateAccount={() => setUser({ name: 'Jane Doe' })} 20 | /> 21 | 22 |
    23 |

    Pages in Storybook

    24 |

    25 | We recommend building UIs with a{' '} 26 | 27 | component-driven 28 | {' '} 29 | process starting with atomic components and ending with pages. 30 |

    31 |

    32 | Render pages with mock data. This makes it easy to build and review page states without 33 | needing to navigate to them in your app. Here are some handy patterns for managing page 34 | data in Storybook: 35 |

    36 |
      37 |
    • 38 | Use a higher-level connected component. Storybook helps you compose such data from the 39 | "args" of child component stories 40 |
    • 41 |
    • 42 | Assemble data in the page component from your services. You can mock these services out 43 | using Storybook. 44 |
    • 45 |
    46 |

    47 | Get a guided tutorial on component-driven development at{' '} 48 | 49 | Storybook tutorials 50 | 51 | . Read more in the{' '} 52 | 53 | docs 54 | 55 | . 56 |

    57 |
    58 | Tip Adjust the width of the canvas with the{' '} 59 | 60 | 61 | 66 | 67 | 68 | Viewports addon in the toolbar 69 |
    70 |
    71 |
    72 | ); 73 | }; 74 | -------------------------------------------------------------------------------- /src/components/Input/_style.scss: -------------------------------------------------------------------------------- 1 | .better-input-wrapper { 2 | display: flex; 3 | width: 350px; 4 | margin-bottom: 15px; 5 | position: relative; 6 | .icon-wrapper { 7 | position: absolute; 8 | height: 100%; 9 | width: 35px; 10 | justify-content: center; 11 | color: $input-color; 12 | right: 0; 13 | top: 0; 14 | display: flex; 15 | align-items: center; 16 | cursor: pointer; 17 | svg { 18 | color: $input-placeholder-color; 19 | } 20 | } 21 | } 22 | .icon-wrapper+.better-input-inner { 23 | padding-right: 35px; 24 | } 25 | .better-input-inner { 26 | width: 100%; 27 | padding: $input-padding-y $input-padding-x; 28 | font-family: $input-font-family; 29 | font-size: $input-font-size; 30 | font-weight: $input-font-weight; 31 | line-height: $input-line-height; 32 | color: $input-color; 33 | background-color: $input-bg; 34 | background-clip: padding-box; 35 | border: $input-border-width solid $input-border-color; 36 | border-radius: $input-border-radius; 37 | 38 | box-shadow: $input-box-shadow; 39 | transition: $input-transition; 40 | 41 | &:focus { 42 | color: $input-focus-color; 43 | background-color: $input-focus-bg; 44 | border-color: $input-focus-border-color; 45 | outline: 0; 46 | box-shadow: $input-focus-box-shadow; 47 | } 48 | &::placeholder { 49 | color: $input-placeholder-color; 50 | opacity: 1; 51 | } 52 | &:disabled, 53 | &[readonly] { 54 | background-color: $input-disabled-bg; 55 | border-color: $input-disabled-border-color; 56 | opacity: 1; 57 | cursor: not-allowed; 58 | } 59 | } 60 | .better-input-group-prepend, 61 | .better-input-group-append { 62 | display: flex; 63 | align-items: center; 64 | padding: $input-padding-y $input-padding-x; 65 | margin-bottom: 0; 66 | font-size: $input-font-size; 67 | font-weight: $font-weight-normal; 68 | line-height: $input-line-height; 69 | color: $input-group-addon-color; 70 | text-align: center; 71 | white-space: nowrap; 72 | background-color: $input-group-addon-bg; 73 | border: $input-border-width solid $input-group-addon-border-color; 74 | border-radius: $input-border-radius; 75 | } 76 | .better-input-group-append + .btn { 77 | padding: 0; 78 | border: 0; 79 | } 80 | .input-group > .better-input-group-prepend, 81 | .input-group.input-group-append > .better-input-inner { 82 | @include border-right-radius(0); 83 | } 84 | 85 | .input-group > .better-input-group-append, 86 | .input-group.input-group-prepend > .better-input-inner { 87 | @include border-left-radius(0); 88 | } 89 | 90 | .input-size-sm .better-input-inner { 91 | padding: $input-padding-y-sm $input-padding-x-sm; 92 | font-size: $input-font-size-sm; 93 | border-radius: $input-border-radius-sm; 94 | } 95 | 96 | .input-size-lg .better-input-inner { 97 | padding: $input-padding-y-lg $input-padding-x-lg; 98 | font-size: $input-font-size-lg; 99 | border-radius: $input-border-radius-lg; 100 | } 101 | -------------------------------------------------------------------------------- /src/components/Menu/menu.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, createContext, useState } from "react"; 2 | import classNames from "classnames"; 3 | import { MenuItemProps } from "./menuItem"; 4 | 5 | type MenuMode = "horizontal" | "vertical"; 6 | type SelectCallback = (selectedIndex: string) => void; 7 | 8 | export interface MenuProps { 9 | /**选填,设置 active 菜单项的索引值 */ 10 | defaultIndex?: string; 11 | /**选填,设置 Menu 的自定义类名 */ 12 | className?: string; 13 | /**选填,设置 Menu 的展示类型,分为横向模式(horizontal)和纵向模式(vertical) */ 14 | mode?: MenuMode; 15 | /**选填,设置 Menu 的自定义样式 */ 16 | style?: React.CSSProperties; 17 | /**选填,设置 Menu 的子元素 */ 18 | children?: ReactNode; 19 | /**选填,点击菜单项触发的回调函数 */ 20 | onSelect?: SelectCallback; 21 | /**选填,设置默认展开的子菜单数组,仅当 mode 为纵向模式(vertical)时生效 */ 22 | defaultOpenSubMenus?: string[]; 23 | }; 24 | interface IMenuContext { 25 | index: string; 26 | onSelect?: SelectCallback; 27 | mode?: MenuMode; 28 | defaultOpenSubMenus?: string[]; 29 | }; 30 | 31 | export const MenuContext = createContext({ index: '0' }); 32 | 33 | /** 34 | * 为网站提供导航功能的菜单。支持横向纵向两种模式,支持下拉菜单 35 | * 36 | * ~~~js 37 | * // 这样引用,再分别使用 38 | * import { BetterMenu } from 'betterui'; 39 | * ~~~ 40 | * 41 | */ 42 | export const Menu: React.FC = (props) => { 43 | const { 44 | className, 45 | mode, 46 | style, 47 | children, 48 | defaultIndex, 49 | onSelect, 50 | defaultOpenSubMenus, 51 | } = props; 52 | const [ currentActive, setActive ] = useState(defaultIndex); 53 | const classes = classNames("better-menu", className, { 54 | "menu-vertical": mode === "vertical", 55 | 'menu-horizontal': mode !== 'vertical', 56 | }); 57 | const handleClick = (index: string) => { 58 | setActive(index); 59 | if (onSelect) { 60 | onSelect(index); 61 | } 62 | } 63 | const passedContext: IMenuContext = { 64 | index: currentActive || '0', 65 | onSelect: handleClick, 66 | mode, 67 | defaultOpenSubMenus, 68 | }; 69 | const renderChildren = () => { 70 | return React.Children.map(children, (child, index) => { 71 | const childElement = child as React.FunctionComponentElement; 72 | const { displayName } = childElement.type; 73 | if (displayName === 'MenuItem' || displayName === 'SubMenu') { 74 | return React.cloneElement(childElement, { 75 | index: index.toString(), 76 | }); 77 | } else { 78 | console.error('Warning: Menu has a child which is not a MenuItem component'); 79 | } 80 | }); 81 | } 82 | 83 | return ( 84 |
      89 | 90 | { renderChildren() } 91 | 92 |
    93 | ); 94 | } 95 | 96 | Menu.defaultProps = { 97 | defaultIndex: '0', 98 | mode: "horizontal", 99 | defaultOpenSubMenus: [], 100 | }; 101 | 102 | export default Menu; -------------------------------------------------------------------------------- /storybook-static/824.a95daad82d70a59c564b.manager.bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | object-assign 3 | (c) Sindre Sorhus 4 | @license MIT 5 | */ 6 | 7 | /*! 8 | * Fuse.js v3.6.1 - Lightweight fuzzy-search (http://fusejs.io) 9 | * 10 | * Copyright (c) 2012-2017 Kirollos Risk (http://kiro.me) 11 | * All Rights Reserved. Apache Software License 2.0 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | */ 15 | 16 | /*! 17 | * https://github.com/es-shims/es5-shim 18 | * @license es5-shim Copyright 2009-2020 by contributors, MIT License 19 | * see https://github.com/es-shims/es5-shim/blob/master/LICENSE 20 | */ 21 | 22 | /*! 23 | * isobject 24 | * 25 | * Copyright (c) 2014-2017, Jon Schlinkert. 26 | * Released under the MIT License. 27 | */ 28 | 29 | /*! ***************************************************************************** 30 | Copyright (c) Microsoft Corporation. 31 | 32 | Permission to use, copy, modify, and/or distribute this software for any 33 | purpose with or without fee is hereby granted. 34 | 35 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 36 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 37 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 38 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 39 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 40 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 41 | PERFORMANCE OF THIS SOFTWARE. 42 | ***************************************************************************** */ 43 | 44 | /*! store2 - v2.13.1 - 2021-12-20 45 | * Copyright (c) 2021 Nathan Bubna; Licensed (MIT OR GPL-3.0) */ 46 | 47 | /** 48 | * @license React 49 | * react-dom.production.min.js 50 | * 51 | * Copyright (c) Facebook, Inc. and its affiliates. 52 | * 53 | * This source code is licensed under the MIT license found in the 54 | * LICENSE file in the root directory of this source tree. 55 | */ 56 | 57 | /** 58 | * @license React 59 | * react.production.min.js 60 | * 61 | * Copyright (c) Facebook, Inc. and its affiliates. 62 | * 63 | * This source code is licensed under the MIT license found in the 64 | * LICENSE file in the root directory of this source tree. 65 | */ 66 | 67 | /** 68 | * @license React 69 | * scheduler.production.min.js 70 | * 71 | * Copyright (c) Facebook, Inc. and its affiliates. 72 | * 73 | * This source code is licensed under the MIT license found in the 74 | * LICENSE file in the root directory of this source tree. 75 | */ 76 | 77 | /** 78 | * React Router DOM v6.0.2 79 | * 80 | * Copyright (c) Remix Software Inc. 81 | * 82 | * This source code is licensed under the MIT license found in the 83 | * LICENSE.md file in the root directory of this source tree. 84 | * 85 | * @license MIT 86 | */ 87 | 88 | /** 89 | * React Router v6.0.2 90 | * 91 | * Copyright (c) Remix Software Inc. 92 | * 93 | * This source code is licensed under the MIT license found in the 94 | * LICENSE.md file in the root directory of this source tree. 95 | * 96 | * @license MIT 97 | */ 98 | -------------------------------------------------------------------------------- /src/components/Menu/subMenu.tsx: -------------------------------------------------------------------------------- 1 | import React,{ useContext, useState, FunctionComponentElement, ReactNode } from "react"; 2 | import classNames from "classnames"; 3 | import { MenuContext } from "./menu"; 4 | import { MenuItemProps } from './menuItem'; 5 | import Icon from '../Icon'; 6 | import Transition from '../Transition'; 7 | 8 | export interface SubMenuProps { 9 | /**选填,设置 SubMenu 的索引值 */ 10 | index?: string; 11 | /**必填,设置 SubMenu 的标题文字 */ 12 | title: string; 13 | /**选填,设置 SubMenu 的自定义类名 */ 14 | className?: string; 15 | /**选填,设置 SubMenu 的子元素 */ 16 | children?: ReactNode, 17 | }; 18 | 19 | const SubMenu: React.FC = (props) => { 20 | const { index, title, className, children } = props; 21 | const context = useContext(MenuContext); 22 | const openedSubMenus = context.defaultOpenSubMenus as Array; 23 | const isOpen = (index && context.mode === 'vertical') ? openedSubMenus.includes(index) : false; 24 | const [ menuOpen, setOpen ] = useState(isOpen); 25 | const classes = classNames("menu-item submenu-item", className, { 26 | "is-active": context.index.includes(`${index}`), 27 | 'is-opened': menuOpen, 28 | 'is-vertical': context.mode === 'vertical', 29 | }); 30 | 31 | const handleClick = (e: React.MouseEvent) => { 32 | e.preventDefault(); 33 | setOpen(!menuOpen); 34 | } 35 | let timer: any; 36 | const handleMouse = (e: React.MouseEvent, toggle: boolean) => { 37 | clearTimeout(timer); 38 | e.preventDefault(); 39 | timer = setTimeout(() => { 40 | setOpen(toggle); 41 | }, 300); 42 | } 43 | const allEvents = ( 44 | context.mode !== 'vertical' ? 45 | { 46 | onClick: handleClick, 47 | onMouseEnter: (e: React.MouseEvent) => { handleMouse(e, true) }, 48 | onMouseLeave: (e: React.MouseEvent) => { handleMouse(e, false) }, 49 | } : 50 | { 51 | onClick: handleClick, 52 | } 53 | ); 54 | const renderChildren = () => { 55 | const subMenuClasses = classNames("better-submenu", { 56 | "menu-opened": menuOpen, 57 | }); 58 | const childrenComponent = React.Children.map(children, (child, i) => { 59 | const childElement = child as FunctionComponentElement; 60 | if (childElement.type.displayName === "MenuItem") { 61 | return React.cloneElement(childElement, { 62 | index: `${index}-${i}`, 63 | }); 64 | } else { 65 | console.error("Warning: SubMenu has a child which is not a MenuItem component"); 66 | } 67 | }); 68 | return ( 69 | 74 |
      75 | { childrenComponent } 76 |
    77 |
    78 | ); 79 | } 80 | return ( 81 |
  • 82 |
    83 | { title } 84 | 85 |
    86 | { renderChildren() } 87 |
  • 88 | ); 89 | } 90 | 91 | SubMenu.displayName = 'SubMenu'; 92 | export default SubMenu; -------------------------------------------------------------------------------- /src/components/AutoComplete/autoComplete.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ComponentStory, ComponentMeta } from '@storybook/react'; 3 | import BetterAutoComplete, { AutoCompleteProps, DataSourceType } from './autoComplete'; 4 | interface LakerPlayerProps { 5 | value: string; 6 | number: number; 7 | }; 8 | interface GithubUserProps { 9 | login: string; 10 | url: string; 11 | avatar_url: string; 12 | }; 13 | export default { 14 | id: 'BetterAutoComplete', 15 | title: 'AutoComplete 联想搜索', 16 | component: BetterAutoComplete, 17 | parameters: { 18 | docs: { 19 | source: { 20 | type: "code", 21 | }, 22 | }, 23 | }, 24 | } as ComponentMeta; 25 | 26 | export const SimpleAutoComplete: ComponentStory = (args) => { 27 | const playerArr = ['bradley', 'pope', 'caruso', 'cook', 'cousins', 'james', 'AD', 'green', 'howard', 'kuzma', 'McGee', 'rando']; 28 | const handleFetch = (query: string) => playerArr.filter(name => name.includes(query)).map(name => ({ value: name, })); 29 | return ( 30 | 35 | ); 36 | }; 37 | SimpleAutoComplete.storyName = '支持基本的联想搜索'; 38 | 39 | export const CustomAutoComplete = (args: AutoCompleteProps) => { 40 | const playerArr = [ 41 | {value: 'bradley', number: 11}, 42 | {value: 'pope', number: 1}, 43 | {value: 'caruso', number: 4}, 44 | {value: 'cook', number: 2}, 45 | {value: 'cousins', number: 15}, 46 | {value: 'james', number: 23}, 47 | {value: 'AD', number: 3}, 48 | {value: 'green', number: 14}, 49 | {value: 'howard', number: 39}, 50 | {value: 'kuzma', number: 0}, 51 | ] ; 52 | const handleFetch = (query: string) => playerArr.filter(player => player.value.includes(query)); 53 | const renderOption = (item: DataSourceType) => { 54 | const player = item as DataSourceType; 55 | return ( 56 | <> 57 | 球星名字: {player.value}   58 | 球衣号码: {player.number} 59 | 60 | ); 61 | }; 62 | return ( 63 | 69 | ); 70 | }; 71 | CustomAutoComplete.storyName = '支持自定义搜索结果模版'; 72 | 73 | export const AysncAutoComplete = (args: AutoCompleteProps) => { 74 | const handleFetch = (query: string) => { 75 | return fetch(`https://api.github.com/search/users?q=${query}`) 76 | .then(res => res.json()) 77 | .then(({ items }) => items.slice(0, 10).map((item: any) => ({ value: item.login, ...item, }))); 78 | }; 79 | 80 | const renderOption = (item: DataSourceType) => { 81 | const user = item as DataSourceType; 82 | return ( 83 | <> 84 | { user.value } 85 | 86 | ); 87 | }; 88 | return ( 89 | 95 | ); 96 | }; 97 | AysncAutoComplete.storyName = '支持异步搜索'; -------------------------------------------------------------------------------- /storybook-static/320.8d8728bb.iframe.bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | Copyright (c) 2018 Jed Watson. 3 | Licensed under the MIT License (MIT), see 4 | http://jedwatson.github.io/classnames 5 | */ 6 | 7 | /*! 8 | * The buffer module from node.js, for the browser. 9 | * 10 | * @author Feross Aboukhadijeh 11 | * @license MIT 12 | */ 13 | 14 | /*! 15 | * https://github.com/es-shims/es5-shim 16 | * @license es5-shim Copyright 2009-2020 by contributors, MIT License 17 | * see https://github.com/es-shims/es5-shim/blob/master/LICENSE 18 | */ 19 | 20 | /*! 21 | * is-plain-object 22 | * 23 | * Copyright (c) 2014-2017, Jon Schlinkert. 24 | * Released under the MIT License. 25 | */ 26 | 27 | /*! 28 | * isobject 29 | * 30 | * Copyright (c) 2014-2017, Jon Schlinkert. 31 | * Released under the MIT License. 32 | */ 33 | 34 | /*! ***************************************************************************** 35 | Copyright (c) Microsoft Corporation. 36 | 37 | Permission to use, copy, modify, and/or distribute this software for any 38 | purpose with or without fee is hereby granted. 39 | 40 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 41 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 42 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 43 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 44 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 45 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 46 | PERFORMANCE OF THIS SOFTWARE. 47 | ***************************************************************************** */ 48 | 49 | /** 50 | * @license React 51 | * react-dom.production.min.js 52 | * 53 | * Copyright (c) Facebook, Inc. and its affiliates. 54 | * 55 | * This source code is licensed under the MIT license found in the 56 | * LICENSE file in the root directory of this source tree. 57 | */ 58 | 59 | /** 60 | * @license React 61 | * react-jsx-runtime.production.min.js 62 | * 63 | * Copyright (c) Facebook, Inc. and its affiliates. 64 | * 65 | * This source code is licensed under the MIT license found in the 66 | * LICENSE file in the root directory of this source tree. 67 | */ 68 | 69 | /** 70 | * @license React 71 | * react.production.min.js 72 | * 73 | * Copyright (c) Facebook, Inc. and its affiliates. 74 | * 75 | * This source code is licensed under the MIT license found in the 76 | * LICENSE file in the root directory of this source tree. 77 | */ 78 | 79 | /** 80 | * @license React 81 | * scheduler.production.min.js 82 | * 83 | * Copyright (c) Facebook, Inc. and its affiliates. 84 | * 85 | * This source code is licensed under the MIT license found in the 86 | * LICENSE file in the root directory of this source tree. 87 | */ 88 | 89 | /** @license React v17.0.2 90 | * react-is.production.min.js 91 | * 92 | * Copyright (c) Facebook, Inc. and its affiliates. 93 | * 94 | * This source code is licensed under the MIT license found in the 95 | * LICENSE file in the root directory of this source tree. 96 | */ 97 | 98 | //! stable.js 0.1.8, https://github.com/Two-Screen/stable 99 | 100 | //! © 2018 Angry Bytes and contributors. MIT licensed. 101 | -------------------------------------------------------------------------------- /src/components/Upload/upload.test.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable testing-library/no-wait-for-multiple-assertions */ 2 | /* eslint-disable testing-library/prefer-presence-queries */ 3 | /* eslint-disable testing-library/prefer-screen-queries */ 4 | /* eslint-disable testing-library/no-node-access */ 5 | /* eslint-disable testing-library/no-render-in-setup */ 6 | import '@testing-library/jest-dom/extend-expect'; 7 | import React from 'react'; 8 | import axios from 'axios'; 9 | import { render, RenderResult, fireEvent, waitFor } from '@testing-library/react'; 10 | import Upload, { UploadProps } from './upload'; 11 | 12 | jest.mock('../Icon', () => { 13 | return (props: any) => { 14 | const { onClick, icon } = props; 15 | return { icon }; 16 | }; 17 | }); 18 | jest.mock('axios'); 19 | 20 | const mockedAxios = axios as jest.Mocked; 21 | const testProps: UploadProps = { 22 | action: "fakeurl.com", 23 | onSuccess: jest.fn(), 24 | onChange: jest.fn(), 25 | onRemove: jest.fn(), 26 | drag: true, 27 | }; 28 | let wrapper: RenderResult, fileInput: HTMLInputElement, uploadArea: HTMLElement; 29 | const testFile = new File(['xyz'], 'test.png', { type: 'image/png' }); 30 | describe('test upload component', () => { 31 | beforeEach(() => { 32 | wrapper = render(Click to upload); 33 | fileInput = wrapper.container.querySelector('.better-file-input') as HTMLInputElement; 34 | uploadArea = wrapper.queryByText('Click to upload') as HTMLElement; 35 | }); 36 | it('upload process should works fine', async () => { 37 | const { queryByText, getByText } = wrapper; 38 | mockedAxios.post.mockResolvedValue({'data': 'cool'}); 39 | expect(uploadArea).toBeInTheDocument(); 40 | expect(fileInput).not.toBeVisible(); 41 | fireEvent.change(fileInput, { target: { files: [testFile] }}); 42 | // expect(queryByText('spinner')).toBeInTheDocument(); 43 | await waitFor(() => { 44 | expect(queryByText('test.png')).toBeInTheDocument(); 45 | expect(queryByText('check-circle')).toBeInTheDocument(); 46 | }); 47 | // expect(testProps.onSuccess).toHaveBeenCalledWith('cool', expect.objectContaining({ 48 | // raw: testFile, 49 | // status: 'success', 50 | // response: 'cool', 51 | // name: 'test.png', 52 | // })); 53 | // expect(testProps.onChange).toHaveBeenCalledWith(expect.objectContaining({ 54 | // raw: testFile, 55 | // status: 'success', 56 | // response: 'cool', 57 | // name: 'test.png', 58 | // })); 59 | expect(queryByText('times')).toBeInTheDocument(); 60 | fireEvent.click(getByText('times')); 61 | expect(queryByText('test.png')).not.toBeInTheDocument(); 62 | expect(testProps.onRemove).toHaveBeenCalledWith(expect.objectContaining({ 63 | raw: testFile, 64 | status: 'success', 65 | name: 'test.png', 66 | })); 67 | }); 68 | it('drag and drop files should works fine', async () => { 69 | mockedAxios.post.mockResolvedValue({'data': 'cool'}); 70 | fireEvent.dragOver(uploadArea); 71 | expect(uploadArea).toHaveClass('is-dragover'); 72 | fireEvent.dragLeave(uploadArea); 73 | expect(uploadArea).not.toHaveClass('is-dragover'); 74 | fireEvent.drop(uploadArea, { 75 | dataTransfer: { 76 | files: [testFile], 77 | }, 78 | }); 79 | await waitFor(() => { 80 | expect(wrapper.queryByText('test.png')).toBeInTheDocument(); 81 | }); 82 | // expect(testProps.onSuccess).toHaveBeenCalledWith('cool', expect.objectContaining({ 83 | // raw: testFile, 84 | // status: 'success', 85 | // response: 'cool', 86 | // name: 'test.png', 87 | // })); 88 | }); 89 | }); -------------------------------------------------------------------------------- /storybook-static/192.b37a6581.iframe.bundle.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunk_zhuangjiaqing_betterui=self.webpackChunk_zhuangjiaqing_betterui||[]).push([[192],{"./node_modules/@storybook/preview-web/dist/esm/renderDocs.js":(__unused_webpack_module,__webpack_exports__,__webpack_require__)=>{__webpack_require__.r(__webpack_exports__),__webpack_require__.d(__webpack_exports__,{renderDocs:()=>renderDocs,unmountDocs:()=>unmountDocs});__webpack_require__("./node_modules/@storybook/preview-web/node_modules/regenerator-runtime/runtime.js"),__webpack_require__("./node_modules/core-js/modules/es.object.to-string.js"),__webpack_require__("./node_modules/core-js/modules/es.promise.js");var react=__webpack_require__("./node_modules/react/index.js"),react_dom=__webpack_require__("./node_modules/react-dom/index.js"),wrapper={fontSize:"14px",letterSpacing:"0.2px",margin:"10px 0"},main={margin:"auto",padding:30,borderRadius:10,background:"rgba(0,0,0,0.03)"},heading={textAlign:"center"},NoDocs=function NoDocs(){return react.createElement("div",{style:wrapper,className:"sb-nodocs sb-wrapper"},react.createElement("div",{style:main},react.createElement("h1",{style:heading},"No Docs"),react.createElement("p",null,"Sorry, but there are no docs for the selected story. To add them, set the story's ",react.createElement("code",null,"docs")," parameter. If you think this is an error:"),react.createElement("ul",null,react.createElement("li",null,"Please check the story definition."),react.createElement("li",null,"Please check the Storybook config."),react.createElement("li",null,"Try reloading the page.")),react.createElement("p",null,"If the problem persists, check the browser console, or the terminal you've run Storybook from.")))};function asyncGeneratorStep(gen,resolve,reject,_next,_throw,key,arg){try{var info=gen[key](arg),value=info.value}catch(error){return void reject(error)}info.done?resolve(value):Promise.resolve(value).then(_next,_throw)}function renderDocs(story,docsContext,element,callback){return function renderDocsAsync(_x,_x2,_x3){return _renderDocsAsync.apply(this,arguments)}(story,docsContext,element).then(callback)}function _renderDocsAsync(){return _renderDocsAsync=function _asyncToGenerator(fn){return function(){var self=this,args=arguments;return new Promise((function(resolve,reject){var gen=fn.apply(self,args);function _next(value){asyncGeneratorStep(gen,resolve,reject,_next,_throw,"next",value)}function _throw(err){asyncGeneratorStep(gen,resolve,reject,_next,_throw,"throw",err)}_next(void 0)}))}}(regeneratorRuntime.mark((function _callee(story,docsContext,element){var _docs$getContainer,_docs$getPage,docs,DocsContainer,Page,docsElement;return regeneratorRuntime.wrap((function _callee$(_context){for(;;)switch(_context.prev=_context.next){case 0:if(!(null!=(docs=story.parameters.docs)&&docs.getPage||null!=docs&&docs.page)||(null!=docs&&docs.getContainer||null!=docs&&docs.container)){_context.next=3;break}throw new Error("No `docs.container` set, did you run `addon-docs/preset`?");case 3:if(_context.t1=docs.container,_context.t1){_context.next=8;break}return _context.next=7,null===(_docs$getContainer=docs.getContainer)||void 0===_docs$getContainer?void 0:_docs$getContainer.call(docs);case 7:_context.t1=_context.sent;case 8:if(_context.t0=_context.t1,_context.t0){_context.next=11;break}_context.t0=function(_ref){var children=_ref.children;return react.createElement(react.Fragment,null,children)};case 11:if(DocsContainer=_context.t0,_context.t3=docs.page,_context.t3){_context.next=17;break}return _context.next=16,null===(_docs$getPage=docs.getPage)||void 0===_docs$getPage?void 0:_docs$getPage.call(docs);case 16:_context.t3=_context.sent;case 17:if(_context.t2=_context.t3,_context.t2){_context.next=20;break}_context.t2=NoDocs;case 20:return Page=_context.t2,docsElement=react.createElement(DocsContainer,{key:story.componentId,context:docsContext},react.createElement(Page,null)),_context.next=24,new Promise((function(resolve){react_dom.render(docsElement,element,resolve)}));case 24:case"end":return _context.stop()}}),_callee)}))),_renderDocsAsync.apply(this,arguments)}function unmountDocs(element){react_dom.unmountComponentAtNode(element)}NoDocs.displayName="NoDocs"}}]); -------------------------------------------------------------------------------- /src/components/Form/formItem.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | import React, { FC, ReactNode, useContext, useEffect } from 'react'; 3 | import classNames from 'classnames'; 4 | import { FormContext } from './form'; 5 | import { CustomRule } from './useStore'; 6 | 7 | export type SomeRequired = T & Required> & Omit; 8 | export interface FormItemProps { 9 | /**必填,设置表单项字段名 */ 10 | name: string, 11 | /**选填,设置表单项标签文本 */ 12 | label?: string; 13 | /**选填,设置表单项子元素 */ 14 | children?: ReactNode; 15 | /**选填,设置表单项值的属性,例如 checkbox 的是 'checked' */ 16 | valuePropName?: string; 17 | /**选填,设置表单项值的更新触发事件 */ 18 | trigger?: string; 19 | /**选填,设置如何将 event 的值转换成字段值 */ 20 | getValueFromEvent?: (...args: any) => any; 21 | /**选填,设置校验规则 */ 22 | rules?: CustomRule[]; 23 | /**选填,设置校验触发事件 */ 24 | validateTrigger?: string; 25 | }; 26 | 27 | export const FormItem: FC = (props) => { 28 | const { 29 | name, 30 | label, 31 | children, 32 | valuePropName, 33 | trigger, 34 | getValueFromEvent, 35 | rules, 36 | validateTrigger, 37 | } = props as SomeRequired; 38 | const { dispatch, fields, initialValues, validateField } = useContext(FormContext); 39 | const rowClass = classNames('better-row', { 40 | 'better-row-no-label': !label, 41 | }); 42 | useEffect(() => { 43 | const value = (initialValues && initialValues[name]) || ''; 44 | dispatch({ 45 | type: 'addField', 46 | name, 47 | value: { 48 | label, 49 | name, 50 | value, 51 | rules: rules || [], 52 | errors: [], 53 | isValid: true, 54 | }, 55 | }); 56 | }, []); 57 | const onValueUpdate = (e: any) => { 58 | const value = getValueFromEvent(e); 59 | dispatch({ 60 | type: 'updateValue', 61 | name, 62 | value, 63 | }); 64 | } 65 | const onValueValidate = async () => { 66 | await validateField(name); 67 | } 68 | const fieldState = fields[name]; 69 | const value = fieldState && fieldState.value; 70 | const errors = fieldState && fieldState.errors; 71 | const isRequired = rules && rules.some(rule => (typeof rule !== 'function' && rule.required)); 72 | const hasError = errors && errors.length > 0; 73 | const labelClass = classNames({ 74 | 'better-form-item-required': isRequired, 75 | }); 76 | const itemClass = classNames('better-form-item-control', { 77 | 'better-form-item-has-error': hasError, 78 | }); 79 | 80 | const controlProps: Record = {}; 81 | controlProps[valuePropName] = value; 82 | controlProps[trigger] = onValueUpdate; 83 | 84 | if (rules) { 85 | controlProps[validateTrigger] = onValueValidate; 86 | } 87 | 88 | const childList = React.Children.toArray(children); 89 | if (childList.length === 0) { 90 | console.error('FormItem must have a child element'); 91 | } 92 | if (childList.length > 1) { 93 | console.warn('FormItem must have only one child element'); 94 | } 95 | if (!React.isValidElement(childList[0])) { 96 | console.error('Child Element is not a valid React Element'); 97 | } 98 | const child = childList[0] as React.ReactElement; 99 | const returnChildNode = React.cloneElement(child, { ...child.props, ...controlProps }); 100 | return ( 101 |
    102 | { label && 103 |
    104 | 105 |
    106 | } 107 |
    108 |
    109 | { returnChildNode } 110 |
    111 | { 112 | hasError && 113 |
    114 | { errors[0]?.message } 115 |
    116 | } 117 |
    118 |
    119 | ); 120 | }; 121 | 122 | FormItem.defaultProps = { 123 | valuePropName: 'value', 124 | trigger: 'onChange', 125 | validateTrigger: 'onBlur', 126 | getValueFromEvent: (e: any) => e.target.value, 127 | }; 128 | export default FormItem; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@zhuangjiaqing/betterui", 3 | "version": "1.0.3", 4 | "description": "React components library", 5 | "author": "better", 6 | "main": "./dist/index.js", 7 | "module": "./dist/index.js", 8 | "unpkg": "dist/index.umd.js", 9 | "types": "./dist/index.d.ts", 10 | "dependencies": { 11 | "@fortawesome/fontawesome-svg-core": "^6.5.1", 12 | "@fortawesome/free-solid-svg-icons": "^6.5.1", 13 | "@fortawesome/react-fontawesome": "^0.2.0", 14 | "@testing-library/jest-dom": "^5.16.5", 15 | "@testing-library/react": "^13.4.0", 16 | "@testing-library/user-event": "^13.5.0", 17 | "async-validator": "^4.2.5", 18 | "axios": "^1.6.8", 19 | "classnames": "^2.3.2", 20 | "lodash-es": "^4.17.21", 21 | "react-transition-group": "^4.4.5", 22 | "tslib": "^2.7.0", 23 | "web-vitals": "^2.1.4" 24 | }, 25 | "peerDependencies": { 26 | "react": "^18.2.0", 27 | "react-dom": "^18.2.0" 28 | }, 29 | "scripts": { 30 | "start": "react-scripts start", 31 | "test": "react-scripts test --transformIgnorePatterns \"node_modules/(?!axios)/\"", 32 | "test:nowatch": "cross-env CI=true react-scripts test --transformIgnorePatterns \"node_modules/(?!axios)/\"", 33 | "eject": "react-scripts eject", 34 | "clean": "rimraf ./dist", 35 | "lint": "eslint --ext .js,.jsx,.ts,.tsx src/ --max-warnings 5", 36 | "storybook": "start-storybook -p 6006 -s public", 37 | "prepublishOnly": "npm run test:nowatch && npm run lint && npm run build", 38 | "build-ts": "tsc -p tsconfig.build.json", 39 | "build-css": "node-sass ./src/styles/index.scss ./dist/index.css", 40 | "build-storybook": "build-storybook -s public", 41 | "build": "npm run clean && npm run build-es && npm run build-umd", 42 | "build-es": "rollup --config rollup/rollup.esm.config.js", 43 | "build-umd": "rollup --config rollup/rollup.umd.config.js" 44 | }, 45 | "husky": { 46 | "hooks": { 47 | "pre-commit": "npm run test:nowatch && npm run lint" 48 | } 49 | }, 50 | "eslintConfig": { 51 | "extends": [ 52 | "react-app", 53 | "react-app/jest" 54 | ], 55 | "overrides": [ 56 | { 57 | "files": [ 58 | "**/*.stories.*" 59 | ], 60 | "rules": { 61 | "import/no-anonymous-default-export": "off" 62 | } 63 | } 64 | ] 65 | }, 66 | "browserslist": { 67 | "production": [ 68 | ">0.2%", 69 | "not dead", 70 | "not op_mini all" 71 | ], 72 | "development": [ 73 | "last 1 chrome version", 74 | "last 1 firefox version", 75 | "last 1 safari version" 76 | ] 77 | }, 78 | "devDependencies": { 79 | "@rollup/plugin-commonjs": "^22.0.2", 80 | "@rollup/plugin-json": "^4.1.0", 81 | "@rollup/plugin-node-resolve": "^13.3.0", 82 | "@rollup/plugin-replace": "^4.0.0", 83 | "@storybook/addon-actions": "^6.4.22", 84 | "@storybook/addon-essentials": "^6.4.22", 85 | "@storybook/addon-interactions": "^6.4.22", 86 | "@storybook/addon-links": "^6.4.22", 87 | "@storybook/builder-webpack5": "^6.4.22", 88 | "@storybook/manager-webpack5": "^6.4.22", 89 | "@storybook/node-logger": "^6.4.22", 90 | "@storybook/preset-create-react-app": "^4.1.0", 91 | "@storybook/react": "^6.5.16", 92 | "@storybook/testing-library": "^0.0.11", 93 | "@types/classnames": "^2.3.1", 94 | "@types/jest": "^27.5.2", 95 | "@types/lodash-es": "^4.17.12", 96 | "@types/node": "^16.18.34", 97 | "@types/react": "^18.2.7", 98 | "@types/react-dom": "^18.2.4", 99 | "@types/react-transition-group": "^4.4.10", 100 | "@types/tapable": "^2.2.7", 101 | "cross-env": "^7.0.3", 102 | "husky": "^4.2.1", 103 | "node-sass": "^9.0.0", 104 | "react": "^18.2.0", 105 | "react-dom": "^18.2.0", 106 | "react-scripts": "5.0.1", 107 | "rimraf": "^5.0.5", 108 | "rollup-plugin-exclude-dependencies-from-bundle": "^1.1.23", 109 | "rollup-plugin-sass": "^1.13.2", 110 | "rollup-plugin-terser": "^7.0.2", 111 | "rollup-plugin-typescript2": "^0.31.2", 112 | "typescript": "^4.6.4", 113 | "webpack": "^5.72.0" 114 | }, 115 | "publishConfig": { 116 | "access": "public", 117 | "registry": "https://registry.npmjs.org/" 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/components/Select/select.test.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable testing-library/no-container */ 2 | /* eslint-disable testing-library/no-node-access */ 3 | /* eslint-disable testing-library/prefer-screen-queries */ 4 | import React from 'react'; 5 | import { config } from 'react-transition-group'; 6 | import { render, fireEvent } from '@testing-library/react'; 7 | import Select, { SelectProps } from './select'; 8 | import Option from './option'; 9 | 10 | config.disabled = true; 11 | 12 | jest.mock('../Icon', () => ((props: any) => ({ props.icon }))); 13 | 14 | const testProps: SelectProps = { 15 | defaultValue: '', 16 | placeholder: 'test', 17 | onChange: jest.fn(), 18 | onVisibleChange: jest.fn(), 19 | }; 20 | 21 | const multipleProps: SelectProps = { 22 | ...testProps, 23 | multiple: true, 24 | }; 25 | describe('test Select component', () => { 26 | it('should render the correct Select component', () => { 27 | const { getByPlaceholderText, getByText } = render( 28 | 33 | ); 34 | const inputEle = getByPlaceholderText('test') as HTMLInputElement; 35 | expect(inputEle).toBeInTheDocument(); 36 | // click the input 37 | fireEvent.click(inputEle); 38 | const firstItem = getByText('better1'); 39 | const disabledItem = getByText('better3'); 40 | expect(firstItem).toBeInTheDocument(); 41 | expect(testProps.onVisibleChange).toHaveBeenCalledWith(true); 42 | // click disabled item should not working 43 | fireEvent.click(disabledItem); 44 | expect(disabledItem).toBeInTheDocument(); 45 | // click the dropdown 46 | fireEvent.click(firstItem); 47 | expect(firstItem).not.toBeInTheDocument(); 48 | // check the events 49 | expect(testProps.onVisibleChange).toHaveBeenCalledWith(false); 50 | expect(testProps.onChange).toHaveBeenCalledWith('better1', ['better1']); 51 | expect(inputEle.value).toEqual('better1'); 52 | // test focus 53 | expect(document.activeElement).toEqual(inputEle); 54 | }); 55 | 56 | it('Select in multiple mode should works fine', () => { 57 | const { getByPlaceholderText, getByText, container } = render( 58 | 63 | ); 64 | const inputEle = getByPlaceholderText('test') as HTMLInputElement; 65 | fireEvent.click(inputEle); 66 | const firstItem = getByText('better1'); 67 | const secondItem = getByText('better2'); 68 | fireEvent.click(firstItem); 69 | expect(firstItem).toBeInTheDocument(); 70 | // add selected classname 71 | expect(firstItem).toHaveClass('is-selected'); 72 | // add check icon 73 | expect(getByText('check')).toBeInTheDocument(); 74 | // fire events 75 | expect(multipleProps.onChange).toHaveBeenCalledWith('better1', ['better1']); 76 | // add tags 77 | expect(container.querySelectorAll('.better-tag').length).toEqual(2); 78 | //remove placeholder 79 | expect(inputEle.placeholder).toEqual(''); 80 | // click 2nd item 81 | fireEvent.click(secondItem); 82 | expect(multipleProps.onChange).toHaveBeenLastCalledWith('better2', ['better1', 'better2']); 83 | expect(container.querySelectorAll('.better-tag').length).toEqual(3); 84 | //reclick 2nd item 85 | fireEvent.click(secondItem); 86 | // remove acitve class 87 | expect(secondItem).not.toHaveClass('is-selected'); 88 | // remove tags 89 | expect(container.querySelectorAll('.better-tag').length).toEqual(2); 90 | expect(multipleProps.onChange).toHaveBeenLastCalledWith('better2', ['better1']); 91 | // click tag close 92 | fireEvent.click(getByText('times')); 93 | expect(multipleProps.onChange).toHaveBeenLastCalledWith('better1', []); 94 | //remove all tags 95 | expect(container.querySelectorAll('.better-tag').length).toEqual(1); 96 | //refill placeholder text 97 | expect(inputEle.placeholder).toEqual('test'); 98 | }); 99 | }); 100 | 101 | -------------------------------------------------------------------------------- /src/components/Menu/menu.test.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable testing-library/no-node-access */ 2 | /* eslint-disable testing-library/no-render-in-setup */ 3 | import React from 'react'; 4 | import { render, RenderResult, fireEvent, waitFor } from '@testing-library/react'; 5 | import Menu, {MenuProps} from './menu'; 6 | import MenuItem from './menuItem'; 7 | import SubMenu from './subMenu'; 8 | 9 | const testProps: MenuProps = { 10 | defaultIndex: '0', 11 | onSelect: jest.fn(), 12 | className: 'test', 13 | } 14 | const testVerProps: MenuProps = { 15 | defaultIndex: '0', 16 | mode: 'vertical', 17 | } 18 | const generateMenu = (props: MenuProps) => { 19 | return ( 20 | 21 | 22 | active 23 | 24 | 25 | disabled 26 | 27 | 28 | xyz 29 | 30 | {/* 31 | 32 | drop1 33 | 34 | */} 35 | 36 | ) 37 | } 38 | const createStyleFile = () => { 39 | const cssFile: string = ` 40 | .better-submenu { 41 | display: none; 42 | } 43 | .better-submenu.menu-opened { 44 | display:block; 45 | } 46 | `; 47 | const style = document.createElement('style'); 48 | style.type = 'text/css'; 49 | style.innerHTML = cssFile; 50 | return style; 51 | } 52 | let screen: RenderResult, menuElement: HTMLElement, activeElement: HTMLElement, disabledElement: HTMLElement; 53 | describe('test Menu and MenuItem component in default(horizontal) mode', () => { 54 | beforeEach(() => { 55 | screen = render(generateMenu(testProps)); 56 | screen.container.append(createStyleFile()); 57 | menuElement= screen.getByTestId('test-menu'); 58 | activeElement = screen.getByText('active'); 59 | disabledElement = screen.getByText('disabled'); 60 | }); 61 | it('should render correct Menu and MenuItem based on default props', () => { 62 | expect(menuElement).toBeInTheDocument(); 63 | expect(menuElement).toHaveClass('better-menu test'); 64 | expect(menuElement.querySelectorAll(':scope > li').length).toEqual(3); 65 | expect(activeElement).toHaveClass('menu-item is-active'); 66 | expect(disabledElement).toHaveClass('menu-item is-disabled'); 67 | }); 68 | // it('click items should change active and call the right callback', () => { 69 | // const thirdItem = screen.getByText('xyz'); 70 | // fireEvent.click(thirdItem); 71 | // expect(thirdItem).toHaveClass('is-active'); 72 | // expect(activeElement).not.toHaveClass('is-active'); 73 | // expect(testProps.onSelect).toHaveBeenCalledWith('2'); 74 | // fireEvent.click(disabledElement); 75 | // expect(disabledElement).not.toHaveClass('is-active'); 76 | // expect(testProps.onSelect).not.toHaveBeenCalledWith('1'); 77 | // }); 78 | // it('should show dropdown items when click on subMenu', async () => { 79 | // expect(screen.queryByText('drop1')).not.toBeVisible(); 80 | // const dropdownElement = screen.getByText('dropdown'); 81 | // fireEvent.click(dropdownElement); 82 | // expect(screen.queryByText('drop1')).toBeVisible(); 83 | // }) 84 | }) 85 | describe('test Menu and MenuItem component in vertical mode', () => { 86 | beforeEach(() => { 87 | screen = render(generateMenu(testVerProps)); 88 | screen.container.append(createStyleFile()); 89 | }); 90 | it('should render vertical mode when mode is set to vertical', () => { 91 | const menuElement = screen.getByTestId('test-menu'); 92 | expect(menuElement).toHaveClass('menu-vertical'); 93 | }); 94 | // it('should show subMenu dropdown when defaultOpenSubMenus contains SubMenu index', () => { 95 | // expect(screen.queryByText('opened1')).toBeVisible(); 96 | // }); 97 | // it('should show dropdown items when hover on subMenu for vertical mode', async () => { 98 | // const dropdownElement = screen.getByText('dropdown'); 99 | // fireEvent.mouseEnter(dropdownElement); 100 | // await waitFor(() => { 101 | // expect(screen.queryByText('drop1')).toBeVisible(); 102 | // }); 103 | // fireEvent.mouseLeave(dropdownElement); 104 | // await waitFor(() => { 105 | // expect(screen.queryByText('drop1')).not.toBeVisible(); 106 | // }); 107 | // }); 108 | }); -------------------------------------------------------------------------------- /src/components/Form/form.test.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable testing-library/no-wait-for-multiple-assertions */ 2 | /* eslint-disable testing-library/await-async-utils */ 3 | /* eslint-disable testing-library/no-render-in-setup */ 4 | import React from 'react'; 5 | import { render, fireEvent, screen, waitFor } from '@testing-library/react'; 6 | import Form, { FormProps } from './form'; 7 | import Item from './formItem'; 8 | import Input from '../Input'; 9 | import Button from '../Button'; 10 | 11 | const testProps: FormProps = { 12 | name: 'test-form', 13 | initialValues: { username: 'better', password: '12345', confirmPwd: '23456' }, 14 | onSuccessfulSubmit: jest.fn(), 15 | onFailedSubmit: jest.fn(), 16 | }; 17 | let nameInput: HTMLInputElement, pwdInput: HTMLInputElement, conPwdInput: HTMLInputElement, submitButton: HTMLButtonElement; 18 | 19 | describe('testing Form component', () => { 20 | beforeEach(() => { 21 | render( 22 |
    23 | 29 | 30 | 31 | 37 | 38 | 39 | ({ 44 | asyncValidator(rule, value) { 45 | return new Promise((resolve, reject) => { 46 | if (value !== getFieldValue('password')) { 47 | reject('Do not match!'); 48 | } 49 | resolve(); 50 | }); 51 | } 52 | }), 53 | ]} 54 | > 55 | 56 | 57 | 58 |
    59 | ) 60 | nameInput = screen.getByDisplayValue('better'); 61 | pwdInput = screen.getByDisplayValue('12345'); 62 | conPwdInput = screen.getByDisplayValue('23456'); 63 | submitButton = screen.getByText('Login in'); 64 | }); 65 | it('should render the correct Form component', () => { 66 | // should contains two labels 67 | expect(screen.getByText('username')).toBeInTheDocument(); 68 | expect(screen.getByText('password')).toBeInTheDocument(); 69 | expect(screen.getByText('confirmPwd')).toBeInTheDocument(); 70 | // should fill in three inputs 71 | expect(nameInput).toBeInTheDocument(); 72 | expect(pwdInput).toBeInTheDocument(); 73 | expect(conPwdInput).toBeInTheDocument(); 74 | // should render the submit button 75 | expect(submitButton).toBeInTheDocument(); 76 | }) 77 | it('submit form with invliad values should show the error message', () => { 78 | fireEvent.change(nameInput, {target: {value: ''}}); 79 | fireEvent.change(pwdInput, {target: {value: ''}}); 80 | fireEvent.click(submitButton); 81 | waitFor(() => { 82 | expect(screen.getByText('name error')).toBeInTheDocument(); 83 | expect(screen.getByText('password error')).toBeInTheDocument(); 84 | expect(testProps.onFailedSubmit).toHaveBeenCalled(); 85 | }); 86 | }); 87 | it('change single input to invalid values should trigger the validate', () => { 88 | // name input, type: string 89 | fireEvent.change(nameInput, {target: {value: ''}}); 90 | fireEvent.blur(nameInput); 91 | waitFor(() => { 92 | expect(screen.getByText('name error')).toBeInTheDocument(); 93 | }); 94 | fireEvent.change(nameInput, {target: {value: '12'}}); 95 | fireEvent.blur(nameInput); 96 | waitFor(() => { 97 | expect(screen.getByText('less than 3')).toBeInTheDocument(); 98 | }); 99 | }); 100 | it('custom rules should work', () => { 101 | // change and blur comfirmPwd 102 | fireEvent.change(conPwdInput, {target: {value: '23456'}}); 103 | fireEvent.blur(conPwdInput); 104 | waitFor(() => { 105 | expect(screen.getByText('Do not match!')).toBeInTheDocument(); 106 | }); 107 | // change to the same 108 | fireEvent.change(conPwdInput, {target: {value: '12345'}}); 109 | fireEvent.blur(conPwdInput); 110 | waitFor(() => { 111 | expect(screen.queryByText('Do not match!')).not.toBeInTheDocument(); 112 | }); 113 | fireEvent.click(submitButton); 114 | // submit the form with the right data 115 | waitFor(() => { 116 | expect(testProps.onSuccessfulSubmit).toHaveBeenCalled(); 117 | }); 118 | }); 119 | }); -------------------------------------------------------------------------------- /src/components/AutoComplete/autoComplete.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState, ChangeEvent, KeyboardEvent, ReactElement, useEffect, useRef } from 'react'; 2 | import classNames from 'classnames'; 3 | import Input, { InputProps } from '../Input/input'; 4 | import Icon from '../Icon'; 5 | import Transition from '../Transition'; 6 | import useDebounce from '../../hooks/useDebounce'; 7 | import useClickOutside from '../../hooks/useClickOutside'; 8 | interface DataSourceObject { 9 | value: string; 10 | }; 11 | export type DataSourceType = T & DataSourceObject; 12 | export interface AutoCompleteProps extends Omit { 13 | /** 必填,可以拿到当前的输入,然后返回同步的数组或者是异步的 Promise */ 14 | fetchSuggestions: (str: string) => DataSourceType[] | Promise; 15 | /** 选填,选中后执行的回调函数 */ 16 | onSelect?: (item: DataSourceType) => void; 17 | /** 选填,支持自定义渲染下拉列表 */ 18 | renderOption?: (item: DataSourceType) => ReactElement; 19 | } 20 | 21 | /** 22 | * 联想搜索,通过鼠标或键盘输入内容进行自动联想,支持同步和异步两种方式。 23 | * 支持 Input 组件的所有属性,支持键盘事件选择 24 | * 25 | * ~~~js 26 | * // 这样引用 27 | * import { BetterAutoComplete } from 'betterui'; 28 | * ~~~ 29 | * 30 | */ 31 | export const AutoComplete: FC = (props) => { 32 | const { fetchSuggestions, onSelect, value, renderOption, ...restProps } = props; 33 | const [inputValue, setInputValue] = useState(value as string); 34 | const [suggestions, setSuggestions] = useState([]); 35 | const [ loading, setLoading ] = useState(false); 36 | const [ highlightIndex, setHighlightIndex] = useState(-1); 37 | const triggerSearch = useRef(false); 38 | const componentRef = useRef(null); 39 | const debounceValue = useDebounce(inputValue, 500); 40 | useClickOutside(componentRef, () => { setSuggestions([]); }); 41 | useEffect(() => { 42 | if (debounceValue && triggerSearch.current) { 43 | const results = fetchSuggestions(debounceValue); 44 | if (results instanceof Promise) { 45 | setLoading(true); 46 | results.then(data => { 47 | setLoading(false); 48 | setSuggestions(data); 49 | }); 50 | } else { 51 | setSuggestions(results); 52 | } 53 | } else { 54 | setSuggestions([]); 55 | } 56 | setHighlightIndex(-1); 57 | }, [debounceValue, fetchSuggestions]); 58 | const handleChange = (e: ChangeEvent) => { 59 | const value = e.target.value.trim(); 60 | setInputValue(value); 61 | triggerSearch.current = true; 62 | }; 63 | const handleSelect = (item: DataSourceType) => { 64 | setInputValue(item.value); 65 | setSuggestions([]); 66 | if (onSelect) { 67 | onSelect(item); 68 | } 69 | triggerSearch.current = false; 70 | } 71 | const highlight = (index: number) => { 72 | if (index < 0) index = 0; 73 | if (index >= suggestions.length) { 74 | index = suggestions.length - 1; 75 | } 76 | setHighlightIndex(index); 77 | } 78 | const handleKeyDown = (e: KeyboardEvent) => { 79 | switch(e.keyCode) { 80 | // 回车键 81 | case 13: 82 | if (suggestions[highlightIndex]) { 83 | handleSelect(suggestions[highlightIndex]); 84 | } 85 | break; 86 | // 向上键 87 | case 38: 88 | highlight(highlightIndex - 1); 89 | break; 90 | // 向下键 91 | case 40: 92 | highlight(highlightIndex + 1); 93 | break; 94 | // ESC键 95 | case 27: 96 | setSuggestions([]); 97 | break; 98 | default: 99 | break; 100 | } 101 | } 102 | const renderTemplate = (item: DataSourceType) => { 103 | return renderOption ? renderOption(item) : item.value; 104 | } 105 | const generateDropdown = () => { 106 | return ( 107 | 0 } 109 | animation="zoom-in-top" 110 | timeout={ 300 } 111 | > 112 |
      113 | { 114 | suggestions.map((item, index) => { 115 | const activeItem = classNames('suggestion-item', { 116 | 'is-active': index === highlightIndex, 117 | }); 118 | return ( 119 |
    • handleSelect(item) }> 120 | { renderTemplate(item) } 121 |
    • 122 | ); 123 | }) 124 | } 125 |
    126 |
    127 | ); 128 | }; 129 | return ( 130 |
    131 | 137 | 138 | { loading && 139 |
    140 | 141 |
    142 | } 143 | { suggestions.length > 0 && generateDropdown() } 144 |
    145 | ); 146 | } 147 | 148 | export default AutoComplete; --------------------------------------------------------------------------------