diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..33b1999c --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,15 @@ +# These are supported funding model platforms + +github: ant-design # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: ant-design # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +polar: # Replace with a single Polar username +buy_me_a_coffee: # Replace with a single Buy Me a Coffee username +thanks_dev: # Replace with a single thanks.dev username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 65ad47f9..5735e2d2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,113 +1,6 @@ -name: CI - -on: - push: - branches: [master] - pull_request: - +name: ✅ test +on: [push, pull_request] jobs: - setup: - runs-on: ubuntu-latest - steps: - - name: checkout - uses: actions/checkout@master - - - uses: actions/setup-node@v1 - with: - node-version: '16' - - - name: cache package-lock.json - uses: actions/cache@v2 - with: - path: package-temp-dir - key: lock-${{ github.sha }} - - - name: create package-lock.json - run: npm i --package-lock-only - - - name: hack for singe file - run: | - if [ ! -d "package-temp-dir" ]; then - mkdir package-temp-dir - fi - cp package-lock.json package-temp-dir - - name: cache node_modules - id: node_modules_cache_id - uses: actions/cache@v2 - with: - path: node_modules - key: node_modules-${{ hashFiles('**/package-temp-dir/package-lock.json') }} - - - name: install - if: steps.node_modules_cache_id.outputs.cache-hit != 'true' - run: npm ci - - lint: - runs-on: ubuntu-latest - steps: - - name: checkout - uses: actions/checkout@master - - - name: restore cache from package-lock.json - uses: actions/cache@v2 - with: - path: package-temp-dir - key: lock-${{ github.sha }} - - - name: restore cache from node_modules - uses: actions/cache@v2 - with: - path: node_modules - key: node_modules-${{ hashFiles('**/package-temp-dir/package-lock.json') }} - - - name: lint - run: npm run lint - - needs: setup - - compile: - runs-on: ubuntu-latest - steps: - - name: checkout - uses: actions/checkout@master - - - name: restore cache from package-lock.json - uses: actions/cache@v2 - with: - path: package-temp-dir - key: lock-${{ github.sha }} - - - name: restore cache from node_modules - uses: actions/cache@v2 - with: - path: node_modules - key: node_modules-${{ hashFiles('**/package-temp-dir/package-lock.json') }} - - - name: compile - run: npm run compile - - needs: setup - - coverage: - runs-on: ubuntu-latest - steps: - - name: checkout - uses: actions/checkout@master - - - name: restore cache from package-lock.json - uses: actions/cache@v2 - with: - path: package-temp-dir - key: lock-${{ github.sha }} - - - name: restore cache from node_modules - uses: actions/cache@v2 - with: - path: node_modules - key: node_modules-${{ hashFiles('**/package-temp-dir/package-lock.json') }} - - name: tsc - run: npm run lint:tsc - - name: coverage - run: npm test -- --coverage && bash <(curl -s https://codecov.io/bash) - - needs: setup + test: + uses: react-component/rc-test/.github/workflows/test.yml@main + secrets: inherit \ No newline at end of file diff --git a/.gitignore b/.gitignore index 96235bf4..e1eb4fbd 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,6 @@ coverage .dumi/tmp .dumi/tmp-production dist -.vscode \ No newline at end of file +.vscode + +bun.lockb \ No newline at end of file diff --git a/README.md b/README.md index 39bf2059..7349b95c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # rc-cascader -React Cascader Component +React Cascader Component. [![NPM version][npm-image]][npm-url] [![npm download][download-image]][download-url] diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 00000000..80d57b63 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,2 @@ +[install] +peer = false \ No newline at end of file diff --git a/docs/demo/multiple-search.md b/docs/demo/multiple-search.md new file mode 100644 index 00000000..f02257b8 --- /dev/null +++ b/docs/demo/multiple-search.md @@ -0,0 +1,8 @@ +--- +title: multiple-search +nav: + title: Demo + path: /demo +--- + + diff --git a/examples/multiple-search.tsx b/examples/multiple-search.tsx new file mode 100644 index 00000000..410dbe7a --- /dev/null +++ b/examples/multiple-search.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import '../assets/index.less'; +import Cascader from '../src'; + +const options = [ + { + value: 'zhejiang', + label: 'Zhejiang', + children: [ + { + value: 'hangzhou', + label: 'Hangzhou', + children: [ + { + value: 'xihu', + label: 'West Lake', + }, + { + value: 'xiasha', + label: 'Xia Sha', + }, + ], + }, + ], + }, + { + value: 'jiangsu', + label: 'Jiangsu', + children: [ + { + value: 'nanjing', + label: 'Nanjing', + children: [ + { + value: 'zhonghuamen', + label: 'Zhong Hua men', + }, + ], + }, + ], + }, +]; + +const Demo = () => { + return ; +}; + +export default Demo; diff --git a/examples/panel.tsx b/examples/panel.tsx index 1f985476..277061a7 100644 --- a/examples/panel.tsx +++ b/examples/panel.tsx @@ -61,6 +61,8 @@ export default () => { const [value2, setValue2] = React.useState([]); + const [disabled, setDisabled] = React.useState(false); + return ( <>

