Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(elements/ino-snackbar): add open prop functionality #1320

Merged
merged 10 commits into from
Apr 4, 2024
4 changes: 2 additions & 2 deletions packages/elements-angular/src/directives/proxies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1081,14 +1081,14 @@ export declare interface InoSelect extends Components.InoSelect {


@ProxyCmp({
inputs: ['a11yLabels', 'actionText', 'message', 'stayVisibleOnHover', 'timeout', 'type']
inputs: ['a11yLabels', 'actionText', 'message', 'open', 'stayVisibleOnHover', 'timeout', 'type']
})
@Component({
selector: 'ino-snackbar',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: ['a11yLabels', 'actionText', 'message', 'stayVisibleOnHover', 'timeout', 'type'],
inputs: ['a11yLabels', 'actionText', 'message', 'open', 'stayVisibleOnHover', 'timeout', 'type'],
})
export class InoSnackbar {
protected el: HTMLElement;
Expand Down
1 change: 1 addition & 0 deletions packages/elements-vue/src/proxies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,7 @@ export const InoSelect = /*@__PURE__*/ defineContainer<JSX.InoSelect, JSX.InoSel
export const InoSnackbar = /*@__PURE__*/ defineContainer<JSX.InoSnackbar>('ino-snackbar', undefined, [
'message',
'actionText',
'open',
'type',
'timeout',
'stayVisibleOnHover',
Expand Down
8 changes: 8 additions & 0 deletions packages/elements/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1408,6 +1408,10 @@ export namespace Components {
* @deprecated
*/
"message"?: string;
/**
* Controls the visibility state of the snackbar. When set to `true`, the snackbar is displayed; otherwise, it is hidden.
*/
"open": boolean;
/**
* If set to true, the timeout that closes the snackbar is paused when the user hovers over the snackbar.
*/
Expand Down Expand Up @@ -4264,6 +4268,10 @@ declare namespace LocalJSX {
* Event that emits as soon as the snackbar hides. Listen to this event to hide or destroy this element.
*/
"onHideEl"?: (event: InoSnackbarCustomEvent<any>) => void;
/**
* Controls the visibility state of the snackbar. When set to `true`, the snackbar is displayed; otherwise, it is hidden.
*/
"open"?: boolean;
/**
* If set to true, the timeout that closes the snackbar is paused when the user hovers over the snackbar.
*/
Expand Down
43 changes: 31 additions & 12 deletions packages/elements/src/components/ino-snackbar/ino-snackbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
Host,
Listen,
Prop,
Watch,
} from '@stencil/core';
import classNames from 'classnames';
import { hasSlotContent } from '../../util/component-utils';
Expand Down Expand Up @@ -44,6 +45,12 @@ export class Snackbar implements ComponentInterface {
*/
@Prop() actionText?: string;

/**
* Controls the visibility state of the snackbar.
* When set to `true`, the snackbar is displayed; otherwise, it is hidden.
*/
@Prop() open = true;

/**
* Changes the snackbar type. There are four types of messages: info, success, warning and error.
*/
Expand Down Expand Up @@ -86,22 +93,35 @@ export class Snackbar implements ComponentInterface {
}
}

@Watch('open')
openChanged(open: boolean) {
if (open) {
this.snackbarInstance?.open();
this.setupTimeout();
} else {
this.snackbarInstance?.close();
}
}

componentDidLoad() {
this.snackbarInstance = new MDCSnackbar(this.snackbarElement);
this.setupTimeout();

this.snackbarElement.addEventListener(
'MDCSnackbar:closing',
this.handleSnackbarHide,
);
this.setupTimeout();
if (this.stayVisibleOnHover) {
this.snackbarElement.addEventListener(
'mouseenter',
this.interruptTimeout,
);
this.snackbarElement.addEventListener('mouseleave', this.setupTimeout);
}
this.snackbarInstance.open();

if (this.open) {
this.snackbarInstance.open();
}

if (this.message) {
console.warn(
Expand All @@ -126,10 +146,9 @@ export class Snackbar implements ComponentInterface {
private setupTimeout = () => {
this.snackbarInstance.timeoutMs = -1;
if (this.timeout >= 0) {
this.nodeTimeout = setTimeout(
() => this.snackbarInstance.close(),
this.timeout,
);
this.nodeTimeout = setTimeout(() => {
this.snackbarInstance.close();
}, this.timeout);
}
};

Expand Down Expand Up @@ -217,13 +236,13 @@ export class Snackbar implements ComponentInterface {
</div>
)}
</div>
<ino-icon-button
aria-label={this.a11yLabels.closeLabel}
onClick={this.handleSnackbarHide}
icon="close"
class="ino-snackbar-close-btn"
/>
</div>
<ino-icon-button
aria-label={this.a11yLabels.closeLabel}
onClick={this.handleSnackbarHide}
icon="close"
class="ino-snackbar-close-btn"
/>
</div>
</Host>
);
Expand Down
1 change: 1 addition & 0 deletions packages/elements/src/components/ino-snackbar/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Snackbars provide brief messages about app processes at the bottom of the screen
| `a11yLabels` | -- | The aria-labels used to provide accessible snackbar context as well as close icon button label. | `{ snackbarLabel: string; closeLabel: string; }` | `{ snackbarLabel: this.type, closeLabel: 'Close notification', }` |
| `actionText` | `action-text` | The text to display for the action button. If no text is defined, the snack bar is displayed in an alternative feedback style. | `string` | `undefined` |
| `message` | `message` | <span style="color:red">**[DEPRECATED]**</span> <br/><br/>[DEPRECATED] Please use the default slot instead The text message to display. | `string` | `undefined` |
| `open` | `open` | Controls the visibility state of the snackbar. When set to `true`, the snackbar is displayed; otherwise, it is hidden. | `boolean` | `true` |
| `stayVisibleOnHover` | `stay-visible-on-hover` | If set to true, the timeout that closes the snackbar is paused when the user hovers over the snackbar. | `boolean` | `false` |
| `timeout` | `timeout` | Sets the timeout in ms until the snackbar disappears. The timeout can be disabled by setting it to a negative value. | `number` | `5000` |
| `type` | `type` | Changes the snackbar type. There are four types of messages: info, success, warning and error. | `"error" \| "info" \| "success" \| "warning"` | `'info'` |
Expand Down
22 changes: 22 additions & 0 deletions packages/storybook/elements-stencil-docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -9242,6 +9242,28 @@
"optional": true,
"required": false
},
{
"name": "open",
"type": "boolean",
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"mutable": false,
"attr": "open",
"reflectToAttr": false,
"docs": "Controls the visibility state of the snackbar.\nWhen set to `true`, the snackbar is displayed; otherwise, it is hidden.",
"docsTags": [],
"default": "true",
"values": [
{
"type": "boolean"
}
],
"optional": false,
"required": false
},
{
"name": "stayVisibleOnHover",
"type": "boolean",
Expand Down
51 changes: 51 additions & 0 deletions packages/storybook/src/stories/ino-snackbar/ino-snackbar.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { expect, Locator, test } from '@playwright/test';
import { goToStory } from '../test-utils';

test.describe('ino-snackbar', () => {
let showButton: Locator;
let inoSnackbar: Locator;

test.beforeEach(async ({ page }) => {
showButton = page.getByRole('button', { name: 'Show Snackbar' });
inoSnackbar = page.getByRole('button', { name: 'Some Action' });
});

test('should not be visible if open is set to false', async ({ page }) => {
await goToStory(page, ['Notification', 'ino-snackbar', 'default']);
await expect(inoSnackbar).toBeHidden();
});

test('should display the snackbar when "Show Snackbar" button is clicked', async ({
page,
}) => {
await goToStory(page, ['Notification', 'ino-snackbar', 'default']);
await showButton.click();
await expect(inoSnackbar).toBeVisible();
});

test('should close the snackbar when the close icon is clicked', async ({
page,
}) => {
await goToStory(page, ['Notification', 'ino-snackbar', 'default']);
await showButton.click();
const closeIcon = page.getByLabel('Close notification').getByRole('button');
await closeIcon.click();
await expect(inoSnackbar).toBeHidden();
});

test('should close the snackbar when ESC is pressed', async ({ page }) => {
await goToStory(page, ['Notification', 'ino-snackbar', 'default']);
await showButton.click();
await page.press('body', 'Escape');
await expect(inoSnackbar).toBeHidden();
});

test('should automatically close the snackbar after the default timeout period', async ({
page,
}) => {
await goToStory(page, ['Notification', 'ino-snackbar', 'timeout']);
await showButton.click();
await expect(inoSnackbar).toBeVisible();
await expect(inoSnackbar).toBeHidden(); // should automatically be hidden
});
});
49 changes: 27 additions & 22 deletions packages/storybook/src/stories/ino-snackbar/ino-snackbar.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,29 +29,33 @@ const InoSnackbarMeta = {
const button = e.target.closest('.snackbar-trigger');
if (button) {
const snackbarId = button.getAttribute('data-template-id');
const snackbarWrapper = document.getElementById(snackbarId);
if (snackbarWrapper) {
snackbarWrapper.classList.remove('hidden');
const snackbar = document.getElementById(snackbarId);
if (snackbar) {
snackbar.setAttribute('open', 'true');
}
}
};

const snackbarHideHandler = (e) => {
const snackbarElement = e.target.closest('ino-snackbar');
if (snackbarElement) {
const snackbarWrapper = snackbarElement.parentElement;
snackbarWrapper.classList.add('hidden');
const handleClose = (e) => {
const snackbar = e.target;
if (snackbar) {
snackbar.setAttribute('open', 'false');
}
};

const btns = document.querySelectorAll('.snackbar-trigger');
btns.forEach((btn) => btn.addEventListener('click', handleOpen));

document.addEventListener('hideEl', snackbarHideHandler);
const snackbarEls = document.querySelectorAll('ino-snackbar');
snackbarEls.forEach((snackbar) =>
snackbar.addEventListener('hideEl', handleClose),
);

return () => {
btns.forEach((btn) => btn.removeEventListener('click', handleOpen));
document.removeEventListener('hideEl', snackbarHideHandler);
snackbarEls.forEach((snackbar) =>
snackbar.removeEventListener('hideEl', handleClose),
);
};
});

Expand All @@ -62,19 +66,18 @@ const InoSnackbarMeta = {
<ino-button class="snackbar-trigger" data-template-id="${args.id}">
Show Snackbar
</ino-button>
<div class="hidden" id="${args.id}">
<ino-snackbar
id="${args.id}"
action-text="${args.actionText}"
timeout="${args.timeout}"
type="${args.type}"
stay-visible-on-hover="${args.stayVisibleOnHover}"
a11yLabels="
<ino-snackbar
id="${args.id}"
open="${args.open}"
action-text="${args.actionText}"
timeout="${args.timeout}"
type="${args.type}"
stay-visible-on-hover="${args.stayVisibleOnHover}"
a11yLabels="
${args.a11yLabels}"
>
${args.defaultSlot}
</ino-snackbar>
</div>
>
${args.defaultSlot}
</ino-snackbar>
`,
argTypes: {
// hide custom attributes from table
Expand All @@ -91,6 +94,7 @@ const InoSnackbarMeta = {
},
args: {
type: 'info',
open: false,
actionText: 'Some Action',
defaultSlot: 'This is a message',
timeout: -1,
Expand Down Expand Up @@ -146,6 +150,7 @@ export const StayVisibleOnHover = Story({
args: {
stayVisibleOnHover: true,
id: 'snackbar-stayVisibleOnHover',
timeout: 5000,
defaultSlot:
'This snackbar stays visible on hover otherwise it will disappear in 5s',
},
Expand Down
Loading