diff --git a/src/annotator/guest.ts b/src/annotator/guest.ts index ff431a682d9..3db9a526f86 100644 --- a/src/annotator/guest.ts +++ b/src/annotator/guest.ts @@ -365,11 +365,6 @@ export class Guest extends TinyEmitter implements Annotator, Destroyable { _setupElementEvents() { // Hide the sidebar in response to a document click or tap, so it doesn't obscure // the document content. - // - // Hypothesis UI elements (``) have logic to prevent clicks in - // them from propagating out of their shadow roots, and hence clicking on - // elements in the sidebar's vertical toolbar or adder won't close the - // sidebar. const maybeCloseSidebar = (event: PointerEvent) => { // Don't hide the sidebar if event was disabled because the sidebar // doesn't overlap the content. @@ -377,6 +372,20 @@ export class Guest extends TinyEmitter implements Annotator, Destroyable { return; } + // Don't hide the sidebar if clicking inside a `` UI + // element. This includes the controls that open and close the sidebar. + if ( + event + .composedPath() + .some( + target => + target instanceof Element && + target.localName.startsWith('hypothesis-'), + ) + ) { + return; + } + // Don't hide the sidebar if the event comes from an element that contains a highlight if (annotationsAt(event.target as Element).length) { return; diff --git a/src/annotator/test/guest-test.js b/src/annotator/test/guest-test.js index de514e78609..75993878bff 100644 --- a/src/annotator/test/guest-test.js +++ b/src/annotator/test/guest-test.js @@ -710,6 +710,26 @@ describe('Guest', () => { assert.isFalse(sidebarClosed()); }); + + it('does not hide sidebar if event is inside a `` element', () => { + createGuest(); + + const hypothesisElement = document.createElement('hypothesis-sidebar'); + const nonHypothesisElement = document.createElement('other-element'); + + try { + rootElement.append(hypothesisElement, nonHypothesisElement); + + simulateClick(hypothesisElement); + assert.isFalse(sidebarClosed()); + + simulateClick(nonHypothesisElement); + assert.isTrue(sidebarClosed()); + } finally { + hypothesisElement.remove(); + nonHypothesisElement.remove(); + } + }); }); it('does not reposition the adder if hidden when the window is resized', () => { diff --git a/src/annotator/util/shadow-root.ts b/src/annotator/util/shadow-root.ts index 277b6824db3..f20b81ec372 100644 --- a/src/annotator/util/shadow-root.ts +++ b/src/annotator/util/shadow-root.ts @@ -36,25 +36,5 @@ export function createShadowRoot(container: HTMLElement): ShadowRoot { applyFocusVisible(shadowRoot); } - stopEventPropagation(shadowRoot); return shadowRoot; } - -/** - * Stop bubbling up of several events. - * - * This makes the host page a little bit less aware of the annotator activity. - * It is still possible for the host page to manipulate the events on the capturing - * face. - * - * Another benefit is that click and touchstart typically causes the sidebar to close. - * By preventing the bubble up of these events, we don't have to individually stop - * the propagation. - */ -function stopEventPropagation(element: HTMLElement | ShadowRoot) { - element.addEventListener('mouseup', event => event.stopPropagation()); - element.addEventListener('mousedown', event => event.stopPropagation()); - element.addEventListener('touchstart', event => event.stopPropagation(), { - passive: true, - }); -} diff --git a/src/annotator/util/test/shadow-root-test.js b/src/annotator/util/test/shadow-root-test.js index dcbc9095998..6e2effc5556 100644 --- a/src/annotator/util/test/shadow-root-test.js +++ b/src/annotator/util/test/shadow-root-test.js @@ -60,53 +60,5 @@ describe('annotator/util/shadow-root', () => { assert.isNull(linkEl); link.setAttribute('rel', 'stylesheet'); }); - - it('stops propagation of "mouseup" events', () => { - const onClick = sinon.stub(); - container.addEventListener('click', onClick); - - const shadowRoot = createShadowRoot(container); - const innerElement = document.createElement('div'); - shadowRoot.appendChild(innerElement); - innerElement.dispatchEvent( - // `composed` property is necessary to bubble up the event out of the shadow DOM. - // browser generated events, have this property set to true. - new Event('mouseup', { bubbles: true, composed: true }), - ); - - assert.notCalled(onClick); - }); - - it('stops propagation of "mousedown" events', () => { - const onClick = sinon.stub(); - container.addEventListener('mousedown', onClick); - - const shadowRoot = createShadowRoot(container); - const innerElement = document.createElement('div'); - shadowRoot.appendChild(innerElement); - innerElement.dispatchEvent( - // `composed` property is necessary to bubble up the event out of the shadow DOM. - // browser generated events, have this property set to true. - new Event('mousedown', { bubbles: true, composed: true }), - ); - - assert.notCalled(onClick); - }); - - it('stops propagation of "touchstart" events', () => { - const onTouch = sinon.stub(); - container.addEventListener('touchstart', onTouch); - - const shadowRoot = createShadowRoot(container); - const innerElement = document.createElement('div'); - shadowRoot.appendChild(innerElement); - // `composed` property is necessary to bubble up the event out of the shadow DOM. - // browser generated events, have this property set to true. - innerElement.dispatchEvent( - new Event('touchstart', { bubbles: true, composed: true }), - ); - - assert.notCalled(onTouch); - }); }); });