Skip to content

Commit

Permalink
amp-consent CMP a11y improvement (#26456)
Browse files Browse the repository at this point in the history
* ConsentTitle & ButtonTitle

* setting invisible style

* CSS styling and tryFocus()

* Remove SR alert on hide + nits

* Suggested Changes

* CSS fix
  • Loading branch information
Micajuine Ho committed Feb 6, 2020
1 parent 39b1f61 commit c3cebfe
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 5 deletions.
2 changes: 1 addition & 1 deletion examples/amp-consent/diy-consent.html
Expand Up @@ -143,7 +143,7 @@ <h1>Thank you!</h1>
// Add event listener for focus for a11y
window.addEventListener('focus', () => {
const headerElement = document.querySelector('h1');
console.log('Focused!', headerElement);
console.log('I am focused after you clicked on me!', headerElement);
if (headerElement) {
headerElement.focus();
}
Expand Down
9 changes: 9 additions & 0 deletions extensions/amp-consent/0.1/amp-consent.css
Expand Up @@ -85,6 +85,15 @@ amp-consent.amp-hidden {
animation: 1000ms amp-consent-ui-placeholder-spin linear infinite;
}

.i-amphtml-consent-alertdialog {
overflow: hidden;
position: absolute;
height: 1px;
width: 1px;
left: -10000px;
top: auto;
}

.i-amphtml-consent-ui-fill {
position: absolute;
top: 0;
Expand Down
79 changes: 79 additions & 0 deletions extensions/amp-consent/0.1/consent-ui.js
Expand Up @@ -24,6 +24,7 @@ import {
insertAfterOrAtStart,
isAmpElement,
removeElement,
tryFocus,
whenUpgradedToCustomElement,
} from '../../../src/dom';
import {getConsentStateValue} from './consent-info';
Expand All @@ -37,6 +38,8 @@ const TAG = 'amp-consent-ui';
const CONSENT_STATE_MANAGER = 'consentStateManager';
const DEFAULT_INITIAL_HEIGHT = '30vh';
const DEFAULT_ENABLE_BORDER = true;
const CONSENT_PROMPT_CAPTION = 'User Consent Prompt';
const BUTTON_ACTION_CAPTION = 'Focus Prompt';

// Classes for consent UI
export const consentUiClasses = {
Expand All @@ -48,6 +51,7 @@ export const consentUiClasses = {
placeholder: 'i-amphtml-consent-ui-placeholder',
mask: 'i-amphtml-consent-ui-mask',
enableBorder: 'i-amphtml-consent-ui-enable-border',
screenReaderDialog: 'i-amphtml-consent-alertdialog',
};

export class ConsentUI {
Expand Down Expand Up @@ -84,6 +88,19 @@ export class ConsentUI {
config['uiConfig'] &&
config['uiConfig']['overlay'] === true;

/** @private {string} */
this.consentPromptCaption_ =
(config['captions'] && config['captions']['consentPromptCaption']) ||
CONSENT_PROMPT_CAPTION;

/** @private {string} */
this.buttonActionCaption_ =
(config['captions'] && config['captions']['buttonActionCaption']) ||
BUTTON_ACTION_CAPTION;

/** @private {boolean} */
this.srAlertShown_ = false;

/** @private {boolean} */
this.restrictFullscreenOn_ = isExperimentOn(
baseInstance.win,
Expand All @@ -96,6 +113,9 @@ export class ConsentUI {
/** @private {?Element} */
this.maskElement_ = null;

/** @private {?Element} */
this.srAlert_ = null;

/** @private {?Element} */
this.elementWithFocusBeforeShowing_ = null;

Expand Down Expand Up @@ -206,6 +226,10 @@ export class ConsentUI {

this.maybeShowOverlay_();

// Create and append SR alert for the when iframe
// initially loads.
this.maybeShowSrAlert_();

this.showIframe_();

if (!this.isPostPrompt_ && !this.restrictFullscreenOn_) {
Expand Down Expand Up @@ -275,6 +299,8 @@ export class ConsentUI {

// Hide the overlay
this.maybeHideOverlay_();
// Remove the SR alert from DOM
this.maybeRemoveSrAlert_();
// Enable the scroll, in case we were fullscreen with no overlay
this.enableScroll_();
// Reset any animation styles set by style attribute
Expand Down Expand Up @@ -552,6 +578,59 @@ export class ConsentUI {
removeElement(dev().assertElement(this.ui_));
}

/**
* If this is the first time viewing the iframe, create
* an 'invisible' alert dialog with a title and a button.
* Clicking on the button will transfer focus to the iframe.
*/
maybeShowSrAlert_() {
if (this.restrictFullscreenOn_) {
// If the SR alert has been shown, don't show it again
if (this.srAlertShown_) {
return;
}

const alertDialog = this.document_.createElement('div');
const button = this.document_.createElement('button');
const titleDiv = this.document_.createElement('div');

alertDialog.setAttribute('role', 'alertdialog');

titleDiv.textContent = this.consentPromptCaption_;
button.textContent = this.buttonActionCaption_;
button.onclick = () => {
tryFocus(dev().assertElement(this.ui_));
};

alertDialog.appendChild(titleDiv);
alertDialog.appendChild(button);

// Style to be visiblly hidden, but not hidden from the SR
const {classList} = alertDialog;
classList.add(consentUiClasses.screenReaderDialog);

this.baseInstance_.element.appendChild(alertDialog);
tryFocus(button);

// SR alert was shown when consent prompt loaded for
// the first time. Don't show it again
this.srAlertShown_ = true;

// Keep reference of the SR alert to remove later
this.srAlert_ = alertDialog;
}
}

/**
* Remove the SR alert from the DOM once it has been shown once
*/
maybeRemoveSrAlert_() {
if (this.srAlert_) {
removeElement(this.srAlert_);
delete this.srAlert_;
}
}

/**
* Reset transition and transform styles
* Set by the fixed layer, and us
Expand Down
106 changes: 102 additions & 4 deletions extensions/amp-consent/0.1/test/test-consent-ui.js
Expand Up @@ -304,8 +304,7 @@ describes.realWin(
toggleExperiment(win, 'amp-consent-restrict-fullscreen', false);
});

it('should not change document.activeElement', () => {
const {activeElement} = doc;
it('should focus on the SR alert button', () => {
consentUI = new ConsentUI(mockInstance, {
'promptUISrc': 'https//promptUISrc',
});
Expand All @@ -314,9 +313,100 @@ describes.realWin(
consentUI.show(false);
consentUI.iframeReady_.resolve();

return whenCalled(showIframeSpy).then(() =>
expect(activeElement).to.equal(doc.activeElement)
return whenCalled(showIframeSpy).then(() => {
const srButton = consentUI.srAlert_.children[1];
expect(doc.activeElement).to.equal(srButton);
});
});

it('should style SR dialog correctly', () => {
consentUI = new ConsentUI(mockInstance, {
'promptUISrc': 'https//promptUISrc',
});
const showIframeSpy = env.sandbox.spy(consentUI, 'showIframe_');

consentUI.show(false);
consentUI.iframeReady_.resolve();

return whenCalled(showIframeSpy).then(() => {
expect(consentUI.srAlert_.classList[0]).to.equal(
'i-amphtml-consent-alertdialog'
);
});
});

it('should append, remove, & not show the SR alert and have default titles', async () => {
consentUI = new ConsentUI(mockInstance, {
'promptUISrc': 'https//promptUISrc',
});

consentUI.show(false);
consentUI.iframeReady_.resolve();
await macroTask();

// iframe, placeholder, div, div, srAlert
const lastChild = consentUI.baseInstance_.element.children[4];
expect(lastChild).to.equal(consentUI.srAlert_);
expect(lastChild.hasAttribute('hidden')).to.be.false;
expect(lastChild.children[0].innerText).to.equal(
'User Consent Prompt'
);
expect(lastChild.children[1].innerText).to.equal('Focus Prompt');

consentUI.hide();
await macroTask();
// SR alert removed from DOM
expect(consentUI.srAlert_).to.be.undefined;
expect(consentUI.srAlertShown_).to.be.true;
// placeholder, div, div
expect(consentUI.baseInstance_.element.children.length).to.equal(3);

consentUI.show(false);
consentUI.iframeReady_.resolve();
await macroTask();
expect(consentUI.srAlertShown_).to.be.true;
// iframe, placeholder, div, div
expect(consentUI.baseInstance_.element.children.length).to.equal(4);
});

it('should have configurable captions', async () => {
const newConsentPromptCaption = 'New Consent Policy Title';
const newButtonActionCaption = 'New Button Action Caption';
consentUI = new ConsentUI(mockInstance, {
'promptUISrc': 'https//promptUISrc',
'captions': {
'consentPromptCaption': newConsentPromptCaption,
'buttonActionCaption': newButtonActionCaption,
},
});

consentUI.show(false);
consentUI.iframeReady_.resolve();
await macroTask();

// Get the last child of the amp-consent element
const dialog = consentUI.baseInstance_.element.children[4];
expect(dialog.children[0].innerText).to.equal(
newConsentPromptCaption
);
expect(dialog.children[1].innerText).to.equal(newButtonActionCaption);
});

it('should focus on iframe when button is clicked', function*() {
consentUI = new ConsentUI(mockInstance, {
'promptUISrc': 'https//promptUISrc',
});

consentUI.show(false);
consentUI.iframeReady_.resolve();
yield macroTask();

const button =
consentUI.baseInstance_.element.children[4].children[1];
const iframe = consentUI.ui_;
expect(doc.activeElement).to.equal(button);
button.click();
expect(doc.activeElement).to.equal(iframe);
});

it('should not expand if iframe is not in focus', async () => {
Expand Down Expand Up @@ -362,6 +452,14 @@ describes.realWin(
consentUI.iframeReady_.resolve();
await macroTask();

const button =
consentUI.baseInstance_.element.children[4].children[1];
const iframe = consentUI.ui_;
expect(doc.activeElement).to.equal(button);
button.click();
expect(doc.activeElement).to.equal(iframe);
await macroTask();

// not currently fullscreen
expect(consentUI.isFullscreen_).to.be.false;
expect(
Expand Down
9 changes: 9 additions & 0 deletions test/manual/amp-consent/amp-consent-cmp-overlay.html
Expand Up @@ -238,6 +238,10 @@ <h3>Image that is NOT blocked by consent</h3>
},
"uiConfig": {
"overlay": true
},
"captions": {
"consentPromptCaption": "Ping Consent Dialog",
"buttonActionCaption": "CLICK ME TO FOCUS"
}
}
</script>
Expand Down Expand Up @@ -357,6 +361,11 @@ <h1 itemprop="headline">Lorem Ipsum</h1>
<footer>
<div class="brand-logo">PublisherLogo</div>
</footer>
<script>
(self.AMP = self.AMP || []).push(function(AMP) {
AMP.toggleExperiment('amp-consent-restrict-fullscreen', true);
});
</script>
</body>

</html>

0 comments on commit c3cebfe

Please sign in to comment.