Skip to content

Commit

Permalink
feat(list): support multi-selection (#1080)
Browse files Browse the repository at this point in the history
Co-authored-by: Sagiv Dayan <[email protected]>
  • Loading branch information
PeterShershov and TheAlmightyCrumb authored Jan 13, 2025
1 parent 5670795 commit 3ce9a7f
Show file tree
Hide file tree
Showing 22 changed files with 514 additions and 143 deletions.
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@
"packages/*"
],
"scripts": {
"clean": "rimraf -g ./packages/*/dist",
"build": "npm run build:boards && npm run build:typescript && npm run build:stylable",
"build:typescript": "tsc --build",
"build:stylable": "stc",
"build:boards": "node ./build-board-index.js",
"build:stylable": "stc",
"build:typescript": "tsc --build",
"clean": "rimraf -g ./packages/*/dist",
"lint": "eslint",
"pretest": "npm run lint && npm run build",
"prettify": "prettier . --write",
"test": "npm run test:spec",
"test:spec": "mocha-web \"packages/*/dist/test/**/*.spec.js\"",
"prettify": "prettier . --write"
"test:spec": "mocha-web \"packages/*/dist/test/**/*.spec.js\""
},
"devDependencies": {
"@playwright/browser-chromium": "^1.49.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/components/src/board-assets/items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@ export const createItems = (number = 1000, startingId = 0) =>
({
id: 'a' + (id + startingId),
title: 'item number ' + (id + startingId),
} as ItemData)
}) as ItemData,
);
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,32 @@
-st-states: focused, selected, open;
--indent: 0;
display: flex;
justify-content: space-between;
margin-left: calc(var(--indent) * 8px);
padding: 8px;
padding-left: calc(var(--indent) * 8px);
height: 24px;
user-select: none;
outline: none;
cursor: pointer;
border-radius: 12px;
align-items: center;
margin: 4px 0px;
transition: background .1s;
}

.root:hover {
.root:hover:not(:selected) {
color: purple;
background: gainsboro;
}

.root:focused {
color: blue
color: royalblue;

&:not(:selected) {
background: gainsboro;
}
}

.root:selected {
text-decoration: underline;
background: powderblue;
}

.root:open .chevron {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const TreeItemRenderer: React.FC<TreeItemProps<TreeItemData>> = (props) =
props.open();
}
}}
></ChevronRightWixUiIcon>
/>
) : null}

<SearchableText className={classes.text} text={props.data.title} />
Expand Down
2 changes: 2 additions & 0 deletions packages/components/src/board-index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import searchable_text from './searchable-text/boards/searchable-text.board.js';
import tree from './tree/boards/tree.board.js';
import tree_focus from './tree/boards/tree-focus.board.js';
import tree_keyboard from './tree/boards/tree-keyboard.board.js';
import tree_multi_selection from './tree/boards/tree-multi-selection.board.js';
import tree_with_lanes from './tree/boards/tree-with-lanes.board.js';
import use_element_size from './_codux/boards/hooks/use-element-size/use-element-size.board.js';
import use_scroll_horizontal_window from './hooks/boards/use-scroll/use-scroll-horizontal-window.board.js';
Expand Down Expand Up @@ -66,6 +67,7 @@ export default [
tree,
tree_focus,
tree_keyboard,
tree_multi_selection,
tree_with_lanes,
use_element_size,
use_scroll_horizontal_window,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ export const hoverAction = (selector?: string, timeout = 2_000): Action => {
};
};

