Tooltip Dialog
Tooltip Dialog
A tooltip dialog is a dialog that pops up in response to a user action, near the current point of focus, similar to the way a tooltip does. It is usually intended to be modal, but this is not as strictly observed as it would normally be with a regular dialog. And though a tooltip often appears on focus or on hover, forcing a dialog to appear on focus or hover is not expected or advisable, because it would move the focus without any prior warning. It is better to allow the user to activate the dialog purposefully.
Turn on a screen reader to experience this example in action.
| Attribute/Option | Description |
|---|---|
horizontalPlacing |
This attribute/option specifies the horizontal placement of the tooltip dialog box.
The valid values are LEFT, RIGHT and CENTER.
By default, the tooltip dialog is placed in the center.
|
verticalPlacing |
This attribute/option specifies the vertical placement of the tooltip dialog box.
The valid values are TOP and BOTTOM.
By default, the tooltip dialog is placed at the top.
|
HTML Source Code
<table class="data">
<tr>
<th width="150px" >Attribute/Option</th>
<th>Description</th>
</tr>
<tr>
<td><code>horizontalPlacing</code></td>
<td>This attribute/option specifies the horizontal placement of the tooltip dialog box.
The valid values are <code>LEFT</code>, <code>RIGHT</code> and <code>CENTER</code>.
By default, the tooltip dialog is placed in the center.
</td>
</tr>
<tr>
<td><code>verticalPlacing</code></td>
<td>This attribute/option specifies the vertical placement of the tooltip dialog box.
The valid values are <code>TOP</code> and <code>BOTTOM</code>.
By default, the tooltip dialog is placed at the top.
</td>
</tr>
</table>
<div class="tooltip-dialog-container">
<div class="panel-1">
<label for="targetField">Recipient</label>
<input type="text" id="targetField" />
<button class="icon-button primary" id="targetFieldHelper" type="button" aria-label="Prepopulate recipient" >
<span class="fa-solid fa-circle-info"></span></button>
</div>
<div role="dialog" class="tooltip select-user" aria-hidden="true" id="tooltip-dialog-1" aria-label="Prepopulate Recipients" >
<form action="javascript:void(0)">
<fieldset class="deque" tabindex="-1">
<legend>Select a name:</legend>
<label>Alice
<input type="radio" name="contactList" value="Alice" checked="">
</label>
<label>Bob
<input type="radio" name="contactList" value="Bob">
</label>
<label>Paul
<input type="radio" name="contactList" value="Paul" aria-labelledby="radioLabel">
</label>
</fieldset>
<p><button id="confirm" class="button primary">Confirm</button></p>
</form>
</div>
</div>
JavaScript Source Code
var langText = {
"errorNodeNotObject": "Node provided is not a DOM object.",
"errorNoTooltip": "tooltip attribute 'data-tooltip' not defined.",
"errorTriggerNotObject": "Trigger button provided is not a DOM object.",
"errorTriggerNotButton": "Trigger button provided is not a Button object.",
}
class TooltipDialog {
static LEFT = 0;
static CENTER = 1;
static RIGHT = 2;
static TOP = 3;
static BOTTOM = 4;
constructor(domNode, triggerButton, options) {
this.showOnFocus = true;
this.horizontalPlacing = TooltipDialog.CENTER;
this.verticalPlacing = TooltipDialog.TOP;
if ( typeof options != "object" ) options = {};
if( (typeof domNode != "object" ) && (typeof domNode.nodeName === "undefined") )
{
console.log(langText.errorNodeNotObject);
return;
}
if( (typeof triggerButton != "object" ) && (typeof triggerButton.nodeName === "undefined") )
{
console.log(langText.errorTriggerNotObject);
return;
}
if( triggerButton.nodeName.toLowerCase() !== "button" )
{
console.log(langText.errorTriggerNotButton);
return;
}
this.tooltip = domNode;
this.triggerButton = triggerButton;
this.hide();
if(domNode.id.trim() == "")
domNode.id = crypto.randomUUID();
if ( typeof options != "object" ) options = {};
var validValues = [TooltipDialog.LEFT, TooltipDialog.CENTER, TooltipDialog.RIGHT];
if( (typeof options["horizontalPlacing"] === "number") && (validValues.includes(options["horizontalPlacing"]) ) )
this.horizontalPlacing = options["horizontalPlacing"];
validValues = [TooltipDialog.TOP, TooltipDialog.BOTTOM];
if( (typeof options["verticalPlacing"] === "number") && (validValues.includes(options["verticalPlacing"]) ) )
this.verticalPlacing = options["verticalPlacing"];
this._boundUpdatePosition = this.updatePosition.bind(this);
this._boundShow = this.show.bind(this);
this._boundEscapeKey = this.handleEscapeKey.bind(this);
window.addEventListener("resize", this._boundUpdatePosition);
triggerButton.addEventListener("click", this._boundShow);
document.body.addEventListener("keydown", this._boundEscapeKey);
var focusElements = domNode.querySelectorAll('a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])');
if(focusElements.length > 0)
{
var firstFocusElement = focusElements[0];
var lastFocusElement = focusElements[focusElements.length-1];
this.firstFocusElement = firstFocusElement;
this.lastFocusElement = lastFocusElement;
lastFocusElement.addEventListener("keydown", function(event){
var isTabPressed = event.key === 'Tab';
if(isTabPressed && !event.shiftKey)
{
event.preventDefault();
firstFocusElement.focus();
}
});
firstFocusElement.addEventListener("keydown", function(event){
var isTabPressed = event.key === 'Tab';
if(isTabPressed && event.shiftKey )
{
event.preventDefault();
lastFocusElement.focus();
}
});
}
}
hide()
{
this.tooltip.classList.add("hidden");
this.tooltip.classList.remove("visible");
this.tooltip.setAttribute("aria-hidden", "true");
this.triggerButton.focus();
}
show()
{
this.tooltip.classList.remove("hidden");
this.tooltip.classList.add("visible");
this.tooltip.setAttribute("aria-hidden", "false");
var top = this.tooltip.parentElement.offsetTop+ this.tooltip.parentElement.offsetHeight;
if( this.horizontalPlacing == TooltipDialog.LEFT )
var left = this.tooltip.parentElement.offsetLeft+"px";
if( this.horizontalPlacing == TooltipDialog.CENTER )
var left = this.tooltip.parentElement.offsetLeft + ( this.tooltip.parentElement.offsetWidth - this.tooltip.offsetWidth)/2 + "px";
if( this.horizontalPlacing == TooltipDialog.RIGHT )
var left = this.tooltip.parentElement.offsetLeft + ( this.tooltip.parentElement.offsetWidth - (this.tooltip.offsetWidth)) + "px";
if( this.verticalPlacing == TooltipDialog.TOP)
top = this.tooltip.parentElement.offsetTop - this.tooltip.offsetHeight;
this.tooltip.style.top = top + "px";
this.tooltip.style.left = left ;
this.firstFocusElement.focus();
}
handleEscapeKey(event)
{
if(event.key == "Escape")
this.hide();
}
updatePosition()
{
if(this.tooltip.classList.contains("visible") )
this.show();
}
destroy()
{
window.removeEventListener("resize", this._boundUpdatePosition);
this.triggerButton.removeEventListener("click", this._boundShow);
document.body.removeEventListener("keydown", this._boundEscapeKey);
}
}
var options = {};
var tooltipDialog1 = new TooltipDialog(document.getElementById("tooltip-dialog-1"), document.getElementById("targetFieldHelper"), options);
window.addEventListener('load', function () {
document.getElementById("confirm").addEventListener("click", selectUser);
});
function selectUser()
{
var selected = document.querySelector('input[name=contactList]:checked');
if (!selected) return;
document.getElementById("targetField").value = selected.value;
tooltipDialog1.hide();
}
CSS Source Code
/*
Tooltip Dialog — Restyled
Deque University ARIA Component
*/
:root {
--dqu-interactive: #2e5f7a;
--dqu-interactive-hover: #3a7a9a;
--dqu-interactive-light: rgba(46, 95, 122, 0.08);
--dqu-bg-primary: #fcfaf8;
--dqu-bg-secondary: #f6f3ed;
--dqu-border-secondary: #8c827d;
--dqu-text-primary: #21201e;
--dqu-font-family: "Noto Sans", sans-serif;
}
/* Buttons */
.button {
padding: 8px 14px;
display: inline-block;
border: 1px solid var(--dqu-interactive);
border-radius: 9999px;
margin: 4px;
font-family: var(--dqu-font-family);
font-size: 0.875rem;
font-weight: 600;
color: var(--dqu-interactive);
background: #ffffff;
cursor: pointer;
transition: background-color 0.15s ease;
}
.icon-button {
padding: 6px;
border: 1px solid var(--dqu-interactive);
border-radius: 9999px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 16px;
line-height: 1;
font-family: var(--dqu-font-family);
cursor: pointer;
background: #ffffff;
color: var(--dqu-interactive);
transition: background-color 0.15s ease;
}
.button:hover,
.icon-button:hover {
background-color: var(--dqu-interactive-light);
outline: 2px solid var(--dqu-interactive) !important;
outline-offset: 2px;
}
.button:focus,
.icon-button:focus {
outline: 3px solid var(--dqu-interactive) !important;
outline-offset: 2px;
}
.primary {
background-color: var(--dqu-interactive);
color: #ffffff;
border-color: var(--dqu-interactive);
}
.primary:hover {
background-color: var(--dqu-interactive-hover);
}
/* Container panel — slightly warmer cream so the input contrasts with it */
.tooltip-dialog-container,
.container {
border: 1px solid var(--dqu-border-secondary);
border-radius: 8px;
padding: 20px;
margin-top: 24px;
max-width: 600px;
background: var(--dqu-bg-secondary);
font-family: var(--dqu-font-family);
}
/* Input panel */
.panel-1 {
padding: 8px 0;
}
.panel-1 label {
margin: 5px;
font-family: var(--dqu-font-family);
font-size: 0.875rem;
font-weight: 500;
color: var(--dqu-text-primary);
}
.panel-1 input {
vertical-align: middle;
width: 276px;
max-width: 70%;
padding: 8px 12px;
border: 1px solid var(--dqu-border-secondary);
border-radius: 8px;
font-family: var(--dqu-font-family);
font-size: 1rem;
background: #ffffff;
color: var(--dqu-text-primary);
}
.panel-1 input:focus {
outline: 2px solid var(--dqu-interactive) !important;
outline-offset: 2px;
border-color: var(--dqu-interactive);
}
.panel-1 button {
margin: 5px;
vertical-align: middle;
}
/* Tooltip dialog popup */
.tooltip-wrapper {
position: relative;
}
.tooltip {
box-sizing: border-box;
font-family: var(--dqu-font-family);
font-size: 0.875rem;
line-height: 1.5;
position: absolute;
background: var(--dqu-bg-primary);
border: 1px solid var(--dqu-border-secondary);
border-radius: 8px;
min-width: 104px;
margin-right: -340px;
padding: 12px;
z-index: 70;
top: -25px;
left: 80%;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
}
.tooltip label {
margin: 5px;
font-family: var(--dqu-font-family);
font-size: 0.875rem;
color: var(--dqu-text-primary);
}
.select-user {
padding: 12px 16px 0 12px;
width: 350px;
}
fieldset.deque {
border: 1px solid var(--dqu-border-secondary);
border-radius: 8px;
padding: 12px;
margin: 0;
}
fieldset.deque legend {
font-family: var(--dqu-font-family);
font-weight: 600;
font-size: 1rem;
color: var(--dqu-text-primary);
padding: 0 4px;
}
/* Native radio inputs inside the tooltip dialog */
input[type="radio"] {
appearance: none;
-webkit-appearance: none;
width: 16px;
height: 16px;
border: 2px solid var(--dqu-interactive);
border-radius: 50%;
background: var(--dqu-bg-primary);
vertical-align: middle;
margin: 0 4px;
cursor: pointer;
position: relative;
}
input[type="radio"]:checked {
border-color: var(--dqu-interactive);
background: var(--dqu-bg-primary);
}
input[type="radio"]:checked::after {
content: "";
position: absolute;
top: 3px;
left: 3px;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--dqu-interactive);
}
input[type="radio"]:focus-visible {
outline: 3px solid var(--dqu-interactive) !important;
outline-offset: 2px;
}
.hidden { display: none; }
.visible { display: block; }
/* Data table */
table.data {
border-collapse: collapse;
font-family: var(--dqu-font-family);
font-size: 0.9375rem;
margin-bottom: 96px;
}
table.data th,
table.data td {
border: 1px solid var(--dqu-border-secondary);
padding: 10px 12px;
text-align: left;
color: var(--dqu-text-primary);
}
table.data th {
background: var(--dqu-bg-secondary);
font-weight: 600;
}
/* Responsive */
@media screen and (max-width: 500px) {
.tooltip-dialog-container,
.container {
padding: 12px;
}
.select-user {
padding: 4px 8px 0 4px;
width: 260px;
}
.panel-1 label {
display: inline-block;
width: 100%;
}
}
@media screen and (max-width: 300px) {
.panel-1 input {
width: 236px;
max-width: 60%;
}
}