vue-material-adapter
Version:
Vue 3 wrapper arround Material Components for the Web
354 lines (283 loc) • 10.2 kB
JavaScript
import { CssClasses, events, MDCTooltipFoundation } from '@material/tooltip';
import { onBeforeUnmount, onMounted, reactive, toRefs, watchEffect } from 'vue';
export default {
name: 'mcw-tooltip',
props: {
position: { type: [Object, String] },
boundaryType: { type: [String, Number] },
showDelay: { type: Number },
hideDelay: { type: Number },
addEventListenerHandlerFn: { type: Function },
removeEventListenerHandlerFn: { type: Function },
rich: Boolean,
},
setup(props, { emit }) {
const uiState = reactive({
classes: { 'mdc-tooltip--rich': props.rich },
styles: {},
surfaceStyle: {},
rootAttrs: { 'aria-hidden': true },
root: undefined,
isTooltipPersistent: false,
isTooltipRich: false,
});
let foundation;
let anchorElement;
const adapter = {
getAttribute: name => {
return uiState.root.getAttribute(name);
},
setAttribute: (attributeName, value) => {
uiState.rootAttrs = { ...uiState.rootAttrs, [attributeName]: value };
},
removeAttribute: attribute => {
const { [attribute]: removed, ...rest } = uiState.rootAttrs;
uiState.rootAttrs = rest;
},
addClass: className =>
(uiState.classes = { ...uiState.classes, [className]: true }),
hasClass: className => uiState.root.classList.contains(className),
removeClass: className => {
const { [className]: removed, ...rest } = uiState.classes;
uiState.classes = rest;
},
getComputedStyleProperty: propertyName => {
return window
.getComputedStyle(uiState.root)
.getPropertyValue(propertyName);
},
setStyleProperty: (property, value) =>
(uiState.styles = { ...uiState.styles, [property]: value }),
setSurfaceAnimationStyleProperty: (propertyName, value) => {
uiState.surfaceStyle = {
...uiState.surfaceStyle,
[propertyName]: value,
};
},
getViewportWidth: () => window.innerWidth,
getViewportHeight: () => window.innerHeight,
getTooltipSize: () => {
return {
width: uiState.root.offsetWidth,
height: uiState.root.offsetHeight,
};
},
getAnchorBoundingRect: () => {
return anchorElement
? anchorElement.getBoundingClientRect()
: undefined;
},
getParentBoundingRect: () => {
return uiState.root.parentElement?.getBoundingClientRect() ?? undefined;
},
getAnchorAttribute: attribute => {
return anchorElement
? anchorElement.getAttribute(attribute)
: undefined;
},
setAnchorAttribute: (attribute, value) => {
anchorElement?.setAttribute(attribute, value);
},
isRTL: () => getComputedStyle(uiState.root).direction === 'rtl',
anchorContainsElement: element => {
return !!anchorElement?.contains(element);
},
tooltipContainsElement: element => {
return uiState.root.contains(element);
},
focusAnchorElement: () => {
anchorElement?.focus();
},
registerEventHandler: (event_, handler) => {
uiState.root.addEventListener(event_, handler);
},
deregisterEventHandler: (event_, handler) => {
uiState.root.removeEventListener(event_, handler);
},
registerAnchorEventHandler: (event_, handler) => {
anchorElement?.addEventListener(event_, handler);
},
deregisterAnchorEventHandler: (event_, handler) => {
anchorElement?.removeEventListener(event_, handler);
},
registerDocumentEventHandler: (event_, handler) => {
document.body.addEventListener(event_, handler);
},
deregisterDocumentEventHandler: (event_, handler) => {
document.body.removeEventListener(event_, handler);
},
registerWindowEventHandler: (event_, handler) => {
window.addEventListener(event_, handler);
},
deregisterWindowEventHandler: (event_, handler) => {
window.removeEventListener(event_, handler);
},
notifyHidden: () => {
emit(events.HIDDEN.toLowerCase(), {});
},
getTooltipCaretBoundingRect: () => {
const caret =
uiState.root.querySelector <
HTMLElement >
`.${CssClasses.TOOLTIP_CARET_TOP}`;
if (!caret) {
return;
}
return caret.getBoundingClientRect();
},
setTooltipCaretStyle: (propertyName, value) => {
const topCaret =
uiState.root.querySelector <
HTMLElement >
`.${CssClasses.TOOLTIP_CARET_TOP}`;
const bottomCaret =
uiState.root.querySelector <
HTMLElement >
`.${CssClasses.TOOLTIP_CARET_BOTTOM}`;
if (!topCaret || !bottomCaret) {
return;
}
topCaret.style.setProperty(propertyName, value);
bottomCaret.style.setProperty(propertyName, value);
},
clearTooltipCaretStyles: () => {
const topCaret =
uiState.root.querySelector <
HTMLElement >
`.${CssClasses.TOOLTIP_CARET_TOP}`;
const bottomCaret =
uiState.root.querySelector <
HTMLElement >
`.${CssClasses.TOOLTIP_CARET_BOTTOM}`;
if (!topCaret || !bottomCaret) {
return;
}
topCaret.removeAttribute('style');
bottomCaret.removeAttribute('style');
},
getActiveElement: () => {
return document.activeElement;
},
};
const handleMouseEnter = () => {
foundation.handleAnchorMouseEnter();
};
const handleFocus = event_ => {
foundation.handleAnchorFocus(event_);
};
const handleMouseLeave = () => {
foundation.handleAnchorMouseLeave();
};
const handleTouchstart = () => {
foundation.handleAnchorTouchstart();
};
const handleTouchend = () => {
foundation.handleAnchorTouchend();
};
const handleBlur = event_ => {
foundation.handleAnchorBlur(event_);
};
const handleTransitionEnd = () => {
foundation.handleTransitionEnd();
};
const handleClick = () => {
foundation.handleAnchorClick();
};
const onPosition = position => {
if (position) {
let xPos;
let yPos;
if (typeof position == 'string') {
[xPos, yPos = xPos] = position.split(',');
} else {
({ xPos, yPos } = position);
}
foundation.setTooltipPosition({
xPos: toXposition(xPos),
yPos: toYposition(yPos),
});
}
};
const onBoundaryType = type => {
if (type != undefined) {
foundation.setAnchorBoundaryType(toAnchorBoundaryType(type));
}
};
onMounted(() => {
const tooltipId = uiState.root.getAttribute('id');
if (!tooltipId) {
throw new Error('MDCTooltip: Tooltip component must have an id.');
}
anchorElement =
document.querySelector(`[aria-describedby="${tooltipId}"]`) ||
document.querySelector(`[data-tooltip-id="${tooltipId}"]`);
if (!anchorElement) {
throw new Error(
// eslint-disable-next-line max-len
'MDCTooltip: Tooltip component requires an anchor element annotated with [aria-describedby] or [data-tooltip-id] anchor element.',
);
}
foundation = new MDCTooltipFoundation(adapter);
foundation.init();
uiState.isTooltipRich = foundation.isRich();
uiState.isTooltipPersistent = foundation.isPersistent();
if (uiState.isTooltipRich && uiState.isTooltipPersistent) {
anchorElement.addEventListener('click', handleClick);
} else {
anchorElement.addEventListener('mouseenter', handleMouseEnter);
// TODO(b/157075286): Listening for a 'focus' event is too broad.
anchorElement.addEventListener('focus', handleFocus);
anchorElement.addEventListener('mouseleave', handleMouseLeave);
anchorElement.addEventListener('touchstart', handleTouchstart);
anchorElement.addEventListener('touchend', handleTouchend);
}
anchorElement.addEventListener('blur', handleBlur);
if (props.showDelay !== undefined) {
foundation.setShowDelay(props.showDelay);
}
if (props.hideDelay !== undefined) {
foundation.setHideDelay(props.hideDelay);
}
if (props.addEventListenerHandlerFn) {
foundation.attachScrollHandler(props.addEventListenerHandlerFn);
}
if (props.removeEventListenerHandlerFn) {
foundation.removeScrollHandler(props.removeEventListenerHandlerFn);
}
watchEffect(() => onPosition(props.position));
watchEffect(() => onBoundaryType(props.boundaryType));
});
onBeforeUnmount(() => {
if (anchorElement) {
if (uiState.isTooltipRich && uiState.isTooltipPersistent) {
anchorElement.removeEventListener('click', handleClick);
} else {
anchorElement.removeEventListener('mouseenter', handleMouseEnter);
// TODO(b/157075286): Listening for a 'focus' event is too broad.
anchorElement.removeEventListener('focus', handleFocus);
anchorElement.removeEventListener('mouseleave', handleMouseLeave);
anchorElement.removeEventListener('touchstart', handleTouchstart);
anchorElement.removeEventListener('touchend', handleTouchend);
anchorElement.removeEventListener('blur', handleBlur);
}
}
foundation?.destroy();
});
return { ...toRefs(uiState), handleTransitionEnd };
},
};
// ===
// Private functions
// ===
const XPosition_ = { detected: 0, start: 1, center: 2, end: 3 };
function toXposition(x) {
return typeof x == 'string' ? XPosition_[x] ?? 0 : x;
}
const YPosition_ = { detected: 0, above: 1, below: 2 };
function toYposition(y) {
return typeof y == 'string' ? YPosition_[y] ?? 0 : y;
}
const AnchorBoundaryType_ = { bounded: 0, unbounded: 1 };
function toAnchorBoundaryType(type) {
return typeof type == 'string' ? AnchorBoundaryType_[type] ?? '0' : type;
}