diff --git a/packages/sortable-list/src/hooks/useActiveItem.ts b/packages/sortable-list/src/hooks/useActiveItem.ts deleted file mode 100644 index dc83e012..00000000 --- a/packages/sortable-list/src/hooks/useActiveItem.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { useCallback, useState } from 'react'; - -export const useActiveItem = () => { - const [activeId, setActiveId] = useState(null); - - const activateItem = useCallback((id: string) => { - setActiveId(id); - }, []); - - const deactivateItem = useCallback(() => { - setActiveId(null); - }, []); - - const isDragging = activeId !== null; - - return { isDragging, activeId, activateItem, deactivateItem }; -}; diff --git a/packages/sortable-list/src/hooks/useSortableList.test.ts b/packages/sortable-list/src/hooks/useSortableList.test.ts deleted file mode 100644 index 3b568b03..00000000 --- a/packages/sortable-list/src/hooks/useSortableList.test.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { useState } from 'react'; -import { act, renderHook } from '@testing-library/react-hooks'; - -import { useSortableList } from './useSortableList'; - -import type { SortableItemList } from '../types'; - -describe('useSortableList', () => { - it('默认值', () => { - const { result } = renderHook(() => useSortableList()); - - expect(result.current.items).toEqual([]); - }); - it('外部传入初始值', () => { - const { result } = renderHook(() => - useSortableList({ defaultValue: [{ id: '123' }] }), - ); - expect(result.current.items).toEqual([{ id: '123' }]); - }); - - describe('updateTableStore', () => { - describe('添加item', () => { - it('基础添加', () => { - const { result } = renderHook(() => useSortableList()); - - act(() => { - result.current.dispatchSortable({ - type: 'addItem', - item: { id: '333' }, - }); - }); - - expect(result.current.items).toEqual([{ id: '333' }]); - }); - - it('携带索引的添加', () => { - const { result } = renderHook(() => - useSortableList({ defaultValue: [{ id: '1' }] }), - ); - - act(() => { - result.current.dispatchSortable({ - type: 'addItem', - item: { id: '2' }, - addIndex: 0, - }); - }); - - expect(result.current.items).toEqual([{ id: '2' }, { id: '1' }]); - }); - }); - it('删除item', () => { - const { result } = renderHook(() => - useSortableList({ - defaultValue: [{ id: '1' }, { id: '2' }, { id: '3' }], - }), - ); - - act(() => { - result.current.dispatchSortable({ - type: 'removeItem', - id: '2', - }); - }); - - expect(result.current.items).toEqual([{ id: '1' }, { id: '3' }]); - }); - it('重排序item', () => { - const { result } = renderHook(() => - useSortableList({ - defaultValue: [{ id: '1' }, { id: '2' }, { id: '3' }], - }), - ); - - act(() => { - result.current.dispatchSortable({ - type: 'reorder', - startIndex: 2, - endIndex: 0, - }); - }); - - expect(result.current.items).toEqual([ - { id: '3' }, - { id: '1' }, - { id: '2' }, - ]); - }); - }); - - describe('受控模式', () => { - it('没有 onChange 时不更改', () => { - const { result } = renderHook(() => { - const [value] = useState([{ id: '1' }]); - const { dispatchSortable } = useSortableList({ - value: [{ id: '1' }], - }); - return { value, dispatchSortable }; - }); - act(() => { - result.current.dispatchSortable({ - type: 'addItem', - item: { id: '333' }, - }); - }); - - expect(result.current.value).toEqual([{ id: '1' }]); - }); - - it('外部控制 value', () => { - const controlledValue: SortableItemList = [ - { id: '1' }, - { id: '2' }, - { id: '3' }, - ]; - - const { result } = renderHook(() => { - const [value, setConfig] = useState(); - const { items } = useSortableList({ - value, - }); - - return { items, setConfig }; - }); - - act(() => { - result.current.setConfig(controlledValue); - }); - - expect(result.current.items).toEqual([ - { id: '1' }, - { id: '2' }, - { id: '3' }, - ]); - }); - it('内部修改值 外部 value 更新', () => { - const { result } = renderHook(() => { - const [value, setConfig] = useState([ - { id: '1' }, - { id: '2' }, - { id: '3' }, - ]); - - const { dispatchSortable } = useSortableList({ - value, - onChange: setConfig, - }); - - return { value, dispatchSortable }; - }); - - expect(result.current.value).toEqual([ - { id: '1' }, - { id: '2' }, - { id: '3' }, - ]); - - act(() => { - result.current.dispatchSortable({ - type: 'reorder', - startIndex: 2, - endIndex: 0, - }); - }); - - expect(result.current.value).toEqual([ - { id: '3' }, - { id: '1' }, - { id: '2' }, - ]); - - act(() => { - result.current.dispatchSortable({ - type: 'reorder', - startIndex: 2, - endIndex: 0, - }); - }); - - expect(result.current.value).toEqual([ - { id: '2' }, - { id: '3' }, - { id: '1' }, - ]); - }); - }); -}); diff --git a/packages/sortable-list/src/store/index.test.ts b/packages/sortable-list/src/store/index.test.ts new file mode 100644 index 00000000..de949ba8 --- /dev/null +++ b/packages/sortable-list/src/store/index.test.ts @@ -0,0 +1,79 @@ +import { act, renderHook } from '@testing-library/react-hooks'; + +import { createStore } from './index'; + +const useStore = createStore(); +describe('useStore', () => { + it('默认值', () => { + const { result } = renderHook(() => useStore()); + + expect(result.current.data).toEqual([]); + }); + + describe('updateTableStore', () => { + describe('添加item', () => { + it('基础添加', () => { + const { result } = renderHook(() => useStore()); + + act(() => { + result.current.addItem({ id: '333' }); + }); + + expect(result.current.data).toEqual([{ id: '333' }]); + }); + + it('携带索引的添加', () => { + const { result } = renderHook(() => useStore()); + + act(() => { + result.current.internalUpdateData([ + { id: '1' }, + { id: '2' }, + { id: '3' }, + ]); + + result.current.addItem({ id: '4' }, 0); + }); + + expect(result.current.data).toEqual([ + { id: '4' }, + { id: '1' }, + { id: '2' }, + { id: '3' }, + ]); + }); + }); + it('删除item', () => { + const { result } = renderHook(() => useStore()); + + act(() => { + result.current.internalUpdateData([ + { id: '1' }, + { id: '2' }, + { id: '3' }, + ]); + result.current.removeItem('2'); + }); + + expect(result.current.data).toEqual([{ id: '1' }, { id: '3' }]); + }); + it('重排序item', () => { + const { result } = renderHook(() => useStore()); + + act(() => { + result.current.internalUpdateData([ + { id: '1' }, + { id: '2' }, + { id: '3' }, + ]); + result.current.reorder(2, 0); + }); + + expect(result.current.data).toEqual([ + { id: '3' }, + { id: '1' }, + { id: '2' }, + ]); + }); + }); +}); diff --git a/packages/sortable-list/src/store/index.ts b/packages/sortable-list/src/store/index.ts new file mode 100644 index 00000000..ffd848ca --- /dev/null +++ b/packages/sortable-list/src/store/index.ts @@ -0,0 +1,70 @@ +import create from 'zustand'; +import createContext from 'zustand/context'; + +import { arrayMove } from '@dnd-kit/sortable'; +import produce from 'immer'; +import initialState from './initialState'; +import type { SortableListStore } from '../types'; + +const createStore = () => + create((set, get) => ({ + // 内部值 + ...initialState, + activateItem: (id) => { + set({ activeId: id }); + }, + deactivateItem: () => { + set({ activeId: null }); + }, + // 内部更新 data 方法 + internalUpdateData: (data) => { + const { onDataChange } = get(); + set({ data }); + + if (onDataChange) { + onDataChange(data); + } + }, + + syncOutsideData: (data) => { + set({ data }); + }, + + // 重新排序 + reorder: (startIndex, endIndex) => { + const { data, internalUpdateData } = get(); + const nextData = produce(data, (state) => { + if (startIndex !== endIndex) { + return arrayMove(state, startIndex, endIndex); + } + + return state; + }); + + internalUpdateData(nextData); + }, + // 添加元素 + addItem: (item, addIndex) => { + const { data, internalUpdateData } = get(); + const nextData = produce(data, (state) => { + if (typeof addIndex !== 'number') { + // 如果没有提供添加位的 index 值,默认添加在最后 + state.push(item); + } + + state.splice(addIndex, 0, item); + }); + + internalUpdateData(nextData); + }, + removeItem: (id) => { + const { data, internalUpdateData } = get(); + const nextData = data.filter((item) => item.id !== id); + + internalUpdateData(nextData); + }, + })); + +const { Provider, useStore, useStoreApi } = createContext(); + +export { Provider, useStore, createStore, useStoreApi }; diff --git a/packages/sortable-list/src/store/initialState.ts b/packages/sortable-list/src/store/initialState.ts new file mode 100644 index 00000000..2997ab51 --- /dev/null +++ b/packages/sortable-list/src/store/initialState.ts @@ -0,0 +1,9 @@ +import type { SortableListState } from '../types'; + +const initialState: SortableListState = { + data: [], + onDataChange: null, + activeId: null, +}; + +export default initialState; diff --git a/packages/sortable-list/src/store/utils.test.ts b/packages/sortable-list/src/store/utils.test.ts new file mode 100644 index 00000000..b62acc1d --- /dev/null +++ b/packages/sortable-list/src/store/utils.test.ts @@ -0,0 +1,15 @@ +import { getIndexOfActiveItem } from './utils'; + +const list = [{ id: '123' }, { id: '3245' }]; + +describe('getIndexOfActiveItem', () => { + it('找到 index', () => { + const index = getIndexOfActiveItem(list, '123'); + expect(index).toEqual(0); + }); + it('没找到 index', () => { + const index = getIndexOfActiveItem(list, '135'); + + expect(index).toEqual(-1); + }); +}); diff --git a/packages/sortable-list/src/utils.ts b/packages/sortable-list/src/store/utils.ts similarity index 78% rename from packages/sortable-list/src/utils.ts rename to packages/sortable-list/src/store/utils.ts index 8937f097..9834f574 100644 --- a/packages/sortable-list/src/utils.ts +++ b/packages/sortable-list/src/store/utils.ts @@ -1,4 +1,4 @@ -import type { SortableItemList } from './types'; +import type { SortableItemList } from '../types'; export const getIndexOfActiveItem = < T extends SortableItemList = SortableItemList, diff --git a/packages/sortable-list/src/types/common.ts b/packages/sortable-list/src/types/common.ts index f58b3d48..99357937 100644 --- a/packages/sortable-list/src/types/common.ts +++ b/packages/sortable-list/src/types/common.ts @@ -3,7 +3,7 @@ import type { UniqueIdentifier, } from '@dnd-kit/core'; import type { CSSProperties, ReactElement, Ref } from 'react'; -import type { SortableItem } from './data'; +import type { SortableItem } from './store'; export type RenderItem = ( item: T, diff --git a/packages/sortable-list/src/types/components.ts b/packages/sortable-list/src/types/components.ts index e7fa44e8..910b063f 100644 --- a/packages/sortable-list/src/types/components.ts +++ b/packages/sortable-list/src/types/components.ts @@ -14,7 +14,7 @@ import type { SortingStrategy } from '@dnd-kit/sortable'; import type { Transform } from '@dnd-kit/utilities'; import type { GetItemStyles, GetWrapperStyle, RenderItem } from './common'; -import type { SortableItem, SortableItemList } from './data'; +import type { SortableItem, SortableItemList } from './store'; export interface BaseItemProps extends Pick< @@ -69,7 +69,6 @@ export interface DraggingOverlayProps dragging: boolean; item: SortableItem; activeIndex: number; - activeId: string; } export interface SortableProps { diff --git a/packages/sortable-list/src/types/data.ts b/packages/sortable-list/src/types/data.ts deleted file mode 100644 index f46458e6..00000000 --- a/packages/sortable-list/src/types/data.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { UniqueIdentifier } from '@dnd-kit/core'; - -export interface SortableBaseItem { - id: UniqueIdentifier; -} - -export type SortableItem> = SortableBaseItem & T; - -export type SortableItemList> = SortableItem[]; diff --git a/packages/sortable-list/src/types/index.ts b/packages/sortable-list/src/types/index.ts index d3c74246..06989a50 100644 --- a/packages/sortable-list/src/types/index.ts +++ b/packages/sortable-list/src/types/index.ts @@ -1,2 +1,2 @@ export * from './components'; -export * from './data'; +export * from './store'; diff --git a/packages/sortable-list/src/types/store.ts b/packages/sortable-list/src/types/store.ts new file mode 100644 index 00000000..88363ad3 --- /dev/null +++ b/packages/sortable-list/src/types/store.ts @@ -0,0 +1,48 @@ +import type { UniqueIdentifier } from '@dnd-kit/core'; + +/** + * 基础项 + */ +export interface SortableBaseItem { + id: UniqueIdentifier; +} + +export type SortableItem> = SortableBaseItem & T; + +export type SortableItemList> = SortableItem[]; + +/** + * 状态 + */ +export interface SortableListState { + data: SortableItemList; + onDataChange?: (data: SortableItemList) => void; + activeId: string; +} + +/** + * 动作 + */ +export interface SortableListAction { + /** + * 同步外部数据源 + */ + syncOutsideData: (data: SortableItemList) => void; + internalUpdateData: (data: SortableItemList) => void; + removeItem: (id: string) => void; + reorder: (startIndex: number, endIndex: number) => void; + addItem: (item: SortableItem, addIndex?: number) => void; + + // 激活 + activateItem: (id: string) => void; + deactivateItem: () => void; +} + +export type SortableListStore = SortableListState & SortableListAction; + +// 外部值更新 + +export interface StoreUpdaterProps { + data: SortableItemList; + onDataChange?: (data: SortableItemList) => void; +}