Merge pull request #268 from ing-bank/fix/tooltip-a11y
Fix/tooltip a11y
This commit is contained in:
commit
1e698cf529
4 changed files with 118 additions and 35 deletions
|
|
@ -59,8 +59,8 @@ storiesOf('Local Overlay System|Popup', module)
|
||||||
</style>
|
</style>
|
||||||
<div class="demo-box">
|
<div class="demo-box">
|
||||||
<lion-popup .popperConfig="${{ placement: 'top' }}">
|
<lion-popup .popperConfig="${{ placement: 'top' }}">
|
||||||
<div slot="content" class="popup">Hello there!</div>
|
|
||||||
<lion-button slot="invoker">Popup</lion-button>
|
<lion-button slot="invoker">Popup</lion-button>
|
||||||
|
<div slot="content" class="popup">Hello there!</div>
|
||||||
</lion-popup>
|
</lion-popup>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
|
|
@ -73,20 +73,20 @@ storiesOf('Local Overlay System|Popup', module)
|
||||||
</style>
|
</style>
|
||||||
<div class="demo-box_placements">
|
<div class="demo-box_placements">
|
||||||
<lion-popup .popperConfig="${{ placement: 'top' }}">
|
<lion-popup .popperConfig="${{ placement: 'top' }}">
|
||||||
<div slot="content" class="popup">Its top placement</div>
|
|
||||||
<lion-button slot="invoker">Top</lion-button>
|
<lion-button slot="invoker">Top</lion-button>
|
||||||
|
<div slot="content" class="popup">Its top placement</div>
|
||||||
</lion-popup>
|
</lion-popup>
|
||||||
<lion-popup .popperConfig="${{ placement: 'right' }}">
|
<lion-popup .popperConfig="${{ placement: 'right' }}">
|
||||||
<div slot="content" class="popup">Its right placement</div>
|
|
||||||
<lion-button slot="invoker">Right</lion-button>
|
<lion-button slot="invoker">Right</lion-button>
|
||||||
|
<div slot="content" class="popup">Its right placement</div>
|
||||||
</lion-popup>
|
</lion-popup>
|
||||||
<lion-popup .popperConfig="${{ placement: 'bottom' }}">
|
<lion-popup .popperConfig="${{ placement: 'bottom' }}">
|
||||||
<div slot="content" class="popup">Its bottom placement</div>
|
|
||||||
<lion-button slot="invoker">Bottom</lion-button>
|
<lion-button slot="invoker">Bottom</lion-button>
|
||||||
|
<div slot="content" class="popup">Its bottom placement</div>
|
||||||
</lion-popup>
|
</lion-popup>
|
||||||
<lion-popup .popperConfig="${{ placement: 'left' }}">
|
<lion-popup .popperConfig="${{ placement: 'left' }}">
|
||||||
<div slot="content" class="popup">Its left placement</div>
|
|
||||||
<lion-button slot="invoker">Left</lion-button>
|
<lion-button slot="invoker">Left</lion-button>
|
||||||
|
<div slot="content" class="popup">Its left placement</div>
|
||||||
</lion-popup>
|
</lion-popup>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
|
|
@ -123,8 +123,8 @@ storiesOf('Local Overlay System|Popup', module)
|
||||||
},
|
},
|
||||||
})}"
|
})}"
|
||||||
>
|
>
|
||||||
<div slot="content" class="popup">${text('Content text', 'Hello, World!')}</div>
|
|
||||||
<lion-button slot="invoker">${text('Invoker text', 'Click me!')}</lion-button>
|
<lion-button slot="invoker">${text('Invoker text', 'Click me!')}</lion-button>
|
||||||
|
<div slot="content" class="popup">${text('Content text', 'Hello, World!')}</div>
|
||||||
</lion-popup>
|
</lion-popup>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,17 @@ import { LionPopup } from '@lion/popup';
|
||||||
import { overlays, LocalOverlayController } from '@lion/overlays';
|
import { overlays, LocalOverlayController } from '@lion/overlays';
|
||||||
|
|
||||||
export class LionTooltip extends LionPopup {
|
export class LionTooltip extends LionPopup {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.mouseActive = false;
|
||||||
|
this.keyActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this.contentNode = this.querySelector('[slot="content"]');
|
this.contentNode = this.querySelector('[slot="content"]');
|
||||||
this.invokerNode = this.querySelector('[slot="invoker"]');
|
this.invokerNode = this.querySelector('[slot="invoker"]');
|
||||||
|
this.contentNode.setAttribute('role', 'tooltip');
|
||||||
|
|
||||||
this._controller = overlays.add(
|
this._controller = overlays.add(
|
||||||
new LocalOverlayController({
|
new LocalOverlayController({
|
||||||
|
|
@ -16,20 +23,51 @@ export class LionTooltip extends LionPopup {
|
||||||
invokerNode: this.invokerNode,
|
invokerNode: this.invokerNode,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
this._show = () => this._controller.show();
|
|
||||||
this._hide = () => this._controller.hide();
|
|
||||||
|
|
||||||
this.invokerNode.addEventListener('mouseenter', this._show);
|
this.__resetActive = () => {
|
||||||
this.invokerNode.addEventListener('mouseleave', this._hide);
|
this.mouseActive = false;
|
||||||
this.invokerNode.addEventListener('focusin', this._show);
|
this.keyActive = false;
|
||||||
this.invokerNode.addEventListener('focusout', this._hide);
|
};
|
||||||
|
|
||||||
|
this.__showMouse = () => {
|
||||||
|
if (!this.keyActive) {
|
||||||
|
this.mouseActive = true;
|
||||||
|
this._controller.show();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.__hideMouse = () => {
|
||||||
|
if (!this.keyActive) {
|
||||||
|
this._controller.hide();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.__showKey = () => {
|
||||||
|
if (!this.mouseActive) {
|
||||||
|
this.keyActive = true;
|
||||||
|
this._controller.show();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.__hideKey = () => {
|
||||||
|
if (!this.mouseActive) {
|
||||||
|
this._controller.hide();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this._controller.addEventListener('hide', this.__resetActive);
|
||||||
|
this.addEventListener('mouseenter', this.__showMouse);
|
||||||
|
this.addEventListener('mouseleave', this.__hideMouse);
|
||||||
|
this.invokerNode.addEventListener('focusin', this.__showKey);
|
||||||
|
this.invokerNode.addEventListener('focusout', this.__hideKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
this.invokerNode.removeEventListener('mouseenter', this._show);
|
this._controller.removeEventListener('hide', this.__resetActive);
|
||||||
this.invokerNode.removeEventListener('mouseleave', this._hide);
|
this.removeEventListener('mouseenter', this.__showMouse);
|
||||||
this.invokerNode.removeEventListener('focusin', this._show);
|
this.removeEventListener('mouseleave', this._hideMouse);
|
||||||
this.invokerNode.removeEventListener('focusout', this._hide);
|
this.invokerNode.removeEventListener('focusin', this._showKey);
|
||||||
|
this.invokerNode.removeEventListener('focusout', this._hideKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,8 +59,8 @@ storiesOf('Local Overlay System|Tooltip', module)
|
||||||
</style>
|
</style>
|
||||||
<div class="demo-box">
|
<div class="demo-box">
|
||||||
<lion-tooltip .popperConfig=${{ placement: 'right' }}>
|
<lion-tooltip .popperConfig=${{ placement: 'right' }}>
|
||||||
<div slot="content" class="tooltip">Hello there!</div>
|
|
||||||
<lion-button slot="invoker">Tooltip</lion-button>
|
<lion-button slot="invoker">Tooltip</lion-button>
|
||||||
|
<div slot="content" class="tooltip">Hello there!</div>
|
||||||
</lion-tooltip>
|
</lion-tooltip>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
|
|
@ -73,20 +73,20 @@ storiesOf('Local Overlay System|Tooltip', module)
|
||||||
</style>
|
</style>
|
||||||
<div class="demo-box_placements">
|
<div class="demo-box_placements">
|
||||||
<lion-tooltip .popperConfig=${{ placement: 'top' }}>
|
<lion-tooltip .popperConfig=${{ placement: 'top' }}>
|
||||||
<div slot="content" class="tooltip">Its top placement</div>
|
|
||||||
<lion-button slot="invoker">Top</lion-button>
|
<lion-button slot="invoker">Top</lion-button>
|
||||||
|
<div slot="content" class="tooltip">Its top placement</div>
|
||||||
</lion-tooltip>
|
</lion-tooltip>
|
||||||
<lion-tooltip .popperConfig=${{ placement: 'right' }}>
|
<lion-tooltip .popperConfig=${{ placement: 'right' }}>
|
||||||
<div slot="content" class="tooltip">Its right placement</div>
|
|
||||||
<lion-button slot="invoker">Right</lion-button>
|
<lion-button slot="invoker">Right</lion-button>
|
||||||
|
<div slot="content" class="tooltip">Its right placement</div>
|
||||||
</lion-tooltip>
|
</lion-tooltip>
|
||||||
<lion-tooltip .popperConfig=${{ placement: 'bottom' }}>
|
<lion-tooltip .popperConfig=${{ placement: 'bottom' }}>
|
||||||
<div slot="content" class="tooltip">Its bottom placement</div>
|
|
||||||
<lion-button slot="invoker">Bottom</lion-button>
|
<lion-button slot="invoker">Bottom</lion-button>
|
||||||
|
<div slot="content" class="tooltip">Its bottom placement</div>
|
||||||
</lion-tooltip>
|
</lion-tooltip>
|
||||||
<lion-tooltip .popperConfig=${{ placement: 'left' }}>
|
<lion-tooltip .popperConfig=${{ placement: 'left' }}>
|
||||||
<div slot="content" class="tooltip">Its left placement</div>
|
|
||||||
<lion-button slot="invoker">Left</lion-button>
|
<lion-button slot="invoker">Left</lion-button>
|
||||||
|
<div slot="content" class="tooltip">Its left placement</div>
|
||||||
</lion-tooltip>
|
</lion-tooltip>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
|
|
@ -123,8 +123,8 @@ storiesOf('Local Overlay System|Tooltip', module)
|
||||||
},
|
},
|
||||||
})}"
|
})}"
|
||||||
>
|
>
|
||||||
<div slot="content" class="tooltip">${text('Content text', 'Hello, World!')}</div>
|
|
||||||
<lion-button slot="invoker">${text('Invoker text', 'Click me!')}</lion-button>
|
<lion-button slot="invoker">${text('Invoker text', 'Click me!')}</lion-button>
|
||||||
|
<div slot="content" class="tooltip">${text('Content text', 'Hello, World!')}</div>
|
||||||
</lion-tooltip>
|
</lion-tooltip>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
|
|
|
||||||
|
|
@ -21,15 +21,67 @@ describe('lion-tooltip', () => {
|
||||||
<lion-button slot="invoker">Tooltip button</lion-button>
|
<lion-button slot="invoker">Tooltip button</lion-button>
|
||||||
</lion-tooltip>
|
</lion-tooltip>
|
||||||
`);
|
`);
|
||||||
const invoker = el.querySelector('[slot="invoker"]');
|
|
||||||
const eventMouseEnter = new Event('mouseenter');
|
const eventMouseEnter = new Event('mouseenter');
|
||||||
invoker.dispatchEvent(eventMouseEnter);
|
el.dispatchEvent(eventMouseEnter);
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.querySelector('[slot="content"]').style.display).to.be.equal('inline-block');
|
||||||
|
const eventMouseLeave = new Event('mouseleave');
|
||||||
|
el.dispatchEvent(eventMouseLeave);
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.querySelector('[slot="content"]').style.display).to.be.equal('none');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show content on mouseenter and remain shown on focusout', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-tooltip>
|
||||||
|
<div slot="content">Hey there</div>
|
||||||
|
<lion-button slot="invoker">Tooltip button</lion-button>
|
||||||
|
</lion-tooltip>
|
||||||
|
`);
|
||||||
|
const eventMouseEnter = new Event('mouseenter');
|
||||||
|
el.dispatchEvent(eventMouseEnter);
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.querySelector('[slot="content"]').style.display).to.be.equal('inline-block');
|
||||||
|
const eventFocusOut = new Event('focusout');
|
||||||
|
el.dispatchEvent(eventFocusOut);
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.querySelector('[slot="content"]').style.display).to.be.equal('inline-block');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show content on focusin and hide on focusout', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-tooltip>
|
||||||
|
<div slot="content">Hey there</div>
|
||||||
|
<lion-button slot="invoker">Tooltip button</lion-button>
|
||||||
|
</lion-tooltip>
|
||||||
|
`);
|
||||||
|
const invoker = el.querySelector('[slot="invoker"]');
|
||||||
|
const eventFocusIn = new Event('focusin');
|
||||||
|
invoker.dispatchEvent(eventFocusIn);
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.querySelector('[slot="content"]').style.display).to.be.equal('inline-block');
|
||||||
|
const eventFocusOut = new Event('focusout');
|
||||||
|
invoker.dispatchEvent(eventFocusOut);
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.querySelector('[slot="content"]').style.display).to.be.equal('none');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show content on focusin and remain shown on mouseleave', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-tooltip>
|
||||||
|
<div slot="content">Hey there</div>
|
||||||
|
<lion-button slot="invoker">Tooltip button</lion-button>
|
||||||
|
</lion-tooltip>
|
||||||
|
`);
|
||||||
|
const invoker = el.querySelector('[slot="invoker"]');
|
||||||
|
const eventFocusIn = new Event('focusin');
|
||||||
|
invoker.dispatchEvent(eventFocusIn);
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.querySelector('[slot="content"]').style.display).to.be.equal('inline-block');
|
expect(el.querySelector('[slot="content"]').style.display).to.be.equal('inline-block');
|
||||||
const eventMouseLeave = new Event('mouseleave');
|
const eventMouseLeave = new Event('mouseleave');
|
||||||
invoker.dispatchEvent(eventMouseLeave);
|
invoker.dispatchEvent(eventMouseLeave);
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.querySelector('[slot="content"]').style.display).to.be.equal('none');
|
expect(el.querySelector('[slot="content"]').style.display).to.be.equal('inline-block');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should tooltip contains html when specified in tooltip content body', async () => {
|
it('should tooltip contains html when specified in tooltip content body', async () => {
|
||||||
|
|
@ -50,22 +102,15 @@ describe('lion-tooltip', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Accessibility', () => {
|
describe('Accessibility', () => {
|
||||||
it('should visible on focusin and hide on focusout', async () => {
|
it('should have a tooltip role set on the tooltip', async () => {
|
||||||
const el = await fixture(html`
|
const el = await fixture(html`
|
||||||
<lion-tooltip>
|
<lion-tooltip>
|
||||||
<div slot="content">Hey there</div>
|
<div slot="content">Hey there</div>
|
||||||
<lion-button slot="invoker">Tooltip button</lion-button>
|
<lion-button slot="invoker">Tooltip button</lion-button>
|
||||||
</lion-tooltip>
|
</lion-tooltip>
|
||||||
`);
|
`);
|
||||||
const invoker = el.querySelector('[slot="invoker"]');
|
const invoker = el.querySelector('[slot="content"]');
|
||||||
const eventFocusIn = new Event('focusin');
|
expect(invoker.getAttribute('role')).to.be.equal('tooltip');
|
||||||
invoker.dispatchEvent(eventFocusIn);
|
|
||||||
await el.updateComplete;
|
|
||||||
expect(el.querySelector('[slot="content"]').style.display).to.be.equal('inline-block');
|
|
||||||
const eventFocusOut = new Event('focusout');
|
|
||||||
invoker.dispatchEvent(eventFocusOut);
|
|
||||||
await el.updateComplete;
|
|
||||||
expect(el.querySelector('[slot="content"]').style.display).to.be.equal('none');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have aria-controls attribute set to the invoker', async () => {
|
it('should have aria-controls attribute set to the invoker', async () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue