Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(list): support multi-selection #1080

Merged
merged 25 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
7e20c88
refactor: change selection type to `string[]` and adjust usages (init…
TheAlmightyCrumb Dec 8, 2024
456704a
replace faulty `[-1]` notation
TheAlmightyCrumb Dec 8, 2024
2e3feb6
restore auto-complete behavior
TheAlmightyCrumb Dec 8, 2024
1953eda
refactor: change scroll hook to focused item
TheAlmightyCrumb Dec 9, 2024
a91c7a3
wip: add `enableMultiselect` boolean to select/deselect using Ctrl
TheAlmightyCrumb Dec 9, 2024
63380ce
feat: support multiselect and deselection in on-click listener
TheAlmightyCrumb Dec 10, 2024
9c0aeb3
feat: support multiselect and deselection in on-click listener
TheAlmightyCrumb Dec 10, 2024
f24b57d
fix: adjust focused item behavior
TheAlmightyCrumb Dec 10, 2024
a40b63e
Merge branch 'master' into sagivd/list-multiselect
TheAlmightyCrumb Dec 10, 2024
025ca19
fix: adjust tests following changes to selection and focus
TheAlmightyCrumb Dec 11, 2024
64ffeee
remove comment
TheAlmightyCrumb Dec 11, 2024
6e2deba
base
PeterShershov Dec 15, 2024
1c57a55
Merge branch 'sagivd/list-multiselect' into peter/shift
PeterShershov Dec 15, 2024
76b421c
Merge branch 'master' into peter/shift
PeterShershov Dec 18, 2024
e309741
fix "enter" issue with range selection
PeterShershov Dec 18, 2024
75c2e47
passing tests
PeterShershov Dec 18, 2024
91f7ed5
new tests
PeterShershov Dec 18, 2024
29a22d5
refactor list component to improve range selection logic
PeterShershov Dec 18, 2024
c69cf01
change list selection state to include `mainSelection`
PeterShershov Dec 19, 2024
26adf4f
rename `mainSelection` to `lastSelectedId`
PeterShershov Dec 19, 2024
ba9d8ff
export list types to expose `ListSelection`
TheAlmightyCrumb Jan 9, 2025
a0a3fde
fix focus in keyboard interaction
TheAlmightyCrumb Jan 12, 2025
8e31d6e
fix expectElementsStyle and multi-select tree board
PeterShershov Jan 13, 2025
930776a
Merge branch 'master' into peter/shift
PeterShershov Jan 13, 2025
7d85fa5
change tree-focus board
TheAlmightyCrumb Jan 13, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading