Skip to content

Commit

Permalink
feat(Cascader): 增加级联选择框 (#711)
Browse files Browse the repository at this point in the history
  • Loading branch information
nullptr-z committed Mar 24, 2022
1 parent bb6e613 commit 58023ff
Show file tree
Hide file tree
Showing 12 changed files with 353 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/deploy.yml
Expand Up @@ -128,6 +128,11 @@ jobs:
with:
token: ${{ secrets.NPM_TOKEN }}
package: ./packages/react-carousel/package.json
- name: 📦 @uiw/react-cascader publish to NPM
uses: JS-DevTools/npm-publish@v1
with:
token: ${{ secrets.NPM_TOKEN }}
package: ./packages/react-cascader/package.json
- name: 📦 @uiw/react-checkbox publish to NPM
uses: JS-DevTools/npm-publish@v1
with:
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/npm.yml
Expand Up @@ -197,6 +197,11 @@ jobs:
with:
token: ${{ secrets.NPM_TOKEN }}
package: ./packages/react-carousel/package.json
- name: 📦 @uiw/react-cascader publish to NPM
uses: JS-DevTools/npm-publish@v1
with:
token: ${{ secrets.NPM_TOKEN }}
package: ./packages/react-cascader/package.json
- name: 📦 @uiw/react-checkbox publish to NPM
uses: JS-DevTools/npm-publish@v1
with:
Expand Down
77 changes: 77 additions & 0 deletions packages/react-cascader/README.md
@@ -0,0 +1,77 @@
Cascader 级联选择
===

[![Open in unpkg](https://img.shields.io/badge/Open%20in-unpkg-blue)](https://uiwjs.github.io/npm-unpkg/#/pkg/@uiw/react-cascader/file/README.md)
[![NPM Downloads](https://img.shields.io/npm/dm/@uiw/react-cascader.svg?style=flat)](https://www.npmjs.com/package/@uiw/react-cascader)
[![npm version](https://img.shields.io/npm/v/@uiw/react-cascader.svg?label=@uiw/react-cascader)](https://npmjs.com/@uiw/react-cascader)

级联选择框。v4.16.0中添加

```jsx
import { Cascader } from 'uiw';
// or
import Cascader from '@uiw/react-cascader';
```

## 基础示例

<!--rehype:bgWhite=true&codeSandbox=true&codePen=true-->
```jsx
import ReactDOM from 'react-dom';
import { Cascader } from 'uiw';

const Demo = () => {

const options = [
{
value: 'zhejiang',
label: 'Zhejiang',
children: [
{
value: 'hangzhou',
label: 'Hangzhou',
children: [
{
value: 'xihu',
label: 'West Lake',
},
],
},
],
},
{
value: 'jiangsu',
label: 'Jiangsu',
children: [
{
value: 'nanjing',
label: 'Nanjing',
children: [
{
value: 'zhonghuamen',
label: 'Zhong Hua Men',
},
],
},
],
},
];

return(
<div style={{ width: 200 }}>
<Cascader placeholder="请选择" option={options} onChange={(value,seleteds)=>console.log(value,seleteds)}/>
</div>
)
};
ReactDOM.render(<Demo />, _mount_);
```

## Props

| 参数 | 说明 | 类型 | 默认值 | 版本 |
| ---- | ---- | ---- | ---- | ---- |
| allowClear | 支持清除 | Boolean | `false` | - |
| placeholder | 选择框默认文字 | String | - | - |
| option | 选项菜单 | { value: String \| Number, label: React.ReactNode, children: Array<String \| Number>} | - | - |
| value | 指定当前选中的条目,多选时为一个数组 | String[] \| Number[] | - | - |
| onChange | 选中选项调用此函数 | function(value, selectedOptions) | - | - |
54 changes: 54 additions & 0 deletions packages/react-cascader/package.json
@@ -0,0 +1,54 @@
{
"name": "@uiw/react-cascader",
"version": "4.15.1",
"description": "Cascader component",
"author": "Kenny Wong <wowohoo@qq.com>",
"homepage": "https://uiwjs.github.io/#/components/cascader",
"repository": {
"type": "git",
"url": "https://github.com/uiwjs/uiw.git"
},
"license": "MIT",
"main": "cjs/index.js",
"module": "esm/index.js",
"files": [
"dist.css",
"cjs",
"esm",
"src"
],
"publishConfig": {
"access": "public"
},
"keywords": [
"cascader",
"design",
"uiw",
"uiw-react",
"react.js",
"react",
"react-component",
"component",
"components",
"ui",
"css",
"uikit",
"react-ui",
"framework",
"front-end",
"frontend"
],
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
},
"dependencies": {
"@uiw/react-dropdown": "^4.15.1",
"@uiw/react-icon": "^4.15.1",
"@uiw/react-input": "^4.15.1",
"@uiw/react-loader": "^4.15.1",
"@uiw/react-menu": "^4.15.1",
"@uiw/react-tag": "^4.15.1",
"@uiw/utils": "^4.15.1"
}
}
109 changes: 109 additions & 0 deletions packages/react-cascader/src/index.tsx
@@ -0,0 +1,109 @@
import React, { useState, useEffect, useMemo } from 'react';
import Input from '@uiw/react-input';
import { IProps } from '@uiw/utils';
import Dropdown, { DropdownProps } from '@uiw/react-dropdown';
import Menu from '@uiw/react-menu';
import Icon from '@uiw/react-icon';

type ValueType = Array<string | number>;
type optionType = { value: string | number; label: React.ReactNode; children?: Array<optionType> };

export interface CascaderProps extends IProps, DropdownProps {
option?: Array<optionType>;
value?: ValueType;
onChange?: (value: ValueType, selectedOptions: Array<optionType>) => void;
allowClear?: boolean;
placeholder?: string;
isOpen?: boolean;
}

function Cascader(props: CascaderProps) {
const { placeholder, prefixCls = 'w-search-select', className, style = { width: 200 }, option = [], others } = props;

const cls = [prefixCls, className].filter(Boolean).join(' ').trim();
const [innerIsOpen, setInnerIsOpen] = useState(false);
const [selectedValue, setSelectedValue] = useState<Array<optionType>>([]);

function onVisibleChange(isOpen: boolean) {
setInnerIsOpen(isOpen);
}

const handleItemClick = (optionsItem: optionType, level: number) => {
selectedValue.splice(level, selectedValue.length - level, optionsItem);
setSelectedValue([...selectedValue]);

handelChange();
};

const handelChange = () => {
const value = selectedValue.map((item) => item.value);
props.onChange?.(value, selectedValue);
};

const widths = (style?.width as number) * 0.6 || undefined;

const OptionIter = (option: Array<optionType>, level: number = 0) => {
if (!option) return;

return (
<Menu
key={level}
bordered
style={{
minHeight: 25,
minWidth: widths,
overflowY: 'scroll',
width: widths,
}}
>
{!option || option.length === 0 ? (
<div style={{ color: '#c7c7c7', fontSize: 12 }}>{'没有数据'}</div>
) : (
option.map((item, index) => {
const active = selectedValue[level]?.value === item.value;
return (
<Menu.Item
active={active}
key={index}
text={item.label}
addonAfter={item.children ? <Icon type="right" /> : undefined}
onClick={() => handleItemClick(item, level)}
/>
);
})
)}
</Menu>
) as JSX.Element;
};

const inputValue = useMemo(() => {
return selectedValue.map((item) => item.label).join(' / ');
}, [selectedValue.length]);

return (
<Dropdown
className={cls}
trigger="click"
style={{ marginTop: 5, ...style }}
overlayStyl={{ width: 100 }}
{...others}
onVisibleChange={onVisibleChange}
isOpen={innerIsOpen}
menu={
<div style={{ display: 'flex' }}>
{new Array(selectedValue.length + 1)
.fill(0)
.map((_, index) => {
const options = index ? selectedValue[index - 1]?.children! : option;
return OptionIter(options, index);
})
.filter((m) => !!m)}
</div>
}
>
<Input value={inputValue} onChange={() => {}} placeholder={placeholder} style={{ width: style?.width }} />
</Dropdown>
);
}

export default Cascader;
75 changes: 75 additions & 0 deletions packages/react-cascader/src/style/index.less
@@ -0,0 +1,75 @@
@w-search-select: ~'w-search-select';

.@{w-search-select} {
&-input-contents {
input {
box-shadow: none;
padding: 0px;
min-width: 50px;
height: 22px;
}

.w-input-inner {
&:hover {
box-shadow: none !important;
}

&:focus {
box-shadow: none !important;
}

&.disabled {
box-shadow: none;
background: #dddddd;
opacity: 0.75;
color: #a5a5a5;
cursor: not-allowed;
resize: none;
}
}
}

&-inner {
display: flex;
justify-content: space-between;
outline: none;
border: none;
align-items: center;
border-radius: 3px;
box-shadow: 0 0 0 0 rgba(19, 124, 189, 0), 0 0 0 0 rgba(19, 124, 189, 0), inset 0 0 0 1px rgba(16, 22, 26, 0.15),
inset 0 1px 1px rgba(16, 22, 26, 0.2);
box-sizing: border-box;
background: #fff;
min-height: 30px;
margin: 0 !important;
padding: 3px 10px 3px 10px;
vertical-align: middle;
line-height: 30px;
color: #393e48;
font-weight: 400;
font-size: inherit;
transition: box-shadow 0.3s cubic-bezier(0.4, 1, 0.75, 0.9);
appearance: none;

&:focus {
box-shadow: 0 0 0 1px #393e48, 0 0 0 3px rgba(57, 62, 72, 0.17), inset 0 1px 1px rgba(16, 22, 26, 0.2);
}

&:hover {
box-shadow: 0 0 0 1px #6e6e6e, 0 0 0 3px rgba(57, 62, 72, 0), inset 0 1px 1px rgba(16, 22, 26, 0.2);
}

&:focus&:hover {
box-shadow: 0 0 0 1px #6e6e6e, 0 0 0 3px rgba(57, 62, 72, 0.17), inset 0 1px 1px rgba(16, 22, 26, 0.2);
}

&:disabled {
box-shadow: none;
background: #dddddd;
opacity: 0.75;
color: #a5a5a5;
cursor: not-allowed;
resize: none;
}
}
}
8 changes: 8 additions & 0 deletions packages/react-cascader/tsconfig.json
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig",
"include": ["src/**/*"],
"compilerOptions": {
"outDir": "./cjs",
"baseUrl": "."
}
}
1 change: 1 addition & 0 deletions packages/uiw/package.json
Expand Up @@ -68,6 +68,7 @@
"@uiw/react-calendar": "^4.15.1",
"@uiw/react-card": "^4.15.1",
"@uiw/react-carousel": "^4.15.1",
"@uiw/react-cascader": "^4.15.1",
"@uiw/react-checkbox": "^4.15.1",
"@uiw/react-collapse": "^4.15.1",
"@uiw/react-copy-to-clipboard": "^4.15.1",
Expand Down
2 changes: 2 additions & 0 deletions packages/uiw/src/index.ts
Expand Up @@ -10,6 +10,7 @@ export * from '@uiw/react-button-group';
export * from '@uiw/react-calendar';
export * from '@uiw/react-card';
export * from '@uiw/react-carousel';
export * from '@uiw/react-cascader';
export * from '@uiw/react-checkbox';
export * from '@uiw/react-collapse';
export * from '@uiw/react-copy-to-clipboard';
Expand Down Expand Up @@ -69,6 +70,7 @@ export { default as Breadcrumb } from '@uiw/react-breadcrumb';
export { default as Button } from '@uiw/react-button';
export { default as ButtonGroup } from '@uiw/react-button-group';
export { default as Calendar } from '@uiw/react-calendar';
export { default as Cascader } from '@uiw/react-cascader';
export { default as Card } from '@uiw/react-card';
export { default as Carousel } from '@uiw/react-carousel';
export { default as Checkbox } from '@uiw/react-checkbox';
Expand Down
1 change: 1 addition & 0 deletions website/src/menu.json
Expand Up @@ -43,6 +43,7 @@
{ "name": "Form 表单", "path": "form" },
{ "name": "Radio 单选框", "path": "radio" },
{ "name": "Checkbox 多选框", "path": "checkbox" },
{ "name": "Cascader 级联选择", "path": "cascader" },
{ "name": "Input 输入框", "path": "input" },
{ "name": "FileInput 上传输入框", "path": "file-input" },
{ "name": "PinCode PIN码", "path": "pin-code" },
Expand Down
2 changes: 2 additions & 0 deletions website/src/routers.tsx
Expand Up @@ -30,6 +30,7 @@ const ResetCss = Loadable(lazy(() => import('./routes/components/reset-css')));
const Avatar = Loadable(lazy(() => import('./routes/components/avatar')));
const Affix = Loadable(lazy(() => import('./routes/components/affix')));
const Calendar = Loadable(lazy(() => import('./routes/components/calendar')));
const Cascader = Loadable(lazy(() => import('./routes/components/cascader')));
const Checkbox = Loadable(lazy(() => import('./routes/components/checkbox')));
const CopyToClipboard = Loadable(lazy(() => import('./routes/components/copy-to-clipboard')));
const Collapse = Loadable(lazy(() => import('./routes/components/collapse')));
Expand Down Expand Up @@ -128,6 +129,7 @@ export const routes: RouteObject[] = [
{ path: '/components/collapse', element: <Collapse /> },
{ path: '/components/card', element: <Card /> },
{ path: '/components/carousel', element: <Carousel /> },
{ path: '/components/cascader', element: <Cascader /> },
{ path: '/components/descriptions', element: <Descriptions /> },
{ path: '/components/loader', element: <Loader /> },
{ path: '/components/icon', element: <Icon /> },
Expand Down

0 comments on commit 58023ff

Please sign in to comment.