Skip to content

Commit

Permalink
WebUI NTP: use clip-path to show OneGoogleBar overlays
Browse files Browse the repository at this point in the history
The OneGoogleBar loads in an iframe. It has content that can appear
outside the top bar that we're calling overlays. The overlays include
tooltips, app launcher and accounts management. These overlays need to
be layered on top of all other content on the page.

Using a MutationObserver, we can examine all elements that undergo a
change. If an element extends outside of the top bar, it can be an
overlay or a descendent of an overlay. The descendent elements of
overlays are ignored and the remaining overlays are tracked.

When mutation occurs or the window is resized, the tracked overlays are
each checked to determine if they are shown.

The prior approach would send a message to the top frame indicating if
any overlays are shown. When an overlay is shown, the top frame would
layer the OneGoogleBar iframe on top of everything on the page. The
content underneath (search box, most-visited etc.) would be visible but
they would not receive pointer events since they would be handled by the
iframe on top of them. In order to communicate this to the user, a
backdrop similar to the the dialog backdrop was shown.

This introduced an issue with the call-out promo which can be shown on
load. The NTP would load and show the OneGoogleBar in a modal
dialog-like state.

If it was possible to resize the OneGoogleBar iframe dynamically such
it only covers the minimum region needed to display the top bar and
overlays, that would be ideal. This CL implements that ideal.

Now when a mutation or resize occurs, the bounding rects of the shown
overlays are sent to the top frame. clip-path is used in the top frame
to only show parts of the OneGoogleBar that should layer on top of the
NTP content area. A clip-path rect is defined for the top bar and every
shown overlay.

Bug: 1039913, b/154811950
Change-Id: Ifcd55d45523297a918e725f8620b6f60214f504b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2193913
Commit-Queue: Esmael Elmoslimany <aee@chromium.org>
Reviewed-by: Tibor Goldschwendt <tiborg@chromium.org>
Cr-Commit-Position: refs/heads/master@{#768033}
  • Loading branch information
Esmael El-Moslimany authored and Commit Bot committed May 12, 2020
1 parent d4f2b2c commit 38b31e9
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 137 deletions.
9 changes: 9 additions & 0 deletions chrome/browser/resources/new_tab_page/app.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@
}

#oneGoogleBar {
clip-path: url(#oneGoogleBarClipPath);
height: 100%;
position: absolute;
top: 0;
width: 100%;
z-index: 1000;
}

#content {
Expand Down Expand Up @@ -266,3 +268,10 @@
</ntp-customize-dialog>
</template>
</dom-if>
<svg hidden>
<defs>
<clipPath id="oneGoogleBarClipPath">
<rect x="0" y="0" width="100vw" height="56"></rect>
</clipPath>
</defs>
</svg>
77 changes: 18 additions & 59 deletions chrome/browser/resources/new_tab_page/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ class AppElement extends PolymerElement {

/** @private */
oneGoogleBarLoaded_: {
observer: 'oneGoogleBarLoadedChange_',
type: Boolean,
value: false,
},
Expand Down Expand Up @@ -570,9 +569,9 @@ class AppElement extends PolymerElement {

/**
* Handles messages from the OneGoogleBar iframe. The messages that are
* handled include show bar on load and activate/deactivate.
* The activate/deactivate controls if the OneGoogleBar is layered on top of
* #content. This would happen when OneGoogleBar has an overlay open.
* handled include show bar on load and overlay updates.
* 'overlaysUpdated' message includes the updated array of overlay rects that
* are shown.
* @param {!Object} data
* @private
*/
Expand All @@ -581,10 +580,21 @@ class AppElement extends PolymerElement {
this.oneGoogleBarLoaded_ = true;
BrowserProxy.getInstance().handler.onOneGoogleBarRendered(
BrowserProxy.getInstance().now());
} else if (data.messageType === 'activate') {
$$(this, '#oneGoogleBar').style.zIndex = '1000';
} else if (data.messageType === 'deactivate') {
$$(this, '#oneGoogleBar').style.zIndex = '0';
} else if (data.messageType === 'overlaysUpdated') {
this.$.oneGoogleBarClipPath.querySelectorAll('rect:not(:first-child)')
.forEach(el => {
el.remove();
});
const overlayRects = /** @type {!Array<!DOMRect>} */ (data.data);
overlayRects.forEach(({x, y, width, height}) => {
const rectElement =
document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rectElement.setAttribute('x', x);
rectElement.setAttribute('y', y);
rectElement.setAttribute('width', width);
rectElement.setAttribute('height', height);
this.$.oneGoogleBarClipPath.appendChild(rectElement);
});
}
}

Expand All @@ -608,57 +618,6 @@ class AppElement extends PolymerElement {
}
}

/** @private */
oneGoogleBarLoadedChange_() {
if (this.oneGoogleBarLoaded_ && this.iframeOneGoogleBarEnabled_) {
this.setupShortcutDragDropOneGoogleBarWorkaround_();
}
}

/**
* During a shortcut drag, an iframe behind ntp-most-visited will prevent
* 'dragover' events from firing. To workaround this, 'pointer-events: none'
* can be set on the iframe. When doing this after the 'dragstart' event is
* fired is too late. We can instead set 'pointer-events: none' when the
* pointer enters ntp-most-visited.
*
* 'pointerenter' and pointerleave' events fire during drag. The iframe
* 'pointer-events' needs to be reset to the original value when 'dragend'
* fires if the pointer has left ntp-most-visited.
* @private
*/
setupShortcutDragDropOneGoogleBarWorkaround_() {
const iframe = $$(this, '#oneGoogleBar');
let resetAtDragEnd = false;
let dragging = false;
let originalPointerEvents;
this.eventTracker_.add(this.$.mostVisited, 'pointerenter', () => {
if (dragging) {
resetAtDragEnd = false;
return;
}
originalPointerEvents = getComputedStyle(iframe).pointerEvents;
iframe.style.pointerEvents = 'none';
});
this.eventTracker_.add(this.$.mostVisited, 'pointerleave', () => {
if (dragging) {
resetAtDragEnd = true;
return;
}
iframe.style.pointerEvents = originalPointerEvents;
});
this.eventTracker_.add(this.$.mostVisited, 'dragstart', () => {
dragging = true;
});
this.eventTracker_.add(this.$.mostVisited, 'dragend', () => {
dragging = false;
if (resetAtDragEnd) {
resetAtDragEnd = false;
iframe.style.pointerEvents = originalPointerEvents;
}
});
}

/** @private */
printPerformanceDatum_(name, time, auxTime = 0) {
if (!this.shouldPrintPerformance_) {
Expand Down
7 changes: 0 additions & 7 deletions chrome/browser/resources/new_tab_page/most_visited.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,6 @@ class MostVisitedElement extends PolymerElement {
/** @private */
dialogTitle_: String,

/** @private */
isRtl_: {
type: Boolean,
value: false,
reflectToAttribute: true,
},

/**
* Used to hide hover style and cr-icon-button of tiles while the tiles
* are being reordered.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,11 @@
overflow: hidden;
}

#overlayBackdrop {
background: rgba(0, 0, 0, .6);
display: none;
height: 100vh;
position: fixed;
width: 100%;
}

#overlayBackdrop[show] {
display: block;
}

$i18nRaw{inHeadStyle}
</style>
<script>$i18nRaw{inHeadScript}</script>
</head>
<body>
<div id="overlayBackdrop"></div>
$i18nRaw{barHtml}
<script>$i18nRaw{afterBarScript}</script>
$i18nRaw{endOfBodyHtml}
Expand Down
119 changes: 61 additions & 58 deletions chrome/browser/resources/new_tab_page/untrusted/one_google_bar.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@

const oneGoogleBarHeightInPixels = 64;

let darkThemeEnabled = false;
let shouldUndoDarkTheme = false;

/**
* @param {boolean} enabled
* @return {!Promise}
Expand All @@ -15,36 +12,81 @@ async function enableDarkTheme(enabled) {
if (!window.gbar) {
return;
}
darkThemeEnabled = enabled;
const ogb = await window.gbar.a.bf();
ogb.pc.call(ogb, enabled ? 1 : 0);
}

/**
* The following |messageType|'s are sent to the parent frame:
* - loaded: initial load
* - activate/deactivate: When an overlay is open, 'activate' is sent to the
* to ntp-app so it can layer the OneGoogleBar over the NTP content. When
* no overlays are open, 'deactivate' is sent to ntp-app so the NTP
* content can be on top. The top bar of the OneGoogleBar is always on
* top.
* - loaded: sent on initial load.
* - overlaysUpdated: sent when an overlay is updated. The overlay bounding
* rects are included in the |data|.
* @param {string} messageType
* @param {Object} data
*/
function postMessage(messageType) {
function postMessage(messageType, data) {
if (window === window.parent) {
return;
}
window.parent.postMessage(
{frameType: 'one-google-bar', messageType}, 'chrome://new-tab-page');
{frameType: 'one-google-bar', messageType, data},
'chrome://new-tab-page');
}

const overlays = new Set();

function sendOverlayUpdate() {
// Remove overlays detached from DOM or elements in a parent overlay.
Array.from(overlays).forEach(overlay => {
if (!overlay.parentElement) {
overlays.delete(overlay);
return;
}
let parent = overlay.parentElement;
while (parent) {
if (overlays.has(parent)) {
overlays.delete(overlay);
return;
}
parent = parent.parentElement;
}
});
// Check if an overlay and its parents are visible.
const overlayRects =
Array.from(overlays)
.filter(overlay => {
if (window.getComputedStyle(overlay).visibility === 'hidden') {
return false;
}
let current = overlay;
while (current) {
if (window.getComputedStyle(current).display === 'none') {
return false;
}
current = current.parentElement;
}
return true;
})
.map(el => el.getBoundingClientRect());
postMessage('overlaysUpdated', overlayRects);
}

function trackOverlayState() {
const overlays = new Set();
const observer = new MutationObserver(mutations => {
// After loaded, there could exist overlays that are shown, but not mutated.
// Add all elements that could be an overlay. The children of the actual
// overlay element are removed before sending any overlay update message.
if (overlays.size === 0) {
Array.from(document.body.querySelectorAll('*')).forEach(el => {
if (el.offsetTop + el.offsetHeight > oneGoogleBarHeightInPixels) {
overlays.add(el);
}
});
}
// Add any mutated element that is an overlay to |overlays|.
mutations.forEach(({target}) => {
if (target.id === 'gb' || target.tagName === 'BODY' ||
target.parentElement && target.parentElement.tagName === 'BODY') {
overlays.has(target)) {
return;
}
if (target.offsetTop + target.offsetHeight > oneGoogleBarHeightInPixels) {
Expand All @@ -61,50 +103,7 @@ function trackOverlayState() {
});
}
});
// Remove overlays detached from DOM.
Array.from(overlays).forEach(overlay => {
if (!overlay.parentElement) {
overlays.delete(overlay);
}
});
// Check if an overlay and its parents are visible.
const overlayShown = Array.from(overlays).some(overlay => {
if (window.getComputedStyle(overlay).visibility === 'hidden') {
return false;
}
let current = overlay;
while (current) {
if (window.getComputedStyle(current).display === 'none') {
return false;
}
current = current.parentElement;
}
return true;
});
const backdropElement = document.querySelector('#overlayBackdrop');
if (!overlayShown) {
// Hide backdrop before z-level update so NTP content cannot appear above
// backdrop.
backdropElement.toggleAttribute('show', false);
if (shouldUndoDarkTheme) {
shouldUndoDarkTheme = false;
enableDarkTheme(false);
}
}
postMessage(overlayShown ? 'activate' : 'deactivate');
if (overlayShown) {
// Allow the iframe z-level update to take effect before updating the
// backdrop.
setTimeout(() => {
backdropElement.toggleAttribute('show', true);
// When showing the backdrop, turn on dark theme for better visibility
// if it is off.
if (!darkThemeEnabled) {
shouldUndoDarkTheme = true;
enableDarkTheme(true);
}
});
}
sendOverlayUpdate();
});
observer.observe(
document, {attributes: true, childList: true, subtree: true});
Expand All @@ -116,6 +115,10 @@ window.addEventListener('message', ({data}) => {
}
});

// Need to send overlay updates on resize because overlay bounding rects are
// absolutely positioned.
window.addEventListener('resize', sendOverlayUpdate);

document.addEventListener('DOMContentLoaded', () => {
// TODO(crbug.com/1039913): remove after OneGoogleBar links are updated.
// Updates <a>'s so they load on the top frame instead of the iframe.
Expand Down

0 comments on commit 38b31e9

Please sign in to comment.