From 0c024cb6d7eb52f891b82032ba34fc61669c658a Mon Sep 17 00:00:00 2001 From: Stephen King Date: Fri, 2 Feb 2018 22:00:20 -0700 Subject: [PATCH 1/4] Missing head element --- docs/index.html | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/index.html b/docs/index.html index 0a5d941..7b1e3de 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,9 +1,11 @@ - - - react-toggle - + + + + react-toggle + +
From e8c7aea8989c1a6d03109517e2882ffae20baa03 Mon Sep 17 00:00:00 2001 From: Stephen King Date: Tue, 6 Feb 2018 08:23:05 -0700 Subject: [PATCH 2/4] Create checkForLabel, hasParentLabel, hasIdThatMatchesLabelFor, hasAriaLabelOrAraiLabelledbyAttr utils --- src/component/util.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/component/util.js b/src/component/util.js index 44b2c19..0d0d270 100644 --- a/src/component/util.js +++ b/src/component/util.js @@ -18,3 +18,26 @@ export function pointerCoord (event) { } return { x: 0, y: 0 } } + +export function checkForLabel (toggleContainer, input) { + return (hasParentLabel(toggleContainer, 'LABEL') || hasIdThatMatchesLabelFor(input) || hasAriaLabelOrAriaLabelledbyAttr(input)) || + console.warn('There seems to not be any associated label for the react-toggle element. https://www.w3.org/standards/webdesign/accessibility') +} + +export function hasParentLabel (element, tagName) { + return (element.parentNode && element.parentNode.nodeName === tagName) + ? true + : (!element.parentNode) + ? false + : hasParentLabel(element.parentNode, tagName) +} + +export function hasIdThatMatchesLabelFor (element, document = window.document) { + return element.id + ? Array.from(document.querySelectorAll('label')).some(label => label.htmlFor === element.id) + : false +} + +export function hasAriaLabelOrAriaLabelledbyAttr (element) { + return !!element.getAttribute('aria-label') || !!element.getAttribute('aria-labelledby') +} From fa9102906da06681259115d1fd31c7c05d426d50 Mon Sep 17 00:00:00 2001 From: Stephen King Date: Tue, 6 Feb 2018 08:24:03 -0700 Subject: [PATCH 3/4] Test hasParentLabel, hasIdThatMatchesLabelFor, and hasAriaLabelOrAriaLabelledbyAttr utils --- spec/util.spec.js | 179 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 spec/util.spec.js diff --git a/spec/util.spec.js b/spec/util.spec.js new file mode 100644 index 0000000..7eed09c --- /dev/null +++ b/spec/util.spec.js @@ -0,0 +1,179 @@ + +import { expect } from 'chai' +import { hasParentLabel, hasIdThatMatchesLabelFor, hasAriaLabelOrAriaLabelledbyAttr, checkForLabel } from '../src/component/util' + +describe('utils', () => { + + describe('hasParentLabel util', () => { + + it('will return false if there isn\'t a node.parentNode', () => { + // Arrange + const mockElement = { + parentNode: null, + } + + // Act + const result = hasParentLabel(mockElement) + + // Assert + expect(result).to.be.false + }) + + it('will return true if there is a node.parentNode and', () => { + // Arrange + const mockTagName = 'David Bowie' + const mockElement = { + parentNode: { + nodeName: 'David Bowie', + }, + } + + // Act + const result = hasParentLabel(mockElement, mockTagName) + + // Assert + expect(result).to.be.true + }) + + it('will recursively call itself until there is no parentNode', () => { + // Arrange + const mockTagName = 'Prince' + const mockElement = { + parentNode: { + nodeName: 'David Bowie', + parentNode: { + nodeName: 'Billy Corgan', + parentNode: { + nodeName: 'Jack White', + parentNode: null, + }, + }, + }, + } + + // Act + const result = hasParentLabel(mockElement, mockTagName) + + // Assert + expect(result).to.be.false + }) + + it('will recursively call itself until the parentNode.nodeName equals the given tag name', () => { + // Arrange + const mockTagName = 'Prince' + const mockElement = { + parentNode: { + nodeName: 'David Bowie', + parentNode: { + nodeName: 'Billy Corgan', + parentNode: { + nodeName: 'Jack White', + parentNode: { + nodeName: 'Prince', + }, + }, + }, + }, + } + + // Act + const result = hasParentLabel(mockElement, mockTagName) + + // Assert + expect(result).to.be.true + }) + }) // End describe hasParentlabel util + + describe('hasIdThatMatchesLabelFor util', () => { + + it('will return false if the node does not have an id', () => { + // Arrange + const mockElement = { + id: '', + } + + // Act + const result = hasIdThatMatchesLabelFor(mockElement) + + // Assert + expect(result).to.be.false + }) + + it('will return false if node.id is not in any nodes returned by document.querySelectorAll', () => { + // Arrange + const mockElement = { + id: 'space-oddity', + } + const mockDocumentObj = { + querySelectorAll: () => [], + } + + // Act + const result = hasIdThatMatchesLabelFor(mockElement, mockDocumentObj) + + // Assert + expect(result).to.be.false + }) + + it('will return true if node.id is in any nodes returned by document.querySelectorAll', () => { + // Arrange + const mockElement = { + id: 'raspberry-beret', + } + const mockLabelNode = { + htmlFor: 'raspberry-beret', + } + const mockDocumentObj = { + querySelectorAll: () => [mockLabelNode], + } + + // Act + const result = hasIdThatMatchesLabelFor(mockElement, mockDocumentObj) + + // Assert + expect(result).to.be.true + }) + }) + + describe('hasAriaLabelOrAriaLabelledbyAttr util', () => { + + it('will return false if the element does not have the aria-label or aria-labelledby attribute', () => { + // Arrange + const mockElement = { + getAttribute: () => null, + } + + // Act + const result = hasAriaLabelOrAriaLabelledbyAttr(mockElement) + + // Assert + expect(result).to.be.false + }) + + it('will return true if the element has the aria-label attribute', () => { + // Arrange + const mockElement = { + getAttribute: (attr) => attr === 'aria-label', + } + + // Act + const result = hasAriaLabelOrAriaLabelledbyAttr(mockElement) + + // Assert + expect(result).to.be.true + }) + + it('will return true if the element has the aria-labelledby attribute', () => { + // Arrange + const mockElement = { + getAttribute: (attr) => attr === 'aria-labelledby', + } + + // Act + const result = hasAriaLabelOrAriaLabelledbyAttr(mockElement) + + // Assert + expect(result).to.be.true + }) + }) +}) From b6a09dc7c5bbc7490bf3cf1acfcf64fc7551069b Mon Sep 17 00:00:00 2001 From: Stephen King Date: Tue, 6 Feb 2018 08:26:51 -0700 Subject: [PATCH 4/4] Add ref to root toggle div, import checkForLabel util, use checkForLabel util in componentDidMount passing both refs --- src/component/index.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/component/index.js b/src/component/index.js index 859de19..5414306 100644 --- a/src/component/index.js +++ b/src/component/index.js @@ -3,7 +3,7 @@ import classNames from 'classnames' import PropTypes from 'prop-types' import Check from './check' import X from './x' -import { pointerCoord } from './util' +import { checkForLabel, pointerCoord } from './util' export default class Toggle extends PureComponent { constructor (props) { @@ -27,6 +27,10 @@ export default class Toggle extends PureComponent { } } + componentDidMount () { + checkForLabel(this.toggleContainer, this.input) + } + handleClick (event) { const checkbox = this.input if (event.target !== checkbox && !this.moved) { @@ -135,7 +139,8 @@ export default class Toggle extends PureComponent { onClick={this.handleClick} onTouchStart={this.handleTouchStart} onTouchMove={this.handleTouchMove} - onTouchEnd={this.handleTouchEnd}> + onTouchEnd={this.handleTouchEnd} + ref={ref => { this.toggleContainer = ref }}>
{this.getIcon('checked')}