Skip to content

Commit

Permalink
history: adding multi-select
Browse files Browse the repository at this point in the history
  • Loading branch information
shakyShane committed Feb 13, 2025
1 parent 8a043e3 commit f2e558c
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 13 deletions.
4 changes: 2 additions & 2 deletions special-pages/pages/history/app/components/Results.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Empty } from './Empty.js';
/**
* @param {object} props
* @param {import("@preact/signals").Signal<import("../global-state/GlobalStateProvider.js").Results>} props.results
* @param {import("@preact/signals").Signal<string[]>} props.selected
* @param {import("@preact/signals").Signal<number[]>} props.selected
*/
export function Results({ results, selected }) {
if (results.value.items.length === 0) {
Expand All @@ -23,7 +23,7 @@ export function Results({ results, selected }) {
heights={results.value.heights}
overscan={OVERSCAN_AMOUNT}
renderItem={({ item, cssClassName, style, index }) => {
const isSelected = selected.value.includes(item.id);
const isSelected = selected.value.includes(index);
return (
<li key={item.id} data-id={item.id} class={cssClassName} style={style}>
<Item
Expand Down
109 changes: 98 additions & 11 deletions special-pages/pages/history/app/global-state/SelectionProvider.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
import { h, createContext } from 'preact';
import { useContext } from 'preact/hooks';
import { signal, useSignal, useSignalEffect } from '@preact/signals';
import { useQueryContext } from './QueryProvider.js';
import { eventToIntention } from '../../../../shared/handlers.js';
import { usePlatformName } from '../types.js';
import { useGlobalState } from './GlobalStateProvider.js';

const SelectionContext = createContext({
selected: signal(/** @type {string[]} */ ([])),
});
/**
* @typedef SelectionState
* @property {import("@preact/signals").Signal<number[]>} selected
*/

/**
* @typedef {(s: (d: number[]) => number[]) => void} UpdateSelected
*/

const SelectionContext = createContext(
/** @type {SelectionState} */ ({
selected: signal(/** @type {number[]} */ ([])),
}),
);

/**
* Provides a context for the selections
Expand All @@ -13,24 +28,96 @@ const SelectionContext = createContext({
* @param {import("preact").ComponentChild} props.children - The child components that will consume the history service context.
*/
export function SelectionProvider({ children }) {
const selected = useSignal(/** @type {string[]} */ ([]));
const selected = useSignal(/** @type {number[]} */ ([]));
/** @type {UpdateSelected} */
const update = (fn) => {
selected.value = fn(selected.value);
console.log(selected.value);
};

useResetOnQueryChange(update);
useRowClick(update, selected);

return <SelectionContext.Provider value={{ selected }}>{children}</SelectionContext.Provider>;
}

/**
* @param {UpdateSelected} update
*/
function useResetOnQueryChange(update) {
const query = useQueryContext();
useSignalEffect(() => {
const unsubs = [
// when anything about the query changes, reset selections
query.subscribe(() => {
update((prev) => []);
}),
];

return () => {
for (const unsub of unsubs) {
unsub();
}
};
});
}

/**
* @param {UpdateSelected} update
* @param {import("@preact/signals").Signal<number[]>} selected
*/
function useRowClick(update, selected) {
const platformName = usePlatformName();
const { results } = useGlobalState();

Check failure on line 71 in special-pages/pages/history/app/global-state/SelectionProvider.js

View workflow job for this annotation

GitHub Actions / unit (ubuntu-latest)

'results' is assigned a value but never used
const lastSelected = useSignal(/** @type {{index: number; id: string}|null} */ (null));
useSignalEffect(() => {
function handler(/** @type {MouseEvent} */ event) {
if (!(event.target instanceof Element)) return;
if (event.target.matches('a')) return;
const itemRow = /** @type {HTMLElement|null} */ (event.target.closest('[data-history-entry][data-index]'));
const selection = toRowSelection(itemRow);
if (selection) {
event.preventDefault();
event.stopImmediatePropagation();
if (!itemRow || !selection) return;

// MVP for getting the tests to pass. Next PRs will expand functionality
selected.value = [selection.id];
event.preventDefault();
event.stopImmediatePropagation();

const intention = eventToIntention(event, platformName);
const currentSelected = itemRow.getAttribute('aria-selected') === 'true';

switch (intention) {
case 'click': {
// MVP for getting the tests to pass. Next PRs will expand functionality
update((prev) => [selection.index]);
lastSelected.value = selection;
break;
}
case 'ctrl+click': {
update((prev) => {
const index = prev.indexOf(selection.index);
if (index > -1) {
const next = prev.slice();
next.splice(index, 1);
return next;
}
return prev.concat(selection.index);
});
if (!currentSelected) {
lastSelected.value = selection;
} else {
lastSelected.value = null;
}
break;
}
case 'shift+click': {
// todo
break;
}
}
}
document.addEventListener('click', handler);
return () => {
document.removeEventListener('click', handler);
};
});
return <SelectionContext.Provider value={{ selected }}>{children}</SelectionContext.Provider>;
}

// Hook for consuming the context
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,31 @@ test.describe('history selections', () => {
await hp.selectsRow(1);
await hp.selectsRow(2);
});
test('resets selection with new query', async ({ page }, workerInfo) => {
const hp = HistoryTestPage.create(page, workerInfo).withEntries(2000);
await hp.openPage({});
await hp.selectsRow(0);
await hp.types('example.com');
await hp.rowIsNotSelected(0);
});
test('adds to selection', async ({ page }, workerInfo) => {
const hp = HistoryTestPage.create(page, workerInfo).withEntries(2000);
await hp.openPage({});

await hp.selectsRow(0);
await hp.selectsRowWithCtrl(1);

await hp.rowIsSelected(0);
await hp.rowIsSelected(1);
});
test('removes from a selection', async ({ page }, workerInfo) => {
const hp = HistoryTestPage.create(page, workerInfo).withEntries(2000);
await hp.openPage({});

await hp.selectsRow(0);
await hp.selectsRowWithCtrl(1);
await hp.selectsRowWithCtrl(1);
await hp.rowIsSelected(0);
await hp.rowIsNotSelected(1);
});
});
27 changes: 27 additions & 0 deletions special-pages/pages/history/integration-tests/history.page.js
Original file line number Diff line number Diff line change
Expand Up @@ -343,4 +343,31 @@ export class HistoryTestPage {
await expect(rows.nth(nth)).toHaveAttribute('aria-selected', 'true');
await expect(selected).toHaveCount(1);
}

/**
* @param {number} nth
*/
async rowIsSelected(nth) {
const { page } = this;
const rows = page.locator('main').locator('[aria-selected]');
await expect(rows.nth(nth)).toHaveAttribute('aria-selected', 'true');

Check failure on line 353 in special-pages/pages/history/integration-tests/history.page.js

View workflow job for this annotation

GitHub Actions / integration

[integration] › pages/history/integration-tests/history-selections.spec.js:19:5 › history selections › adds to selection

1) [integration] › pages/history/integration-tests/history-selections.spec.js:19:5 › history selections › adds to selection Error: Timed out 5000ms waiting for expect(locator).toHaveAttribute(expected) Locator: locator('main').locator('[aria-selected]').first() Expected string: "true" Received string: "false" Call log: - expect.toHaveAttribute with timeout 5000ms - waiting for locator('main').locator('[aria-selected]').first() 9 × locator resolved to <div data-index="0" aria-selected="false" class="Item_row Item_hover" data-history-entry="history-id-00">…</div> - unexpected value "false" at pages/history/integration-tests/history.page.js:353 351 | const { page } = this; 352 | const rows = page.locator('main').locator('[aria-selected]'); > 353 | await expect(rows.nth(nth)).toHaveAttribute('aria-selected', 'true'); | ^ 354 | } 355 | 356 | /** at HistoryTestPage.rowIsSelected (/home/runner/work/content-scope-scripts/content-scope-scripts/special-pages/pages/history/integration-tests/history.page.js:353:37) at /home/runner/work/content-scope-scripts/content-scope-scripts/special-pages/pages/history/integration-tests/history-selections.spec.js:26:18

Check failure on line 353 in special-pages/pages/history/integration-tests/history.page.js

View workflow job for this annotation

GitHub Actions / integration

[integration] › pages/history/integration-tests/history-selections.spec.js:19:5 › history selections › adds to selection

1) [integration] › pages/history/integration-tests/history-selections.spec.js:19:5 › history selections › adds to selection Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: Timed out 5000ms waiting for expect(locator).toHaveAttribute(expected) Locator: locator('main').locator('[aria-selected]').first() Expected string: "true" Received string: "false" Call log: - expect.toHaveAttribute with timeout 5000ms - waiting for locator('main').locator('[aria-selected]').first() 9 × locator resolved to <div data-index="0" aria-selected="false" class="Item_row Item_hover" data-history-entry="history-id-00">…</div> - unexpected value "false" at pages/history/integration-tests/history.page.js:353 351 | const { page } = this; 352 | const rows = page.locator('main').locator('[aria-selected]'); > 353 | await expect(rows.nth(nth)).toHaveAttribute('aria-selected', 'true'); | ^ 354 | } 355 | 356 | /** at HistoryTestPage.rowIsSelected (/home/runner/work/content-scope-scripts/content-scope-scripts/special-pages/pages/history/integration-tests/history.page.js:353:37) at /home/runner/work/content-scope-scripts/content-scope-scripts/special-pages/pages/history/integration-tests/history-selections.spec.js:26:18

Check failure on line 353 in special-pages/pages/history/integration-tests/history.page.js

View workflow job for this annotation

GitHub Actions / integration

[integration] › pages/history/integration-tests/history-selections.spec.js:19:5 › history selections › adds to selection

1) [integration] › pages/history/integration-tests/history-selections.spec.js:19:5 › history selections › adds to selection Retry #2 ─────────────────────────────────────────────────────────────────────────────────────── Error: Timed out 5000ms waiting for expect(locator).toHaveAttribute(expected) Locator: locator('main').locator('[aria-selected]').first() Expected string: "true" Received string: "false" Call log: - expect.toHaveAttribute with timeout 5000ms - waiting for locator('main').locator('[aria-selected]').first() 9 × locator resolved to <div data-index="0" aria-selected="false" class="Item_row Item_hover" data-history-entry="history-id-00">…</div> - unexpected value "false" at pages/history/integration-tests/history.page.js:353 351 | const { page } = this; 352 | const rows = page.locator('main').locator('[aria-selected]'); > 353 | await expect(rows.nth(nth)).toHaveAttribute('aria-selected', 'true'); | ^ 354 | } 355 | 356 | /** at HistoryTestPage.rowIsSelected (/home/runner/work/content-scope-scripts/content-scope-scripts/special-pages/pages/history/integration-tests/history.page.js:353:37) at /home/runner/work/content-scope-scripts/content-scope-scripts/special-pages/pages/history/integration-tests/history-selections.spec.js:26:18

Check failure on line 353 in special-pages/pages/history/integration-tests/history.page.js

View workflow job for this annotation

GitHub Actions / integration

[integration] › pages/history/integration-tests/history-selections.spec.js:29:5 › history selections › removes from a selection

2) [integration] › pages/history/integration-tests/history-selections.spec.js:29:5 › history selections › removes from a selection Error: Timed out 5000ms waiting for expect(locator).toHaveAttribute(expected) Locator: locator('main').locator('[aria-selected]').first() Expected string: "true" Received string: "false" Call log: - expect.toHaveAttribute with timeout 5000ms - waiting for locator('main').locator('[aria-selected]').first() 9 × locator resolved to <div data-index="0" aria-selected="false" class="Item_row Item_hover" data-history-entry="history-id-00">…</div> - unexpected value "false" at pages/history/integration-tests/history.page.js:353 351 | const { page } = this; 352 | const rows = page.locator('main').locator('[aria-selected]'); > 353 | await expect(rows.nth(nth)).toHaveAttribute('aria-selected', 'true'); | ^ 354 | } 355 | 356 | /** at HistoryTestPage.rowIsSelected (/home/runner/work/content-scope-scripts/content-scope-scripts/special-pages/pages/history/integration-tests/history.page.js:353:37) at /home/runner/work/content-scope-scripts/content-scope-scripts/special-pages/pages/history/integration-tests/history-selections.spec.js:36:18

Check failure on line 353 in special-pages/pages/history/integration-tests/history.page.js

View workflow job for this annotation

GitHub Actions / integration

[integration] › pages/history/integration-tests/history-selections.spec.js:29:5 › history selections › removes from a selection

2) [integration] › pages/history/integration-tests/history-selections.spec.js:29:5 › history selections › removes from a selection Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: Timed out 5000ms waiting for expect(locator).toHaveAttribute(expected) Locator: locator('main').locator('[aria-selected]').first() Expected string: "true" Received string: "false" Call log: - expect.toHaveAttribute with timeout 5000ms - waiting for locator('main').locator('[aria-selected]').first() 9 × locator resolved to <div data-index="0" aria-selected="false" class="Item_row Item_hover" data-history-entry="history-id-00">…</div> - unexpected value "false" at pages/history/integration-tests/history.page.js:353 351 | const { page } = this; 352 | const rows = page.locator('main').locator('[aria-selected]'); > 353 | await expect(rows.nth(nth)).toHaveAttribute('aria-selected', 'true'); | ^ 354 | } 355 | 356 | /** at HistoryTestPage.rowIsSelected (/home/runner/work/content-scope-scripts/content-scope-scripts/special-pages/pages/history/integration-tests/history.page.js:353:37) at /home/runner/work/content-scope-scripts/content-scope-scripts/special-pages/pages/history/integration-tests/history-selections.spec.js:36:18

Check failure on line 353 in special-pages/pages/history/integration-tests/history.page.js

View workflow job for this annotation

GitHub Actions / integration

[integration] › pages/history/integration-tests/history-selections.spec.js:29:5 › history selections › removes from a selection

2) [integration] › pages/history/integration-tests/history-selections.spec.js:29:5 › history selections › removes from a selection Retry #2 ─────────────────────────────────────────────────────────────────────────────────────── Error: Timed out 5000ms waiting for expect(locator).toHaveAttribute(expected) Locator: locator('main').locator('[aria-selected]').first() Expected string: "true" Received string: "false" Call log: - expect.toHaveAttribute with timeout 5000ms - waiting for locator('main').locator('[aria-selected]').first() 9 × locator resolved to <div data-index="0" aria-selected="false" class="Item_row Item_hover" data-history-entry="history-id-00">…</div> - unexpected value "false" at pages/history/integration-tests/history.page.js:353 351 | const { page } = this; 352 | const rows = page.locator('main').locator('[aria-selected]'); > 353 | await expect(rows.nth(nth)).toHaveAttribute('aria-selected', 'true'); | ^ 354 | } 355 | 356 | /** at HistoryTestPage.rowIsSelected (/home/runner/work/content-scope-scripts/content-scope-scripts/special-pages/pages/history/integration-tests/history.page.js:353:37) at /home/runner/work/content-scope-scripts/content-scope-scripts/special-pages/pages/history/integration-tests/history-selections.spec.js:36:18
}

/**
* @param {number} nth
*/
async rowIsNotSelected(nth) {
const { page } = this;
const rows = page.locator('main').locator('[aria-selected]');
await expect(rows.nth(nth)).toHaveAttribute('aria-selected', 'false');
}

/**
* @param {number} nth
*/
async selectsRowWithCtrl(nth) {
const { page } = this;
const rows = page.locator('main').locator('[aria-selected]');
await rows.nth(nth).click({ modifiers: ['ControlOrMeta'] });
}
}
15 changes: 15 additions & 0 deletions special-pages/shared/handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,18 @@ export function eventToTarget(event, platformName) {
}
return 'same-tab';
}

/**
* @param {MouseEvent} event
* @param {ImportMeta['platform']} platformName
* @return {'ctrl+click' | 'shift+click' | 'click'}
*/
export function eventToIntention(event, platformName) {
const isControlClick = platformName === 'macos' ? event.metaKey : event.ctrlKey;
if (isControlClick) {
return 'ctrl+click';
} else if (event.shiftKey) {
return 'shift+click';
}
return 'click';
}

0 comments on commit f2e558c

Please sign in to comment.