diff --git a/src/defaults/components.js b/src/defaults/components.js index df58ebf2..61f4775a 100644 --- a/src/defaults/components.js +++ b/src/defaults/components.js @@ -310,6 +310,7 @@ const defaults = { }, text: { title: 'cart', + countAccessibilityLabel: 'Number of items in your cart:', }, }, window: { diff --git a/src/styles/host/host.js b/src/styles/host/host.js index 49aef4c0..8bb6c773 100644 --- a/src/styles/host/host.js +++ b/src/styles/host/host.js @@ -1 +1 @@ -export default ".shopify-buy-modal-is-active {\n height: 100%;\n overflow: auto;\n}\n\n.shopify-buy-frame {\n display: inline-block\n}\n\n.shopify-buy-frame iframe {\n width: 100%;\n display: block;\n height: 0;\n overflow: hidden;\n }\n\n.shopify-buy-frame--cart {\n width: 100%;\n max-width: 350px;\n position: fixed;\n top: 0;\n right: 0;\n height: 100%;\n z-index: 2147483647;\n transform: translateX(100%);\n -webkit-transform: translateX(100%);\n visibility: hidden\n}\n\n.shopify-buy-frame--cart iframe {\n height: 100%;\n display: none\n }\n\n.shopify-buy-frame--cart iframe.is-block {\n display: block;\n }\n\n.shopify-buy-frame--cart.is-initialized {\n -webkit-transition: -webkit-transform 250ms cubic-bezier(0.165, 0.84, 0.44, 1);\n transition: -webkit-transform 250ms cubic-bezier(0.165, 0.84, 0.44, 1);\n transition: transform 250ms cubic-bezier(0.165, 0.84, 0.44, 1);\n transition: transform 250ms cubic-bezier(0.165, 0.84, 0.44, 1), -webkit-transform 250ms cubic-bezier(0.165, 0.84, 0.44, 1);\n }\n\n.shopify-buy-frame--cart.is-active {\n transform: translateX(0);\n -webkit-transform: translateX(0);\n }\n\n.shopify-buy-frame--cart.is-visible {\n visibility: visible;\n }\n\n.shopify-buy-frame--product {\n display: block\n}\n\n.shopify-buy-frame--product.shopify-buy__layout-horizontal {\n display: block;\n margin-left: auto;\n\n margin-right: auto\n }\n\n.shopify-buy-frame--product.shopify-buy__layout-horizontal iframe {\n max-width: 100%\n }\n\n@media (min-width: 950px) {\n\n.shopify-buy-frame--product.shopify-buy__layout-horizontal iframe {\n max-width: 950px;\n margin-left: auto;\n margin-right: auto\n }\n }\n\n.shopify-buy-frame--toggle {\n display: inline-block\n}\n\n.shopify-buy-frame--toggle:not(.is-sticky) {\n overflow: hidden;\n padding: 5px;\n }\n\n.shopify-buy-frame--toggle.is-sticky {\n display: none;\n position: fixed;\n right: 0;\n top: 50%;\n transform: translateY(-50%);\n -webkit-transform: translateY(-50%);\n z-index: 2147483645;\n }\n\n.shopify-buy-frame--toggle.is-active.is-sticky {\n display: block;\n }\n\n.is-active .shopify-buy-frame--toggle iframe {\n min-height: 67px;\n }\n\n.shopify-buy-frame--productSet {\n width: 100%;\n}\n\n.shopify-buy-frame--modal {\n position: fixed;\n width: 100%;\n height: 100%;\n top: 0;\n left: 0;\n z-index: 2147483646;\n display: none;\n -webkit-transition: background 300ms ease;\n transition: background 300ms ease\n}\n\n.shopify-buy-frame--modal iframe {\n height: 100%;\n width: 100%;\n max-width: none;\n }\n\n.shopify-buy-frame--modal.is-active {\n background: rgba(0,0,0,0.6);\n }\n\n.shopify-buy-frame--modal.is-block {\n display: block;\n }\n" \ No newline at end of file +export default ".shopify-buy-modal-is-active {\n height: 100%;\n overflow: auto;\n}\n\n.shopify-buy-frame {\n display: inline-block\n}\n\n.shopify-buy-frame iframe {\n width: 100%;\n display: block;\n height: 0;\n overflow: hidden;\n }\n\n.shopify-buy-frame--cart {\n width: 100%;\n max-width: 350px;\n position: fixed;\n top: 0;\n right: 0;\n height: 100%;\n z-index: 2147483647;\n transform: translateX(100%);\n -webkit-transform: translateX(100%);\n visibility: hidden\n}\n\n.shopify-buy-frame--cart iframe {\n height: 100%;\n display: none\n }\n\n.shopify-buy-frame--cart iframe.is-block {\n display: block;\n }\n\n.shopify-buy-frame--cart.is-initialized {\n -webkit-transition: -webkit-transform 250ms cubic-bezier(0.165, 0.84, 0.44, 1);\n transition: -webkit-transform 250ms cubic-bezier(0.165, 0.84, 0.44, 1);\n transition: transform 250ms cubic-bezier(0.165, 0.84, 0.44, 1);\n transition: transform 250ms cubic-bezier(0.165, 0.84, 0.44, 1), -webkit-transform 250ms cubic-bezier(0.165, 0.84, 0.44, 1);\n }\n\n.shopify-buy-frame--cart.is-active {\n transform: translateX(0);\n -webkit-transform: translateX(0);\n }\n\n.shopify-buy-frame--cart.is-visible {\n visibility: visible;\n }\n\n.shopify-buy-frame--product {\n display: block\n}\n\n.shopify-buy-frame--product.shopify-buy__layout-horizontal {\n display: block;\n margin-left: auto;\n\n margin-right: auto\n }\n\n.shopify-buy-frame--product.shopify-buy__layout-horizontal iframe {\n max-width: 100%\n }\n\n@media (min-width: 950px) {\n\n.shopify-buy-frame--product.shopify-buy__layout-horizontal iframe {\n max-width: 950px;\n margin-left: auto;\n margin-right: auto\n }\n }\n\n.shopify-buy-frame--toggle {\n display: inline-block\n}\n\n.shopify-buy-frame--toggle:not(.is-sticky) {\n overflow: hidden;\n padding: 5px;\n }\n\n.shopify-buy-frame--toggle.is-sticky {\n display: none;\n position: fixed;\n right: 0;\n top: 50%;\n transform: translateY(-50%);\n -webkit-transform: translateY(-50%);\n z-index: 2147483645;\n }\n\n.shopify-buy-frame--toggle.is-active.is-sticky {\n display: block;\n }\n\n.is-active .shopify-buy-frame--toggle iframe {\n min-height: 67px;\n }\n\n.shopify-buy-frame--productSet {\n width: 100%;\n}\n\n.shopify-buy-frame--modal {\n position: fixed;\n width: 100%;\n height: 100%;\n top: 0;\n left: 0;\n z-index: 2147483646;\n display: none;\n -webkit-transition: background 300ms ease;\n transition: background 300ms ease\n}\n\n.shopify-buy-frame--modal iframe {\n height: 100%;\n width: 100%;\n max-width: none;\n }\n\n.shopify-buy-frame--modal.is-active {\n background: rgba(0,0,0,0.6);\n }\n\n.shopify-buy-frame--modal.is-block {\n display: block;\n }\n\n.shopify-buy--visually-hidden {\n position: absolute !important;\n clip: rect(1px, 1px, 1px, 1px);\n padding:0 !important;\n border:0 !important;\n height: 1px !important;\n width: 1px !important;\n overflow: hidden;\n}\n" \ No newline at end of file diff --git a/src/styles/host/sass/host.css b/src/styles/host/sass/host.css index 2a9350e6..074ccfa0 100644 --- a/src/styles/host/sass/host.css +++ b/src/styles/host/sass/host.css @@ -133,3 +133,13 @@ display: block; } } + +.shopify-buy--visually-hidden { + position: absolute !important; + clip: rect(1px, 1px, 1px, 1px); + padding:0 !important; + border:0 !important; + height: 1px !important; + width: 1px !important; + overflow: hidden; +} diff --git a/src/views/toggle.js b/src/views/toggle.js index 8c34b166..fea15c5b 100644 --- a/src/views/toggle.js +++ b/src/views/toggle.js @@ -31,6 +31,21 @@ export default class ToggleView extends View { return `
${this.component.options.text.title}
`; } + get accessibilityLabel() { + return `${this.component.options.text.title}`; + } + + get countAccessibilityLabel() { + if (!this.component.options.contents.count) { + return ''; + } + return `${this.component.options.text.countAccessibilityLabel} ${this.component.count}`; + } + + get summaryHtml() { + return `${this.accessibilityLabel}${this.countAccessibilityLabel}`; + } + render() { super.render(); if (this.component.options.sticky) { @@ -44,8 +59,9 @@ export default class ToggleView extends View { if (this.iframe) { this.iframe.parent.setAttribute('tabindex', 0); this.iframe.parent.setAttribute('role', 'button'); - this.iframe.parent.setAttribute('aria-label', this.component.options.text.title); + this.iframe.el.setAttribute('aria-hidden', true); this.resize(); + this.node.insertAdjacentHTML('afterbegin', this.summaryHtml); } } diff --git a/test/unit/toggle/toggle-view.js b/test/unit/toggle/toggle-view.js index 03b81260..f599a10c 100644 --- a/test/unit/toggle/toggle-view.js +++ b/test/unit/toggle/toggle-view.js @@ -1,5 +1,7 @@ import Toggle from '../../../src/components/toggle'; import View from '../../../src/view'; +import { assert } from 'chai'; +import iframe from '../../../src/iframe'; describe('Toggle View class', () => { let toggle; @@ -25,6 +27,8 @@ describe('Toggle View class', () => { let addClassStub; let removeClassStub; let resizeStub; + let insertAdjacentHtmlStub; + const summaryHtml = 'summary'; beforeEach(() => { superRenderStub = sinon.stub(View.prototype, 'render'); @@ -34,6 +38,11 @@ describe('Toggle View class', () => { toggle.view = Object.defineProperty(toggle.view, 'isVisible', { writable: true, }); + toggle.view = Object.defineProperty(toggle.view, 'summaryHtml', { + writable: true, + value: summaryHtml, + }); + insertAdjacentHtmlStub = sinon.stub(toggle.view.node, 'insertAdjacentHTML'); }); afterEach(() => { @@ -41,6 +50,7 @@ describe('Toggle View class', () => { addClassStub.restore(); removeClassStub.restore(); resizeStub.restore(); + insertAdjacentHtmlStub.restore(); }); it('calls super\'s render()', () => { @@ -75,36 +85,47 @@ describe('Toggle View class', () => { }); describe('when iframe exists', () => { - let setAttributeSpy; + let parentSetAttributeSpy; + let elSetAttributeSpy; beforeEach(() => { - setAttributeSpy = sinon.spy(); + parentSetAttributeSpy = sinon.spy(); + elSetAttributeSpy = sinon.spy(); toggle.view.iframe = { parent: { - setAttribute: setAttributeSpy, + setAttribute: parentSetAttributeSpy, }, + el: { + setAttribute: elSetAttributeSpy, + } }; toggle.view.render(); }); - it('updates three attributes', () => { - assert.calledThrice(setAttributeSpy); + it('updates two attributes on the iframe\'s parent and one attribute on the iframe\'s el', () => { + assert.calledTwice(parentSetAttributeSpy); + assert.calledOnce(elSetAttributeSpy); }); it('sets tabindex of iframe\'s parent to zero', () => { - assert.calledWith(setAttributeSpy.getCall(0), 'tabindex', 0); + assert.calledWith(parentSetAttributeSpy.getCall(0), 'tabindex', 0); }); it('sets role of iframe\'s parent to button', () => { - assert.calledWith(setAttributeSpy.getCall(1), 'role', 'button'); + assert.calledWith(parentSetAttributeSpy.getCall(1), 'role', 'button'); }); - it('sets aria-label of iframe\'s parent to text title', () => { - assert.calledWith(setAttributeSpy.getCall(2), 'aria-label', toggle.options.text.title); + it('sets aria-hidden to true on the iframe\'s el', () => { + assert.calledWith(elSetAttributeSpy.getCall(0), 'aria-hidden', true); }); it('resizes view', () => { assert.calledOnce(resizeStub); }); + + it('inserts the summaryHtml at the beginning of the node', () => { + assert.calledOnce(insertAdjacentHtmlStub); + assert.calledWith(insertAdjacentHtmlStub, 'afterbegin', summaryHtml); + }); }); }); @@ -284,5 +305,55 @@ describe('Toggle View class', () => { assert.equal(toggle.view.readableLabel, `${toggle.options.text.title}
`); }); }); + + describe('accessibilityLabel', () => { + it('returns the title wrapped in a span', () => { + assert.equal(toggle.view.accessibilityLabel, `${toggle.options.text.title}`) + }); + }); + + describe('countAccessibilityLabel', () => { + it('returns an empty string if count is false in the options contents', () => { + toggle.config.toggle = { + contents: {count: false}, + text: {countAccessibilityLabel: 'count label'}, + }; + + assert.equal(toggle.view.countAccessibilityLabel, ''); + }); + + it('returns the accessibililty label with the count wrapped in a span if count is true in the options contents', () => { + const count = 2; + toggle = Object.defineProperty(toggle, 'count', { + writable: true, + }); + toggle.count = count; + toggle.config.toggle = { + contents: {count: true}, + text: {countAccessibilityLabel: 'count label'}, + }; + + assert.equal(toggle.view.countAccessibilityLabel, `${toggle.options.text.countAccessibilityLabel} ${count}`); + }); + }); + + describe('summaryHtml', () => { + it('returns the accessibilityLabel and countAccessibilityLabel wrapped in a visually hidden span', () => { + const accessibilityLabel = 'accessibility label'; + const countAccessibilityLabel = 'count accessibility label'; + + toggle.view = Object.defineProperty(toggle.view, 'accessibilityLabel', { + writable: true, + value: accessibilityLabel, + }); + toggle.view = Object.defineProperty(toggle.view, 'countAccessibilityLabel', { + writable: true, + value: countAccessibilityLabel, + }); + + assert.equal(toggle.view.summaryHtml, `${accessibilityLabel}${countAccessibilityLabel}`) + }); + }); + }); });