Skip to content

Commit

Permalink
Add BulkController support for shift+click behaviour (#10861)
Browse files Browse the repository at this point in the history
  • Loading branch information
lb- committed Oct 19, 2023
1 parent 68c4183 commit 74aada0
Show file tree
Hide file tree
Showing 4 changed files with 273 additions and 23 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.txt
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ Changelog
* Support extending Wagtail client-side with Stimulus (LB (Ben) Johnston)
* Update all `FieldPanel('title')` examples to use the recommended `TitleFieldPanel('title')` panel (Chinedu Ihedioha)
* The `purge_revisions` management command now respects revisions that have a `on_delete=PROTECT` foreign key relation and won't delete them (Neeraj P Yetheendran, Meghana Reddy, Sage Abdullah, Storm Heg)
* Add support for Shift + Click behaviour in form submissions and simple tranlations submissions (LB (Ben) Johnston)
* Fix: Ensure that StreamField's `FieldBlock`s correctly set the `required` and `aria-describedby` attributes (Storm Heg)
* Fix: Avoid an error when the moderation panel (admin dashboard) contains both snippets and private pages (Matt Westcott)
* Fix: When deleting collections, ensure the collection name is correctly shown in the success message (LB (Ben) Johnston)
Expand Down
173 changes: 164 additions & 9 deletions client/src/controllers/BulkController.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { Application } from '@hotwired/stimulus';
import { BulkController } from './BulkController';

describe('BulkController', () => {
beforeEach(() => {
document.body.innerHTML = `
<div id="bulk-container" data-controller="w-bulk">
const setup = async (
html = `
<div id="bulk-container" data-controller="w-bulk" data-action="custom:event@document->w-bulk#toggleAll">
<input id="select-all" type="checkbox" data-w-bulk-target="all" data-action="w-bulk#toggleAll">
<div id="checkboxes">
<input type="checkbox" data-w-bulk-target="item" disabled data-action="w-bulk#toggle">
Expand All @@ -13,12 +13,17 @@ describe('BulkController', () => {
</div>
<button id="clear" data-action="w-bulk#toggleAll" data-w-bulk-force-param="false">Clear all</button>
<button id="set" data-action="w-bulk#toggleAll" data-w-bulk-force-param="true">Select all</button>
</div>`;
</div>`,
) => {
document.body.innerHTML = `<main>${html}</main>`;

const application = Application.start();
application.register('w-bulk', BulkController);
});
};

it('selects all checkboxes when the select all checkbox is clicked', async () => {
await setup();

it('selects all checkboxes when the select all checkbox is clicked', () => {
const allCheckbox = document.getElementById('select-all');

allCheckbox.click();
Expand All @@ -38,7 +43,9 @@ describe('BulkController', () => {
).toEqual(0);
});

it('should keep the select all checkbox in sync when individual checkboxes are all ticked', () => {
it('should keep the select all checkbox in sync when individual checkboxes are all ticked', async () => {
await setup();

const allCheckbox = document.getElementById('select-all');
expect(allCheckbox.checked).toBe(false);

Expand All @@ -63,7 +70,9 @@ describe('BulkController', () => {
expect(allCheckbox.checked).toBe(false);
});

it('executes the correct action when the Clear all button is clicked', () => {
it('executes the correct action when the Clear all button is clicked', async () => {
await setup();

const allCheckbox = document.getElementById('select-all');
const clearAllButton = document.getElementById('clear');
expect(allCheckbox.checked).toBe(false);
Expand All @@ -88,7 +97,9 @@ describe('BulkController', () => {
expect(allCheckbox.checked).toBe(false);
});

it('executes the correct action when the Set all button is clicked', () => {
it('executes the correct action when the Set all button is clicked', async () => {
await setup();

const allCheckbox = document.getElementById('select-all');
const setAllButton = document.getElementById('set');

Expand All @@ -115,7 +126,50 @@ describe('BulkController', () => {
});
});

it('should support using another method (e.g. CustomEvent) to toggle all', async () => {
await setup();

const allCheckbox = document.getElementById('select-all');
expect(allCheckbox.checked).toBe(false);

document.dispatchEvent(new CustomEvent('custom:event'));

expect(allCheckbox.checked).toBe(true);
expect(document.querySelectorAll(':checked')).toHaveLength(3);

// calling again, should switch the toggles back

document.dispatchEvent(new CustomEvent('custom:event'));

expect(allCheckbox.checked).toBe(false);
expect(document.querySelectorAll(':checked')).toHaveLength(0);
});

it('should support a force value in a CustomEvent to override the select all checkbox', async () => {
await setup();

const allCheckbox = document.getElementById('select-all');
expect(allCheckbox.checked).toBe(false);

document.dispatchEvent(
new CustomEvent('custom:event', { detail: { force: true } }),
);

expect(allCheckbox.checked).toBe(true);
expect(document.querySelectorAll(':checked')).toHaveLength(3);

// calling again, should not change the state of the checkboxes
document.dispatchEvent(
new CustomEvent('custom:event', { detail: { force: true } }),
);

expect(allCheckbox.checked).toBe(true);
expect(document.querySelectorAll(':checked')).toHaveLength(3);
});

it('should allow for action targets to have classes toggled when any checkboxes are clicked', async () => {
await setup();

const container = document.getElementById('bulk-container');

// create innerActions container that will be conditionally hidden with test classes
Expand Down Expand Up @@ -151,4 +205,105 @@ describe('BulkController', () => {

expect(innerActionsElement.className).toEqual('keep-me hidden w-invisible');
});

it('should support shift+click to select a range of checkboxes', async () => {
await setup(`
<div id="bulk-container-multi" data-controller="w-bulk">
<input id="select-all-multi" type="checkbox" data-w-bulk-target="all" data-action="w-bulk#toggleAll">
<div id="checkboxes">
<input id="c0" type="checkbox" data-w-bulk-target="item" data-action="w-bulk#toggle">
<input id="c1" type="checkbox" data-w-bulk-target="item" data-action="w-bulk#toggle">
<input id="cx" type="checkbox" data-w-bulk-target="item" data-action="w-bulk#toggle" disabled>
<input id="c2" type="checkbox" data-w-bulk-target="item" data-action="w-bulk#toggle">
<input id="c3" type="checkbox" data-w-bulk-target="item" data-action="w-bulk#toggle">
<input id="c4" type="checkbox" data-w-bulk-target="item" data-action="w-bulk#toggle">
<input id="c5" type="checkbox" data-w-bulk-target="item" data-action="w-bulk#toggle">
</div>
</div>`);

const getClickedIds = () =>
Array.from(document.querySelectorAll(':checked')).map(({ id }) => id);

const shiftClick = async (element) => {
document.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'Shift',
shiftKey: true,
}),
);
element.click();
document.dispatchEvent(
new KeyboardEvent('keyup', {
key: 'Shift',
shiftKey: true,
}),
);
await Promise.resolve();
};

// initial shift usage should have no impact
await shiftClick(document.getElementById('c0'));
expect(getClickedIds()).toHaveLength(1);

// shift click should select all checkboxes between the first and last clicked
await shiftClick(document.getElementById('c2'));
expect(getClickedIds()).toEqual(['c0', 'c1', 'c2']);

await shiftClick(document.getElementById('c5'));
expect(getClickedIds()).toEqual([
'select-all-multi',
'c0',
'c1',
'c2',
'c3',
'c4',
'c5',
]);

// it should allow reverse clicks
document.getElementById('c4').click(); // un-click
expect(getClickedIds()).toEqual(['c0', 'c1', 'c2', 'c3', 'c5']);

// now shift click in reverse, un-clicking those between the last (4) and the new click (1)
await shiftClick(document.getElementById('c1'));
expect(getClickedIds()).toEqual(['c0', 'c5']);

// reset the clicks, then using shift click should do nothing
document.getElementById('select-all-multi').click();
document.getElementById('select-all-multi').click();
expect(getClickedIds()).toHaveLength(0);

await shiftClick(document.getElementById('c4'));
expect(getClickedIds()).toEqual(['c4']);

// finally, do a shift click to the first checkbox, check the select all works after a final click
await shiftClick(document.getElementById('c0'));
expect(getClickedIds()).toEqual(['c0', 'c1', 'c2', 'c3', 'c4']);

document.getElementById('c5').click();

expect(getClickedIds()).toEqual([
'select-all-multi',
'c0',
'c1',
'c2',
'c3',
'c4',
'c5',
]);

// now ensure that it still works if some element gets changed (not disabled)
document.getElementById('cx').removeAttribute('disabled');
document.getElementById('select-all-multi').click();
expect(getClickedIds()).toHaveLength(0);

await Promise.resolve();

document.getElementById('c3').click(); // click

await shiftClick(document.getElementById('c1'));

// it should include the previously disabled element, tracking against the DOM, not indexes
expect(getClickedIds()).toEqual(['c1', 'cx', 'c2', 'c3']);
});
});

0 comments on commit 74aada0

Please sign in to comment.