uikit
Version:
UIkit is a lightweight and modular front-end framework for developing fast and powerful web interfaces.
184 lines (154 loc) • 5.15 kB
JavaScript
import {
append,
attr,
flipPosition,
data as getData,
includes,
isFocusable,
isTouch,
matches,
offset,
on,
once,
overflowParents,
pointerDown,
pointerEnter,
pointerLeave,
remove,
} from 'uikit-util';
import { generateId } from '../api/instance';
import { parseOptions } from '../api/options';
import Container from '../mixin/container';
import Position from '../mixin/position';
import Togglable from '../mixin/togglable';
import { keyMap } from '../util/keys';
export default {
mixins: [Container, Togglable, Position],
data: {
pos: 'top',
animation: ['uk-animation-scale-up'],
duration: 100,
cls: 'uk-active',
},
connected() {
makeFocusable(this.$el);
},
disconnected() {
this.hide();
},
methods: {
show() {
if (this.isToggled(this.tooltip || null)) {
return;
}
const { delay = 0, title } = parseProps(this.$options);
if (!title) {
return;
}
const titleAttr = attr(this.$el, 'title');
const off = on(this.$el, ['blur', pointerLeave], (e) => !isTouch(e) && this.hide());
this.reset = () => {
attr(this.$el, { title: titleAttr, 'aria-describedby': null });
off();
};
const id = generateId(this);
attr(this.$el, { title: null, 'aria-describedby': id });
clearTimeout(this.showTimer);
this.showTimer = setTimeout(() => this._show(title, id), delay);
},
async hide() {
if (matches(this.$el, 'input:focus')) {
return;
}
clearTimeout(this.showTimer);
if (this.isToggled(this.tooltip || null)) {
await this.toggleElement(this.tooltip, false, false);
}
this.reset?.();
remove(this.tooltip);
this.tooltip = null;
},
async _show(title, id) {
this.tooltip = append(
this.container,
`<div id="${id}" class="uk-${this.$options.name}" role="tooltip">
<div class="uk-${this.$options.name}-inner">${title}</div>
</div>`,
);
on(this.tooltip, 'toggled', (e, toggled) => {
if (!toggled) {
return;
}
const update = () => this.positionAt(this.tooltip, this.$el);
update();
const [dir, align] = getAlignment(this.tooltip, this.$el, this.pos);
this.origin =
this.axis === 'y'
? `${flipPosition(dir)}-${align}`
: `${align}-${flipPosition(dir)}`;
const handlers = [
once(
document,
`keydown ${pointerDown}`,
this.hide,
false,
(e) =>
(e.type === pointerDown && !this.$el.contains(e.target)) ||
(e.type === 'keydown' && e.keyCode === keyMap.ESC),
),
on([document, ...overflowParents(this.$el)], 'scroll', update, {
passive: true,
}),
];
once(this.tooltip, 'hide', () => handlers.forEach((handler) => handler()), {
self: true,
});
});
if (!(await this.toggleElement(this.tooltip, true))) {
this.hide();
}
},
},
events: {
// Clicking a button does not give it focus on all browsers and platforms
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#clicking_and_focus
[`focus ${pointerEnter} ${pointerDown}`](e) {
if (!isTouch(e) || e.type === pointerDown) {
this.show();
}
},
},
};
function makeFocusable(el) {
if (!isFocusable(el)) {
attr(el, 'tabindex', '0');
}
}
function getAlignment(el, target, [dir, align]) {
const elOffset = offset(el);
const targetOffset = offset(target);
const properties = [
['left', 'right'],
['top', 'bottom'],
];
for (const props of properties) {
if (elOffset[props[0]] >= targetOffset[props[1]]) {
dir = props[1];
break;
}
if (elOffset[props[1]] <= targetOffset[props[0]]) {
dir = props[0];
break;
}
}
const props = includes(properties[0], dir) ? properties[1] : properties[0];
align = props.find((prop) => elOffset[prop] === targetOffset[prop]) || 'center';
return [dir, align];
}
function parseProps(options) {
const { el, id, data } = options;
return ['delay', 'title'].reduce((obj, key) => ({ [key]: getData(el, key), ...obj }), {
...parseOptions(getData(el, id), ['title']),
...data,
});
}