Skip to content

Commit

Permalink
change list selection state to include mainSelection
Browse files Browse the repository at this point in the history
  • Loading branch information
PeterShershov committed Dec 19, 2024
1 parent 29a22d5 commit c69cf01
Show file tree
Hide file tree
Showing 9 changed files with 98 additions and 52 deletions.
5 changes: 3 additions & 2 deletions packages/components/src/hooks/use-keyboard-nav.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React from 'react';
import { childrenById, KeyCodes } from '../common/index.js';
import { ListSelection } from '../list/types.js';

export const getHandleKeyboardNav = (
elementsParent: React.RefObject<HTMLElement | null>,
focusedId: string | undefined,
setFocusedId: (id: string) => void,
setSelectedIds: (ids: string[]) => void,
setSelectedIds: (selectedData: ListSelection) => void,
) => {
const onKeyPress = (ev: React.KeyboardEvent) => {
if (
Expand Down Expand Up @@ -117,7 +118,7 @@ export const getHandleKeyboardNav = (
break;
case KeyCodes.Space:
case KeyCodes.Enter:
setSelectedIds([focusedId]);
setSelectedIds({ mainSelection: focusedId, ids: [focusedId] });
break;
default:
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useCallback, useEffect } from 'react';
import { KeyCodes } from '../common/index.js';
import { ProcessedControlledState } from './use-state-controls.js';
import { ListSelection } from '../list/types.js';

export type KeyboardSelectMeta = 'keyboard';
export interface TreeViewKeyboardInteractionsParams {
Expand All @@ -9,7 +10,7 @@ export interface TreeViewKeyboardInteractionsParams {
open: (itemId: string) => void;
close: (itemId: string) => void;
focus: (itemId: string) => void;
select: ProcessedControlledState<string[], KeyboardSelectMeta>[1];
select: ProcessedControlledState<ListSelection, KeyboardSelectMeta>[1];
isOpen: (itemId: string) => boolean;
isEndNode: (itemId: string) => boolean;
getPrevious: (itemId: string) => string | undefined;
Expand Down Expand Up @@ -62,7 +63,7 @@ export const useTreeViewKeyboardInteraction = ({
if (!itemId) return;

if (selectionFollowsFocus) {
select([itemId], 'keyboard');
select({ mainSelection: itemId, ids: [itemId] }, 'keyboard');
} else {
focus(itemId);
}
Expand All @@ -74,7 +75,7 @@ export const useTreeViewKeyboardInteraction = ({
if (!focusedItemId) {
return;
}
select([focusedItemId]);
select({ mainSelection: focusedItemId, ids: [focusedItemId] });
}, [focusedItemId, select]);

const handleArrowRight = useCallback(() => {
Expand Down Expand Up @@ -111,9 +112,9 @@ export const useTreeViewKeyboardInteraction = ({

if (event.shiftKey) {
if (!selectedIds.includes(previous)) {
select([...selectedIds, previous]);
select({ mainSelection: previous, ids: [...selectedIds, previous] });
} else {
select(selectedIds.filter((id) => id !== focusedItemId));
select({ mainSelection: focusedItemId, ids: selectedIds.filter((id) => id !== focusedItemId) });
}
}
}
Expand All @@ -130,9 +131,9 @@ export const useTreeViewKeyboardInteraction = ({

if (event.shiftKey) {
if (!selectedIds.includes(next)) {
select([...selectedIds, next]);
select({ mainSelection: next, ids: [...selectedIds, next] });
} else {
select(selectedIds.filter((id) => id !== focusedItemId));
select({ mainSelection: focusedItemId, ids: selectedIds.filter((id) => id !== focusedItemId) });
}
}
}
Expand Down
39 changes: 22 additions & 17 deletions packages/components/src/list/list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useIdListener } from '../hooks/use-id-based-event.js';
import { getHandleKeyboardNav } from '../hooks/use-keyboard-nav.js';
import { StateControls, useStateControls } from '../hooks/use-state-controls.js';
import type { UseTransmit } from '../hooks/use-transmitted-events.js';
import { ListSelection } from './types.js';

export type ListRootMinimalProps = Pick<
React.HTMLAttributes<HTMLDivElement> & React.RefAttributes<HTMLDivElement>,
Expand Down Expand Up @@ -42,7 +43,7 @@ export interface ListItemProps<T> {
isFocused: boolean;
isSelected: boolean;
focus: (id?: string) => void;
select: (ids: string[]) => void;
select: (params: ListSelection) => void;
}

export interface ListProps<T> {
Expand All @@ -51,7 +52,7 @@ export interface ListProps<T> {
items: T[];
ItemRenderer: React.ComponentType<ListItemProps<T>>;
focusControl?: StateControls<string | undefined>;
selectionControl?: StateControls<string[]>;
selectionControl?: StateControls<ListSelection>;
transmitKeyPress?: UseTransmit<React.KeyboardEventHandler>;
onItemMount?: (item: T) => void;
onItemUnmount?: (item: T) => void;
Expand All @@ -74,7 +75,7 @@ export function List<T, EL extends HTMLElement = HTMLDivElement>({
disableKeyboard,
enableMultiselect = true,
}: ListProps<T>): React.ReactElement {
const [selectedIds, setSelectedIds] = useStateControls(selectionControl, []);
const [selectedIds, setSelectedIds] = useStateControls(selectionControl, { ids: [] });
const [focusedId, setFocusedId] = useStateControls(focusControl, undefined);
const defaultRef = useRef<EL>(null);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
Expand All @@ -90,9 +91,9 @@ export function List<T, EL extends HTMLElement = HTMLDivElement>({
const rangeSelectionAnchorId = useRef<string | undefined>(undefined);

useEffect(() => {
if (selectedIds.length === 1) {
rangeSelectionAnchorId.current = selectedIds[0];
} else if (selectedIds.length === 0) {
if (selectedIds.ids.length === 1) {
rangeSelectionAnchorId.current = selectedIds.ids[0];
} else if (selectedIds.ids.length === 0) {
rangeSelectionAnchorId.current = undefined;
}
}, [selectedIds]);
Expand All @@ -117,7 +118,7 @@ export function List<T, EL extends HTMLElement = HTMLDivElement>({
data={item}
focus={setFocusedId}
isFocused={focusedId === id}
isSelected={selectedIds.includes(id)}
isSelected={selectedIds.ids.includes(id)}
select={setSelectedIds}
/>,
);
Expand All @@ -142,44 +143,48 @@ export function List<T, EL extends HTMLElement = HTMLDivElement>({
(id, ev: React.MouseEvent): void => {
// allowing to clear selection when providing an empty select ids array
if (!id) {
setSelectedIds([]);
setSelectedIds({ ids: [] });
setFocusedId(undefined);
return;
}

setFocusedId(id);

const isAlreadySelected = selectedIds.includes(id);
const isAlreadySelected = selectedIds.ids.includes(id);

if (!enableMultiselect) {
if (isAlreadySelected) {
return;
}

setSelectedIds([id]);
setSelectedIds({ mainSelection: id, ids: [id] });
return;
}

const isCtrlPressed = ev.ctrlKey || ev.metaKey;
const isShiftPressed = ev.shiftKey;

if (isCtrlPressed && isAlreadySelected) {
setSelectedIds(selectedIds.filter((selectedId) => selectedId !== id));
setSelectedIds({
mainSelection: id,
ids: selectedIds.ids.filter((selectedId) => selectedId !== id),
});
} else if (isCtrlPressed) {
setSelectedIds([...selectedIds, id]);
setSelectedIds({ mainSelection: id, ids: [...selectedIds.ids, id] });
} else if (isShiftPressed) {
setSelectedIds(
getRangeSelection({
setSelectedIds({
mainSelection: id,
ids: getRangeSelection({
items,
id,
indexMap,
selectedIds,
selectedIds: selectedIds.ids,
rangeSelectionAnchorId: rangeSelectionAnchorId.current,
getId,
}),
);
});
} else {
setSelectedIds([id]);
setSelectedIds({ mainSelection: id, ids: [id] });
}
},
[setFocusedId, selectedIds, enableMultiselect, setSelectedIds, items, indexMap, getId],
Expand Down
4 changes: 4 additions & 0 deletions packages/components/src/list/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface ListSelection {
mainSelection?: string;
ids: string[];
}
7 changes: 4 additions & 3 deletions packages/components/src/scroll-list/scroll-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
useScrollListScrollToFocused,
} from './hooks/index.js';
import { classes } from './scroll-list.st.css';
import { ListSelection } from '../list/types.js';

type ScrollListRootMinimalProps = Pick<
React.HTMLAttributes<HTMLElement> & React.RefAttributes<HTMLElement>,
Expand Down Expand Up @@ -169,14 +170,14 @@ export function ScrollList<T, EL extends HTMLElement = HTMLDivElement>({
});
const scrollWindowSize = useElementSize(scrollWindow, !isHorizontal);
const mountedItems = useRef(new Set(''));
const [selected, setSelected] = useStateControls(selectionControl, []);
const [selected, setSelected] = useStateControls(selectionControl, { ids: [] });
const [focused, setFocused] = useStateControls(focusControl, undefined);

const getItemInfo = useCallback(
(data: T): ScrollListItemInfo<T> => ({
data,
isFocused: focused === getId(data),
isSelected: selected.includes(getId(data)),
isSelected: selected.ids.includes(getId(data)),
}),
[getId, focused, selected],
);
Expand Down Expand Up @@ -283,7 +284,7 @@ export function ScrollList<T, EL extends HTMLElement = HTMLDivElement>({
() => [focused, setFocused],
[focused, setFocused],
);
const selectionControlMemoized: ProcessedControlledState<string[]> = useMemo(
const selectionControlMemoized: ProcessedControlledState<ListSelection> = useMemo(
() => [selected, setSelected],
[selected, setSelected],
);
Expand Down
65 changes: 47 additions & 18 deletions packages/components/src/tree/boards/tree-multi-selection.board.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createBoard } from '@wixc3/react-board';
import React, { useRef, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { Tree } from '../tree.js';
import { TreeItemData } from '../../board-assets/index.js';
import { TreeItemRenderer } from '../../board-assets/tree-items/tree-item-renderer.js';
Expand Down Expand Up @@ -45,23 +45,52 @@ export default createBoard({
Board: () => {
const openItemsControl = useState<string[]>([]);
const scrollRef = useRef<HTMLDivElement>(null);

const [keys, setKeys] = useState<string[]>([]);

useEffect(() => {
const keyDownHandler = (event: KeyboardEvent) => {
if (!keys.includes(event.key)) {
setKeys([...keys, event.key]);
}
};

const keyUpHandler = (event: KeyboardEvent) => {
setKeys(keys.filter((key) => key !== event.key));
};

document.addEventListener('keydown', keyDownHandler);
document.addEventListener('keyup', keyUpHandler);

return () => {
document.removeEventListener('keydown', keyDownHandler);
document.removeEventListener('keyup', keyUpHandler);
};
}, [keys]);

return (
<Tree<typeof data>
data={data}
getId={(it) => it.id}
ItemRenderer={TreeItemRenderer}
getChildren={(it) => it.children || []}
openItemsControls={openItemsControl}
overlay={{ el: () => null, props: {} }}
listRoot={{
props: {
ref: scrollRef,
id: 'LIST',
style: { outline: 'none', width: '12rem' },
},
}}
eventRoots={[scrollRef]}
/>
<div>
<div>
<span>Key pressed: {keys.join(', ')}</span>
</div>

<Tree<typeof data>
data={data}
getId={(it) => it.id}
ItemRenderer={TreeItemRenderer}
getChildren={(it) => it.children || []}
openItemsControls={openItemsControl}
overlay={{ el: () => null, props: {} }}
listRoot={{
props: {
ref: scrollRef,
id: 'LIST',
style: { outline: 'none', width: '12rem' },
},
}}
eventRoots={[scrollRef]}
/>
</div>
);
},
plugins: [
Expand Down Expand Up @@ -143,6 +172,6 @@ export default createBoard({
],
environmentProps: {
windowWidth: 600,
windowHeight: 400,
windowHeight: 559,
},
});
8 changes: 6 additions & 2 deletions packages/components/src/tree/boards/tree-with-lanes.board.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from '../../board-assets/tree-items/tree-item-with-lane-renderer.js';
import { createTreeOverlay } from '../utils.js';
import { Tree } from '../tree.js';
import { ListSelection } from '../../list/types.js';

let idCounter = 0;
const nextId = () => 'id' + idCounter++;
Expand Down Expand Up @@ -171,7 +172,7 @@ const treeOverlay = createTreeOverlay(OverlayRenderer, {});
export default createBoard({
name: 'Tree with lanes',
Board: () => {
const [selection, updateSelection] = useState<string[]>([]);
const [selection, updateSelection] = useState<ListSelection>({ ids: [] });
const [openItems, updateOpen] = useState<string[]>(allIds);

return (
Expand All @@ -181,7 +182,10 @@ export default createBoard({
getIndent,
getParents,
selectItem: (item) => {
updateSelection([item.id]);
updateSelection({
mainSelection: item.id,
ids: [item.id],
});
},
}),
[],
Expand Down
4 changes: 2 additions & 2 deletions packages/components/src/tree/tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export function Tree<T, EL extends HTMLElement = HTMLElement>(props: TreeProps<T
} = props;
const [openItemIds, setOpenItemIds] = useStateControls(openItemsControls, []);
const [focusedItemId, focus] = useStateControls(focusControl, undefined);
const [selectedIds, select] = useStateControls(scrollListProps.selectionControl, []);
const [selectedIds, select] = useStateControls(scrollListProps.selectionControl, { ids: [] });

const { items, treeItemDepths } = useMemo(
() => getItems({ item: data, getChildren, getId, openItemIds }),
Expand Down Expand Up @@ -170,7 +170,7 @@ export function Tree<T, EL extends HTMLElement = HTMLElement>(props: TreeProps<T
select,
selectionFollowsFocus,
endNodeExpandSelectsNext,
selectedIds,
selectedIds: selectedIds.ids,
});

const overlay = forwardListOverlay(props.overlay, {
Expand Down
3 changes: 2 additions & 1 deletion packages/components/src/tree/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
import type { ListItemProps } from '../list/list.js';
import type { OverlayProps, ScrollListItemInfo, ScrollListProps } from '../scroll-list/scroll-list.js';
import type { overlayRoot } from './utils.js';
import { ListSelection } from '../list/types.js';

export interface TreeItemInfo<T> extends ScrollListItemInfo<T> {
isOpen: boolean;
Expand Down Expand Up @@ -35,8 +36,8 @@ export interface TreeAddedProps<T> {
openItemsControls: StateControls<string[]>;
eventRoots?: TreeViewKeyboardInteractionsParams['eventRoots'];
ItemRenderer: React.ComponentType<TreeItemProps<T>>;
selectionControl?: StateControls<ListSelection, KeyboardSelectMeta | undefined>;
overlay?: typeof overlayRoot;
selectionControl?: StateControls<string[], KeyboardSelectMeta | undefined>;
}

export type TreeProps<T, EL extends HTMLElement> = Omit<
Expand Down

0 comments on commit c69cf01

Please sign in to comment.