Skip to content

Commit 936d750

Browse files
feat: Invoker commands integration for components (#2225)
This commit integrates invoker commands into our components, allowing for more flexible and dynamic interactions. The changes include updates to button components, the addition of a new command controller, and modifications to related stories and configurations. --------- Co-authored-by: damyanpetev <damyanpetev@users.noreply.github.com>
1 parent ace24ac commit 936d750

29 files changed

Lines changed: 2171 additions & 322 deletions

.vscode/cspell.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"aria-valuenow",
1212
"aria-valuetext",
1313
"combobox",
14+
"commandfor",
1415
"listbox",
1516
"listitem",
1617
"progressbar",
@@ -32,9 +33,9 @@
3233
"igniteui",
3334
"slotchange",
3435
"stylelint",
35-
"webcomponents"
36-
],
37-
"ignoreRegExpList": [
38-
"θ"
36+
"webcomponents",
37+
"noopener",
38+
"noreferrer"
3939
],
40+
"ignoreRegExpList": ["θ"]
4041
}

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1111
- Added `keepOpenOnEscape` property — prevents the drawer from closing when the user presses the **Escape** key (non-relative positions only).
1212
- Added `igcClosing` event — emitted just before the drawer is closed by user interaction. Cancelable.
1313
- Added `igcClosed` event — emitted just after the drawer is closed by user interaction.
14+
- #### Invoker Commands API
15+
- `igc-button` and `igc-icon-button` now support `command` and `commandfor` properties, enabling declarative control of target components without JavaScript.
16+
- `igc-banner`, `igc-dialog`, `igc-nav-drawer`, `igc-snackbar`, and `igc-toast` now respond to `--show`, `--hide`, and `--toggle` commands dispatched by an invoker button.
1417

1518
### Changed
1619
- #### Nav Drawer

src/components/banner/banner.spec.ts

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,19 @@ import {
44
fixture,
55
html,
66
nextFrame,
7+
waitUntil,
78
} from '@open-wc/testing';
89
import { spy } from 'sinon';
910

11+
import IgcButtonComponent from '../button/button.js';
1012
import { defineComponents } from '../common/definitions/defineComponents.js';
1113
import { finishAnimationsFor, simulateClick } from '../common/utils.spec.js';
1214
import IgcIconComponent from '../icon/icon.js';
1315
import IgcBannerComponent from './banner.js';
1416

1517
describe('Banner', () => {
1618
before(() => {
17-
defineComponents(IgcBannerComponent, IgcIconComponent);
19+
defineComponents(IgcBannerComponent, IgcButtonComponent, IgcIconComponent);
1820
});
1921

2022
const createDefaultBanner = () => html`
@@ -287,4 +289,89 @@ describe('Banner', () => {
287289
expect(banner.open).to.be.true;
288290
});
289291
});
292+
293+
describe('Invoker Commands API', () => {
294+
afterEach(async () => {
295+
if (banner.open) {
296+
await banner.hide();
297+
}
298+
});
299+
300+
describe('with igc-button', () => {
301+
let invoker: IgcButtonComponent;
302+
303+
beforeEach(async () => {
304+
const container = await fixture<HTMLElement>(html`
305+
<div>
306+
<igc-button command="--show" commandfor="invoker-banner"
307+
>Show</igc-button
308+
>
309+
<igc-banner id="invoker-banner"
310+
>You are currently offline.</igc-banner
311+
>
312+
</div>
313+
`);
314+
315+
invoker = container.querySelector<IgcButtonComponent>('igc-button')!;
316+
banner = container.querySelector<IgcBannerComponent>('igc-banner')!;
317+
});
318+
319+
it('`--show` opens the banner', async () => {
320+
expect(banner.open).to.be.false;
321+
322+
invoker.click();
323+
await waitUntil(() => banner.open);
324+
325+
expect(banner.open).to.be.true;
326+
});
327+
328+
it('`--hide` closes an open banner', async () => {
329+
await banner.show();
330+
expect(banner.open).to.be.true;
331+
332+
invoker.command = '--hide';
333+
await elementUpdated(invoker);
334+
335+
invoker.click();
336+
await waitUntil(() => !banner.open);
337+
338+
expect(banner.open).to.be.false;
339+
});
340+
341+
it('`--toggle` opens a closed banner', async () => {
342+
expect(banner.open).to.be.false;
343+
344+
invoker.command = '--toggle';
345+
await elementUpdated(invoker);
346+
347+
invoker.click();
348+
await waitUntil(() => banner.open);
349+
350+
expect(banner.open).to.be.true;
351+
});
352+
353+
it('`--toggle` closes an open banner', async () => {
354+
await banner.show();
355+
expect(banner.open).to.be.true;
356+
357+
invoker.command = '--toggle';
358+
await elementUpdated(invoker);
359+
360+
invoker.click();
361+
await waitUntil(() => !banner.open);
362+
363+
expect(banner.open).to.be.false;
364+
});
365+
366+
it('a disabled igc-button does not invoke commands', async () => {
367+
invoker.disabled = true;
368+
await elementUpdated(invoker);
369+
370+
invoker.click();
371+
await elementUpdated(banner);
372+
373+
expect(banner.open).to.be.false;
374+
});
375+
});
376+
});
290377
});

src/components/banner/banner.ts

Lines changed: 58 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { addAnimationController } from '../../animations/player.js';
55
import { growVerIn, growVerOut } from '../../animations/presets/grow/index.js';
66
import { addThemingController } from '../../theming/theming-controller.js';
77
import IgcButtonComponent from '../button/button.js';
8+
import { addCommandController } from '../common/controllers/command.js';
89
import { addInternalsController } from '../common/controllers/internals.js';
910
import { addSlotController, setSlots } from '../common/controllers/slot.js';
1011
import { registerComponent } from '../common/definitions/register.js';
@@ -19,25 +20,39 @@ export interface IgcBannerComponentEventMap {
1920
}
2021

2122
/**
22-
* The `igc-banner` component displays important and concise message(s) for a user to address, that is specific to a page or feature.
23+
* A non-modal notification banner that displays important, concise messages
24+
* requiring user acknowledgement.
25+
*
26+
* The banner slides into view with an animated grow transition and renders
27+
* inline, pushing the surrounding page content rather than overlaying it.
28+
*
29+
* The component integrates with the
30+
* [Invoker Commands API](https://developer.mozilla.org/en-US/docs/Web/API/Invoker_Commands_API):
31+
* an Ignite button or a native `<button>` with `command="--show"` / `"--hide"` /
32+
* `"--toggle"` and `commandfor` pointing to this element will call the
33+
* corresponding method declaratively without any JavaScript.
2334
*
2435
* @element igc-banner
2536
*
26-
* @slot - Renders the text content of the banner message.
27-
* @slot prefix - Renders additional content at the start of the message block.
28-
* @slot actions - Renders any action elements.
37+
* @fires igcClosing - Emitted just before the banner closes in response to the
38+
* default action button being clicked. Cancelable — call
39+
* `event.preventDefault()` to abort the closing sequence.
40+
* @fires igcClosed - Emitted after the banner has fully closed and its exit
41+
* animation has completed.
2942
*
30-
* @fires igcClosing - Emitted before closing the banner - when a user interacts (click) with the default action of the banner.
31-
* @fires igcClosed - Emitted after the banner is closed - when a user interacts (click) with the default action of the banner.
43+
* @slot - The banner message text content.
44+
* @slot prefix - An icon or illustration rendered to the left of the message.
45+
* Useful for reinforcing the message type (info, warning, success, etc.).
46+
* @slot actions - Custom action elements rendered in the banner's action area.
47+
* When provided, replaces the default "OK" dismiss button.
3248
*
33-
* @csspart base - The base wrapper of the banner component.
34-
* @csspart spacer - The inner wrapper that sets the space around the banner.
35-
* @csspart message - The part that holds the text and the illustration.
36-
* @csspart illustration - The part that holds the banner icon/illustration.
37-
* @csspart content - The part that holds the banner text content.
38-
* @csspart actions - The part that holds the banner action buttons.
49+
* @csspart base - The root wrapper element of the banner.
50+
* @csspart spacer - The inner wrapper that controls the spacing around the banner content.
51+
* @csspart message - The container that holds the illustration and text content.
52+
* @csspart illustration - The container for the prefix slot (icon/illustration).
53+
* @csspart content - The container for the default message slot.
54+
* @csspart actions - The container for the action buttons slot.
3955
*/
40-
4156
export default class IgcBannerComponent extends EventEmitterMixin<
4257
IgcBannerComponentEventMap,
4358
Constructor<LitElement>
@@ -54,8 +69,14 @@ export default class IgcBannerComponent extends EventEmitterMixin<
5469
private readonly _player = addAnimationController(this, this._bannerRef);
5570

5671
/**
57-
* Determines whether the banner is being shown/hidden.
58-
* @attr
72+
* Whether the banner is open.
73+
*
74+
* Setting this property programmatically will immediately show or hide the
75+
* banner without animation and without emitting close events.
76+
* Prefer the `show()`, `hide()`, and `toggle()` methods for animated
77+
* transitions.
78+
* @attr open
79+
* @default false
5980
*/
6081
@property({ type: Boolean, reflect: true })
6182
public open = false;
@@ -71,6 +92,10 @@ export default class IgcBannerComponent extends EventEmitterMixin<
7192
ariaLive: 'polite',
7293
},
7394
});
95+
addCommandController(this)
96+
.set('--show', this.show)
97+
.set('--hide', this.hide)
98+
.set('--toggle', this.toggle);
7499
}
75100

76101
private async _handleClick(): Promise<void> {
@@ -80,7 +105,12 @@ export default class IgcBannerComponent extends EventEmitterMixin<
80105
}
81106
}
82107

83-
/** Shows the banner if not already shown. Returns `true` when the animation has completed. */
108+
/**
109+
* Opens the banner with an animated grow-in transition.
110+
*
111+
* Returns `true` when the banner was successfully opened, or `false` if
112+
* it was already open.
113+
*/
84114
public async show(): Promise<boolean> {
85115
if (this.open) {
86116
return false;
@@ -90,7 +120,12 @@ export default class IgcBannerComponent extends EventEmitterMixin<
90120
return this._player.playExclusive(growVerIn());
91121
}
92122

93-
/** Hides the banner if not already hidden. Returns `true` when the animation has completed. */
123+
/**
124+
* Closes the banner with an animated grow-out transition.
125+
*
126+
* Returns `true` when the banner was successfully closed, or `false` if
127+
* it was already closed.
128+
*/
94129
public async hide(): Promise<boolean> {
95130
if (!this.open) {
96131
return false;
@@ -101,7 +136,12 @@ export default class IgcBannerComponent extends EventEmitterMixin<
101136
return true;
102137
}
103138

104-
/** Toggles between shown/hidden state. Returns `true` when the animation has completed. */
139+
/**
140+
* Toggles the banner open or closed depending on its current state.
141+
*
142+
* Equivalent to calling `show()` when closed and `hide()` when open.
143+
* Returns `true` when the transition completed successfully.
144+
*/
105145
public async toggle(): Promise<boolean> {
106146
return this.open ? this.hide() : this.show();
107147
}

0 commit comments

Comments
 (0)