Skip to content
27 changes: 27 additions & 0 deletions src/js/htmlutils.js
Original file line number Diff line number Diff line change
Expand Up @@ -334,4 +334,31 @@ let htmlUtils = {

htmlUtils.escape = escape_html;

/*
* Code from focusable-selectors micro-library <https://github.com/KittyGiraudel/focusable-selectors/tree/main>
* Copyright (c) 2021 Kitty Giraudel
* Available under MIT license <https://github.com/KittyGiraudel/focusable-selectors/blob/main/LICENSE>
*/
const notInert = ':not([inert]):not([inert] *)';
const notNegTabIndex = ':not([tabindex^="-"])';
const notDisabled = ':not(:disabled)';
htmlUtils.focusableSelectors = [
`a[href]${notInert}${notNegTabIndex}`,
`area[href]${notInert}${notNegTabIndex}`,
`input:not([type="hidden"]):not([type="radio"])${notInert}${notNegTabIndex}${notDisabled}`,
`input[type="radio"]${notInert}${notNegTabIndex}${notDisabled}`,
`select${notInert}${notNegTabIndex}${notDisabled}`,
`textarea${notInert}${notNegTabIndex}${notDisabled}`,
`button${notInert}${notNegTabIndex}${notDisabled}`,
`details${notInert} > summary:first-of-type${notNegTabIndex}`,
// Discard until Firefox supports `:has()`
// See: https://github.com/KittyGiraudel/focusable-selectors/issues/12
// `details:not(:has(> summary))${notInert}${notNegTabIndex}`,
`iframe${notInert}${notNegTabIndex}`,
`audio[controls]${notInert}${notNegTabIndex}`,
`video[controls]${notInert}${notNegTabIndex}`,
`[contenteditable]${notInert}${notNegTabIndex}`,
`[tabindex]${notInert}${notNegTabIndex}`,
].join(',');

export default htmlUtils;
127 changes: 94 additions & 33 deletions src/js/popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -563,39 +563,102 @@ function revertDomainControl(event) {
* Tooltip that explains how to enable signing into websites with Google.
*/
function createBreakageNote(domain, i18n_message_key) {
let $slider_allow = $(`#blockedResourcesInner label[for="allow-${domain.replace(/\./g, '-')}"]`);

// first remove the Allow tooltip so that future tooltipster calls
// return the tooltip we want (the breakage note, not Allow)
$slider_allow.tooltipster('destroy').tooltipster({
autoClose: false,
content: chrome.i18n.getMessage(i18n_message_key),
functionReady: function (tooltip) {
// close on tooltip click/tap
$(tooltip.elementTooltip()).on('click', function (e) {
e.preventDefault();
tooltip.hide();
});
// also when Report Broken Site or Share overlays get activated
$('#error, #share').off('click.breakage-note').on('click.breakage-note', function (e) {
e.preventDefault();
tooltip.hide();
});
},
interactive: true,
position: ['top'],
trigger: 'custom',
theme: 'tooltipster-badger-breakage-note'
if (!POPUP_DATA.settings.seenComic || POPUP_DATA.showLearningPrompt || POPUP_DATA.criticalError) {
return;
}

let $clicker = $(`.clicker[data-origin="${domain}"]`);
let $switchContainer = $clicker.find('.switch-container');

// Create tooltip HTML
let $tooltip = $(`
<div class="breakage-note-tooltip" role="tooltip">
<div class="tooltip-box">
<div class="tooltip-content">${chrome.i18n.getMessage(i18n_message_key)}</div>
<button class="dismiss-tooltip" aria-label="${chrome.i18n.getMessage("report_close")}">×</button>
</div>
<div class="tooltip-arrow">
<div class="tooltip-arrow-inner"></div>
</div>
</div>`);

// Cannot insert inside blockedResourcesInner because it'll be cut off
$('#blockedResourcesInner').before($tooltip);

let switchOffset = $switchContainer.offset();
let switch_width = $switchContainer.outerWidth();
let switch_height = $switchContainer.outerHeight();
let arrow_width = $tooltip.find('.tooltip-arrow-inner').outerWidth();
let tooltip_height = $tooltip.outerHeight();

// Tooltip should be above the the switch container
$tooltip.css({
top: (switchOffset.top - tooltip_height - switch_height) + 'px',
left: '0px',
visibility: 'visible',
});

// now restore the Allow tooltip
}).tooltipster(Object.assign({}, DOMAIN_TOOLTIP_CONF, {
content: chrome.i18n.getMessage('domain_slider_allow_tooltip'),
multiple: true
}));
// Arrow should point to the allow toggle of the slider
let arrow_left = switchOffset.left + (switch_width * 5/6) - (arrow_width / 2);
$tooltip.find('.tooltip-arrow').css('left', arrow_left + 'px');

if (POPUP_DATA.settings.seenComic && !POPUP_DATA.showLearningPrompt && !POPUP_DATA.criticalError) {
$slider_allow.tooltipster('show');
let intObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
$tooltip.show(); // show tooltip when slider is visible
suppressOverlapping();
} else {
$tooltip.hide(); // hide tooltip when slider is not visible
restoreSuppressed();
}
});
}, { threshold: 0 });
intObserver.observe($switchContainer[0]);

