diff --git a/src/js/htmlutils.js b/src/js/htmlutils.js index 4758f131e2..74be8f1458 100644 --- a/src/js/htmlutils.js +++ b/src/js/htmlutils.js @@ -334,4 +334,31 @@ let htmlUtils = { htmlUtils.escape = escape_html; +/* + * Code from focusable-selectors micro-library + * Copyright (c) 2021 Kitty Giraudel + * Available under MIT 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; diff --git a/src/js/popup.js b/src/js/popup.js index 9463f87a23..98288fca67 100644 --- a/src/js/popup.js +++ b/src/js/popup.js @@ -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 = $(` + + + ${chrome.i18n.getMessage(i18n_message_key)} + × + + + + + `); + + // 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(); + }); } /** @@ -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(); } diff --git a/src/lib/i18n.js b/src/lib/i18n.js index 37e2d8e463..e5cfa6a193 100644 --- a/src/lib/i18n.js +++ b/src/lib/i18n.js @@ -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 diff --git a/src/skin/popup.css b/src/skin/popup.css index 50760a44f6..2ef5e3e86b 100644 --- a/src/skin/popup.css +++ b/src/skin/popup.css @@ -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 {