export const clickAction = (selector?: string, timeout = 2_000): Action => {
export const clickAction = (selector?: string, timeout = 2_000, eventData: MouseEventInit = {}): Action => {
return {
title: 'Click ' + (selector || 'window'),
execute: () => {
Expand All @@ -288,18 +288,21 @@ export const clickAction = (selector?: string, timeout = 2_000): Action => {
new MouseEvent('mousedown', {
bubbles: true,
relatedTarget: target,
...eventData,
}),
);
target.dispatchEvent(
new MouseEvent('click', {
bubbles: true,
relatedTarget: target,
...eventData,
}),
);
target.dispatchEvent(
new MouseEvent('mouseup', {
bubbles: true,
relatedTarget: target,
...eventData,
}),
);
}
Expand All @@ -309,7 +312,7 @@ export const clickAction = (selector?: string, timeout = 2_000): Action => {
};
};

export const keyDownAction = (selector: string, keyCode: string, which: number) => {
export const keyDownAction = (selector: string, keyCode: string, eventData: KeyboardEventInit = {}) => {
return {
title: `key down ${keyCode}`,
execute: () => {
Expand All @@ -323,7 +326,7 @@ export const keyDownAction = (selector: string, keyCode: string, which: number)
key: keyCode,
bubbles: true,
composed: true,
which: which,
...eventData,
}),
);
},
Expand Down Expand Up @@ -490,7 +493,18 @@ export const expectElementsStyle = (
title: title || 'expectElementsStyle ' + Object.keys(elements).join(', '),
execute() {
for (const [selector, styles] of Object.entries(elements)) {
expectElementStyle(selector, styles, title, timeout);
const exp = expectElement(
selector,
(el) => {
const style = window.getComputedStyle(el);
for (const [key, val] of Object.entries(styles)) {
expect(style[key as keyof CSSStyleDeclaration]).to.eql(val);
}
},
title,
);

return exp.execute();
}
},
timeout,
Expand Down
4 changes: 2 additions & 2 deletions packages/components/src/hooks/use-id-based-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import React, { useCallback } from 'react';
import { getElementWithId } from '../common/index.js';

export function useIdListener<EVType extends React.KeyboardEvent | React.MouseEvent>(
idSetter: (id: string | undefined, element?: Element) => void,
idSetter: (id: string | undefined, ev: EVType, element?: Element) => void,
): (ev: EVType) => any {
return useCallback(
(ev: EVType) => {
if (!ev.currentTarget || !ev.target) {
return;
}
const res = getElementWithId(ev.target as Element, ev.currentTarget as unknown as Element);
idSetter(res?.id || undefined, res?.element);
idSetter(res?.id || undefined, ev, res?.element);
},
[idSetter],
);
Expand Down
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,
setSelectedId: (id: 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:
setSelectedId(focusedId);
setSelectedIds({ lastSelectedId: focusedId, ids: [focusedId] });
break;
default:
}
Expand Down
67 changes: 51 additions & 16 deletions packages/components/src/hooks/use-tree-view-keyboard-interaction.ts
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 All @@ -18,6 +19,7 @@ export interface TreeViewKeyboardInteractionsParams {
getFirstChild: (itemId: string) => string | undefined;
getFirst: () => string | undefined;
getLast: () => string | undefined;
selectedIds: string[];
}

export interface KeyboardInteractionConfiguration {
Expand Down Expand Up @@ -54,15 +56,15 @@ export const useTreeViewKeyboardInteraction = ({
select,
endNodeExpandSelectsNext,
selectionFollowsFocus,
selectedIds,
}: TreeViewKeyboardInteractionsParams & KeyboardInteractionConfiguration) => {
const handleFocus = useCallback(
(itemId: string | undefined) => {
if (!itemId) return;

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

const handleArrowRight = useCallback(() => {
Expand All @@ -99,17 +101,50 @@ export const useTreeViewKeyboardInteraction = ({
}
}, [focusedItemId, getParent, isOpen, close, handleFocus]);

const handleArrowUp = useCallback(() => {
if (!focusedItemId) return;

handleFocus(getPrevious(focusedItemId));
}, [focusedItemId, getPrevious, handleFocus]);

const handleArrowDown = useCallback(() => {
if (!focusedItemId) return;
const handleArrowUp = useCallback(
(event: KeyboardEvent) => {
if (!focusedItemId) return;

const previous = getPrevious(focusedItemId);
if (previous) {
handleFocus(previous);

if (event.shiftKey) {
if (!selectedIds.includes(previous)) {
select({ lastSelectedId: previous, ids: [...selectedIds, previous] });
} else {
select({
lastSelectedId: focusedItemId,
ids: selectedIds.filter((id) => id !== focusedItemId),
});
}
}
}
},
[focusedItemId, getPrevious, handleFocus, select, selectedIds],
);

handleFocus(getNext(focusedItemId));
}, [focusedItemId, getNext, handleFocus]);
const handleArrowDown = useCallback(
(event: KeyboardEvent) => {
if (!focusedItemId) return;
const next = getNext(focusedItemId);
if (next) {
handleFocus(next);

if (event.shiftKey) {
if (!selectedIds.includes(next)) {
select({ lastSelectedId: next, ids: [...selectedIds, next] });
} else {
select({
lastSelectedId: focusedItemId,
ids: selectedIds.filter((id) => id !== focusedItemId),
});
}
}
}
},
[focusedItemId, getNext, handleFocus, select, selectedIds],
);

const handleHome = useCallback(() => handleFocus(getFirst()), [getFirst, handleFocus]);

Expand All @@ -131,7 +166,7 @@ export const useTreeViewKeyboardInteraction = ({

event.preventDefault();

handler();
handler(event);
},
[handleArrowRight, handleArrowLeft, handleArrowUp, handleArrowDown, handleHome, handleEnd, selectFocused],
);
Expand Down
1 change: 1 addition & 0 deletions packages/components/src/list/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './list.js';
export * from './types.js';
Loading

0 comments on commit 3ce9a7f

Please sign in to comment.