diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 165f56ce17..a790486275 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -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:
diff --git a/.github/workflows/npm.yml b/.github/workflows/npm.yml
index 5ebd7a38c1..2ede65f3cf 100644
--- a/.github/workflows/npm.yml
+++ b/.github/workflows/npm.yml
@@ -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:
diff --git a/packages/react-cascader/README.md b/packages/react-cascader/README.md
new file mode 100644
index 0000000000..d789bdf826
--- /dev/null
+++ b/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';
+```
+
+## 基础示例
+
+
+```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(
+
+ console.log(value,seleteds)}/>
+
+ )
+};
+ReactDOM.render(, _mount_);
+```
+
+## Props
+
+| 参数 | 说明 | 类型 | 默认值 | 版本 |
+| ---- | ---- | ---- | ---- | ---- |
+| allowClear | 支持清除 | Boolean | `false` | - |
+| placeholder | 选择框默认文字 | String | - | - |
+| option | 选项菜单 | { value: String \| Number, label: React.ReactNode, children: Array} | - | - |
+| value | 指定当前选中的条目,多选时为一个数组 | String[] \| Number[] | - | - |
+| onChange | 选中选项调用此函数 | function(value, selectedOptions) | - | - |
diff --git a/packages/react-cascader/package.json b/packages/react-cascader/package.json
new file mode 100644
index 0000000000..70260c347a
--- /dev/null
+++ b/packages/react-cascader/package.json
@@ -0,0 +1,54 @@
+{
+ "name": "@uiw/react-cascader",
+ "version": "4.15.1",
+ "description": "Cascader component",
+ "author": "Kenny Wong ",
+ "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"
+ }
+}
diff --git a/packages/react-cascader/src/index.tsx b/packages/react-cascader/src/index.tsx
new file mode 100644
index 0000000000..c72a6cb285
--- /dev/null
+++ b/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;
+type optionType = { value: string | number; label: React.ReactNode; children?: Array };
+
+export interface CascaderProps extends IProps, DropdownProps {
+ option?: Array;
+ value?: ValueType;
+ onChange?: (value: ValueType, selectedOptions: Array) => 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>([]);
+
+ 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, level: number = 0) => {
+ if (!option) return;
+
+ return (
+
+ ) as JSX.Element;
+ };
+
+ const inputValue = useMemo(() => {
+ return selectedValue.map((item) => item.label).join(' / ');
+ }, [selectedValue.length]);
+
+ return (
+
+ {new Array(selectedValue.length + 1)
+ .fill(0)
+ .map((_, index) => {
+ const options = index ? selectedValue[index - 1]?.children! : option;
+ return OptionIter(options, index);
+ })
+ .filter((m) => !!m)}
+
+ }
+ >
+ {}} placeholder={placeholder} style={{ width: style?.width }} />
+
+ );
+}
+
+export default Cascader;
diff --git a/packages/react-cascader/src/style/index.less b/packages/react-cascader/src/style/index.less
new file mode 100644
index 0000000000..6941205df5
--- /dev/null
+++ b/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;
+ }
+ }
+}
diff --git a/packages/react-cascader/tsconfig.json b/packages/react-cascader/tsconfig.json
new file mode 100644
index 0000000000..41a3f0ac82
--- /dev/null
+++ b/packages/react-cascader/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../tsconfig",
+ "include": ["src/**/*"],
+ "compilerOptions": {
+ "outDir": "./cjs",
+ "baseUrl": "."
+ }
+}
diff --git a/packages/uiw/package.json b/packages/uiw/package.json
index 39f113e046..7949a3e8b8 100644
--- a/packages/uiw/package.json
+++ b/packages/uiw/package.json
@@ -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",
diff --git a/packages/uiw/src/index.ts b/packages/uiw/src/index.ts
index 1940b672e7..12f56d1dfb 100644
--- a/packages/uiw/src/index.ts
+++ b/packages/uiw/src/index.ts
@@ -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';
@@ -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';
diff --git a/website/src/menu.json b/website/src/menu.json
index 78cea3bdbd..650641b915 100755
--- a/website/src/menu.json
+++ b/website/src/menu.json
@@ -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" },
diff --git a/website/src/routers.tsx b/website/src/routers.tsx
index cb02b2ba70..2bfc10dc15 100755
--- a/website/src/routers.tsx
+++ b/website/src/routers.tsx
@@ -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')));
@@ -128,6 +129,7 @@ export const routes: RouteObject[] = [
{ path: '/components/collapse', element: },
{ path: '/components/card', element: },
{ path: '/components/carousel', element: },
+ { path: '/components/cascader', element: },
{ path: '/components/descriptions', element: },
{ path: '/components/loader', element: },
{ path: '/components/icon', element: },
diff --git a/website/src/routes/components/cascader/index.tsx b/website/src/routes/components/cascader/index.tsx
new file mode 100644
index 0000000000..f60de94cd7
--- /dev/null
+++ b/website/src/routes/components/cascader/index.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+import { Form, Row, Col, Cascader } from 'uiw';
+import Markdown from '../../../components/Markdown';
+
+export default () => (
+ {
+ const md = await import('uiw/node_modules/@uiw/react-cascader/README.md');
+ return md.default || md;
+ }}
+ />
+);