diff --git a/packages/hooks/src/createUseStorageState/index.ts b/packages/hooks/src/createUseStorageState/index.ts index c4f8e90df3..70a3e2cd9e 100644 --- a/packages/hooks/src/createUseStorageState/index.ts +++ b/packages/hooks/src/createUseStorageState/index.ts @@ -70,7 +70,10 @@ export function createUseStorageState(getStorage: () => Storage | undefined) { const updateState = (value?: SetState) => { const currentState = isFunction(value) ? value(state) : value; - setState(currentState); + + if (!listenStorageChange) { + setState(currentState); + } try { let newValue: string | null; @@ -126,5 +129,6 @@ export function createUseStorageState(getStorage: () => Storage | undefined) { return [state, useMemoizedFn(updateState)] as const; } + return useStorageState; } diff --git a/packages/hooks/src/useDynamicList/__tests__/index.test.ts b/packages/hooks/src/useDynamicList/__tests__/index.test.ts index 2da76fa026..e6509cfb1d 100644 --- a/packages/hooks/src/useDynamicList/__tests__/index.test.ts +++ b/packages/hooks/src/useDynamicList/__tests__/index.test.ts @@ -3,6 +3,15 @@ import useDynamicList from '../index'; describe('useDynamicList', () => { const setUp = (props: any): any => renderHook(() => useDynamicList(props)); + const warnSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + afterEach(() => { + warnSpy.mockReset(); + }); + + afterAll(() => { + warnSpy.mockRestore(); + }); it('getKey should work', () => { const hook = setUp([1, 2, 3]); @@ -97,6 +106,18 @@ describe('useDynamicList', () => { hook.result.current.remove(7); }); expect(hook.result.current.list.length).toBe(7); + + // batch remove + act(() => { + hook.result.current.batchRemove(1); + }); + expect(warnSpy).toHaveBeenCalledWith( + '`indexes` parameter of `batchRemove` function expected to be an array, but got "number".', + ); + act(() => { + hook.result.current.batchRemove([0, 1, 2]); + }); + expect(hook.result.current.list.length).toBe(4); }); it('same items should have different keys', () => { diff --git a/packages/hooks/src/useDynamicList/demo/demo1.tsx b/packages/hooks/src/useDynamicList/demo/demo1.tsx index e7045a086c..ba137e2e1f 100644 --- a/packages/hooks/src/useDynamicList/demo/demo1.tsx +++ b/packages/hooks/src/useDynamicList/demo/demo1.tsx @@ -8,11 +8,12 @@ import { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons'; import { useDynamicList } from 'ahooks'; -import { Input } from 'antd'; +import { Button, Input, Space } from 'antd'; import React from 'react'; export default () => { - const { list, remove, getKey, insert, replace } = useDynamicList(['David', 'Jack']); + const { list, remove, batchRemove, getKey, insert, replace } = useDynamicList(['David', 'Jack']); + const listIndexes = list.map((item, index) => index); const Row = (index: number, item: any) => (
@@ -44,6 +45,23 @@ export default () => { <> {list.map((ele, index) => Row(index, ele))} + + + + +
{JSON.stringify([list])}
); diff --git a/packages/hooks/src/useDynamicList/index.ts b/packages/hooks/src/useDynamicList/index.ts index 1bcdd162cf..32d4987266 100644 --- a/packages/hooks/src/useDynamicList/index.ts +++ b/packages/hooks/src/useDynamicList/index.ts @@ -1,4 +1,5 @@ import { useCallback, useRef, useState } from 'react'; +import isDev from '../utils/isDev'; const useDynamicList = (initialList: T[] = []) => { const counterRef = useRef(-1); @@ -77,6 +78,37 @@ const useDynamicList = (initialList: T[] = []) => { }); }, []); + const batchRemove = useCallback((indexes: number[]) => { + if (!Array.isArray(indexes)) { + if (isDev) { + console.error( + `\`indexes\` parameter of \`batchRemove\` function expected to be an array, but got "${typeof indexes}".`, + ); + } + return; + } + if (!indexes.length) { + return; + } + + setList((prevList) => { + const newKeyList: number[] = []; + const newList = prevList.filter((item, index) => { + const shouldKeep = !indexes.includes(index); + + if (shouldKeep) { + newKeyList.push(getKey(index)); + } + + return shouldKeep; + }); + + keyList.current = newKeyList; + + return newList; + }); + }, []); + const move = useCallback((oldIndex: number, newIndex: number) => { if (oldIndex === newIndex) { return; @@ -150,6 +182,7 @@ const useDynamicList = (initialList: T[] = []) => { merge, replace, remove, + batchRemove, getKey, getIndex, move, diff --git a/packages/hooks/src/useLocalStorageState/demo/demo4.tsx b/packages/hooks/src/useLocalStorageState/demo/demo4.tsx index ca1c914229..8acae72d72 100644 --- a/packages/hooks/src/useLocalStorageState/demo/demo4.tsx +++ b/packages/hooks/src/useLocalStorageState/demo/demo4.tsx @@ -24,15 +24,12 @@ function Counter() { listenStorageChange: true, }); - const add = () => setCount(count! + 1); - const clear = () => setCount(); - return (
- - +
); } diff --git a/packages/hooks/src/useSelections/__tests__/index.test.ts b/packages/hooks/src/useSelections/__tests__/index.test.ts index 156984aec8..15cfa76191 100644 --- a/packages/hooks/src/useSelections/__tests__/index.test.ts +++ b/packages/hooks/src/useSelections/__tests__/index.test.ts @@ -1,4 +1,5 @@ import { act, renderHook } from '@testing-library/react'; +import { useState } from 'react'; import useSelections from '../index'; import type { Options } from '../index'; @@ -193,4 +194,45 @@ describe('useSelections', () => { expect(result.current.selected).toEqual(_selected); expect(result.current.isSelected(_selectedItem)).toBe(true); }); + + it('clearAll should work correct', async () => { + const runCase = (data, newData, remainData) => { + const { result } = renderHook(() => { + const [list, setList] = useState(data); + const hook = useSelections(list, { + itemKey: 'id', + }); + + return { setList, hook }; + }); + const { setSelected, unSelectAll, clearAll } = result.current.hook; + + act(() => { + setSelected(data); + }); + expect(result.current.hook.selected).toEqual(data); + expect(result.current.hook.allSelected).toBe(true); + + act(() => { + result.current.setList(newData); + }); + expect(result.current.hook.allSelected).toBe(false); + + act(() => { + unSelectAll(); + }); + expect(result.current.hook.selected).toEqual(remainData); + + act(() => { + clearAll(); + }); + expect(result.current.hook.selected).toEqual([]); + expect(result.current.hook.allSelected).toEqual(false); + expect(result.current.hook.noneSelected).toBe(true); + expect(result.current.hook.partiallySelected).toBe(false); + }; + + runCase(_data, [3, 4, 5], [1, 2]); + runCase(_dataObj, [{ id: 3 }, { id: 4 }, { id: 5 }], [{ id: 1 }, { id: 2 }]); + }); }); diff --git a/packages/hooks/src/useSelections/index.en-US.md b/packages/hooks/src/useSelections/index.en-US.md index 1cf0fc2f5a..8cf96832f5 100644 --- a/packages/hooks/src/useSelections/index.en-US.md +++ b/packages/hooks/src/useSelections/index.en-US.md @@ -36,6 +36,14 @@ const result: Result = useSelections(items: T[], options?: Options); const result: Result = useSelections(items: T[], defaultSelected?: T[]); ``` +### Params + + +| Property | Description | Type | Default | +| --- | --- | --- | --- | +| items | Data items | `T[]` | - | +| options | Optional configuration | `Options` | - | + ### Options @@ -48,7 +56,7 @@ const result: Result = useSelections(items: T[], defaultSelected?: T[]); | Property | Description | Type | | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | -| selected | Selected Items | `T[]` | +| selected | Selected items | `T[]` | | allSelected | Is all items selected | `boolean` | | noneSelected | Is no item selected | `boolean` | | partiallySelected | Is partially items selected | `boolean` | @@ -60,3 +68,4 @@ const result: Result = useSelections(items: T[], defaultSelected?: T[]); | selectAll | Select all items | `() => void` | | unSelectAll | UnSelect all items | `() => void` | | toggleAll | Toggle select all items | `() => void` | +| clearAll | Clear all selected (In general, `clearAll` is equivalent to `unSelectAll`. If the items is dynamic, `clearAll` will clear "all selected data", while `unSelectAll` will only clear "the currently selected data in the items") | `() => void` | diff --git a/packages/hooks/src/useSelections/index.ts b/packages/hooks/src/useSelections/index.ts index 1f9c1f547c..e98ce0fa3b 100644 --- a/packages/hooks/src/useSelections/index.ts +++ b/packages/hooks/src/useSelections/index.ts @@ -94,6 +94,11 @@ export default function useSelections(items: T[], options?: T[] | Options) const toggleAll = () => (allSelected ? unSelectAll() : selectAll()); + const clearAll = () => { + selectedMap.clear(); + setSelected([]); + }; + return { selected, noneSelected, @@ -106,6 +111,7 @@ export default function useSelections(items: T[], options?: T[] | Options) toggle: useMemoizedFn(toggle), selectAll: useMemoizedFn(selectAll), unSelectAll: useMemoizedFn(unSelectAll), + clearAll: useMemoizedFn(clearAll), toggleAll: useMemoizedFn(toggleAll), } as const; } diff --git a/packages/hooks/src/useSelections/index.zh-CN.md b/packages/hooks/src/useSelections/index.zh-CN.md index 373de0b1b7..022216b965 100644 --- a/packages/hooks/src/useSelections/index.zh-CN.md +++ b/packages/hooks/src/useSelections/index.zh-CN.md @@ -36,6 +36,14 @@ const result: Result = useSelections(items: T[], options?: Options); const result: Result = useSelections(items: T[], defaultSelected?: T[]); ``` +### Params + + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| items | 元素列表 | `T[]` | - | +| options | 可选配置项 | `Options` | - | + ### Options @@ -46,17 +54,18 @@ const result: Result = useSelections(items: T[], defaultSelected?: T[]); ### Result -| 参数 | 说明 | 类型 | -| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | -| selected | 已经选择的元素 | `T[]` | -| allSelected | 是否全选 | `boolean` | -| noneSelected | 是否一个都没有选择 | `boolean` | -| partiallySelected | 是否半选 | `boolean` | -| isSelected | 是否被选择 | `(value: T) => boolean` | -| setSelected | 选择多个元素。多次执行时,后面的返回值会覆盖前面的,因此如果希望合并多次操作的结果,需要手动处理:`setSelected((oldArray) => oldArray.concat(newArray))` | `(value: T[]) => void \| (value: (prevState: T[]) => T[]) => void` | -| select | 选择单个元素 | `(value: T) => void` | -| unSelect | 取消选择单个元素 | `(value: T) => void` | -| toggle | 反选单个元素 | `(value: T) => void` | -| selectAll | 选择全部元素 | `() => void` | -| unSelectAll | 取消选择全部元素 | `() => void` | -| toggleAll | 反选全部元素 | `() => void` | +| 参数 | 说明 | 类型 | +| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | +| selected | 已经选择的元素 | `T[]` | +| allSelected | 是否全选 | `boolean` | +| noneSelected | 是否一个都没有选择 | `boolean` | +| partiallySelected | 是否半选 | `boolean` | +| isSelected | 是否被选择 | `(value: T) => boolean` | +| setSelected | 选择多个元素。多次执行时,后面的返回值会覆盖前面的,因此如果希望合并多次操作的结果,需要手动处理:`setSelected((oldArray) => oldArray.concat(newArray))` | `(value: T[]) => void \| (value: (prevState: T[]) => T[]) => void` | +| select | 选择单个元素 | `(value: T) => void` | +| unSelect | 取消选择单个元素 | `(value: T) => void` | +| toggle | 反选单个元素 | `(value: T) => void` | +| selectAll | 选择全部元素 | `() => void` | +| unSelectAll | 取消选择全部元素 | `() => void` | +| toggleAll | 反选全部元素 | `() => void` | +| clearAll | 清除所有选中元素(一般情况下,`clearAll` 等价于 `unSelectAll`。如果元素列表是动态的,则 `clearAll` 会清除掉“所有选中过的元素”,而 `unSelectAll` 只会清除掉“当前元素列表里选中的元素”) | `() => void` | diff --git a/packages/hooks/src/utils/getDocumentOrShadow.ts b/packages/hooks/src/utils/getDocumentOrShadow.ts index f4b6f21f18..6b5b844377 100644 --- a/packages/hooks/src/utils/getDocumentOrShadow.ts +++ b/packages/hooks/src/utils/getDocumentOrShadow.ts @@ -8,6 +8,7 @@ const checkIfAllInShadow = (targets: BasicTarget[]): boolean => { const targetElement = getTargetElement(item); if (!targetElement) return false; if (targetElement.getRootNode() instanceof ShadowRoot) return true; + return false; }); };