Merge "Migrate apache commons lang v2 usages to v3"
diff --git a/plugins/replication b/plugins/replication
index ba3a9eb..220ce68 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit ba3a9eb92811feef9c2f47287613badee987a9d2
+Subproject commit 220ce6829d4b8382374b08045dc4a1459db8f001
diff --git a/polygerrit-ui/app/elements/checks/gr-hovercard-run_test.ts b/polygerrit-ui/app/elements/checks/gr-hovercard-run_test.ts
index 352219a..e217a79 100644
--- a/polygerrit-ui/app/elements/checks/gr-hovercard-run_test.ts
+++ b/polygerrit-ui/app/elements/checks/gr-hovercard-run_test.ts
@@ -33,7 +33,7 @@
});
teardown(() => {
- element.hide(new MouseEvent('click'));
+ element.mouseClickHide(new MouseEvent('click'));
});
test('hovercard is shown', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
index b638bc1..48273ef 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
@@ -448,7 +448,7 @@
.then(() => {
this.dispatchEventThroughTarget('hide-alert');
});
- this.hide(e);
+ this.mouseClickHide(e);
}
private handleClickRemoveFromAttentionSet(e: MouseEvent) {
@@ -479,7 +479,7 @@
.then(() => {
this.dispatchEventThroughTarget('hide-alert');
});
- this.hide(e);
+ this.mouseClickHide(e);
}
private reportingDetails() {
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.ts
index 1d67500..7e99bb7 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.ts
@@ -66,12 +66,12 @@
>
</gr-hovercard-account>`
);
- await element.show();
+ await element.show({});
await element.updateComplete;
});
teardown(async () => {
- await element.hide(new MouseEvent('click'));
+ await element.mouseClickHide(new MouseEvent('click'));
await element.updateComplete;
});
diff --git a/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin.ts b/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin.ts
index 0ae88dd..a17b75f 100644
--- a/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin.ts
+++ b/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin.ts
@@ -23,6 +23,8 @@
import {hovercardStyles} from '../../styles/gr-hovercard-styles';
import {sharedStyles} from '../../styles/shared-styles';
import {DependencyRequestEvent} from '../../models/dependency';
+import {addShortcut, Key} from '../../utils/dom-util';
+import {ShortcutController} from '../../elements/lit/shortcut-controller';
interface ReloadEventDetail {
clearPatchset?: boolean;
@@ -36,6 +38,12 @@
*/
const containerId = 'gr-hovercard-container';
+export interface MouseKeyboardOrFocusEvent {
+ keyboardEvent?: KeyboardEvent;
+ mouseEvent?: MouseEvent;
+ focusEvent?: FocusEvent;
+}
+
export function getHovercardContainer(
options: {createIfNotExists: boolean} = {createIfNotExists: false}
): HTMLElement | null {
@@ -127,6 +135,10 @@
isScheduledToHide?: boolean;
+ openedByKeyboard = false;
+
+ private targetCleanups: Array<() => void> = [];
+
static get styles() {
return [sharedStyles, hovercardStyles];
}
@@ -136,9 +148,13 @@
super(...args);
// show the hovercard if mouse moves to hovercard
// this will cancel pending hide as well
- this.addEventListener('mouseenter', this.show);
+ this.addEventListener('mouseenter', () => this.show);
// when leave hovercard, hide it immediately
- this.addEventListener('mouseleave', this.hide);
+ this.addEventListener('mouseleave', () => this.hide);
+ const keyboardController = new ShortcutController(this);
+ keyboardController.addGlobal({key: Key.ESC}, (e: KeyboardEvent) =>
+ this.hide({keyboardEvent: e})
+ );
}
override connectedCallback() {
@@ -165,20 +181,34 @@
// trigger the hovercard, which can annoying for the user, for example
// when added reviewer chips appear in the reply dialog via keyboard
// interaction.
- this._target?.addEventListener('mousemove', this.debounceShow);
- this._target?.addEventListener('focus', this.debounceShow);
- this._target?.addEventListener('mouseleave', this.debounceHide);
- this._target?.addEventListener('blur', this.debounceHide);
- this._target?.addEventListener('click', this.hide);
+ this._target?.addEventListener('mousemove', this.mouseDebounceShow);
+ this._target?.addEventListener('mouseleave', this.mouseDebounceHide);
+ this._target?.addEventListener('blur', this.focusDebounceHide);
+ this._target?.addEventListener('click', this.mouseClickHide);
+ if (this._target) {
+ this.targetCleanups.push(
+ addShortcut(this._target, {key: Key.ENTER}, (e: KeyboardEvent) => {
+ this.show({keyboardEvent: e});
+ })
+ );
+ this.targetCleanups.push(
+ addShortcut(this._target, {key: Key.SPACE}, (e: KeyboardEvent) => {
+ this.show({keyboardEvent: e});
+ })
+ );
+ }
this.addEventListener('request-dependency', this.resolveDep);
}
private removeTargetEventListeners() {
- this._target?.removeEventListener('mousemove', this.debounceShow);
- this._target?.removeEventListener('focus', this.debounceShow);
- this._target?.removeEventListener('mouseleave', this.debounceHide);
- this._target?.removeEventListener('blur', this.debounceHide);
- this._target?.removeEventListener('click', this.hide);
+ this._target?.removeEventListener('mousemove', this.mouseDebounceShow);
+ this._target?.removeEventListener('mouseleave', this.mouseDebounceHide);
+ this._target?.removeEventListener('blur', this.focusDebounceHide);
+ this._target?.removeEventListener('click', this.mouseClickHide);
+ for (const cleanup of this.targetCleanups) {
+ cleanup();
+ }
+ this.targetCleanups = [];
this.removeEventListener('request-dependency', this.resolveDep);
}
@@ -195,7 +225,19 @@
}
}
- readonly debounceHide = () => {
+ readonly mouseDebounceHide = (e: MouseEvent) => {
+ this.debounceHide({mouseEvent: e});
+ };
+
+ readonly mouseDebounceShow = (e: MouseEvent) => {
+ this.debounceShow({mouseEvent: e});
+ };
+
+ readonly focusDebounceHide = (e: FocusEvent) => {
+ this.debounceHide({focusEvent: e});
+ };
+
+ readonly debounceHide = (props: MouseKeyboardOrFocusEvent) => {
this.cancelShowTask();
if (!this._isShowing || this.isScheduledToHide) return;
this.isScheduledToHide = true;
@@ -205,7 +247,7 @@
// This happens when hide immediately through click or mouse leave
// on the hovercard
if (!this.isScheduledToHide) return;
- this.hide();
+ this.hide(props);
},
HIDE_DELAY_MS
);
@@ -277,29 +319,43 @@
);
};
+ mouseClickHide = (e: MouseEvent) => {
+ // If the user is clicking on a link and still hovering over the hovercard
+ // or the user is returning from the hovercard but now hovering over the
+ // target (to stop an annoying flicker effect), just return.
+ if (
+ e &&
+ (e.relatedTarget === this ||
+ (e.target === this && e.relatedTarget === this._target))
+ ) {
+ return;
+ }
+ // We allow hiding hovercards on clicks outside even if the keyboard was
+ // the reason that it was displayed.
+ this.hide({mouseEvent: e});
+ };
+
/**
* Hides/closes the hovercard. This occurs when the user triggers the
* `mouseleave` event on the hovercard's `target` element (as long as the
- * user is not hovering over the hovercard).
+ * user is not hovering over the hovercard). If event not specified
+ * in props, code assumes mouseEvent
*/
- readonly hide = (e?: MouseEvent) => {
+ readonly hide = (props: MouseKeyboardOrFocusEvent) => {
this.cancelHideTask();
this.cancelShowTask();
if (!this._isShowing) {
return;
}
-
- // If the user is now hovering over the hovercard or the user is returning
- // from the hovercard but now hovering over the target (to stop an annoying
- // flicker effect), just return.
- if (e) {
- if (
- e.relatedTarget === this ||
- (e.target === this && e.relatedTarget === this._target)
- ) {
- return;
+ if (!props?.keyboardEvent && this.openedByKeyboard) return;
+ if (this.openedByKeyboard) {
+ if (this._target) {
+ this._target.focus();
}
}
+ // Make sure to reset the keyboard variable so new shows will not
+ // assume keyboard is the reason for opening the hovercard.
+ this.openedByKeyboard = false;
// Mark that the hovercard is not visible and do not allow focusing
this._isShowing = false;
@@ -321,14 +377,14 @@
/**
* Shows/opens the hovercard with a fixed delay.
*/
- readonly debounceShow = () => {
- this.debounceShowBy(SHOW_DELAY_MS);
+ readonly debounceShow = (props: MouseKeyboardOrFocusEvent) => {
+ this.debounceShowBy(SHOW_DELAY_MS, props);
};
/**
* Shows/opens the hovercard with the given delay.
*/
- debounceShowBy(delayMs: number) {
+ debounceShowBy(delayMs: number, props: MouseKeyboardOrFocusEvent) {
this.cancelHideTask();
if (this._isShowing || this.isScheduledToShow) return;
this.isScheduledToShow = true;
@@ -337,7 +393,7 @@
() => {
// This happens when the mouse leaves the target before the delay is over.
if (!this.isScheduledToShow) return;
- this.show();
+ this.show(props);
},
delayMs
);
@@ -352,11 +408,16 @@
/**
* Shows/opens the hovercard. This occurs when the user triggers the
- * `mousenter` event on the hovercard's `target` element.
+ * `mousenter` event on the hovercard's `target` element or when a user
+ * presses enter/space on the hovercard's `target` element. If event not
+ * specified in props, code assumes mouseEvent
*/
- readonly show = async () => {
+ readonly show = async (props: MouseKeyboardOrFocusEvent) => {
this.cancelHideTask();
this.cancelShowTask();
+ // If we are calling show again because of a mouse reason, then keep
+ // the keyboard valuable set.
+ this.openedByKeyboard = this.openedByKeyboard || !!props?.keyboardEvent;
if (this._isShowing || !this.container) {
return;
}
@@ -379,6 +440,9 @@
});
this.updatePosition();
this.classList.remove(HIDE_CLASS);
+ if (props?.keyboardEvent) {
+ this.focus();
+ }
};
updatePosition() {
@@ -490,15 +554,16 @@
_target: HTMLElement | null;
_isShowing: boolean;
dispatchEventThroughTarget(eventName: string, detail?: unknown): void;
- show(): void;
+ show(props: MouseKeyboardOrFocusEvent): void;
// Used for tests
- hide(e: MouseEvent): void;
+ mouseClickHide(e: MouseEvent): void;
+ hide(props: MouseKeyboardOrFocusEvent): void;
container: HTMLElement | null;
hideTask?: DelayedTask;
showTask?: DelayedTask;
position: string;
- debounceShowBy(delayMs: number): void;
+ debounceShowBy(delayMs: number, props: MouseKeyboardOrFocusEvent): void;
updatePosition(): void;
isScheduledToShow?: boolean;
isScheduledToHide?: boolean;
diff --git a/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin_test.ts b/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin_test.ts
index e6b63e6..7bba8c5 100644
--- a/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin_test.ts
+++ b/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin_test.ts
@@ -19,7 +19,8 @@
import {HovercardMixin} from './hovercard-mixin.js';
import {html, LitElement} from 'lit';
import {customElement} from 'lit/decorators';
-import {MockPromise, mockPromise} from '../../test/test-utils.js';
+import {MockPromise, mockPromise, pressKey} from '../../test/test-utils.js';
+import {findActiveElement, Key} from '../../utils/dom-util.js';
const base = HovercardMixin(LitElement);
@@ -60,7 +61,8 @@
});
teardown(() => {
- element.hide(new MouseEvent('click'));
+ pressKey(element, Key.ESC);
+ element.mouseClickHide(new MouseEvent('click'));
button?.remove();
});
@@ -95,7 +97,7 @@
});
test('hide', () => {
- element.hide(new MouseEvent('click'));
+ element.mouseClickHide(new MouseEvent('click'));
const style = getComputedStyle(element);
assert.isFalse(element._isShowing);
assert.isFalse(element.classList.contains('hovered'));
@@ -104,7 +106,7 @@
});
test('show', async () => {
- await element.show();
+ await element.show({});
await element.updateComplete;
const style = getComputedStyle(element);
assert.isTrue(element._isShowing);
@@ -114,14 +116,14 @@
});
test('debounceShow does not show immediately', async () => {
- element.debounceShowBy(100);
+ element.debounceShowBy(100, {});
setTimeout(() => testPromise.resolve(), 0);
await testPromise;
assert.isFalse(element._isShowing);
});
test('debounceShow shows after delay', async () => {
- element.debounceShowBy(1);
+ element.debounceShowBy(1, {});
setTimeout(() => testPromise.resolve(), 10);
await testPromise;
assert.isTrue(element._isShowing);
@@ -174,4 +176,52 @@
assert.isFalse(element.isScheduledToShow);
assert.isFalse(element._isShowing);
});
+
+ test('do not show on focus', async () => {
+ const button = document.querySelector('button');
+ button?.focus();
+ await element.updateComplete;
+ assert.isNotTrue(element.isScheduledToShow);
+ assert.isFalse(element._isShowing);
+ });
+
+ test('show on pressing enter when focused', async () => {
+ const button = document.querySelector('button')!;
+ button.focus();
+ await element.updateComplete;
+ pressKey(button, Key.ENTER);
+ await element.updateComplete;
+ assert.isTrue(element._isShowing);
+ });
+
+ test('show on pressing space when focused', async () => {
+ const button = document.querySelector('button')!;
+ button.focus();
+ await element.updateComplete;
+ pressKey(button, Key.SPACE);
+ await element.updateComplete;
+ assert.isTrue(element._isShowing);
+ });
+
+ test('when on pressing enter, focus is moved to hovercard', async () => {
+ const button = document.querySelector('button')!;
+ button.focus();
+ await element.updateComplete;
+ await element.show({keyboardEvent: new KeyboardEvent('enter')});
+ await element.updateComplete;
+ assert.isTrue(element._isShowing);
+ const activeElement = findActiveElement(document);
+ assert.isTrue(activeElement === element);
+ });
+
+ test('when on mouseEvent, focus is not moved to hovercard', async () => {
+ const button = document.querySelector('button')!;
+ button.focus();
+ await element.updateComplete;
+ await element.show({mouseEvent: new MouseEvent('enter')});
+ await element.updateComplete;
+ assert.isTrue(element._isShowing);
+ const activeElement = findActiveElement(document);
+ assert.isTrue(activeElement !== element);
+ });
});