Panel

@@ -71,6 +73,13 @@ export default () => { > Set Value + { console.log('Change:', nextValue); setValue(nextValue); }} + disabled={disabled} /> +
defaultActiveKey=[bj, haidian]
{ - console.log('Change:', nextValue); - setValue2(nextValue); + console.log('Change:', nextValue); + setValue2(nextValue); }} + defaultActiveKey={['bj', 'haidian']} + disabled={disabled} /> - + diff --git a/package.json b/package.json index d47340a1..d68675e0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rc-cascader", - "version": "3.27.0", + "version": "3.30.0", "description": "cascade select ui component for react", "keywords": [ "react", @@ -42,12 +42,11 @@ "test": "rc-test" }, "dependencies": { - "@babel/runtime": "^7.12.5", - "array-tree-filter": "^2.1.0", + "@babel/runtime": "^7.25.7", "classnames": "^2.3.1", - "rc-select": "~14.15.0", - "rc-tree": "~5.8.1", - "rc-util": "^5.37.0" + "rc-select": "~14.16.2", + "rc-tree": "~5.10.1", + "rc-util": "^5.43.0" }, "devDependencies": { "@rc-component/father-plugin": "^1.0.0", @@ -60,6 +59,8 @@ "@types/react-dom": "^18.0.11", "@types/warning": "^3.0.0", "@umijs/fabric": "^4.0.0", + "array-tree-filter": "^3.0.2", + "cheerio": "1.0.0-rc.12", "cross-env": "^7.0.0", "dumi": "^2.1.10", "enzyme": "^3.3.0", diff --git a/src/Cascader.tsx b/src/Cascader.tsx index 97adcd20..5a66dee1 100644 --- a/src/Cascader.tsx +++ b/src/Cascader.tsx @@ -159,6 +159,7 @@ export interface CascaderProps< value: GetValueType, selectOptions: OptionType[], ) => void; + defaultActiveKey?: React.Key[]; } export type SingleValueType = (string | number)[]; @@ -175,6 +176,7 @@ export type InternalCascaderProps = Omit void; + defaultActiveKey?: React.Key[]; }; export type CascaderRef = Omit; @@ -280,7 +282,7 @@ const Cascader = React.forwardRef((props, re mergedFieldNames, dropdownPrefixCls || prefixCls, searchConfig, - changeOnSelect, + changeOnSelect || multiple, ); // =========================== Values =========================== diff --git a/src/OptionList/Column.tsx b/src/OptionList/Column.tsx index 82ec25cb..c46cc457 100644 --- a/src/OptionList/Column.tsx +++ b/src/OptionList/Column.tsx @@ -23,7 +23,7 @@ export interface ColumnProps; loadingKeys: React.Key[]; isSelectable: (option: DefaultOptionType) => boolean; - searchValue?: string; + disabled?: boolean; } export default function Column({ @@ -39,7 +39,7 @@ export default function Column) { const menuPrefixCls = `${prefixCls}-menu`; const menuItemPrefixCls = `${prefixCls}-menu-item`; @@ -56,6 +56,8 @@ export default function Column propsDisabled || disabled; + // ============================ Option ============================ const optionInfoList = React.useMemo( () => @@ -117,7 +119,7 @@ export default function Column { // >>>>> Open const triggerOpenPath = () => { - if (disabled || searchValue) { + if (isOptionDisabled(disabled)) { return; } const nextValueCells = [...fullPath]; @@ -129,7 +131,7 @@ export default function Column>>>> Selection const triggerSelect = () => { - if (isSelectable(option)) { + if (isSelectable(option) && !isOptionDisabled(disabled)) { onSelect(fullPath, isMergedLeaf); } }; @@ -150,7 +152,7 @@ export default function Column) => { if (disableCheckbox) { diff --git a/src/OptionList/List.tsx b/src/OptionList/List.tsx index 4beedd05..e4f21ba5 100644 --- a/src/OptionList/List.tsx +++ b/src/OptionList/List.tsx @@ -21,11 +21,28 @@ import useKeyboard from './useKeyboard'; export type RawOptionListProps = Pick< ReturnType, - 'prefixCls' | 'multiple' | 'searchValue' | 'toggleOpen' | 'notFoundContent' | 'direction' | 'open' ->; + | 'prefixCls' + | 'multiple' + | 'searchValue' + | 'toggleOpen' + | 'notFoundContent' + | 'direction' + | 'open' + | 'disabled' +> & { defaultActiveKey?: React.Key[]; }; const RawOptionList = React.forwardRef((props, ref) => { - const { prefixCls, multiple, searchValue, toggleOpen, notFoundContent, direction, open } = props; + const { + prefixCls, + multiple, + searchValue, + toggleOpen, + notFoundContent, + direction, + open, + disabled, + defaultActiveKey, + } = props; const containerRef = React.useRef(null); const rtl = direction === 'rtl'; @@ -89,7 +106,7 @@ const RawOptionList = React.forwardRef(( const halfCheckedSet = React.useMemo(() => new Set(toPathKeys(halfValues)), [halfValues]); // ====================== Accessibility ======================= - const [activeValueCells, setActiveValueCells] = useActive(multiple, open); + const [activeValueCells, setActiveValueCells] = useActive(multiple, open, defaultActiveKey); // =========================== Path =========================== const onPathOpen = (nextValueCells: React.Key[]) => { @@ -100,10 +117,14 @@ const RawOptionList = React.forwardRef(( }; const isSelectable = (option: DefaultOptionType) => { - const { disabled } = option; + if (disabled) { + return false; + } + const { disabled: optionDisabled } = option; const isMergedLeaf = isLeaf(option, fieldNames); - return !disabled && (isMergedLeaf || changeOnSelect || multiple); + + return !optionDisabled && (isMergedLeaf || changeOnSelect || multiple); }; const onPathSelect = (valuePath: SingleValueType, leaf: boolean, fromKeyboard = false) => { @@ -166,6 +187,9 @@ const RawOptionList = React.forwardRef(( // >>>>> Active Scroll React.useEffect(() => { + if (searchValue) { + return; + } for (let i = 0; i < activeValueCells.length; i += 1) { const cellPath = activeValueCells.slice(0, i + 1); const cellKeyPath = toPathKey(cellPath); @@ -176,7 +200,7 @@ const RawOptionList = React.forwardRef(( scrollIntoParentView(ele); } } - }, [activeValueCells]); + }, [activeValueCells, searchValue]); // ========================== Render ========================== // >>>>> Empty @@ -213,7 +237,6 @@ const RawOptionList = React.forwardRef(( void] => { const { values } = React.useContext(CascaderContext); @@ -14,7 +15,7 @@ const useActive = ( // Record current dropdown active options // This also control the open status - const [activeValueCells, setActiveValueCells] = React.useState([]); + const [activeValueCells, setActiveValueCells] = React.useState(defaultActiveKey ?? []); React.useEffect( () => { diff --git a/src/Panel.tsx b/src/Panel.tsx index 9fa7e6dd..b4d4cff3 100644 --- a/src/Panel.tsx +++ b/src/Panel.tsx @@ -35,7 +35,9 @@ export type PickType = | 'className' | 'style' | 'direction' - | 'notFoundContent'; + | 'notFoundContent' + | 'defaultActiveKey' + | 'disabled'; export type PanelProps< OptionType extends DefaultOptionType = DefaultOptionType, @@ -68,6 +70,8 @@ export default function Panel< loadingIcon, direction, notFoundContent = 'Not Found', + defaultActiveKey, + disabled, } = props as Pick; // ======================== Multiple ======================== @@ -200,6 +204,8 @@ export default function Panel< toggleOpen={noop} open direction={direction} + defaultActiveKey={defaultActiveKey} + disabled={disabled} /> )} diff --git a/src/hooks/useSearchConfig.ts b/src/hooks/useSearchConfig.ts index e9889ad6..b7b6da80 100644 --- a/src/hooks/useSearchConfig.ts +++ b/src/hooks/useSearchConfig.ts @@ -22,7 +22,7 @@ export default function useSearchConfig(showSearch?: CascaderProps['showSearch'] } if ((searchConfig.limit as number) <= 0) { - delete searchConfig.limit; + searchConfig.limit = false; if (process.env.NODE_ENV !== 'production') { warning(false, "'limit' of showSearch should be positive number or false."); diff --git a/src/hooks/useSearchOptions.ts b/src/hooks/useSearchOptions.ts index 624af9d3..3d99f603 100644 --- a/src/hooks/useSearchOptions.ts +++ b/src/hooks/useSearchOptions.ts @@ -9,13 +9,13 @@ const defaultFilter: ShowSearchType['filter'] = (search, options, { label = '' } const defaultRender: ShowSearchType['render'] = (inputValue, path, prefixCls, fieldNames) => path.map(opt => opt[fieldNames.label as string]).join(' / '); -export default ( +const useSearchOptions = ( search: string, options: DefaultOptionType[], fieldNames: InternalFieldNames, prefixCls: string, config: ShowSearchType, - changeOnSelect?: boolean, + enableHalfPath?: boolean, ) => { const { filter = defaultFilter, render = defaultRender, limit = 50, sort } = config; @@ -46,8 +46,8 @@ export default ( // If is leaf option !children || children.length === 0 || - // If is changeOnSelect - changeOnSelect + // If is changeOnSelect or multiple + enableHalfPath ) { if (filter(search, connectedPathOptions, { label: fieldNames.label })) { filteredOptions.push({ @@ -87,5 +87,7 @@ export default ( return limit !== false && limit > 0 ? filteredOptions.slice(0, limit as number) : filteredOptions; - }, [search, options, fieldNames, prefixCls, render, changeOnSelect, filter, sort, limit]); + }, [search, options, fieldNames, prefixCls, render, enableHalfPath, filter, sort, limit]); }; + +export default useSearchOptions; diff --git a/tests/Panel.spec.tsx b/tests/Panel.spec.tsx index 452f1be4..ad917dc0 100644 --- a/tests/Panel.spec.tsx +++ b/tests/Panel.spec.tsx @@ -79,6 +79,19 @@ describe('Cascader.Panel', () => { expect(onChange).toHaveBeenCalledWith([['bamboo', 'little']], expect.anything()); }); + it('multiple with defaultActiveKey', () => { + const onChange = jest.fn(); + const { container } = render( + , + ); + expect(container.querySelectorAll('.rc-cascader-menu')).toHaveLength(2); + }); + it('rtl', () => { const { container } = render(); @@ -97,4 +110,17 @@ describe('Cascader.Panel', () => { const checkedLi = container.querySelector('[aria-checked="true"]'); expect(checkedLi?.textContent).toEqual('Little'); }); + + it('disabled', () => { + const onChange = jest.fn(); + const { container } = render( + , + ); + + expect(container.querySelector('.rc-cascader-menu-item-disabled')).toBeTruthy(); + + const selectOption = container.querySelector('.rc-cascader-menu-item')!; + fireEvent.click(selectOption); + expect(onChange).not.toHaveBeenCalled(); + }); }); diff --git a/tests/__snapshots__/search.spec.tsx.snap b/tests/__snapshots__/search.spec.tsx.snap index 8fa4d0e4..a08aaac7 100644 --- a/tests/__snapshots__/search.spec.tsx.snap +++ b/tests/__snapshots__/search.spec.tsx.snap @@ -8,26 +8,30 @@ exports[`Cascader.Search should correct render Cascader with same field name of class="rc-cascader-selector" > - + + + -