Skip to content

Commit

Permalink
feat: add Esc key support to dismiss tooltips (#5147)
Browse files Browse the repository at this point in the history
  • Loading branch information
emyarod authored and asudoh committed Jan 27, 2020
1 parent b3a2c84 commit f03a0a1
Show file tree
Hide file tree
Showing 10 changed files with 313 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
LICENSE file in the root directory of this source tree.
-->
{{!-- @todo remove duplicate class on next major release when selectors can be cleaned up --}}
<div class="{{@root.prefix}}--tooltip--definition {{@root.prefix}}--tooltip--a11y">
<div class="{{@root.prefix}}--tooltip--definition {{@root.prefix}}--tooltip--a11y" data-tooltip-definition>
<button aria-describedby="example-start"
class="{{@root.prefix}}--tooltip__trigger {{@root.prefix}}--tooltip--a11y {{@root.prefix}}--tooltip__trigger--definition {{@root.prefix}}--tooltip--bottom {{@root.prefix}}--tooltip--align-start">
Definition Tooltip (start aligned)
Expand All @@ -14,7 +14,7 @@
above.</div>
</div>
<br>
<div class="{{@root.prefix}}--tooltip--definition {{@root.prefix}}--tooltip--a11y">
<div class="{{@root.prefix}}--tooltip--definition {{@root.prefix}}--tooltip--a11y" data-tooltip-definition>
<button aria-describedby="example-center"
class="{{@root.prefix}}--tooltip__trigger {{@root.prefix}}--tooltip--a11y {{@root.prefix}}--tooltip__trigger--definition {{@root.prefix}}--tooltip--bottom {{@root.prefix}}--tooltip--align-center">
Definition Tooltip (center aligned)
Expand All @@ -23,7 +23,7 @@
above.</div>
</div>
<br>
<div class="{{@root.prefix}}--tooltip--definition {{@root.prefix}}--tooltip--a11y">
<div class="{{@root.prefix}}--tooltip--definition {{@root.prefix}}--tooltip--a11y" data-tooltip-definition>
<button aria-describedby="example-end"
class="{{@root.prefix}}--tooltip__trigger {{@root.prefix}}--tooltip--a11y {{@root.prefix}}--tooltip__trigger--definition {{@root.prefix}}--tooltip--bottom {{@root.prefix}}--tooltip--align-end">
Definition Tooltip (end aligned)
Expand Down
36 changes: 24 additions & 12 deletions packages/components/src/components/tooltip/tooltip--icon.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -8,42 +8,50 @@
<br>
{{!-- @todo Had to add a modifier to the parent to correct the animation but in the next major release it could be removed in favor of the new HTML format --}}
<button
class="{{@root.prefix}}--tooltip__trigger {{@root.prefix}}--tooltip--a11y {{@root.prefix}}--tooltip--left {{@root.prefix}}--tooltip--align-start">
class="{{@root.prefix}}--tooltip__trigger {{@root.prefix}}--tooltip--a11y {{@root.prefix}}--tooltip--left {{@root.prefix}}--tooltip--align-start"
data-tooltip-icon>
<span class="{{@root.prefix}}--assistive-text">Filter</span>
{{ carbon-icon 'Filter16' }}
</button>
<button
class="{{@root.prefix}}--tooltip__trigger {{@root.prefix}}--tooltip--a11y {{@root.prefix}}--tooltip--top {{@root.prefix}}--tooltip--align-start">
class="{{@root.prefix}}--tooltip__trigger {{@root.prefix}}--tooltip--a11y {{@root.prefix}}--tooltip--top {{@root.prefix}}--tooltip--align-start"
data-tooltip-icon>
<span class="{{@root.prefix}}--assistive-text">Filter</span>
{{ carbon-icon 'Filter16' }}
</button>
<button
class="{{@root.prefix}}--tooltip__trigger {{@root.prefix}}--tooltip--a11y {{@root.prefix}}--tooltip--bottom {{@root.prefix}}--tooltip--align-start">
class="{{@root.prefix}}--tooltip__trigger {{@root.prefix}}--tooltip--a11y {{@root.prefix}}--tooltip--bottom {{@root.prefix}}--tooltip--align-start"
data-tooltip-icon>
<span class="{{@root.prefix}}--assistive-text">Filter</span>
{{ carbon-icon 'Filter16' }}
</button>
<button
class="{{@root.prefix}}--tooltip__trigger {{@root.prefix}}--tooltip--a11y {{@root.prefix}}--tooltip--right {{@root.prefix}}--tooltip--align-start">
class="{{@root.prefix}}--tooltip__trigger {{@root.prefix}}--tooltip--a11y {{@root.prefix}}--tooltip--right {{@root.prefix}}--tooltip--align-start"
data-tooltip-icon>
<span class="{{@root.prefix}}--assistive-text">Filter</span>
{{ carbon-icon 'Filter16' }}
</button>
<br>
<br>
<p>center</p>
<br>
<button class="{{@root.prefix}}--tooltip__trigger {{@root.prefix}}--tooltip--a11y {{@root.prefix}}--tooltip--left">
<button class="{{@root.prefix}}--tooltip__trigger {{@root.prefix}}--tooltip--a11y {{@root.prefix}}--tooltip--left"
data-tooltip-icon>
<span class="{{@root.prefix}}--assistive-text">Filter</span>
{{ carbon-icon 'Filter16' }}
</button>
<button class="{{@root.prefix}}--tooltip__trigger {{@root.prefix}}--tooltip--a11y {{@root.prefix}}--tooltip--top">
<button class="{{@root.prefix}}--tooltip__trigger {{@root.prefix}}--tooltip--a11y {{@root.prefix}}--tooltip--top"
data-tooltip-icon>
<span class="{{@root.prefix}}--assistive-text">Filter</span>
{{ carbon-icon 'Filter16' }}
</button>
<button class="{{@root.prefix}}--tooltip__trigger {{@root.prefix}}--tooltip--a11y {{@root.prefix}}--tooltip--bottom">
<button class="{{@root.prefix}}--tooltip__trigger {{@root.prefix}}--tooltip--a11y {{@root.prefix}}--tooltip--bottom"
data-tooltip-icon>
<span class="{{@root.prefix}}--assistive-text">Filter</span>
{{ carbon-icon 'Filter16' }}
</button>
<button class="{{@root.prefix}}--tooltip__trigger {{@root.prefix}}--tooltip--a11y {{@root.prefix}}--tooltip--right">
<button class="{{@root.prefix}}--tooltip__trigger {{@root.prefix}}--tooltip--a11y {{@root.prefix}}--tooltip--right"
data-tooltip-icon>
<span class="{{@root.prefix}}--assistive-text">Filter</span>
{{ carbon-icon 'Filter16' }}
</button>
Expand All @@ -52,22 +60,26 @@
<p>end</p>
<br>
<button
class="{{@root.prefix}}--tooltip__trigger {{@root.prefix}}--tooltip--a11y {{@root.prefix}}--tooltip--left {{@root.prefix}}--tooltip--align-end">
class="{{@root.prefix}}--tooltip__trigger {{@root.prefix}}--tooltip--a11y {{@root.prefix}}--tooltip--left {{@root.prefix}}--tooltip--align-end"
data-tooltip-icon>
<span class="{{@root.prefix}}--assistive-text">Filter</span>
{{ carbon-icon 'Filter16' }}
</button>
<button
class="{{@root.prefix}}--tooltip__trigger {{@root.prefix}}--tooltip--a11y {{@root.prefix}}--tooltip--top {{@root.prefix}}--tooltip--align-end">
class="{{@root.prefix}}--tooltip__trigger {{@root.prefix}}--tooltip--a11y {{@root.prefix}}--tooltip--top {{@root.prefix}}--tooltip--align-end"
data-tooltip-icon>
<span class="{{@root.prefix}}--assistive-text">Filter</span>
{{ carbon-icon 'Filter16' }}
</button>
<button
class="{{@root.prefix}}--tooltip__trigger {{@root.prefix}}--tooltip--a11y {{@root.prefix}}--tooltip--bottom {{@root.prefix}}--tooltip--align-end">
class="{{@root.prefix}}--tooltip__trigger {{@root.prefix}}--tooltip--a11y {{@root.prefix}}--tooltip--bottom {{@root.prefix}}--tooltip--align-end"
data-tooltip-icon>
<span class="{{@root.prefix}}--assistive-text">Filter</span>
{{ carbon-icon 'Filter16' }}
</button>
<button
class="{{@root.prefix}}--tooltip__trigger {{@root.prefix}}--tooltip--a11y {{@root.prefix}}--tooltip--right {{@root.prefix}}--tooltip--align-end">
class="{{@root.prefix}}--tooltip__trigger {{@root.prefix}}--tooltip--a11y {{@root.prefix}}--tooltip--right {{@root.prefix}}--tooltip--align-end"
data-tooltip-icon>
<span class="{{@root.prefix}}--assistive-text">Filter</span>
{{ carbon-icon 'Filter16' }}
</button>
94 changes: 94 additions & 0 deletions packages/components/src/components/tooltip/tooltip--simple.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* Copyright IBM Corp. 2016, 2018
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/

import settings from '../../globals/js/settings';
import mixin from '../../globals/js/misc/mixin';
import createComponent from '../../globals/js/mixins/create-component';
import initComponentBySearch from '../../globals/js/mixins/init-component-by-search';
import handles from '../../globals/js/mixins/handles';
import eventMatches from '../../globals/js/misc/event-matches';
import on from '../../globals/js/misc/on';

export default class TooltipSimple extends mixin(
createComponent,
initComponentBySearch,
handles
) {
/**
* Simple Tooltip.
* @extends CreateComponent
* @extends InitComponentBySearch
* @extends Handles
* @param {HTMLElement} element - The element functioning as a text field.
*/
constructor(element, options) {
super(element, options);
this.manage(
on(this.element.ownerDocument, 'keydown', event => {
// ESC
if (event.which === 27) {
this.allowTooltipVisibility({ visible: false });
}
})
);
this.manage(
on(this.element, 'mouseenter', () =>
this.allowTooltipVisibility({ visible: true })
)
);
this.manage(
on(this.element, 'focus', event => {
if (eventMatches(event, this.options.selectorTriggerButton)) {
this.allowTooltipVisibility({ visible: true });
}
})
);
}

allowTooltipVisibility = ({ visible }) => {
const tooltipTriggerButton = this.element.matches(
this.options.selectorTriggerButton
)
? this.element
: this.element.querySelector(this.options.selectorTriggerButton);

if (!tooltipTriggerButton) {
return;
}

if (visible) {
tooltipTriggerButton.classList.remove(this.options.classTooltipHidden);
} else {
tooltipTriggerButton.classList.add(this.options.classTooltipHidden);
}
};

/**
* The component options.
*
* If `options` is specified in the constructor,
* {@linkcode TooltipSimple.create .create()},
* or {@linkcode TooltipSimple.init .init()},
* properties in this object are overriden for the instance being
* created and how {@linkcode TooltipSimple.init .init()} works.
* @property {string} selectorInit The CSS selector to find simple tooltip UIs.
*/
static get options() {
const { prefix } = settings;
return {
selectorInit: '[data-tooltip-definition],[data-tooltip-icon]',
selectorTriggerButton: `.${prefix}--tooltip__trigger.${prefix}--tooltip--a11y`,
classTooltipHidden: `${prefix}--tooltip--hidden`,
};
}

/**
* The map associating DOM element and simple tooltip UI instance.
* @type {WeakMap}
*/
static components /* #__PURE_CLASS_PROPERTY__ */ = new WeakMap();
}
1 change: 1 addition & 0 deletions packages/components/src/globals/js/components.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export { default as CopyButton } from '../../components/copy-button/copy-button'
export { default as Notification } from '../../components/notification/notification';
export { default as Toolbar } from '../../components/toolbar/toolbar';
export { default as Tooltip } from '../../components/tooltip/tooltip';
export { default as TooltipSimple } from '../../components/tooltip/tooltip--simple';
export { default as ProgressIndicator } from '../../components/progress-indicator/progress-indicator';
export { default as FloatingMenu } from '../../components/floating-menu/floating-menu';
export { default as StructuredList } from '../../components/structured-list/structured-list';
Expand Down
12 changes: 12 additions & 0 deletions packages/components/src/globals/scss/_tooltip.scss
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,18 @@
animation: tooltip-fade $duration--fast-01 motion(standard, productive);
}
}