// Collect originally focusable elements we suppress
let suppressed = [];
function suppressOverlapping() {
// Ensure that only visible elements can receive keyboard focus
// (https://www.w3.org/WAI/WCAG22/Understanding/focus-not-obscured-minimum.html)
let tooltipRect = $tooltip[0].getBoundingClientRect();
document.querySelectorAll(htmlUtils.focusableSelectors).forEach(el => {
let elRect = el.getBoundingClientRect();
// Treat elements as hidden if at least 50% of its height overlaps with the tooltip
let overlaps = (elRect.bottom > (tooltipRect.top + (elRect.height * 0.5))) && (elRect.top < (tooltipRect.bottom - (elRect.height * 0.5)));
if (overlaps && !$tooltip[0].contains(el)) {
suppressed.push({ el, originalTabIndex: el.getAttribute('tabindex') });
el.setAttribute('tabindex', '-1');
}
});
}
suppressOverlapping();
// Restore ability to tab navigate to hidden elements once breakage note is closed
function restoreSuppressed() {
suppressed.forEach(({ el, original }) => {
if (original === null) {
el.removeAttribute('tabindex');
} else {
el.setAttribute('tabindex', original);
}
});
suppressed = [];
}

// Close button handler
$tooltip.find('.dismiss-tooltip').on('click', function(e) {
e.preventDefault();
restoreSuppressed();
intObserver.disconnect();
$tooltip.fadeOut(200);
});

// Also close when Report Broken Site or Share overlays get activated
$('#error, #share').off('click.breakage-note').on('click.breakage-note', function (e) {
e.preventDefault();
restoreSuppressed();
intObserver.disconnect();
$tooltip.remove();
});
}

/**
Expand Down Expand Up @@ -912,9 +975,7 @@ function showOverlay(overlay_id) {

// Focus on the first focusable element, per ARIA guidance for dialogs/modals
// https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/
let $focusables = $(overlay_id).find(
'button, [href], input, select, textarea, iframe, [tabindex]:not([tabindex="-1"])'
);
let $focusables = $(overlay_id).find(htmlUtils.focusableSelectors);
if ($focusables.length) {
$focusables[0].focus();
}
Expand Down
13 changes: 13 additions & 0 deletions src/lib/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,19 @@ function setTextDirection() {
toggle_css_value(selector, "float", "right", "left");
});

// Workaround for tooltipster dynamically inserted after localization
let css = document.createElement("style");
css.textContent = `
.breakage-note-tooltip .dismiss-tooltip {
margin-left: unset;
margin-right: 8px;
}
/* part of workaround for .clicker rows being hardcoded to ltr */
.breakage-note-tooltip .tooltip-box {
direction: rtl;
}`;
document.body.appendChild(css);

// options page
} else if (document.location.pathname == "/skin/options.html") {
// apply RTL workaround for jQuery UI tabs
Expand Down
52 changes: 45 additions & 7 deletions src/skin/popup.css
Original file line number Diff line number Diff line change
Expand Up @@ -519,18 +519,56 @@ a.overlay-close:hover {
border: none;
}

.tooltipster-badger-breakage-note.tooltipster-sidetip .tooltipster-box {
background-color: #F06A0A;
padding: 10px 0;
.breakage-note-tooltip {
position: absolute;
z-index: 999999;
/* Firefox Android workaround: must define position before measuring tooltip height for position calculations */
top: 0;
left: 0;
visibility: hidden;
direction: ltr; /* to match .clicker:not(#not-yet-blocked-header):not(#non-trackers-header) */
}
.tooltipster-badger-breakage-note.tooltipster-sidetip .tooltipster-content {

.breakage-note-tooltip .tooltip-box {
background-color: #F06A0A;
color: #fefefe;
cursor: pointer;
font-size: 15px;
line-height: 1.2;
border-radius: 7px;
padding: 16px 12px;
display: flex;
}
.tooltipster-badger-breakage-note.tooltipster-sidetip .tooltipster-arrow-background {
border-top-color: #F06A0A;

.breakage-note-tooltip .dismiss-tooltip {
background: none;
border: none;
color: #fefefe;
font-size: 25px;
font-weight: bold;
line-height: 1;
cursor: pointer;
margin-left: 8px;
padding: 0;
position: relative;
top: -8px;
align-self: flex-start;
}

.breakage-note-tooltip .dismiss-tooltip:hover {
color: #ddd;
}

.breakage-note-tooltip .tooltip-arrow {
position: absolute;
}

.breakage-note-tooltip .tooltip-arrow-inner {
position: absolute;
width: 12px;
height: 12px;
background-color: #F06A0A;
transform: rotate(45deg);
top: -8px;
}

#firstparty-protections-container, #youtube-message-container {
Expand Down