├── 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 |
45 | {label}
46 |
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) => ;
18 |
19 | export const Primary = Template.bind({});
20 | // More on args: https://storybook.js.org/docs/react/writing-stories/args
21 | Primary.args = {
22 | primary: true,
23 | label: 'Button',
24 | };
25 |
26 | export const Secondary = Template.bind({});
27 | Secondary.args = {
28 | label: 'Button',
29 | };
30 |
31 | export const Large = Template.bind({});
32 | Large.args = {
33 | size: 'large',
34 | label: 'Button',
35 | };
36 |
37 | export const Small = Template.bind({});
38 | Small.args = {
39 | size: 'small',
40 | label: 'Button',
41 | };
42 |
--------------------------------------------------------------------------------
/storybook-static/static/media/direction.b770f9af5f20abac0352e73b4676bba2.svg:
--------------------------------------------------------------------------------
1 | illustration/direction
--------------------------------------------------------------------------------
/src/stories/assets/flow.svg:
--------------------------------------------------------------------------------
1 | illustration/flow
--------------------------------------------------------------------------------
/dist/components/Form/form.d.ts:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from 'react';
2 | import { ValidateError } from 'async-validator';
3 | import useStore, { FormState } from './useStore';
4 | export type RenderProps = (form: FormState) => ReactNode;
5 | export interface FormProps {
6 | /**选填,设置表单名称,会作为表单字段 id 前缀使用 */
7 | name?: string;
8 | /**选填,设置表单的初始值 */
9 | initialValues?: Record;
10 | /**选填,设置表单的子元素 */
11 | children?: ReactNode | RenderProps;
12 | /**选填,设置表单提交成功时的回调函数 */
13 | onSuccessfulSubmit?: (values: Record) => void;
14 | /**选填,设置表单提交失败时的回调函数 */
15 | onFailedSubmit?: (values: Record, errors: Record) => void;
16 | }
17 | export type IFormContext = Pick, 'dispatch' | 'fields' | 'validateField'> & Pick;
18 | export type IFormRef = Omit, 'dispatch' | 'fields' | 'form'>;
19 | export declare const FormContext: React.Context;
20 | /**
21 | * Form 表单,用以收集、校验和提交数据,一般由输入框、单选框、复选框、选择器等控件组成。
22 | *
23 | * ~~~js
24 | * // 这样引用,再分别使用 和
25 | * import { BetterForm } from 'betterui';
26 | * ~~~
27 | *
28 | */
29 | export declare const Form: React.ForwardRefExoticComponent>;
30 | export default Form;
31 |
--------------------------------------------------------------------------------
/src/components/Menu/menuItem.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode, useContext } from "react";
2 | import classNames from "classnames";
3 | import { MenuContext } from "./menu";
4 |
5 | export interface MenuItemProps {
6 | /**选填,设置 MenuItem 的索引值 */
7 | index?: string;
8 | /**选填,设置 MenuItem 的自定义类名 */
9 | className?: string;
10 | /**选填,设置 MenuItem 的禁用 */
11 | disabled?: boolean;
12 | /**选填,设置 MenuItem 的自定义样式 */
13 | style?: React.CSSProperties;
14 | /**选填,设置 MenuItem 的子元素 */
15 | children?: ReactNode;
16 | };
17 |
18 | const MenuItem: React.FC = (props) => {
19 | const {
20 | index,
21 | className,
22 | disabled,
23 | style,
24 | children
25 | } = props;
26 | const context = useContext(MenuContext);
27 | const classes = classNames("menu-item", className, {
28 | "is-disabled": disabled,
29 | "is-active": context.index === index,
30 | });
31 | const handleClick = () => {
32 | if (context.onSelect && !disabled && (typeof index === 'string')) {
33 | context.onSelect(index);
34 | }
35 | };
36 |
37 | return (
38 |
43 | { children }
44 |
45 | );
46 | }
47 |
48 | MenuItem.defaultProps = {
49 | disabled: false,
50 | };
51 | MenuItem.displayName = 'MenuItem';
52 |
53 | export default MenuItem;
--------------------------------------------------------------------------------
/src/stories/page.css:
--------------------------------------------------------------------------------
1 | section {
2 | font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
3 | font-size: 14px;
4 | line-height: 24px;
5 | padding: 48px 20px;
6 | margin: 0 auto;
7 | max-width: 600px;
8 | color: #333;
9 | }
10 |
11 | section h2 {
12 | font-weight: 900;
13 | font-size: 32px;
14 | line-height: 1;
15 | margin: 0 0 4px;
16 | display: inline-block;
17 | vertical-align: top;
18 | }
19 |
20 | section p {
21 | margin: 1em 0;
22 | }
23 |
24 | section a {
25 | text-decoration: none;
26 | color: #1ea7fd;
27 | }
28 |
29 | section ul {
30 | padding-left: 30px;
31 | margin: 1em 0;
32 | }
33 |
34 | section li {
35 | margin-bottom: 8px;
36 | }
37 |
38 | section .tip {
39 | display: inline-block;
40 | border-radius: 1em;
41 | font-size: 11px;
42 | line-height: 12px;
43 | font-weight: 700;
44 | background: #e7fdd8;
45 | color: #66bf3c;
46 | padding: 4px 12px;
47 | margin-right: 10px;
48 | vertical-align: top;
49 | }
50 |
51 | section .tip-wrapper {
52 | font-size: 13px;
53 | line-height: 20px;
54 | margin-top: 40px;
55 | margin-bottom: 40px;
56 | }
57 |
58 | section .tip-wrapper svg {
59 | display: inline-block;
60 | height: 12px;
61 | width: 12px;
62 | margin-right: 4px;
63 | vertical-align: top;
64 | margin-top: 3px;
65 | }
66 |
67 | section .tip-wrapper svg path {
68 | fill: #1ea7fd;
69 | }
70 |
--------------------------------------------------------------------------------
/src/components/Tabs/_style.scss:
--------------------------------------------------------------------------------
1 | .better-tabs-nav {
2 | display: flex;
3 | flex-wrap: wrap;
4 | padding-left: 0;
5 | margin-bottom: 0;
6 | list-style: none;
7 | border-bottom: $nav-tabs-border-width solid $nav-tabs-border-color;
8 | }
9 |
10 | .better-tabs-nav-item {
11 | display: block;
12 | padding: $nav-link-padding-y $nav-link-padding-x;
13 | cursor: pointer;
14 | &:hover,
15 | &:focus {
16 | color: $nav-tabs-link-hover-color;
17 | }
18 | &.disabled {
19 | color: $nav-link-disabled-color;
20 | // pointer-events: none;
21 | cursor: not-allowed;
22 | background-color: transparent;
23 | border-color: transparent;
24 | }
25 | &.is-active {
26 | color: $nav-tabs-link-active-color;
27 | }
28 | }
29 | .nav-line {
30 | .better-tabs-nav-item {
31 | &.is-active {
32 | border-bottom: $nav-tabs-border-width * 2 solid $nav-tabs-link-active-color;
33 | }
34 | }
35 | }
36 |
37 | .nav-card {
38 | .better-tabs-nav-item {
39 | border: $nav-tabs-border-width solid transparent;
40 | margin-bottom: -$nav-tabs-border-width;
41 | &.is-active {
42 | @include border-top-radius($nav-tabs-border-radius);
43 | background-color: $nav-tabs-link-active-bg;
44 | border-color: $nav-tabs-link-active-border-color;
45 | }
46 | }
47 | }
48 |
49 | .better-tabs-content {
50 | margin-top: $nav-tabs-content-margin;
51 | }
--------------------------------------------------------------------------------
/src/components/Select/option.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useContext, ReactNode } from 'react';
2 | import classNames from 'classnames';
3 | import Icon from '../Icon';
4 | import { SelectContext } from './select';
5 |
6 | export interface SelectOptionProps {
7 | /** 选填,标记选项的索引下标*/
8 | index?: string;
9 | /** 必填,默认根据此属性值进行筛选,该值不能相同*/
10 | value: string;
11 | /** 选填,选项的标签,若不设置则默认与 value 相同*/
12 | label?: string;
13 | /** 选填,是否禁用该选项*/
14 | disabled?: boolean;
15 | children?: ReactNode;
16 | };
17 |
18 | export const Option: FC = ({value, label, disabled, children, index}) => {
19 | const { onSelect, selectedValues, multiple } = useContext(SelectContext);
20 | const isSelected = selectedValues.includes(value);
21 | const classes = classNames('better-select-item', {
22 | 'is-disabled': disabled,
23 | 'is-selected': isSelected,
24 | });
25 | const handleClick = (e: React.MouseEvent, value: string, isSelected: boolean) => {
26 | e.preventDefault();
27 | if(onSelect && !disabled) {
28 | onSelect(value, isSelected);
29 | }
30 | }
31 | return (
32 | { handleClick(e, value, isSelected) } }>
33 | { children || (label ? label: value) }
34 | { multiple && isSelected && }
35 |
36 | );
37 | };
38 |
39 | Option.displayName = 'Option';
40 |
41 | export default Option;
--------------------------------------------------------------------------------
/storybook-static/static/media/flow.edad2ac1b0bb28e0ce513d5b7a65f8fe.svg:
--------------------------------------------------------------------------------
1 | illustration/flow
--------------------------------------------------------------------------------
/src/components/Progress/progress.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import { ThemeProps } from '../Icon/icon';
3 |
4 | export interface ProgressProps {
5 | /**必填,设置进度条的进度百分比 */
6 | percent: number;
7 | /**选填,设置进度条的高度,默认为15px */
8 | strokeHeight?: number;
9 | /**选填,是否显示进度条的百分比,默认展示 */
10 | showText?: boolean;
11 | /**选填,设置进度条的样式 */
12 | styles?: React.CSSProperties;
13 | /**选填,设置进度条的主题,仅支持以下9种主题 */
14 | theme?: ThemeProps;
15 | };
16 | /**
17 | * 进度条,给予用户当前系统执行中任务运行状态的反馈,多用于运行一段时间的场景,有效减轻用户在等待中产生的焦虑感。
18 | *
19 | * ~~~js
20 | * // 这样引用
21 | * import { BetterProgress } from 'betterui';
22 | * ~~~
23 | *
24 | */
25 | export const Progress: FC = (props) => {
26 | const {
27 | percent,
28 | strokeHeight,
29 | showText,
30 | styles,
31 | theme,
32 | } = props;
33 | return (
34 |
35 |
36 |
40 | { showText && { `${percent}%` } }
41 |
42 |
43 |
44 | );
45 | };
46 |
47 | Progress.defaultProps = {
48 | strokeHeight: 15,
49 | showText: true,
50 | theme: "primary",
51 | };
52 | export default Progress;
53 |
--------------------------------------------------------------------------------
/src/components/Transition/transition.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CSSTransition } from 'react-transition-group';
3 | import { CSSTransitionProps } from 'react-transition-group/CSSTransition';
4 |
5 | type AnimationName = 'zoom-in-top' | 'zoom-in-left' | 'zoom-in-bottom' | 'zoom-in-right';
6 | export type TransitionProps = {
7 | /**选填,设置动画的过渡方向 */
8 | animation?: AnimationName,
9 | /**
10 | * 选填,设置 Transition 的子元素是否有根元素进行包裹,
11 | * 当wrapper为false时,Transition组件的子元素必须包裹一个根元素
12 | * */
13 | wrapper?: boolean,
14 | /**选填,设置 Transition 的子元素 */
15 | children?: React.ReactNode,
16 | /**选填,设置 Transition 的自定义类名 */
17 | classNames?: unknown,
18 | } & CSSTransitionProps;
19 |
20 | /**
21 | * 页面中常用的内置组件,可以帮助你制作基于状态变化的过渡和动画效果
22 | *
23 | * ~~~js
24 | * // 这样引用
25 | * import { BetterTransition } from 'betterui';
26 | * ~~~
27 | *
28 | */
29 | export const Transition: React.FC = (props) => {
30 | const {
31 | children,
32 | classNames,
33 | animation,
34 | wrapper,
35 | ...restProps
36 | } = props;
37 | return (
38 |
42 | { wrapper ? { children }
: children }
43 |
44 | );
45 | }
46 | Transition.defaultProps = {
47 | unmountOnExit: true,
48 | appear: true,
49 | };
50 |
51 | export default Transition;
--------------------------------------------------------------------------------
/src/stories/assets/code-brackets.svg:
--------------------------------------------------------------------------------
1 | illustration/code-brackets
--------------------------------------------------------------------------------
/storybook-static/static/media/code-brackets.2e1112d71f1a3ba28d2461481dce689b.svg:
--------------------------------------------------------------------------------
1 | illustration/code-brackets
--------------------------------------------------------------------------------
/src/stories/assets/comments.svg:
--------------------------------------------------------------------------------
1 | illustration/comments
--------------------------------------------------------------------------------
/storybook-static/static/media/comments.a38590896b951b65e7ada9af32d6915d.svg:
--------------------------------------------------------------------------------
1 | illustration/comments
--------------------------------------------------------------------------------
/src/components/Upload/uploadList.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import { UploadFile } from './upload';
3 | import Icon from '../Icon';
4 | import Progress from '../Progress';
5 |
6 | interface UploadListProps {
7 | fileList: UploadFile[];
8 | onRemove: (file: UploadFile) => void;
9 | };
10 | export const UploadList: FC = (props) => {
11 | const { fileList, onRemove, } = props;
12 | return (
13 |
14 | {
15 | fileList.map(item => {
16 | return (
17 |
18 |
19 |
20 | { item.name }
21 |
22 |
23 | { (item.status === 'uploading' || !item.status) && }
24 | { item.status === 'success' && }
25 | { item.status === 'error' && }
26 |
27 |
28 | { onRemove(item) } } />
29 |
30 | { item.status === 'uploading' && }
31 |
32 | );
33 | })
34 | }
35 |
36 | );
37 | };
38 |
39 | export default UploadList;
--------------------------------------------------------------------------------
/src/stories/assets/repo.svg:
--------------------------------------------------------------------------------
1 | illustration/repo
--------------------------------------------------------------------------------
/src/components/Tabs/tabs.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ComponentStory, ComponentMeta } from '@storybook/react';
3 | import BetterTabs from './index';
4 | import BetterIcon from '../Icon';
5 |
6 | export default {
7 | title: 'Tabs 选项卡',
8 | id: 'BetterTabs',
9 | component: BetterTabs,
10 | subcomponents: {
11 | 'BetterTabs.Item': BetterTabs.Item,
12 | },
13 | parameters: {
14 | docs: {
15 | source: {
16 | type: "code",
17 | },
18 | },
19 | },
20 | } as ComponentMeta;
21 |
22 | export const DefaultTabs: ComponentStory = (args) => (
23 |
24 | 选项卡一
25 | 选项卡二
26 | 选项卡三
27 |
28 | );
29 | DefaultTabs.storyName = '标准版';
30 |
31 | export const CardTabs: ComponentStory = (args) => (
32 |
33 | 选项卡一
34 | 选项卡二
35 | 选项卡三
36 |
37 | );
38 | CardTabs.storyName = '卡片选项卡';
39 |
40 | export const CustomTabs: ComponentStory = (args) => (
41 |
42 | 选项卡一> }>选项卡一
43 | 选项卡二
44 |
45 | );
46 | CustomTabs.storyName = '自定义选项卡';
47 |
--------------------------------------------------------------------------------
/src/components/Upload/_style.scss:
--------------------------------------------------------------------------------
1 | .better-upload-list {
2 | margin: 0;
3 | padding: 0;
4 | list-style-type: none;
5 | }
6 |
7 | .better-uploader-dragger {
8 | background: $gray-100;
9 | border: 1px dashed $gray-300;
10 | border-radius: 4px;
11 | cursor: pointer;
12 | padding: 20px;
13 | width: 360px;
14 | height: 180px;
15 | text-align: center;
16 | &:hover {
17 | border: 1px dashed $primary;
18 | }
19 | &.is-dragover {
20 | border: 2px dashed $primary;
21 | background: rgba($primary, .2);
22 | }
23 | }
24 |
25 |
26 | .better-upload-list-item {
27 | transition: all .5s cubic-bezier(.55,0,.1,1);
28 | font-size: 14px;
29 | line-height: 1.8;
30 | margin-top: 5px;
31 | box-sizing: border-box;
32 | border-radius: 4px;
33 | min-width: 200px;
34 | position: relative;
35 | &:first-child {
36 | margin-top: 10px;
37 | }
38 | .file-name {
39 | margin-left: 5px;
40 | margin-right: 40px;
41 | svg {
42 | margin-right: 5px;
43 | color: $gray-500;
44 | }
45 | }
46 | .file-name-error {
47 | color: $danger;
48 | svg {
49 | color: $danger;
50 | }
51 | }
52 | .file-status {
53 | display: block;
54 | position: absolute;
55 | right: 5px;
56 | top: 0;
57 | line-height: inherit;
58 | }
59 | .file-actions {
60 | display: none;
61 | position: absolute;
62 | right: 7px;
63 | top: 0;
64 | line-height: inherit;
65 | cursor: pointer;
66 | }
67 | &:hover {
68 | background-color: $gray-200;
69 | .file-status {
70 | display: none;
71 | }
72 | .file-actions {
73 | display: block;
74 | }
75 | }
76 | }
--------------------------------------------------------------------------------
/storybook-static/static/media/repo.6d4963229d067828d1326ea3f60f5136.svg:
--------------------------------------------------------------------------------
1 | illustration/repo
--------------------------------------------------------------------------------
/src/components/Menu/menu.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ComponentStory, ComponentMeta } from '@storybook/react';
3 | import BetterMenu from './index';
4 |
5 | const menuMeta: ComponentMeta = {
6 | title: 'Menu 菜单',
7 | id: 'BetterMenu',
8 | component: BetterMenu,
9 | subcomponents: {
10 | 'BetterMenu.SubMenu': BetterMenu.SubMenu,
11 | 'BetterMenu.Item': BetterMenu.Item,
12 | },
13 | args: {
14 | defaultIndex: '1',
15 | },
16 | parameters: {
17 | docs: {
18 | source: {
19 | type: "code",
20 | },
21 | },
22 | },
23 | };
24 | export default menuMeta;
25 |
26 | const Template: ComponentStory = (args) => (
27 |
28 |
29 | 选项一
30 |
31 |
32 | 选项二
33 |
34 |
35 | 选项三
36 |
37 |
38 |
39 | 下拉选项一
40 |
41 |
42 | 下拉选项二
43 |
44 |
45 |
46 | );
47 |
48 | export const DefaultMenu = Template.bind({});
49 | DefaultMenu.storyName = "默认的菜单";
50 |
51 | export const ClickMenu = Template.bind({});
52 | ClickMenu.args = {
53 | mode: 'horizontal',
54 | defaultIndex: '0',
55 | };
56 | ClickMenu.storyName = "横向的菜单";
57 |
58 | export const OpenedMenu = Template.bind({});
59 | OpenedMenu.args = {
60 | mode: 'vertical',
61 | defaultIndex: '0',
62 | defaultOpenSubMenus: ['3'],
63 | };
64 | OpenedMenu.storyName = "纵向的菜单";
--------------------------------------------------------------------------------
/src/components/Form/_style.scss:
--------------------------------------------------------------------------------
1 | .better-form {
2 | .better-row {
3 | display: flex;
4 | align-items: center;
5 | margin-bottom: 25px;
6 | &.better-row-no-label {
7 | flex-direction: row-reverse;
8 | }
9 | .better-form-item-label {
10 | flex-basis: 30%;
11 | text-align: right;
12 | padding-right: 20px;
13 | >label {
14 | margin-bottom: 0;
15 | }
16 | >label.better-form-item-required:before {
17 | display: inline-block;
18 | margin-right: 4px;
19 | color: $danger;
20 | font-size: 14px;
21 | font-family: SimSun,sans-serif;
22 | line-height: 1;
23 | content: "*";
24 | }
25 | }
26 | .better-form-item {
27 | flex-basis: 70%;
28 | position: relative;
29 | .better-input-wrapper {
30 | margin-bottom: 0;
31 | }
32 | .better-form-item-has-error.better-form-item-control {
33 | .better-input-inner {
34 | border: 1px solid $danger;
35 | &:focus {
36 | box-shadow: $input-focus-box-shadow-error;
37 | }
38 | }
39 | }
40 | .better-form-item-explain {
41 | position: absolute;
42 | bottom: -25px;
43 | left: 0;
44 | line-height: 25px;
45 | color: $danger;
46 | min-width: 100px;
47 | }
48 | }
49 | }
50 | .better-form-submit-area {
51 | display: flex;
52 | align-items: center;
53 | justify-content: center;
54 | }
55 | .agreement-section .better-input-wrapper {
56 | display: block !important;
57 | width: auto !important;
58 | margin-bottom: 100px;
59 | position: relative;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/dist/components/Form/useStore.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { RuleItem, ValidateError } from 'async-validator';
3 | export type CustomRuleFunc = ({ getFieldValue }: {
4 | getFieldValue: (key: string) => string;
5 | }) => RuleItem;
6 | export type CustomRule = RuleItem | CustomRuleFunc;
7 | export interface FieldDetail {
8 | name: string;
9 | value: string;
10 | rules: CustomRule[];
11 | isValid: boolean;
12 | errors: ValidateError[];
13 | }
14 | export interface FieldsState {
15 | [key: string]: FieldDetail;
16 | }
17 | export interface FormState {
18 | isValid: boolean;
19 | isSubmit: boolean;
20 | errors: Record;
21 | }
22 | export interface FieldsAction {
23 | type: 'addField' | 'updateValue' | 'updateValidateResult';
24 | name: string;
25 | value: any;
26 | }
27 | export interface validateErrorType extends Error {
28 | errors: ValidateError[];
29 | fields: Record;
30 | }
31 | declare function useStore(initialValues?: Record): {
32 | form: FormState;
33 | fields: FieldsState;
34 | dispatch: import("react").Dispatch;
35 | getFieldValue: (key: string) => string;
36 | getFieldsValue: () => {
37 | [x: string]: string;
38 | };
39 | setFieldValue: (name: string, value: any) => void;
40 | resetFieldsValue: () => void;
41 | validateField: (name: string) => Promise;
42 | validateAllFields: () => Promise<{
43 | isValid: boolean;
44 | errors: Record;
45 | values: {
46 | [x: string]: string;
47 | };
48 | }>;
49 | };
50 | export default useStore;
51 |
--------------------------------------------------------------------------------
/src/components/Input/input.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ComponentStory, ComponentMeta } from '@storybook/react';
3 | import BetterInput from './input';
4 |
5 | export default {
6 | title: 'Input 输入框',
7 | id: 'BetterInput',
8 | component: BetterInput,
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 DefaultInput = Template.bind({});
21 | DefaultInput.args = {
22 | placeholder: 'default Input',
23 | };
24 | DefaultInput.storyName = '默认的输入框';
25 |
26 | export const DisabledInput = () => (
27 |
31 | );
32 | DisabledInput.storyName = '被禁用的输入框';
33 |
34 | export const InputWithIcon = () => {
35 | return (
36 | <>
37 |
41 | >
42 | );
43 |
44 | }
45 | InputWithIcon.storyName = '带图标的输入框';
46 |
47 | export const InputWithSize = () => (
48 | <>
49 |
53 |
57 | >
58 | );
59 | InputWithSize.storyName = '不同尺寸的输入框';
60 |
61 | export const InputWithPand = () => (
62 | <>
63 |
67 |
71 | >
72 | );
73 | InputWithPand.storyName = '带前后缀的输入框';
74 |
75 |
--------------------------------------------------------------------------------
/src/stories/Header.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Button } from './Button';
4 | import './header.css';
5 |
6 | type User = {
7 | name: string;
8 | };
9 |
10 | interface HeaderProps {
11 | user?: User;
12 | onLogin: () => void;
13 | onLogout: () => void;
14 | onCreateAccount: () => void;
15 | }
16 |
17 | export const Header = ({ user, onLogin, onLogout, onCreateAccount }: HeaderProps) => (
18 |
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 | You need to enable JavaScript to run this app.
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 |
69 | { children }
70 |
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(Nice );
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(Nice );
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(Link );
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(Nice );
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 |
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 | { label }
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 |
29 |
30 |
31 |
32 |
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 |
59 |
60 |
61 |
62 |
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 |
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;
--------------------------------------------------------------------------------