Skip to content

Commit

Permalink
Merge 7af5e1c into 26b2623
Browse files Browse the repository at this point in the history
  • Loading branch information
RivaIvanova committed May 8, 2024
2 parents 26b2623 + 7af5e1c commit c83ec32
Show file tree
Hide file tree
Showing 7 changed files with 478 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
### Added
- Banner component [#1174](https://github.com/IgniteUI/igniteui-webcomponents/issues/1174)

## [4.9.0] - 2024-04-30
### Added
- Button group component now allows resetting the selection state via the `selectedItems` property [#1168](https://github.com/IgniteUI/igniteui-webcomponents/pull/1168)
Expand Down
250 changes: 250 additions & 0 deletions src/components/banner/banner.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
import {
elementUpdated,
expect,
fixture,
html,
unsafeStatic,
} from '@open-wc/testing';
import { spy } from 'sinon';

import { IgcBannerComponent, defineComponents } from '../../index.js';

describe('Banner', () => {
before(() => {
defineComponents(IgcBannerComponent);
});

let banner: IgcBannerComponent;

beforeEach(async () => {
banner = await createBannerComponent();
});

const DIFF_OPTIONS = {
ignoreTags: ['igc-button'],
ignoreAttributes: ['inert'],
};

const BUTTON_DIFF_OPTIONS = ['variant', 'size', 'style'];

describe('Initialization Tests', () => {
it('passes the a11y audit', async () => {
await expect(banner).to.be.accessible();
await expect(banner).shadowDom.to.be.accessible();
});

it('is correctly initialized with its default component state', () => {
expect(banner.open).to.be.false;
expect(banner.dir).to.be.empty;
});

it('should render a default action button', () => {
const button = banner.shadowRoot!.querySelector('igc-button');

expect(button).not.to.be.null;
expect(button).dom.to.equal(`<igc-button type="button">OK</igc-button>`, {
ignoreAttributes: BUTTON_DIFF_OPTIONS,
});
});

it('is correctly rendered both in shown/hidden states', async () => {
expect(banner.open).to.be.false;

expect(banner).dom.to.equal(
'<igc-banner>You are currently offline.</igc-banner>'
);
expect(banner).shadowDom.to.equal(
`<div inert>
<slot name="prefix"></slot>
<slot></slot>
<slot name="actions">
<igc-button type="button">OK</igc-button>
</slot>
</div>`,
{
ignoreAttributes: [...BUTTON_DIFF_OPTIONS, 'part'],
}
);

banner.show();
await elementUpdated(banner);

expect(banner).dom.to.equal(
'<igc-banner open>You are currently offline.</igc-banner>'
);
expect(banner).shadowDom.to.equal(
`<div>
<slot name="prefix"></slot>
<slot></slot>
<slot name="actions">
<igc-button type="button">OK</igc-button>
</slot>
</div>`,
{
ignoreAttributes: [...BUTTON_DIFF_OPTIONS, 'part'],
}
);
});

it('should correctly render slotted content', async () => {
banner = await createBannerComponent(`
<igc-banner>
<igc-icon slot="prefix"></igc-icon>
Build <strong>123</strong> completed!
<div slot="actions">
<igc-button>OK 1</igc-button>
<igc-button>View log</igc-button>
</div>
</igc-banner>`);

const prefix = banner.querySelector('igc-icon');
const actions = banner.querySelector('div');

expect(prefix).not.to.be.null;
expect(actions).not.to.be.null;

expect(actions?.children[0]).dom.to.equal(
'<igc-button>OK 1</igc-button>',
{
ignoreAttributes: [...BUTTON_DIFF_OPTIONS, 'type'],
}
);

expect(actions?.children[1]).dom.to.equal(
'<igc-button>View log</igc-button>',
{
ignoreAttributes: [...BUTTON_DIFF_OPTIONS, 'type'],
}
);
});
});

describe('Methods` Tests', () => {
it('calls `show` method successfully', async () => {
expect(banner.open).to.be.false;

banner.show();
await elementUpdated(banner);

expect(banner.open).to.be.true;
expect(banner).dom.to.equal(
'<igc-banner open>You are currently offline.</igc-banner>',
DIFF_OPTIONS
);
});

it('calls `hide` method successfully', async () => {
expect(banner.open).to.be.false;

banner.open = true;
await elementUpdated(banner);

expect(banner).dom.to.equal(
'<igc-banner open>You are currently offline.</igc-banner>',
DIFF_OPTIONS
);

banner.hide();
await elementUpdated(banner);

expect(banner.open).to.be.false;
expect(banner).dom.to.equal(
'<igc-banner>You are currently offline.</igc-banner>',
DIFF_OPTIONS
);
});

it('calls `toggle` method successfully', async () => {
expect(banner.open).to.be.false;

banner.toggle();
await elementUpdated(banner);

expect(banner.open).to.be.true;
expect(banner).dom.to.equal(
'<igc-banner open>You are currently offline.</igc-banner>',
DIFF_OPTIONS
);

banner.toggle();
await elementUpdated(banner);

expect(banner.open).to.be.false;
expect(banner).dom.to.equal(
'<igc-banner>You are currently offline.</igc-banner>',
DIFF_OPTIONS
);
});
});

describe('Action Tests', () => {
it('should close the banner when clicking the default button', async () => {
expect(banner.open).to.be.false;

banner.show();
await elementUpdated(banner);

expect(banner.open).to.be.true;

const button = banner.shadowRoot!.querySelector('igc-button');

button?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
await elementUpdated(banner);

expect(banner.open).to.be.false;
});

it('should emit correct event sequence for the default action button', async () => {
const eventSpy = spy(banner, 'emitEvent');

expect(banner.open).to.be.false;

banner.show();
await elementUpdated(banner);

expect(banner.open).to.be.true;

const button = banner.shadowRoot!.querySelector('igc-button');

button?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
await elementUpdated(banner);

expect(eventSpy.callCount).to.equal(2);
expect(eventSpy.firstCall).calledWith('igcClosing', {
cancelable: true,
});
expect(eventSpy.secondCall).calledWith('igcClosed');
expect(banner.open).to.be.false;
});

it('can cancel `igcClosing` event', async () => {
const eventSpy = spy(banner, 'emitEvent');
const button = banner.shadowRoot!.querySelector('igc-button');

banner.addEventListener('igcClosing', (event) => {
event.preventDefault();
});

banner.show();
await elementUpdated(banner);

expect(banner.open).to.be.true;

button?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
await elementUpdated(banner);

expect(eventSpy).calledWith('igcClosing');
expect(eventSpy).not.calledWith('igcClosed');
expect(banner.open).to.be.true;
});
});

const createBannerComponent = (
template = `
<igc-banner>
You are currently offline.
</igc-banner>`
) => {
return fixture<IgcBannerComponent>(html`${unsafeStatic(template)}`);
};
});
131 changes: 131 additions & 0 deletions src/components/banner/banner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { LitElement, html } from 'lit';
import { property } from 'lit/decorators.js';
import { type Ref, createRef, ref } from 'lit/directives/ref.js';

import { addAnimationController } from '../../animations/player.js';
import { growVerIn, growVerOut } from '../../animations/presets/grow/index.js';
import IgcButtonComponent from '../button/button.js';
import { registerComponent } from '../common/definitions/register.js';
import type { Constructor } from '../common/mixins/constructor.js';
import { EventEmitterMixin } from '../common/mixins/event-emitter.js';
import { styles } from './themes/banner.base.css.js';

export interface IgcBannerComponentEventMap {
igcClosing: CustomEvent<void>;
igcClosed: CustomEvent<void>;
}

/**
* The `igc-banner` component displays important and concise message(s) for a user to address, that is specific to a page or feature.
*
* @element igc-banner
*
* @slot - Renders the text content of the banner message.
* @slot prefix - Renders additional content at the start of the message block.
* @slot actions - Renders any action elements.
*
* @fires igcClosing - Emitted before closing the banner - when a user interacts (click) with the default action of the banner.
* @fires igcClosed - Emitted after the banner is closed - when a user interacts (click) with the default action of the banner.
*
* @csspart base - The base wrapper of the banner component.
*/

export default class IgcBannerComponent extends EventEmitterMixin<
IgcBannerComponentEventMap,
Constructor<LitElement>
>(LitElement) {
public static readonly tagName = 'igc-banner';
public static styles = [styles];

public static register() {
registerComponent(IgcBannerComponent, IgcButtonComponent);
}

private _internals: ElementInternals;
private _bannerRef: Ref<HTMLElement> = createRef();
private _animationPlayer = addAnimationController(this, this._bannerRef);

/**
* Determines whether the banner is being shown/hidden.
* @attr
*/
@property({ type: Boolean, reflect: true })
public open = false;

constructor() {
super();
this._internals = this.attachInternals();

this._internals.role = 'status';
this._internals.ariaLive = 'polite';
}

/** Shows the banner if not already shown. */
public show(): void {
if (this.open) {
return;
}

this.open = true;
this.toggleAnimation('open');
}

/** Hides the banner if not already hidden. */
public hide(): void {
if (!this.open) {
return;
}

this.toggleAnimation('close');
this.open = false;
}

/** Toggles between shown/hidden state. */
public toggle(): void {
this.open ? this.hide() : this.show();
}

private async toggleAnimation(dir: 'open' | 'close') {
const animation = dir === 'open' ? growVerIn : growVerOut;

const [_, event] = await Promise.all([
this._animationPlayer.stopAll(),
this._animationPlayer.play(animation()),
]);

return event.type === 'finish';
}

private handleClick() {
const allowed = this.emitEvent('igcClosing', { cancelable: true });

if (allowed) {
this.hide();
this.emitEvent('igcClosed');
}
}

protected override render() {
return html`
<div ${ref(this._bannerRef)} part="base" .inert=${!this.open}>
<slot name="prefix"></slot>
<slot></slot>
<slot name="actions">
<igc-button
type="button"
variant="flat"
size="small"
@click=${this.handleClick}
>OK</igc-button
>
</slot>
</div>
`;
}
}

declare global {
interface HTMLElementTagNameMap {
'igc-banner': IgcBannerComponent;
}
}

0 comments on commit c83ec32

Please sign in to comment.