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(quantic): make quantic notifications component dismissible #4733

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,31 @@ const exampleEngine = {
id: 'mock engine',
};

const mockSearchStatusState = {
hasResults: true,
};

const mockSearchStatus = {
state: mockSearchStatusState,
subscribe: jest.fn((callback) => {
mockSearchStatus.callback = callback;
return jest.fn();
}),
};

const functionsMocks = {
buildNotifyTrigger: jest.fn(() => ({
state: notificationsState,
subscribe: functionsMocks.subscribe,
})),
dispatchMessage: jest.fn(() => {}),
buildSearchStatus: jest.fn(() => mockSearchStatus),
subscribe: jest.fn((cb) => {
cb();
return functionsMocks.unsubscribe;
}),
unsubscribe: jest.fn(() => {}),
unsubscribeSearchStatus: jest.fn(() => {}),
};

// @ts-ignore
Expand All @@ -43,6 +57,7 @@ AriaLiveRegion.mockImplementation(() => {
const selectors = {
notifications: '[data-test="notification"]',
initializationError: 'c-quantic-component-error',
notificationCloseButton: 'button',
};

const defaultOptions = {
Expand All @@ -68,6 +83,7 @@ function prepareHeadlessState() {
mockHeadlessLoader.getHeadlessBundle = () => {
return {
buildNotifyTrigger: functionsMocks.buildNotifyTrigger,
buildSearchStatus: functionsMocks.buildSearchStatus,
};
};
}
Expand Down Expand Up @@ -97,6 +113,12 @@ function mockErroneousHeadlessInitialization() {
};
}

// Simulates a search status update
function mockSearchStatusUpdate() {
mockSearchStatus.state.hasResults = true;
mockSearchStatus.callback();
}

function cleanup() {
// The jsdom instance is shared across test cases in a single file so reset the DOM
while (document.body.firstChild) {
Expand Down Expand Up @@ -178,6 +200,7 @@ describe('c-quantic-notifications', () => {
describe('when some notifications are present in the state', () => {
it('should render the notifications component', async () => {
const element = createTestComponent();
mockSearchStatusUpdate();
await flushPromises();

const notifications = element.shadowRoot.querySelectorAll(
Expand Down Expand Up @@ -224,4 +247,86 @@ describe('c-quantic-notifications', () => {
});
});
});

describe('when clicking on a notification close button', () => {
it('should properly dismiss that notification', async () => {
const element = createTestComponent();
mockSearchStatusUpdate();
await flushPromises();

const notificationsBeforeClose = element.shadowRoot.querySelectorAll(
selectors.notifications
);

expect(notificationsBeforeClose.length).toEqual(
exampleNotifications.length
);
notificationsBeforeClose.forEach((notification, index) => {
expect(notification.textContent).toEqual(exampleNotifications[index]);
});

const firstNotificationCloseButton =
notificationsBeforeClose[0].querySelector(
selectors.notificationCloseButton
);
firstNotificationCloseButton.click();
await flushPromises();

const notificationsAfterClose = element.shadowRoot.querySelectorAll(
selectors.notifications
);

expect(notificationsAfterClose.length).toEqual(
exampleNotifications.length - 1
);
expect(notificationsAfterClose[0].textContent).toEqual(
exampleNotifications[1]
);
});

describe('when triggering another search with the same query', () => {
it('should reset the visibility of the notifications', async () => {
const element = createTestComponent();
mockSearchStatusUpdate();
await flushPromises();

const notificationsBeforeClose = element.shadowRoot.querySelectorAll(
selectors.notifications
);

expect(notificationsBeforeClose.length).toEqual(
exampleNotifications.length
);

const firstNotificationCloseButton =
notificationsBeforeClose[0].querySelector(
selectors.notificationCloseButton
);
firstNotificationCloseButton.click();
await flushPromises();

const notificationsAfterClose = element.shadowRoot.querySelectorAll(
selectors.notifications
);

expect(notificationsAfterClose.length).toEqual(
exampleNotifications.length - 1
);

mockSearchStatusUpdate();
await flushPromises();

const notificationsAfterSearch = element.shadowRoot.querySelectorAll(
selectors.notifications
);

expect(notificationsAfterSearch.length).toEqual(
exampleNotifications.length
);
notificationsAfterSearch.forEach((notification, index) => {
expect(notification.textContent).toEqual(exampleNotifications[index]);
});
});
});
});
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
:host {
--slds-c-toast-sizing-min-width: 100%;
}

.notification {
background-color: var(---lwc-colorBackgroundAlt, white);
border: 1px solid var(--lwc-colorBackgroundToast, rgb(116, 116, 116));
margin-right: 0;
margin-left: 0;
width: 100%;
}

.notification-container {
z-index: 1;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,31 @@
</template>
<template lwc:else>
<template for:each={notifications} for:item="notification">
<div key={notification.id} data-test="notification" class="slds-notify_container slds-is-relative">
<div data-cy="notification" class="slds-notify slds-notify_toast notification" role="status">
<lightning-icon icon-name="utility:info_alt" alternative-text="info" title="info"
size="small"></lightning-icon>
<div class="slds-notify__content slds-var-m-left_small">
{notification.value}
<template lwc:if={notification.visible}>
<div key={notification.id} data-test="notification" class="slds-notify_container slds-is-relative notification-container">
<div data-cy="notification" class="slds-notify slds-notify_toast notification" role="status">
<lightning-icon icon-name="utility:info_alt" alternative-text="info" title="info"
size="small"></lightning-icon>
<div class="slds-notify__content slds-var-m-left_small">
{notification.value}
</div>
<div class="slds-notify__close">
<button
class="slds-button slds-button_icon slds-button_icon-inverse"
title="Close notification"
onclick={handleNotificationClose}
data-id={notification.id}
>
<lightning-icon
icon-name="utility:close"
alternative-text="Close"
size="x-small"
></lightning-icon>
</button>
</div>
</div>
</div>
</div>
</template>
</template>
</template>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {AriaLiveRegion} from 'c/quanticUtils';
import {LightningElement, api} from 'lwc';

/** @typedef {import("coveo").SearchEngine} SearchEngine */
/** @typedef {import("coveo").SearchStatus} SearchStatus */
/** @typedef {import("coveo").NotifyTrigger} NotifyTrigger */
/** @typedef {import("coveo").NotifyTriggerState} NotifyTriggerState */

Expand All @@ -33,6 +34,12 @@ export default class QuanticNotifications extends LightningElement {
hasInitializationError = false;
/** @type {import('c/quanticUtils').AriaLiveUtils} */
ariaLiveNotificationsRegion;
/** @type {Array} */
notifications = [];
/** @type {Function} */
unsubscribe;
/** @type {Function} */
unsubscribeSearchStatus;

connectedCallback() {
registerComponentForInit(this, this.engineId);
Expand All @@ -48,16 +55,22 @@ export default class QuanticNotifications extends LightningElement {
initialize = (engine) => {
this.headless = getHeadlessBundle(this.engineId);
this.notifyTrigger = this.headless.buildNotifyTrigger(engine);
this.searchStatus = this.headless.buildSearchStatus(engine);
this.ariaLiveNotificationsRegion = AriaLiveRegion('notifications', this);
this.unsubscribe = this.notifyTrigger.subscribe(() => this.updateState());
this.unsubscribeSearchStatus = this.searchStatus.subscribe(() =>
this.handleSearchStatusChange()
);
};

disconnectedCallback() {
this.unsubscribe?.();
this.unsubscribeSearchStatus?.();
}

updateState() {
this.notifyTriggerState = this.notifyTrigger.state;
this.notifyTriggerState = this.notifyTrigger?.state;

this.ariaLiveNotificationsRegion.dispatchMessage(
this.notifyTriggerState?.notifications.reduce(
(value, notification, index) => {
Expand All @@ -68,13 +81,25 @@ export default class QuanticNotifications extends LightningElement {
);
}

get notifications() {
return (
this.notifyTriggerState?.notifications.map((notification, index) => ({
value: notification,
id: index,
})) ?? []
);
handleSearchStatusChange() {
if (!this.searchStatus.state.isLoading) {
this.notifications =
this.notifyTrigger?.state?.notifications.map((notification, index) => ({
value: notification,
id: index.toString(),
visible: true,
})) ?? [];
}
}

handleNotificationClose(event) {
const currentNotificationId = event.currentTarget.dataset.id;
this.notifications = this.notifications.map((notification) => {
if (notification.id === currentNotificationId) {
return {...notification, visible: false};
}
return notification;
});
}

/**
Expand Down
Loading