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 ( + + {!option || option.length === 0 ? ( +
{'没有数据'}
+ ) : ( + option.map((item, index) => { + const active = selectedValue[level]?.value === item.value; + return ( + : undefined} + onClick={() => handleItemClick(item, level)} + /> + ); + }) + )} +
+ ) 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; + }} + /> +);