From 941a5dca181c36db891bffd5abdbd7c83759a704 Mon Sep 17 00:00:00 2001 From: Jason Quense Date: Mon, 20 Jul 2020 13:20:43 -0400 Subject: [PATCH] feat: add aria-describedby modifier for tooltips (#842) * feat: add aria-describedby modifier for tooltips * add test --- src/usePopper.ts | 34 ++++++++++++++++++++- test/usePopperSpec.js | 70 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 99 insertions(+), 5 deletions(-) diff --git a/src/usePopper.ts b/src/usePopper.ts index 4edad3ad..05634231 100644 --- a/src/usePopper.ts +++ b/src/usePopper.ts @@ -59,6 +59,38 @@ export interface UsePopperState { state?: State; } +const ariaDescribedByModifier: Modifier<'ariaDescribedBy', undefined> = { + name: 'ariaDescribedBy', + enabled: true, + phase: 'afterWrite', + effect: ({ state }) => { + return () => { + const { reference, popper } = state.elements; + if ('removeAttribute' in reference) { + const ids = (reference.getAttribute('aria-describedby') || '') + .split(',') + .filter((id) => id.trim() !== popper.id); + + if (!ids.length) reference.removeAttribute('aria-describedby'); + else reference.setAttribute('aria-describedby', ids.join(',')); + } + }; + }, + fn: ({ state }) => { + const { popper, reference } = state.elements; + + const role = popper.getAttribute('role')?.toLowerCase(); + + if (popper.id && role === 'tooltip' && 'setAttribute' in reference) { + const ids = reference.getAttribute('aria-describedby'); + reference.setAttribute( + 'aria-describedby', + ids ? `${ids},${popper.id}` : popper.id, + ); + } + }, +}; + const EMPTY_MODIFIERS = [] as any; /** * Position an element relative some reference element using Popper.js @@ -159,7 +191,7 @@ function usePopper( ...config, placement, strategy, - modifiers: [...modifiers, updateModifier], + modifiers: [...modifiers, ariaDescribedByModifier, updateModifier], }); return () => { diff --git a/test/usePopperSpec.js b/test/usePopperSpec.js index 1c6fbea5..5dae5933 100644 --- a/test/usePopperSpec.js +++ b/test/usePopperSpec.js @@ -3,15 +3,16 @@ import React from 'react'; import usePopper from '../src/usePopper'; describe('usePopper', () => { - function renderHook(fn) { + function renderHook(fn, initialProps) { let result = { current: null }; - function Wrapper() { - result.current = fn(); + function Wrapper(props) { + result.current = fn(props); return null; } - result.mount = mount(); + result.mount = mount(); + result.update = (props) => result.mount.setProps(props); return result; } @@ -46,4 +47,65 @@ describe('usePopper', () => { done(); }); }); + + it('should add aria-describedBy for tooltips', (done) => { + elements.popper.setAttribute('role', 'tooltip'); + elements.popper.setAttribute('id', 'example123'); + + const result = renderHook(() => + usePopper(elements.reference, elements.popper), + ); + + setTimeout(() => { + expect( + document.querySelector('[aria-describedby="example123"]'), + ).to.equal(elements.reference); + + result.mount.unmount(); + + expect( + document.querySelector('[aria-describedby="example123"]'), + ).to.equal(null); + + done(); + }); + }); + + it('should add to existing describedBy', (done) => { + elements.popper.setAttribute('role', 'tooltip'); + elements.popper.setAttribute('id', 'example123'); + elements.reference.setAttribute('aria-describedby', 'foo, bar , baz '); + + const result = renderHook(() => + usePopper(elements.reference, elements.popper), + ); + + setTimeout(() => { + expect( + document.querySelector( + '[aria-describedby="foo, bar , baz ,example123"]', + ), + ).to.equal(elements.reference); + + result.mount.unmount(); + + expect( + document.querySelector('[aria-describedby="foo, bar , baz "]'), + ).to.equal(elements.reference); + + done(); + }); + }); + + it('should not aria-describedBy any other role', (done) => { + renderHook(() => usePopper(elements.reference, elements.popper)); + + setTimeout(() => { + expect( + document.querySelector('[aria-describedby="example123"]'), + ).to.equal(null); + + done(); + }); + }); });