fix(overlays): accessibility attrs setup/teardown
This commit is contained in:
parent
eb02b27010
commit
dfe1905e7c
5 changed files with 227 additions and 46 deletions
|
|
@ -61,6 +61,44 @@ export const placementGlobal = () => html`
|
||||||
`;
|
`;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## isTooltip (placementMode: 'local')
|
||||||
|
|
||||||
|
As specified in the [overlay rationale](/?path=/docs/overlays-system-rationale--page) there are only two official types of overlays: dialogs and tooltips. And their main differences are:
|
||||||
|
|
||||||
|
- Dialogs have a modal option, tooltips don’t
|
||||||
|
- Dialogs have interactive content, tooltips don’t
|
||||||
|
- Dialogs are opened via regular buttons (click/space/enter), tooltips act on focus/mouseover
|
||||||
|
|
||||||
|
Since most overlays have interactive content the default is set to dialogs. To get a tooltip, you can add `isTooltip` to the config object. This only works for local placement and it also needs to have `handlesAccessibility` activated to work.
|
||||||
|
|
||||||
|
```js preview-story
|
||||||
|
export const isTooltip = () => {
|
||||||
|
function showTooltip() {
|
||||||
|
const tooltip = document.querySelector('#tooltip');
|
||||||
|
tooltip.opened = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideTooltip() {
|
||||||
|
const tooltip = document.querySelector('#tooltip');
|
||||||
|
tooltip.opened = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<demo-overlay-system
|
||||||
|
id="tooltip"
|
||||||
|
.config=${{ placementMode: 'local', isTooltip: true, handlesAccessibility: true }}
|
||||||
|
>
|
||||||
|
<button slot="invoker" @mouseenter="${showTooltip}" @mouseleave="${hideTooltip}">
|
||||||
|
Hover me to open the tooltip!
|
||||||
|
</button>
|
||||||
|
<div slot="content" class="demo-overlay">
|
||||||
|
Hello!
|
||||||
|
</div>
|
||||||
|
</demo-overlay-system>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
## trapsKeyboardFocus
|
## trapsKeyboardFocus
|
||||||
|
|
||||||
Boolean property. When true, the focus will rotate through the **focusable elements** inside the `contentNode`.
|
Boolean property. When true, the focus will rotate through the **focusable elements** inside the `contentNode`.
|
||||||
|
|
@ -295,7 +333,7 @@ export const viewportConfig = () => html`
|
||||||
|
|
||||||
## popperConfig for local overlays (placementMode: 'local')
|
## popperConfig for local overlays (placementMode: 'local')
|
||||||
|
|
||||||
For locally DOM positioned overlays that position themselves relative to their invoker, we use <a href="https://popper.js.org/" target="_blank">Popper.js</a> for positioning.
|
For locally DOM positioned overlays that position themselves relative to their invoker, we use [Popper.js](https://popper.js.org/) for positioning.
|
||||||
|
|
||||||
> In Popper, `contentNode` is often referred to as `popperElement`, and `invokerNode` is often referred to as the `referenceElement`.
|
> In Popper, `contentNode` is often referred to as `popperElement`, and `invokerNode` is often referred to as the `referenceElement`.
|
||||||
|
|
||||||
|
|
@ -306,7 +344,7 @@ Features:
|
||||||
|
|
||||||
> Popper strictly is scoped on positioning. **It does not change the dimensions of the content node nor the invoker node**.
|
> Popper strictly is scoped on positioning. **It does not change the dimensions of the content node nor the invoker node**.
|
||||||
> This also means that if you use the arrow feature, you are in charge of styling it properly, use the x-placement attribute for this.
|
> This also means that if you use the arrow feature, you are in charge of styling it properly, use the x-placement attribute for this.
|
||||||
> An example implementation can be found in [lion-tooltip](?path=/docs/overlays-tooltip), where an arrow is set by default.
|
> An example implementation can be found in [lion-tooltip](?path=/docs/overlays-tooltip--main#tooltip), where an arrow is set by default.
|
||||||
|
|
||||||
To override the default options we set for local mode, you add a `popperConfig` object to the config passed to the OverlayController.
|
To override the default options we set for local mode, you add a `popperConfig` object to the config passed to the OverlayController.
|
||||||
Here's a succinct overview of some often used popper properties:
|
Here's a succinct overview of some often used popper properties:
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,7 @@ export class OverlayController {
|
||||||
hidesOnOutsideEsc: false,
|
hidesOnOutsideEsc: false,
|
||||||
hidesOnOutsideClick: false,
|
hidesOnOutsideClick: false,
|
||||||
isTooltip: false,
|
isTooltip: false,
|
||||||
|
invokerRelation: 'description',
|
||||||
handlesUserInteraction: false,
|
handlesUserInteraction: false,
|
||||||
handlesAccessibility: false,
|
handlesAccessibility: false,
|
||||||
popperConfig: {
|
popperConfig: {
|
||||||
|
|
@ -134,7 +135,7 @@ export class OverlayController {
|
||||||
|
|
||||||
this.manager.add(this);
|
this.manager.add(this);
|
||||||
this._contentId = `overlay-content--${Math.random().toString(36).substr(2, 10)}`;
|
this._contentId = `overlay-content--${Math.random().toString(36).substr(2, 10)}`;
|
||||||
|
this.__originalAttrs = new Map();
|
||||||
if (this._defaultConfig.contentNode) {
|
if (this._defaultConfig.contentNode) {
|
||||||
if (!this._defaultConfig.contentNode.isConnected) {
|
if (!this._defaultConfig.contentNode.isConnected) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|
@ -236,18 +237,32 @@ export class OverlayController {
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
__validateConfiguration(newConfig) {
|
__validateConfiguration(newConfig) {
|
||||||
if (!newConfig.placementMode) {
|
if (!newConfig.placementMode) {
|
||||||
throw new Error('You need to provide a .placementMode ("global"|"local")');
|
throw new Error(
|
||||||
|
'[OverlayController] You need to provide a .placementMode ("global"|"local")',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (!['global', 'local'].includes(newConfig.placementMode)) {
|
if (!['global', 'local'].includes(newConfig.placementMode)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`"${newConfig.placementMode}" is not a valid .placementMode, use ("global"|"local")`,
|
`[OverlayController] "${newConfig.placementMode}" is not a valid .placementMode, use ("global"|"local")`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!newConfig.contentNode) {
|
if (!newConfig.contentNode) {
|
||||||
throw new Error('You need to provide a .contentNode');
|
throw new Error('[OverlayController] You need to provide a .contentNode');
|
||||||
}
|
}
|
||||||
if (this.__isContentNodeProjected && !newConfig.contentWrapperNode) {
|
if (this.__isContentNodeProjected && !newConfig.contentWrapperNode) {
|
||||||
throw new Error('You need to provide a .contentWrapperNode when .contentNode is projected');
|
throw new Error(
|
||||||
|
'[OverlayController] You need to provide a .contentWrapperNode when .contentNode is projected',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (newConfig.isTooltip && newConfig.placementMode !== 'local') {
|
||||||
|
throw new Error(
|
||||||
|
'[OverlayController] .isTooltip should be configured with .placementMode "local"',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (newConfig.isTooltip && !newConfig.handlesAccessibility) {
|
||||||
|
throw new Error(
|
||||||
|
'[OverlayController] .isTooltip only takes effect when .handlesAccessibility is enabled',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// if (newConfig.popperConfig.modifiers.arrow && !newConfig.contentWrapperNode) {
|
// if (newConfig.popperConfig.modifiers.arrow && !newConfig.contentWrapperNode) {
|
||||||
// throw new Error('You need to provide a .contentWrapperNode when Popper arrow is enabled');
|
// throw new Error('You need to provide a .contentWrapperNode when Popper arrow is enabled');
|
||||||
|
|
@ -257,9 +272,6 @@ export class OverlayController {
|
||||||
async _init({ cfgToAdd }) {
|
async _init({ cfgToAdd }) {
|
||||||
this.__initcontentWrapperNode({ cfgToAdd });
|
this.__initcontentWrapperNode({ cfgToAdd });
|
||||||
this.__initConnectionTarget();
|
this.__initConnectionTarget();
|
||||||
if (this.handlesAccessibility) {
|
|
||||||
this.__initAccessibility({ cfgToAdd });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.placementMode === 'local') {
|
if (this.placementMode === 'local') {
|
||||||
// Lazily load Popper if not done yet
|
// Lazily load Popper if not done yet
|
||||||
|
|
@ -339,27 +351,58 @@ export class OverlayController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
__initAccessibility() {
|
__setupTeardownAccessibility({ phase }) {
|
||||||
// TODO: remove a11y attributes on teardown
|
if (phase === 'init') {
|
||||||
if (!this.contentNode.id) {
|
this.__storeOriginalAttrs(this.contentNode, ['role', 'id']);
|
||||||
this.contentNode.setAttribute('id', this._contentId);
|
this.__storeOriginalAttrs(this.invokerNode, [
|
||||||
|
'aria-expanded',
|
||||||
|
'aria-labelledby',
|
||||||
|
'aria-describedby',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!this.contentNode.id) {
|
||||||
|
this.contentNode.setAttribute('id', this._contentId);
|
||||||
|
}
|
||||||
|
if (this.isTooltip) {
|
||||||
|
if (this.invokerNode) {
|
||||||
|
this.invokerNode.setAttribute(
|
||||||
|
this.invokerRelation === 'label' ? 'aria-labelledby' : 'aria-describedby',
|
||||||
|
this._contentId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.contentNode.setAttribute('role', 'tooltip');
|
||||||
|
} else {
|
||||||
|
if (this.invokerNode) {
|
||||||
|
this.invokerNode.setAttribute('aria-expanded', this.isShown);
|
||||||
|
}
|
||||||
|
if (!this.contentNode.role) {
|
||||||
|
this.contentNode.setAttribute('role', 'dialog');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (phase === 'teardown') {
|
||||||
|
this.__restorOriginalAttrs();
|
||||||
}
|
}
|
||||||
if (this.isTooltip) {
|
}
|
||||||
if (this.invokerNode) {
|
|
||||||
this.invokerNode.setAttribute(
|
__storeOriginalAttrs(node, attrs) {
|
||||||
this.invokerRelation === 'label' ? 'aria-labelledby' : 'aria-describedby',
|
const attrMap = {};
|
||||||
this._contentId,
|
attrs.forEach(attrName => {
|
||||||
);
|
attrMap[attrName] = node.getAttribute(attrName);
|
||||||
}
|
});
|
||||||
this.contentNode.setAttribute('role', 'tooltip');
|
this.__originalAttrs.set(node, attrMap);
|
||||||
} else {
|
}
|
||||||
if (this.invokerNode) {
|
|
||||||
this.invokerNode.setAttribute('aria-expanded', this.isShown);
|
__restorOriginalAttrs() {
|
||||||
}
|
for (const [node, attrMap] of this.__originalAttrs) {
|
||||||
if (!this.contentNode.role) {
|
Object.entries(attrMap).forEach(([attrName, value]) => {
|
||||||
this.contentNode.setAttribute('role', 'dialog');
|
if (value !== null) {
|
||||||
}
|
node.setAttribute(attrName, value);
|
||||||
|
} else {
|
||||||
|
node.removeAttribute(attrName);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
this.__originalAttrs.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
get isShown() {
|
get isShown() {
|
||||||
|
|
@ -772,6 +815,9 @@ export class OverlayController {
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleAccessibility({ phase }) {
|
_handleAccessibility({ phase }) {
|
||||||
|
if (phase === 'init' || phase === 'teardown') {
|
||||||
|
this.__setupTeardownAccessibility({ phase });
|
||||||
|
}
|
||||||
if (this.invokerNode && !this.isTooltip) {
|
if (this.invokerNode && !this.isTooltip) {
|
||||||
this.invokerNode.setAttribute('aria-expanded', phase === 'show');
|
this.invokerNode.setAttribute('aria-expanded', phase === 'show');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@
|
||||||
* @property {boolean} [isTooltip=false] has a totally different interaction- and accessibility
|
* @property {boolean} [isTooltip=false] has a totally different interaction- and accessibility
|
||||||
* pattern from all other overlays. Will behave as role="tooltip" element instead of a role="dialog"
|
* pattern from all other overlays. Will behave as role="tooltip" element instead of a role="dialog"
|
||||||
* element.
|
* element.
|
||||||
|
* @property {'label'|'description'} [invokerRelation='description']
|
||||||
* @property {boolean} [handlesAccessibility]
|
* @property {boolean} [handlesAccessibility]
|
||||||
* For non `isTooltip`:
|
* For non `isTooltip`:
|
||||||
* - sets aria-expanded="true/false" and aria-haspopup="true" on invokerNode
|
* - sets aria-expanded="true/false" and aria-haspopup="true" on invokerNode
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,12 @@ import { simulateTab } from '../src/utils/simulate-tab.js';
|
||||||
|
|
||||||
const withGlobalTestConfig = () => ({
|
const withGlobalTestConfig = () => ({
|
||||||
placementMode: 'global',
|
placementMode: 'global',
|
||||||
contentNode: fixtureSync(html` <div>my content</div> `),
|
contentNode: fixtureSync(html`<div>my content</div>`),
|
||||||
});
|
});
|
||||||
|
|
||||||
const withLocalTestConfig = () => ({
|
const withLocalTestConfig = () => ({
|
||||||
placementMode: 'local',
|
placementMode: 'local',
|
||||||
contentNode: fixtureSync(html` <div>my content</div> `),
|
contentNode: fixtureSync(html`<div>my content</div>`),
|
||||||
invokerNode: fixtureSync(html`
|
invokerNode: fixtureSync(html`
|
||||||
<div role="button" style="width: 100px; height: 20px;">Invoker</div>
|
<div role="button" style="width: 100px; height: 20px;">Invoker</div>
|
||||||
`),
|
`),
|
||||||
|
|
@ -134,7 +134,7 @@ describe('OverlayController', () => {
|
||||||
it.skip('creates local target next to sibling for placement mode "local"', async () => {
|
it.skip('creates local target next to sibling for placement mode "local"', async () => {
|
||||||
const ctrl = new OverlayController({
|
const ctrl = new OverlayController({
|
||||||
...withLocalTestConfig(),
|
...withLocalTestConfig(),
|
||||||
invokerNode: await fixture(html` <button>Invoker</button> `),
|
invokerNode: await fixture(html`<button>Invoker</button>`),
|
||||||
});
|
});
|
||||||
expect(ctrl._renderTarget).to.be.undefined;
|
expect(ctrl._renderTarget).to.be.undefined;
|
||||||
expect(ctrl.content).to.equal(ctrl.invokerNode.nextElementSibling);
|
expect(ctrl.content).to.equal(ctrl.invokerNode.nextElementSibling);
|
||||||
|
|
@ -293,7 +293,7 @@ describe('OverlayController', () => {
|
||||||
});
|
});
|
||||||
await ctrl.show();
|
await ctrl.show();
|
||||||
|
|
||||||
const elOutside = await fixture(html` <button>click me</button> `);
|
const elOutside = await fixture(html`<button>click me</button>`);
|
||||||
const input1 = ctrl.contentNode.querySelectorAll('input')[0];
|
const input1 = ctrl.contentNode.querySelectorAll('input')[0];
|
||||||
const input2 = ctrl.contentNode.querySelectorAll('input')[1];
|
const input2 = ctrl.contentNode.querySelectorAll('input')[1];
|
||||||
|
|
||||||
|
|
@ -308,7 +308,7 @@ describe('OverlayController', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allows to move the focus outside of the overlay if trapsKeyboardFocus is disabled', async () => {
|
it('allows to move the focus outside of the overlay if trapsKeyboardFocus is disabled', async () => {
|
||||||
const contentNode = await fixture(html` <div><input /></div> `);
|
const contentNode = await fixture(html`<div><input /></div>`);
|
||||||
|
|
||||||
const ctrl = new OverlayController({
|
const ctrl = new OverlayController({
|
||||||
...withGlobalTestConfig(),
|
...withGlobalTestConfig(),
|
||||||
|
|
@ -316,10 +316,10 @@ describe('OverlayController', () => {
|
||||||
trapsKeyboardFocus: true,
|
trapsKeyboardFocus: true,
|
||||||
});
|
});
|
||||||
// add element to dom to allow focus
|
// add element to dom to allow focus
|
||||||
await fixture(html` ${ctrl.content} `);
|
await fixture(html`${ctrl.content}`);
|
||||||
await ctrl.show();
|
await ctrl.show();
|
||||||
|
|
||||||
const elOutside = await fixture(html` <input /> `);
|
const elOutside = await fixture(html`<input />`);
|
||||||
const input = ctrl.contentNode.querySelector('input');
|
const input = ctrl.contentNode.querySelector('input');
|
||||||
|
|
||||||
input.focus();
|
input.focus();
|
||||||
|
|
@ -524,7 +524,7 @@ describe('OverlayController', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('works with 3rd party code using "event.stopPropagation()" on capture phase', async () => {
|
it('works with 3rd party code using "event.stopPropagation()" on capture phase', async () => {
|
||||||
const invokerNode = await fixture(html` <div role="button">Invoker</div> `);
|
const invokerNode = await fixture(html`<div role="button">Invoker</div>`);
|
||||||
const contentNode = await fixture('<div>Content</div>');
|
const contentNode = await fixture('<div>Content</div>');
|
||||||
const ctrl = new OverlayController({
|
const ctrl = new OverlayController({
|
||||||
...withLocalTestConfig(),
|
...withLocalTestConfig(),
|
||||||
|
|
@ -1011,7 +1011,7 @@ describe('OverlayController', () => {
|
||||||
it('reinitializes content', async () => {
|
it('reinitializes content', async () => {
|
||||||
const ctrl = new OverlayController({
|
const ctrl = new OverlayController({
|
||||||
...withLocalTestConfig(),
|
...withLocalTestConfig(),
|
||||||
contentNode: await fixture(html` <div>content1</div> `),
|
contentNode: await fixture(html`<div>content1</div>`),
|
||||||
});
|
});
|
||||||
await ctrl.show(); // Popper adds inline styles
|
await ctrl.show(); // Popper adds inline styles
|
||||||
expect(ctrl.content.style.transform).not.to.be.undefined;
|
expect(ctrl.content.style.transform).not.to.be.undefined;
|
||||||
|
|
@ -1019,13 +1019,13 @@ describe('OverlayController', () => {
|
||||||
|
|
||||||
ctrl.updateConfig({
|
ctrl.updateConfig({
|
||||||
placementMode: 'local',
|
placementMode: 'local',
|
||||||
contentNode: await fixture(html` <div>content2</div> `),
|
contentNode: await fixture(html`<div>content2</div>`),
|
||||||
});
|
});
|
||||||
expect(ctrl.contentNode.textContent).to.include('content2');
|
expect(ctrl.contentNode.textContent).to.include('content2');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('respects the initial config provided to new OverlayController(initialConfig)', async () => {
|
it('respects the initial config provided to new OverlayController(initialConfig)', async () => {
|
||||||
const contentNode = fixtureSync(html` <div>my content</div> `);
|
const contentNode = fixtureSync(html`<div>my content</div>`);
|
||||||
|
|
||||||
const ctrl = new OverlayController({
|
const ctrl = new OverlayController({
|
||||||
// This is the shared config
|
// This is the shared config
|
||||||
|
|
@ -1045,7 +1045,7 @@ describe('OverlayController', () => {
|
||||||
|
|
||||||
// Currently not working, enable again when we fix updateConfig
|
// Currently not working, enable again when we fix updateConfig
|
||||||
it.skip('allows for updating viewport config placement only, while keeping the content shown', async () => {
|
it.skip('allows for updating viewport config placement only, while keeping the content shown', async () => {
|
||||||
const contentNode = fixtureSync(html` <div>my content</div> `);
|
const contentNode = fixtureSync(html`<div>my content</div>`);
|
||||||
|
|
||||||
const ctrl = new OverlayController({
|
const ctrl = new OverlayController({
|
||||||
// This is the shared config
|
// This is the shared config
|
||||||
|
|
@ -1071,7 +1071,7 @@ describe('OverlayController', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Accessibility', () => {
|
describe('Accessibility', () => {
|
||||||
it('adds and removes [aria-expanded] on invoker', async () => {
|
it('synchronizes [aria-expanded] on invoker', async () => {
|
||||||
const invokerNode = await fixture('<div role="button">invoker</div>');
|
const invokerNode = await fixture('<div role="button">invoker</div>');
|
||||||
const ctrl = new OverlayController({
|
const ctrl = new OverlayController({
|
||||||
...withLocalTestConfig(),
|
...withLocalTestConfig(),
|
||||||
|
|
@ -1232,6 +1232,66 @@ describe('OverlayController', () => {
|
||||||
});
|
});
|
||||||
expect(ctrl.contentNode.getAttribute('role')).to.equal('tooltip');
|
expect(ctrl.contentNode.getAttribute('role')).to.equal('tooltip');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Teardown', () => {
|
||||||
|
it('restores [role] on dialog content', async () => {
|
||||||
|
const invokerNode = await fixture('<div role="button">invoker</div>');
|
||||||
|
const ctrl = new OverlayController({
|
||||||
|
...withLocalTestConfig(),
|
||||||
|
handlesAccessibility: true,
|
||||||
|
invokerNode,
|
||||||
|
});
|
||||||
|
expect(ctrl.contentNode.getAttribute('role')).to.equal('dialog');
|
||||||
|
ctrl.teardown();
|
||||||
|
expect(ctrl.contentNode.getAttribute('role')).to.equal(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('restores [role] on tooltip content', async () => {
|
||||||
|
const invokerNode = await fixture('<div role="button">invoker</div>');
|
||||||
|
const contentNode = await fixture('<div role="presentation">content</div>');
|
||||||
|
const ctrl = new OverlayController({
|
||||||
|
...withLocalTestConfig(),
|
||||||
|
handlesAccessibility: true,
|
||||||
|
isTooltip: true,
|
||||||
|
invokerNode,
|
||||||
|
contentNode,
|
||||||
|
});
|
||||||
|
expect(contentNode.getAttribute('role')).to.equal('tooltip');
|
||||||
|
ctrl.teardown();
|
||||||
|
expect(contentNode.getAttribute('role')).to.equal('presentation');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('restores [aria-describedby] on content', async () => {
|
||||||
|
const invokerNode = await fixture('<div role="button">invoker</div>');
|
||||||
|
const contentNode = await fixture('<div role="presentation">content</div>');
|
||||||
|
const ctrl = new OverlayController({
|
||||||
|
...withLocalTestConfig(),
|
||||||
|
handlesAccessibility: true,
|
||||||
|
isTooltip: true,
|
||||||
|
invokerNode,
|
||||||
|
contentNode,
|
||||||
|
});
|
||||||
|
expect(invokerNode.getAttribute('aria-describedby')).to.equal(contentNode.id);
|
||||||
|
ctrl.teardown();
|
||||||
|
expect(invokerNode.getAttribute('aria-describedby')).to.equal(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('restores [aria-labelledby] on content', async () => {
|
||||||
|
const invokerNode = await fixture('<div role="button">invoker</div>');
|
||||||
|
const contentNode = await fixture('<div role="presentation">content</div>');
|
||||||
|
const ctrl = new OverlayController({
|
||||||
|
...withLocalTestConfig(),
|
||||||
|
handlesAccessibility: true,
|
||||||
|
isTooltip: true,
|
||||||
|
invokerNode,
|
||||||
|
contentNode,
|
||||||
|
invokerRelation: 'label',
|
||||||
|
});
|
||||||
|
expect(invokerNode.getAttribute('aria-labelledby')).to.equal(contentNode.id);
|
||||||
|
ctrl.teardown();
|
||||||
|
expect(invokerNode.getAttribute('aria-labelledby')).to.equal(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1244,7 +1304,7 @@ describe('OverlayController', () => {
|
||||||
new OverlayController({
|
new OverlayController({
|
||||||
contentNode,
|
contentNode,
|
||||||
});
|
});
|
||||||
}).to.throw('You need to provide a .placementMode ("global"|"local")');
|
}).to.throw('[OverlayController] You need to provide a .placementMode ("global"|"local")');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws if invalid .placementMode gets passed on', async () => {
|
it('throws if invalid .placementMode gets passed on', async () => {
|
||||||
|
|
@ -1252,7 +1312,9 @@ describe('OverlayController', () => {
|
||||||
new OverlayController({
|
new OverlayController({
|
||||||
placementMode: 'invalid',
|
placementMode: 'invalid',
|
||||||
});
|
});
|
||||||
}).to.throw('"invalid" is not a valid .placementMode, use ("global"|"local")');
|
}).to.throw(
|
||||||
|
'[OverlayController] "invalid" is not a valid .placementMode, use ("global"|"local")',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws if no .contentNode gets passed on', async () => {
|
it('throws if no .contentNode gets passed on', async () => {
|
||||||
|
|
@ -1260,7 +1322,7 @@ describe('OverlayController', () => {
|
||||||
new OverlayController({
|
new OverlayController({
|
||||||
placementMode: 'global',
|
placementMode: 'global',
|
||||||
});
|
});
|
||||||
}).to.throw('You need to provide a .contentNode');
|
}).to.throw('[OverlayController] You need to provide a .contentNode');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws if contentNodewrapper is not provided for projected contentNode', async () => {
|
it('throws if contentNodewrapper is not provided for projected contentNode', async () => {
|
||||||
|
|
@ -1284,7 +1346,39 @@ describe('OverlayController', () => {
|
||||||
...withLocalTestConfig(),
|
...withLocalTestConfig(),
|
||||||
contentNode,
|
contentNode,
|
||||||
});
|
});
|
||||||
}).to.throw('You need to provide a .contentWrapperNode when .contentNode is projected');
|
}).to.throw(
|
||||||
|
'[OverlayController] You need to provide a .contentWrapperNode when .contentNode is projected',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws if placementMode is global for a tooltip', async () => {
|
||||||
|
const contentNode = document.createElement('div');
|
||||||
|
document.body.appendChild(contentNode);
|
||||||
|
expect(() => {
|
||||||
|
new OverlayController({
|
||||||
|
placementMode: 'global',
|
||||||
|
contentNode,
|
||||||
|
isTooltip: true,
|
||||||
|
handlesAccessibility: true,
|
||||||
|
});
|
||||||
|
}).to.throw(
|
||||||
|
'[OverlayController] .isTooltip should be configured with .placementMode "local"',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws if handlesAccessibility is false for a tooltip', async () => {
|
||||||
|
const contentNode = document.createElement('div');
|
||||||
|
document.body.appendChild(contentNode);
|
||||||
|
expect(() => {
|
||||||
|
new OverlayController({
|
||||||
|
placementMode: 'local',
|
||||||
|
contentNode,
|
||||||
|
isTooltip: true,
|
||||||
|
handlesAccessibility: false,
|
||||||
|
});
|
||||||
|
}).to.throw(
|
||||||
|
'[OverlayController] .isTooltip only takes effect when .handlesAccessibility is enabled',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,8 @@ export class LionTooltip extends OverlayMixin(LitElement) {
|
||||||
this.__syncFromPopperState(data);
|
this.__syncFromPopperState(data);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
isTooltip: true,
|
||||||
|
handlesAccessibility: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue