Skip to content

Commit

Permalink
Merge pull request #1427 from City-of-Helsinki/hds-2522-announcement-…
Browse files Browse the repository at this point in the history
…removal

(release-4.0.0) hds-2522: clean up announcements
  • Loading branch information
NikoHelle authored Nov 20, 2024
2 parents 6215fa3 + c788dd9 commit 461d873
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 67 deletions.
88 changes: 78 additions & 10 deletions e2e/tests/react/components/react-cookie-consent-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,24 @@ test.describe(`Banner`, () => {
return page.locator(`button[data-approved="${buttonType}"]`);
};

const getAriaLiveLocator = (page: Page) => {
return page.locator('#hds-cc-aria-live');
};

const getAriaLiveLocatorWhenRendered = async (page: Page) => {
const locator = getAriaLiveLocator(page);
await waitFor(async () => {
const count = await locator.count();
return count === 1;
});
await locator.scrollIntoViewIfNeeded();
await waitFor(async () => {
const bb = await locator.boundingBox();
return !!bb && bb.height > 0;
});
return locator;
};

const waitForBannerToChange = async (banner: Locator, finalState: 'show' | 'hide') => {
const expectedCount = finalState === 'show' ? 1 : 0;
await waitFor(async () => {
Expand All @@ -72,18 +90,11 @@ test.describe(`Banner`, () => {
await waitForBannerToChange(banner, 'hide');
};

const storeConsentsAndWaitForNotification = async (
page: Page,
approveType: 'all' | 'selected',
bannerLocator: Locator,
) => {
const notificationElement = page.locator('#hds-cc-aria-live');
const storeConsentsAndWaitForNotification = async (page: Page, approveType: 'selected' | 'required') => {
const button = getSaveButton(page, approveType);
await button.scrollIntoViewIfNeeded();
await button.click();
await waitFor(async () => {
const count = await notificationElement.count();
return count === 1;
});
return getAriaLiveLocatorWhenRendered(page);
};

const getApprovedConsents = async (page: Page): Promise<string[]> => {
Expand Down Expand Up @@ -138,6 +149,20 @@ test.describe(`Banner`, () => {
return await gotoConsentsAndReturnCurrent(page);
};

const hideAllTimestampsFromScreenshots = async (page: Page) => {
const locator = page.locator(`div[data-timestamp] p.timestamp`);
const count = await locator.count();
expect(count > 0).toBeTruthy();
await locator.evaluateAll((list) => {
list.forEach((el) => {
if (el && el.style) {
el.style.setProperty('opacity', '0');
}
});
return Promise.resolve();
});
};

test('Banner is shown. Changing language changes the contents', async ({ page, isMobile }, testInfo) => {
if (isMobile) {
// viewport is too small to test with mobile view. Banner blocks the ui.
Expand Down Expand Up @@ -211,4 +236,47 @@ test.describe(`Banner`, () => {
const screenshotNameSV = createScreenshotFileName(testInfo, isMobile, 'sv language');
await takeScreenshotWithSpacing(page, banner, screenshotNameSV);
});
test('Aria live is visually shown when page is saved.', async ({ page, isMobile }, testInfo) => {
if (isMobile) {
// viewport is too small to test with mobile view. Banner blocks the ui.
return;
}
await gotoStorybookUrlByName(page, 'Example', componentName, storybook);
await storeConsentsAndWaitForNotification(page, 'selected');
const notificationElementLocator = await getAriaLiveLocatorWhenRendered(page);
await notificationElementLocator.isVisible();
const screenshotName = createScreenshotFileName(testInfo, isMobile, 'first save');
await hideAllTimestampsFromScreenshots(page);
await takeScreenshotWithSpacing(page, page.locator('body'), screenshotName);
// it takes 5000ms for the element to hide, so not waiting for it.
await changeTab(page, 'consents');
await changeTab(page, 'page');
await expect(notificationElementLocator).not.toBeVisible();

const screenshotName2 = createScreenshotFileName(testInfo, isMobile, 'no visible aria-live');
await hideAllTimestampsFromScreenshots(page);
await takeScreenshotWithSpacing(page, page.locator('body'), screenshotName2);

await storeConsentsAndWaitForNotification(page, 'required');
const newNotificationElementLocator = await getAriaLiveLocatorWhenRendered(page);
await newNotificationElementLocator.isVisible();
const screenshotName3 = createScreenshotFileName(testInfo, isMobile, 'second save');
await hideAllTimestampsFromScreenshots(page);
await takeScreenshotWithSpacing(page, page.locator('body'), screenshotName3);
});
test('Aria live is shown to the screenreader when banner is closed.', async ({ page, isMobile }, testInfo) => {
if (isMobile) {
// viewport is too small to test with mobile view. Banner blocks the ui.
return;
}
await gotoStorybookUrlByName(page, 'Banner', componentName, storybook);
const banner = getBannerOrPageLocator(page);
await storeConsentsAndWaitForBannerClose(page, 'all', banner);
const newNotificationElementLocator = await getAriaLiveLocatorWhenRendered(page);
const content = await newNotificationElementLocator.textContent();
expect(content && content.length > 1).toBeTruthy();
const bb = await newNotificationElementLocator.boundingBox();
// width and height are 1, but margin is -1, so result is 0
expect(bb && bb.width === 1 && bb.height === 1).toBeTruthy();
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
114 changes: 60 additions & 54 deletions packages/react/src/components/cookieConsentCore/cookieConsentCore.js
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ export class CookieConsentCore {
*/
removePage() {
this.killTimeout();
this.#clearAnnouncementElement();
if (!this.#settingsPageElement) {
return false;
}
Expand Down Expand Up @@ -356,10 +357,7 @@ export class CookieConsentCore {
this.#bannerElements.spacer.remove();
this.#bannerElements.spacer = null;
}
if (this.#bannerElements.ariaLive) {
this.#bannerElements.ariaLive.remove();
this.#bannerElements.ariaLive = null;
}

// Remove scroll-margin-bottom variable from all elements inside the contentSelector
document.documentElement.style.removeProperty('--hds-cookie-consent-height');
}
Expand Down Expand Up @@ -419,22 +417,68 @@ export class CookieConsentCore {
} else {
window.dispatchEvent(new CustomEvent(cookieEventType.CHANGE, { detail: { acceptedGroups } }));
if (!this.#settingsPageElement) {
this.#announceSettingsSaved();
this.removeBanner();
// removeBanner() removes the setTimeout that shows notification
// announceSettingsSaved() must be called after the removeBanner()
this.#announceSettingsSaved();
return;
}
this.#announceSettingsSaved();
}
}

/**
* animates notification in and out
* @return {boolean} - Returns true notification exists and it controlled by via animationend
*/
#animateNotificationIfFound() {
if (!this.#bannerElements.ariaLive) {
return false;
}
const notificationElem = this.#bannerElements.ariaLive.querySelector('.hds-notification');
if (notificationElem) {
notificationElem.classList.remove('enter');
notificationElem.classList.add('exit');
notificationElem.addEventListener('animationend', (e) => {
e.currentTarget.remove();
});
return true;
}
return false;
}

/**
* clears the aria-live element
*/
#clearAnnouncementElement(keepElement) {
if (!this.#bannerElements.ariaLive) {
return;
}
if (keepElement) {
this.#bannerElements.ariaLive.innerHTML = '';
} else {
this.#bannerElements.ariaLive.remove();
}
this.#bannerElements.ariaLive = null;
}

#removeAnnouncementOrNotification() {
// fail-safe scenario where neither banner nor page exists anymore
if (!this.#bannerElements.bannerContainer && !this.#settingsPageElement) {
this.#clearAnnouncementElement(false);
}
const controlRemovalWithAnimation = this.#animateNotificationIfFound();
if (!controlRemovalWithAnimation) {
this.#clearAnnouncementElement(true);
}
}

/**
* Prepares the aria-live element for announcements.
*/
#prepareAnnouncementElement() {
if (!this.#bannerElements.ariaLive) {
const ariaLiveElement = this.#shadowRootElement.getElementById(TEMPLATE_CONSTANTS.ariaLiveId);

// Render aria-live element depending on rendering mode: page or banner.
this.#bannerElements.ariaLive = ariaLiveElement;
}
}
Expand All @@ -446,9 +490,7 @@ export class CookieConsentCore {
this.killTimeout();
const SHOW_ARIA_LIVE_FOR_MS = 5000;

if (!this.#bannerElements.ariaLive) {
this.#prepareAnnouncementElement();
}
this.#prepareAnnouncementElement();

const message = getTranslation(
this.#siteSettings.translations,
Expand All @@ -457,15 +499,15 @@ export class CookieConsentCore {
this.#directions,
this.#siteSettings.fallbackLanguage,
);
const notificationAriaLabel = getTranslation(
this.#siteSettings.translations,
'notificationAriaLabel',
this.#language,
this.#directions,
this.#siteSettings.fallbackLanguage,
);

if (this.#settingsPageElement) {
const notificationAriaLabel = getTranslation(
this.#siteSettings.translations,
'notificationAriaLabel',
this.#language,
this.#directions,
this.#siteSettings.fallbackLanguage,
);
const notificationHtml = getNotificationHtml(message, notificationAriaLabel, 'success');
this.#bannerElements.ariaLive.innerHTML = notificationHtml;
} else {
Expand All @@ -474,43 +516,7 @@ export class CookieConsentCore {

// Remove ariaLive after 5 seconds
this.#timeoutReference = setTimeout(() => {
if (this.#bannerElements.ariaLive) {
// If banner has been removed, remove ariaLive element too
if (!this.#bannerElements.bannerContainer) {
if (this.#settingsPageElement) {
const notificationElem = this.#bannerElements.ariaLive.querySelector('.hds-notification');
if (notificationElem) {
notificationElem.classList.remove('enter');
notificationElem.classList.add('exit');
notificationElem.addEventListener('animationend', () => {
notificationElem.remove();
this.#bannerElements.ariaLive = null;
});
} else {
this.#bannerElements.ariaLive.innerHTML = '';
this.#bannerElements.ariaLive = null;
}
} else {
this.#bannerElements.ariaLive.remove();
this.#bannerElements.ariaLive = null;
}

// Otherwise, clear the content
} else {
const notificationElem = this.#bannerElements.ariaLive.querySelector('.hds-notification');
if (notificationElem) {
notificationElem.classList.remove('enter');
notificationElem.classList.add('exit');
notificationElem.addEventListener('animationend', () => {
notificationElem.remove();
this.#bannerElements.ariaLive = null;
});
} else {
this.#bannerElements.ariaLive.innerHTML = '';
this.#bannerElements.ariaLive = null;
}
}
}
this.#removeAnnouncementOrNotification();
}, SHOW_ARIA_LIVE_FOR_MS);
}

Expand Down
19 changes: 16 additions & 3 deletions packages/react/src/components/cookieConsentCore/template.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ export const CONSTANTS = {
accordionButtonDetailsClass: 'hds-cc__accordion-button--details',
};

const visuallyHiddenStyleInJS = `
border: 0;
clip: rect(0, 0, 0, 0);
clip-path: inset(50%);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
white-space: nowrap;
width: 1px;`;

/**
*
* @param {object} objects That contain translations, key and optional parameters
Expand Down Expand Up @@ -52,8 +64,9 @@ function getLangAttributes(translated) {
* @return {string} The HTML for the aria-live region.
*/
export function getAriaLiveHtml(isNotificationContainer) {
const ariaLiveClass = isNotificationContainer ? 'hds-cc__notification_container' : 'hds-cc__aria-live-container';
return `<div id="${CONSTANTS.ariaLiveId}" class="${ariaLiveClass}" aria-live="polite"></div>`;
const className = isNotificationContainer ? 'hds-cc__notification_container' : '';
const style = isNotificationContainer ? '' : visuallyHiddenStyleInJS;
return `<div id="${CONSTANTS.ariaLiveId}" class="${className}" style="${style}" aria-live="polite"></div>`;
}

/**
Expand Down Expand Up @@ -216,7 +229,7 @@ export function formatTimestamp(timestamp, groupId, translations, lang, directio
);

return timestamp
? `<p class-"timestamp" data-group="${groupId}" ${getLangAttributes(acceptedTranslation.acceptedAt)}>${acceptedTranslation.acceptedAt.value}</p>`
? `<p class="timestamp" data-group="${groupId}" ${getLangAttributes(acceptedTranslation.acceptedAt)}>${acceptedTranslation.acceptedAt.value}</p>`
: '';
}

Expand Down

0 comments on commit 461d873

Please sign in to comment.