-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
478 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)}`); | ||
}; | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
Oops, something went wrong.