Skip to content

Commit

Permalink
feat: Add max-selection-count option to SelectionActionMixin. (#5196)
Browse files Browse the repository at this point in the history
  • Loading branch information
dbatiste authored Dec 4, 2024
1 parent e6f551c commit 8002ca3
Show file tree
Hide file tree
Showing 39 changed files with 166 additions and 57 deletions.
4 changes: 2 additions & 2 deletions components/selection/demo/selection.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ <h2>Multiple Selection</h2>
<template>
<d2l-demo-selection>
<d2l-selection-controls>
<d2l-selection-action text="Bookmark" icon="tier1:bookmark-hollow" requires-selection></d2l-selection-action>
<d2l-selection-action text="Bookmark" icon="tier1:bookmark-hollow" max-selection-count="2" requires-selection></d2l-selection-action>
<d2l-selection-action text="Settings" icon="tier1:gear"></d2l-selection-action>
<d2l-selection-action-dropdown text="Actions 1" requires-selection>
<d2l-dropdown-menu>
Expand Down Expand Up @@ -100,7 +100,7 @@ <h2>Selection controls with pageable + selection</h2>
<template>
<d2l-demo-selection-pageable item-count="5">
<d2l-selection-controls select-all-pages-allowed>
<d2l-selection-action text="Bookmark" icon="tier1:bookmark-hollow" requires-selection></d2l-selection-action>
<d2l-selection-action text="Bookmark" icon="tier1:bookmark-hollow" max-selection-count="4" requires-selection></d2l-selection-action>
<d2l-selection-action text="Settings" icon="tier1:gear"></d2l-selection-action>
</d2l-selection-controls>

Expand Down
9 changes: 5 additions & 4 deletions components/selection/selection-action-dropdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,14 @@ import { DropdownOpenerMixin } from '../dropdown/dropdown-opener-mixin.js';
import { dropdownOpenerStyles } from '../dropdown/dropdown-opener-styles.js';
import { FocusMixin } from '../../mixins/focus/focus-mixin.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { LocalizeCoreElement } from '../../helpers/localize-core-element.js';
import { SelectionActionMixin } from './selection-action-mixin.js';

/**
* A dropdown opener associated with a selection component.
* @slot - Dropdown content (e.g., "d2l-dropdown-content", "d2l-dropdown-menu" or "d2l-dropdown-tabs")
* @fires d2l-selection-observer-subscribe - Internal event
*/
class ActionDropdown extends FocusMixin(LocalizeCoreElement(SelectionActionMixin(DropdownOpenerMixin(LitElement)))) {
class ActionDropdown extends FocusMixin(SelectionActionMixin(DropdownOpenerMixin(LitElement))) {

static get properties() {
return {
Expand All @@ -33,11 +32,13 @@ class ActionDropdown extends FocusMixin(LocalizeCoreElement(SelectionActionMixin
}

render() {
const disabledTooltip = this._disabledTooltip || (this.disabled && this.disabledTooltip ? this.disabledTooltip : undefined);

return html`
<d2l-button-subtle
class="vdiff-target"
?disabled=${this.disabled}
disabled-tooltip="${ifDefined(this.disabled ? this.localize('components.selection.action-hint') : undefined)}"
?disabled="${this.disabled}"
disabled-tooltip="${ifDefined(disabledTooltip)}"
icon="tier1:chevron-down"
icon-right
text=${this.text}></d2l-button-subtle>
Expand Down
29 changes: 26 additions & 3 deletions components/selection/selection-action-mixin.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
import { formatNumber } from '@brightspace-ui/intl/lib/number.js';
import { LocalizeCoreElement } from '../../helpers/localize-core-element.js';
import { SelectionInfo } from './selection-mixin.js';
import { SelectionObserverMixin } from './selection-observer-mixin.js';

export const SelectionActionMixin = superclass => class extends SelectionObserverMixin(superclass) {
export const SelectionActionMixin = superclass => class extends LocalizeCoreElement(SelectionObserverMixin(superclass)) {

static get properties() {
return {
/**
* Disables bulk actions in the list or table controls once the selection limit is reached, but does not prevent further selection
* @type {number}
*/
maxSelectionCount: { type: Number, attribute: 'max-selection-count' },
/**
* Whether the action requires one or more selected items
* @type {boolean}
*/
requiresSelection: { type: Boolean, attribute: 'requires-selection', reflect: true }
requiresSelection: { type: Boolean, attribute: 'requires-selection', reflect: true },
_disabledTooltip: { state: true }
};
}

constructor() {
super();
this.maxSelectionCount = Infinity;
this.requiresSelection = false;
}

Expand All @@ -24,7 +33,21 @@ export const SelectionActionMixin = superclass => class extends SelectionObserve

set selectionInfo(value) {
super.selectionInfo = value;
this.disabled = (this.requiresSelection && this.selectionInfo.state === SelectionInfo.states.none);

// if these rules are not set, we let the consumer manage the disabled property if they want
if (!this.requiresSelection && this.maxSelectionCount === Infinity) return;

if (this.selectionInfo.keys.length > this.maxSelectionCount || (this.selectionInfo.state === SelectionInfo.states.allPages && this._provider?.itemCount > this.maxSelectionCount)) {
this.disabled = true;
this._disabledTooltip = this.localize('components.selection.action-max-hint', { count: this.maxSelectionCount, countFormatted: formatNumber(this.maxSelectionCount) });
} else if (this.requiresSelection && this.selectionInfo.state === SelectionInfo.states.none) {
this.disabled = true;
this._disabledTooltip = this.localize('components.selection.action-required-hint');
} else {
this.disabled = false;
this._disabledTooltip = undefined;
}

}

};
7 changes: 4 additions & 3 deletions components/selection/selection-action.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { css, html, LitElement } from 'lit';
import { ButtonMixin } from '../button/button-mixin.js';
import { FocusMixin } from '../../mixins/focus/focus-mixin.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { LocalizeCoreElement } from '../../helpers/localize-core-element.js';
import { SelectionActionMixin } from './selection-action-mixin.js';
import { SelectionInfo } from './selection-mixin.js';

Expand All @@ -12,7 +11,7 @@ import { SelectionInfo } from './selection-mixin.js';
* @fires d2l-selection-action-click - Dispatched when the user clicks the action button. The `SelectionInfo` is provided as the event `detail`. If `requires-selection` was specified then the event will only be dispatched if items are selected.
* @fires d2l-selection-observer-subscribe - Internal event
*/
class Action extends FocusMixin(LocalizeCoreElement(SelectionActionMixin(ButtonMixin(LitElement)))) {
class Action extends FocusMixin(SelectionActionMixin(ButtonMixin(LitElement))) {

static get properties() {
return {
Expand Down Expand Up @@ -55,12 +54,14 @@ class Action extends FocusMixin(LocalizeCoreElement(SelectionActionMixin(ButtonM
}

render() {
const disabledTooltip = this._disabledTooltip || (this.disabled && this.disabledTooltip ? this.disabledTooltip : undefined);

return html`
<d2l-button-subtle
class="vdiff-target"
@click="${this._handleActionClick}"
?disabled="${this.disabled}"
disabled-tooltip="${ifDefined(this.disabled ? this.localize('components.selection.action-hint') : undefined)}"
disabled-tooltip="${ifDefined(disabledTooltip)}"
icon="${ifDefined(this.icon)}"
text="${this.text}">
</d2l-button-subtle>
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
83 changes: 76 additions & 7 deletions components/selection/test/selection.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import '../selection-input.js';
import '../selection-select-all.js';
import '../selection-select-all-pages.js';
import '../selection-summary.js';
import { expect, fixture, html, nextFrame, oneEvent, runConstructor } from '@brightspace-ui/testing';
import { clickElem, expect, fixture, html, nextFrame, oneEvent, runConstructor } from '@brightspace-ui/testing';
import { SelectionInfo } from '../selection-mixin.js';
import Sinon from 'sinon';

Expand All @@ -20,7 +20,7 @@ describe('d2l-selection-action', () => {

it('dispatches d2l-selection-action-click event when clicked', async() => {
const el = await fixture(html`<d2l-selection-action></d2l-selection-action>`);
setTimeout(() => el.shadowRoot.querySelector('d2l-button-subtle').click());
clickElem(el);
await oneEvent(el, 'd2l-selection-action-click');
});

Expand All @@ -33,15 +33,47 @@ describe('d2l-selection-action', () => {
it('dispatches d2l-selection-action-click event if requires selection and has selected', async() => {
const el = await fixture(html`<d2l-selection-action requires-selection></d2l-selection-action>`);
el.selectionInfo = { state: 'some', keys: [] };
setTimeout(() => el.shadowRoot.querySelector('d2l-button-subtle').click());
clickElem(el);
await oneEvent(el, 'd2l-selection-action-click');
});

it('does not dispatch d2l-selection-action-click event if requires selection and none selected', async() => {
const el = await fixture(html`<d2l-selection-action requires-selection></d2l-selection-action>`);
let dispatched = false;
el.addEventListener('d2l-selection-action-click', () => dispatched = true);
el.shadowRoot.querySelector('d2l-button-subtle').click();
clickElem(el);
expect(dispatched).to.be.false;
});

it('dispatches d2l-selection-action-click event if less than max-selection-count is selected', async() => {
const el = await fixture(html`<d2l-selection-action max-selection-count="2"></d2l-selection-action>`);
el.selectionInfo = { state: 'some', keys: [ 'first' ] };
clickElem(el);
await oneEvent(el, 'd2l-selection-action-click');
});

it('dispatches d2l-selection-action-click event if max-selection-count is selected', async() => {
const el = await fixture(html`<d2l-selection-action max-selection-count="2"></d2l-selection-action>`);
el.selectionInfo = { state: 'some', keys: [ 'first', 'second' ] };
clickElem(el);
await oneEvent(el, 'd2l-selection-action-click');
});

it('does not dispatch d2l-selection-action-click event if greater than max-selection-count is selected', async() => {
const el = await fixture(html`<d2l-selection-action max-selection-count="2"></d2l-selection-action>`);
let dispatched = false;
el.addEventListener('d2l-selection-action-click', () => dispatched = true);
el.selectionInfo = { state: 'some', keys: [ 'first', 'second', 'third' ] };
clickElem(el);
expect(dispatched).to.be.false;
});

it('does not dispatch d2l-selection-action-click event if disabled', async() => {
const el = await fixture(html`<d2l-selection-action disabled></d2l-selection-action>`);
let dispatched = false;
el.addEventListener('d2l-selection-action-click', () => dispatched = true);
el.selectionInfo = { state: 'some', keys: [ 'first', 'second', 'third' ] };
clickElem(el);
expect(dispatched).to.be.false;
});

Expand All @@ -63,15 +95,16 @@ describe('d2l-selection-action-menu-item', () => {

it('dispatches d2l-selection-action-click event when clicked', async() => {
const el = await fixture(html`<d2l-menu label="Actions"><d2l-selection-action-menu-item text="Action"></d2l-selection-action-menu-item></d2l-menu>`);
setTimeout(() => el.querySelector('d2l-selection-action-menu-item').click());
const item = el.querySelector('d2l-selection-action-menu-item');
clickElem(item);
await oneEvent(el, 'd2l-selection-action-click');
});

it('dispatches d2l-selection-action-click event if requires selection and has selected', async() => {
const el = await fixture(html`<d2l-menu label="Actions"><d2l-selection-action-menu-item text="Action" requires-selection></d2l-selection-action-menu-item></d2l-menu>`);
const item = el.querySelector('d2l-selection-action-menu-item');
item.selectionInfo = { state: 'some', keys: [] };
setTimeout(() => item.click());
clickElem(item);
await oneEvent(el, 'd2l-selection-action-click');
});

Expand All @@ -80,7 +113,43 @@ describe('d2l-selection-action-menu-item', () => {
const item = el.querySelector('d2l-selection-action-menu-item');
let dispatched = false;
item.addEventListener('d2l-selection-action-click', () => dispatched = true);
item.click();
clickElem(item);
expect(dispatched).to.be.false;
});

it('dispatches d2l-selection-action-click event if less than max-selection-count is selected', async() => {
const el = await fixture(html`<d2l-menu label="Actions"><d2l-selection-action-menu-item text="Action" max-selection-count="2"></d2l-selection-action-menu-item></d2l-menu>`);
const item = el.querySelector('d2l-selection-action-menu-item');
item.selectionInfo = { state: 'some', keys: [ 'first' ] };
clickElem(item);
await oneEvent(el, 'd2l-selection-action-click');
});

it('dispatches d2l-selection-action-click event if max-selection-count is selected', async() => {
const el = await fixture(html`<d2l-menu label="Actions"><d2l-selection-action-menu-item text="Action" max-selection-count="2"></d2l-selection-action-menu-item></d2l-menu>`);
const item = el.querySelector('d2l-selection-action-menu-item');
item.selectionInfo = { state: 'some', keys: [ 'first', 'second' ] };
clickElem(item);
await oneEvent(el, 'd2l-selection-action-click');
});

it('does not dispatch d2l-selection-action-click event if greater than max-selection-count is selected', async() => {
const el = await fixture(html`<d2l-menu label="Actions"><d2l-selection-action-menu-item text="Action" max-selection-count="2"></d2l-selection-action-menu-item></d2l-menu>`);
const item = el.querySelector('d2l-selection-action-menu-item');
let dispatched = false;
item.addEventListener('d2l-selection-action-click', () => dispatched = true);
item.selectionInfo = { state: 'some', keys: [ 'first', 'second', 'third' ] };
clickElem(item);
expect(dispatched).to.be.false;
});

it('does not dispatch d2l-selection-action-click event if disabled', async() => {
const el = await fixture(html`<d2l-menu label="Actions"><d2l-selection-action-menu-item text="Action" disabled></d2l-selection-action-menu-item></d2l-menu>`);
const item = el.querySelector('d2l-selection-action-menu-item');
let dispatched = false;
item.addEventListener('d2l-selection-action-click', () => dispatched = true);
item.selectionInfo = { state: 'some', keys: [ 'first', 'second', 'third' ] };
clickElem(item);
expect(dispatched).to.be.false;
});

Expand Down
34 changes: 15 additions & 19 deletions components/selection/test/selection.vdiff.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,45 +35,41 @@ describe('selection-components', () => {
tests: [
{ name: 'text', template: html`<d2l-selection-action text="action"></d2l-selection-action>` },
{ name: 'text-icon', template: html`<d2l-selection-action text="action" icon="tier1:gear"></d2l-selection-action>` },
{ name: 'disabled', template: html`<d2l-selection-action text="action" disabled></d2l-selection-action>`, waitFor: elem => oneEvent(elem, 'd2l-tooltip-show') }
],
requiresSelectionTemplate: html`<d2l-selection-action text="action" requires-selection></d2l-selection-action>`
{ name: 'disabled', template: html`<d2l-selection-action text="action" disabled disabled-tooltip="Disabled message"></d2l-selection-action>`, waitFor: elem => oneEvent(elem, 'd2l-tooltip-show'), selectionInfo: { state: 'some', keys: [ 'first' ] } },
{ name: 'requires-selection', template: html`<d2l-selection-action text="action" requires-selection></d2l-selection-action>`, waitFor: elem => oneEvent(elem, 'd2l-tooltip-show'), selectionInfo: { state: 'none', keys: [] } },
{ name: 'max-selection-count', template: html`<d2l-selection-action text="action" max-selection-count="2"></d2l-selection-action>`, waitFor: elem => oneEvent(elem, 'd2l-tooltip-show'), selectionInfo: { state: 'some', keys: [ 'first', 'second', 'third' ] } }
]
},
{
category: 'dropdown',
tests: [
{ name: 'text', template: html`<d2l-selection-action-dropdown text="action"></d2l-selection-action-dropdown>` },
{ name: 'disabled', template: html`<d2l-selection-action-dropdown text="action" disabled></d2l-selection-action-dropdown>`, waitFor: elem => oneEvent(elem, 'd2l-tooltip-show') }
],
requiresSelectionTemplate: html`<d2l-selection-action-dropdown text="action" requires-selection></d2l-selection-action-dropdown>`
}].forEach(({ category, tests, requiresSelectionTemplate }) => {
{ name: 'disabled', template: html`<d2l-selection-action-dropdown text="action" disabled></d2l-selection-action-dropdown>`, selectionInfo: { state: 'some', keys: [ 'first' ] } },
{ name: 'requires-selection', template: html`<d2l-selection-action-dropdown text="action" requires-selection></d2l-selection-action-dropdown>`, waitFor: elem => oneEvent(elem, 'd2l-tooltip-show'), selectionInfo: { state: 'none', keys: [] } },
{ name: 'max-selection-count', template: html`<d2l-selection-action-dropdown text="action" max-selection-count="2"></d2l-selection-action-dropdown>`, waitFor: elem => oneEvent(elem, 'd2l-tooltip-show'), selectionInfo: { state: 'some', keys: [ 'first', 'second', 'third' ] } }
]
}].forEach(({ category, tests }) => {

describe(category, () => {
tests.forEach(({ name, template, waitFor }) => {
tests.forEach(({ name, template, waitFor, selectionInfo }) => {

it(name, async() => {
const elem = await fixture(template);
if (selectionInfo) elem.selectionInfo = selectionInfo;
await expect(elem).to.be.golden();
});

it(`${name}-focus`, async() => {
const elem = await fixture(template);
if (selectionInfo) elem.selectionInfo = selectionInfo;
await focusElem(elem);
if (waitFor) await waitFor(elem);
await expect(elem).to.be.golden();
});
});

[
{ name: 'requires-selection-none', selectionInfo: { state: 'none', keys: [] } },
{ name: 'requires-selection-some', selectionInfo: { state: 'some', keys: [] } },
{ name: 'requires-selection-all', selectionInfo: { state: 'all', keys: [] } }
].forEach(({ name, selectionInfo }) => {
it(name, async() => {
const elem = await fixture(requiresSelectionTemplate);
elem.selectionInfo = selectionInfo;
await expect(elem).to.be.golden();
});
});
});

});

describe('controls', () => {
Expand Down
3 changes: 2 additions & 1 deletion lang/ar.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ export default {
"components.pageable.info": "{count, plural, one {{countFormatted} مادة واحد} other {{countFormatted} من المواد}}",
"components.pageable.info-with-total": "{totalCount, plural, one {{countFormatted} من أصل {totalCountFormatted} مادة واحدة} other {{countFormatted} من أصل {totalCountFormatted} من المواد}}",
"components.pager-load-more.status-loading": "تحميل المزيد من المواد",
"components.selection.action-hint": "حدد مادة لتنفيذ هذا الإجراء.",
"components.selection.action-max-hint": "{count, plural, one {Disabled when more than {countFormatted} item is selected} other {Disabled when more than {countFormatted} items are selected}}",
"components.selection.action-required-hint": "حدد مادة لتنفيذ هذا الإجراء",
"components.selection.select-all": "تحديد الكل",
"components.selection.select-all-items": "تحديد كل المواد الـ {count}.",
"components.selection.selected": "تم تحديد {count}",
Expand Down
3 changes: 2 additions & 1 deletion lang/cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ export default {
"components.pageable.info": "{count, plural, one {{countFormatted} eitem} other {{countFormatted} o eitemau}}",
"components.pageable.info-with-total": "{totalCount, plural, one {{countFormatted} o {totalCountFormatted} eitem} other {{countFormatted} o {totalCountFormatted} eitemau}}",
"components.pager-load-more.status-loading": "Llwytho rhagor o eitemau",
"components.selection.action-hint": "Dewiswch eitem i gyflawni'r weithred hon.",
"components.selection.action-max-hint": "{count, plural, one {Disabled when more than {countFormatted} item is selected} other {Disabled when more than {countFormatted} items are selected}}",
"components.selection.action-required-hint": "Dewiswch eitem i gyflawni'r weithred hon",
"components.selection.select-all": "Dewis y Cyfan",
"components.selection.select-all-items": "Dewis Pob {count} Eitem",
"components.selection.selected": "{count} wedi’u dewis.",
Expand Down
Loading

0 comments on commit 8002ca3

Please sign in to comment.