&.#{$prefix}--tooltip--hidden .#{$prefix}--assistive-text,
&.#{$prefix}--tooltip--hidden + .#{$prefix}--assistive-text {
clip: rect(0, 0, 0, 0);
margin: -1px;
overflow: hidden;
}

&.#{$prefix}--tooltip--hidden.#{$prefix}--tooltip--a11y::before {
animation: none;
opacity: 0;
}
}

// Tooltip
Expand Down
120 changes: 120 additions & 0 deletions packages/components/tests/spec/tooltip--simple_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import Tooltip from '../../src/components/tooltip/tooltip--simple';
import TooltipDefinitionHTML from '../../html/tooltip/tooltip--definition.html';
import TooltipIconHTML from '../../html/tooltip/tooltip--icon.html';

describe('Test simple tooltip', function() {
describe('Constructor', function() {
it('Should throw if root element is not given', function() {
expect(() => {
new Tooltip();
}).toThrowError(
TypeError,
'DOM element should be given to initialize this widget.'
);
});

it('Should throw if root element is not a DOM element', function() {
expect(() => {
new Tooltip(document.createTextNode(''));
}).toThrowError(
TypeError,
'DOM element should be given to initialize this widget.'
);
});
});

describe('Showing/hiding definition tooltip', function() {
const container = document.createElement('div');
container.innerHTML = TooltipDefinitionHTML;

const element = container.querySelector('[data-tooltip-definition]');
const button = container.querySelector('.bx--tooltip__trigger--definition');
let tooltip;

beforeAll(function() {
document.body.appendChild(container);
tooltip = new Tooltip(element);
});

it('Should not have hidden class after mouseenter', function() {
element.dispatchEvent(new CustomEvent('mouseenter', { bubbles: true }));
expect(button.classList.contains('bx--tooltip--hidden')).toBe(false);
});

it('Should not have hidden class after focus', function() {
element.dispatchEvent(new CustomEvent('focus', { bubbles: true }));
expect(button.classList.contains('bx--tooltip--hidden')).toBe(false);
});

it('Should have hidden class after Esc keydown', function() {
element.dispatchEvent(
Object.assign(new CustomEvent('keydown', { bubbles: true }), {
which: 27,
})
);
expect(button.classList.contains('bx--tooltip--hidden')).toBe(true);
});

afterEach(function() {
button.classList.remove('bx--tooltip--hidden');
});

afterAll(function() {
if (document.body.contains(button)) {
button.parentNode.removeChild(button);
}
if (tooltip) {
tooltip.release();
tooltip = null;
}
document.body.removeChild(container);
});
});

describe('Showing/hiding icon tooltip', function() {
const container = document.createElement('div');
container.innerHTML = TooltipIconHTML;

const element = container.querySelector('[data-tooltip-icon]');
let tooltip;

beforeAll(function() {
document.body.appendChild(container);
tooltip = new Tooltip(element);
});

it('Should not have hidden class after mouseenter', function() {
element.dispatchEvent(new CustomEvent('mouseenter', { bubbles: true }));
expect(element.classList.contains('bx--tooltip--hidden')).toBe(false);
});

it('Should not have hidden class after focus', function() {
element.dispatchEvent(new CustomEvent('focus', { bubbles: true }));
expect(element.classList.contains('bx--tooltip--hidden')).toBe(false);
});

it('Should have hidden class after Esc keydown', function() {
element.dispatchEvent(
Object.assign(new CustomEvent('keydown', { bubbles: true }), {
which: 27,
})
);
expect(element.classList.contains('bx--tooltip--hidden')).toBe(true);
});

afterEach(function() {
element.classList.remove('bx--tooltip--hidden');
});

afterAll(function() {
if (document.body.contains(element)) {
element.parentNode.removeChild(element);
}
if (tooltip) {
tooltip.release();
tooltip = null;
}
document.body.removeChild(container);
});
});
});
Loading

0 comments on commit f03a0a1

Please sign in to comment.