Commit c4f39963 authored by Robert Knight's avatar Robert Knight

Add callback enabling client to test if side-by-side is active

When the host page takes control of side-by-side mode by adding:

```
sideBySide: { mode: 'manual' }
```

To the client's config, the client needs a way to determine whether side-by-side
mode was applied when the user clicks in the document. If it is applied, the
sidebar should remain open, otherwise it should be closed.

This PR adds an `isActive` callback to the `sideBySide` settings which the
client will invoke when it needs to know if side-by-side is active or not.
parent 16e37788
......@@ -467,6 +467,9 @@ loads.
window.hypothesisConfig = () => ({
sideBySide: {
mode: 'manual'
isActive: () => {
// Return true if side-by-side is active.
}
}
});
......@@ -483,6 +486,13 @@ loads.
`listen for layout changes </publishers/events/#cmdoption-arg-hypothesis-layoutchange>`_
and adapt content to fit alongside the sidebar, if it is reasonable to do so.
.. option:: isActive
When ``mode`` is set to ``manual``, Hypothesis will invoke this to determine
if side-by-side is active or not. This is called, for example, when the
user clicks somewhere in the document outside of the sidebar, so the client
can determine whether it should close the sidebar or not.
Asset and Sidebar App Location
##############################
......
......@@ -105,12 +105,24 @@ export function settingsFrom(window_: Window): SettingsGetters {
function sideBySide(): SideBySideOptions {
const value = hostPageSetting('sideBySide');
if (!isObject(value)) {
return { mode: 'auto' };
}
const mode =
'mode' in value && isSideBySideMode(value.mode) ? value.mode : 'auto';
if (mode === 'auto') {
return { mode };
}
const isActive =
'isActive' in value && typeof value.isActive === 'function'
? (value.isActive as () => boolean)
: undefined;
return {
mode:
!isObject(value) || !('mode' in value) || !isSideBySideMode(value.mode)
? 'auto'
: value.mode,
mode,
isActive,
};
}
......
......@@ -397,6 +397,8 @@ describe('annotator/config/settingsFrom', () => {
});
describe('#sideBySide', () => {
const fakeIsActive = () => true;
[
{
input: 'foo',
......@@ -415,8 +417,12 @@ describe('annotator/config/settingsFrom', () => {
expectedResult: { mode: 'auto' },
},
{
input: { mode: 'manual' },
expectedResult: { mode: 'manual' },
input: { mode: 'manual', isActive: 42 },
expectedResult: { mode: 'manual', isActive: undefined },
},
{
input: { mode: 'manual', isActive: fakeIsActive },
expectedResult: { mode: 'manual', isActive: fakeIsActive },
},
].forEach(({ input, expectedResult }) => {
it('parses config from script', () => {
......
......@@ -333,6 +333,13 @@ export class Guest extends TinyEmitter implements Annotator, Destroyable {
this._hoveredAnnotations = new Set();
}
private _sideBySideActive(): boolean {
if (this.sideBySide?.mode === 'manual' && this.sideBySide.isActive) {
return this.sideBySide.isActive();
}
return this._integration.sideBySideActive();
}
// Add DOM event listeners for clicks, taps etc. on the document and
// highlights.
_setupElementEvents() {
......@@ -346,7 +353,7 @@ export class Guest extends TinyEmitter implements Annotator, Destroyable {
const maybeCloseSidebar = (event: PointerEvent) => {
// Don't hide the sidebar if event was disabled because the sidebar
// doesn't overlap the content.
if (this._integration.sideBySideActive()) {
if (this._sideBySideActive()) {
return;
}
......@@ -474,7 +481,7 @@ export class Guest extends TinyEmitter implements Annotator, Destroyable {
this.element.dispatchEvent(
new LayoutChangeEvent({
sidebarLayout,
sideBySideActive: this._integration.sideBySideActive(),
sideBySideActive: this._sideBySideActive(),
})
);
});
......
......@@ -599,14 +599,18 @@ describe('Guest', () => {
describe('document events', () => {
let fakeHighlight;
let fakeSidebarFrame;
let guest;
let rootElement;
const createGuest = (config = {}) => {
const guest = new Guest(rootElement, config, hostFrame);
guest.setHighlightsVisible(true);
guests.push(guest);
return guest;
};
beforeEach(() => {
rootElement = document.createElement('div');
fakeSidebarFrame = null;
guest = createGuest();
guest.setHighlightsVisible(true);
rootElement = guest.element;
// Create a fake highlight as a target for hover and click events.
fakeHighlight = document.createElement('hypothesis-highlight');
......@@ -635,21 +639,47 @@ describe('Guest', () => {
);
it('hides sidebar', () => {
createGuest();
simulateClick();
assert.isTrue(sidebarClosed());
});
it('does not hide sidebar if target is a highlight', () => {
const guest = createGuest();
guest.setHighlightsVisible(true);
simulateClick(fakeHighlight);
assert.isFalse(sidebarClosed());
});
it('does not hide sidebar if side-by-side mode is active', () => {
createGuest();
fakeIntegration.sideBySideActive.returns(true);
simulateClick();
assert.isFalse(sidebarClosed());
});
it('does not hide sidebar if host page reports side-by-side is active', () => {
const isActive = sinon.stub().returns(true);
createGuest({
sideBySide: {
mode: 'manual',
isActive,
},
});
simulateClick();
assert.calledOnce(isActive);
assert.isFalse(sidebarClosed());
isActive.returns(false);
simulateClick();
assert.isTrue(sidebarClosed());
assert.calledTwice(isActive);
});
it('does not hide sidebar if event is within the bounds of the sidebar', () => {
createGuest();
emitHostEvent('sidebarLayoutChanged', { expanded: true, width: 300 });
......@@ -662,11 +692,13 @@ describe('Guest', () => {
});
it('does not reposition the adder if hidden when the window is resized', () => {
createGuest();
window.dispatchEvent(new Event('resize'));
assert.notCalled(FakeAdder.instance.show);
});
it('repositions the adder when the window is resized', () => {
createGuest();
simulateSelectionWithText();
assert.calledOnce(FakeAdder.instance.show);
FakeAdder.instance.show.resetHistory();
......@@ -677,6 +709,8 @@ describe('Guest', () => {
});
it('focuses annotations in the sidebar when hovering highlights in the document', () => {
createGuest();
// Hover the highlight
fakeHighlight.dispatchEvent(new Event('mouseover', { bubbles: true }));
assert.calledWith(highlighter.getHighlightsContainingNode, fakeHighlight);
......@@ -690,6 +724,7 @@ describe('Guest', () => {
});
it('does not focus annotations in the sidebar when a non-highlight element is hovered', () => {
createGuest();
rootElement.dispatchEvent(new Event('mouseover', { bubbles: true }));
assert.calledWith(highlighter.getHighlightsContainingNode, rootElement);
......@@ -697,6 +732,7 @@ describe('Guest', () => {
});
it('does not focus or select annotations in the sidebar if highlights are hidden', () => {
const guest = createGuest();
guest.setHighlightsVisible(false);
fakeHighlight.dispatchEvent(new Event('mouseover', { bubbles: true }));
......@@ -707,6 +743,7 @@ describe('Guest', () => {
});
it('selects annotations in the sidebar when clicking on a highlight', () => {
createGuest();
fakeHighlight.dispatchEvent(new Event('mouseup', { bubbles: true }));
assert.calledWith(sidebarRPC().call, 'showAnnotations', [
......@@ -716,6 +753,7 @@ describe('Guest', () => {
});
it('toggles selected annotations in the sidebar when Ctrl/Cmd-clicking a highlight', () => {
createGuest();
fakeHighlight.dispatchEvent(
new MouseEvent('mouseup', { bubbles: true, ctrlKey: true })
);
......
......@@ -345,8 +345,15 @@ export type DocumentInfo = {
* `manual`: The host app wants to manually take full control of side-by-side,
* effectively disabling the logic in client.
*/
export type SideBySideMode = 'auto' | 'manual';
export type SideBySideOptions = {
mode: SideBySideMode;
};
export type SideBySideOptions =
| { mode: 'auto' }
| {
mode: 'manual';
/**
* A callback that Hypothesis will call to determine whether side-by-side
* layout has been applied or not.
*/
isActive?: () => boolean;
};
export type SideBySideMode = SideBySideOptions['mode'];
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment