Skip to content

Commit

Permalink
[User Education] Enable anchoring WebUI help bubble to arbitrary element
Browse files Browse the repository at this point in the history
* Rename `registerHelpBubbleIdentifier` -> `registerHelpBubble`
* Require `htmlId` parameter has a qualifying `#`
* Allow `HTMLElement` parameter for registration
* Update test error messages

Bug: 1385113
Change-Id: I510cea3e007c952416fa88f25a47354999f122ba
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4032266
Reviewed-by: Dana Fried <dfried@chromium.org>
Reviewed-by: Demetrios Papadopoulos <dpapad@chromium.org>
Commit-Queue: Mickey Burks <mickeyburks@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1073555}
  • Loading branch information
Mickey Burks authored and Chromium LUCI CQ committed Nov 18, 2022
1 parent 0c73c3e commit 311f123
Show file tree
Hide file tree
Showing 5 changed files with 68 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ class UserEducationInternalsElement extends UserEducationInternalsElementBase {
// register the anchor element.
this.$.promos.addEventListener(
'rendered-item-count-changed', (_: Event) => {
this.registerHelpBubbleIdentifier(
'kWebUIIPHDemoElementIdentifier', 'IPH_WebUiHelpBubbleTest');
this.registerHelpBubble(
'kWebUIIPHDemoElementIdentifier', '#IPH_WebUiHelpBubbleTest');
}, {
once: true,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,8 +231,8 @@ export class SettingsSecurityPageElement extends
}
});

this.registerHelpBubbleIdentifier(
'kEnhancedProtectionSettingElementId', 'safeBrowsingEnhanced');
this.registerHelpBubble(
'kEnhancedProtectionSettingElementId', '#safeBrowsingEnhanced');
}

/**
Expand Down
52 changes: 37 additions & 15 deletions chrome/test/data/webui/cr_components/help_bubble_mixin_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {isVisible} from 'chrome://webui-test/test_util.js';
const TITLE_NATIVE_ID: string = 'kHelpBubbleMixinTestTitleElementId';
const PARAGRAPH_NATIVE_ID: string = 'kHelpBubbleMixinTestParagraphElementId';
const LIST_NATIVE_ID: string = 'kHelpBubbleMixinTestListElementId';
const SPAN_NATIVE_ID: string = 'kHelpBubbleMixinTestSpanElementId';
const EVENT1_NAME: string = 'kFirstExampleCustomEvent';
const EVENT2_NAME: string = 'kSecondExampleCustomEvent';
const CLOSE_BUTTON_ALT_TEXT: string = 'Close help bubble.';
Expand All @@ -42,6 +43,7 @@ export interface HelpBubbleMixinTestElement {
let titleBubble: HelpBubbleController;
let p1Bubble: HelpBubbleController;
let bulletListBubble: HelpBubbleController;
let spanBubble: HelpBubbleController;

export class HelpBubbleMixinTestElement extends HelpBubbleMixinTestElementBase {
static get is() {
Expand All @@ -57,15 +59,20 @@ export class HelpBubbleMixinTestElement extends HelpBubbleMixinTestElementBase {
<li>List item 1</li>
<li>List item 2</li>
</ul>
<span>Span text</span>
</div>`;
}

override connectedCallback() {
super.connectedCallback();
titleBubble = this.registerHelpBubbleIdentifier(TITLE_NATIVE_ID, 'title');
p1Bubble = this.registerHelpBubbleIdentifier(PARAGRAPH_NATIVE_ID, 'p1');
bulletListBubble =
this.registerHelpBubbleIdentifier(LIST_NATIVE_ID, 'bulletList');

const spanEl = this.shadowRoot!.querySelector('span');
assertTrue(spanEl !== null, 'connectedCallback: span element exists');

titleBubble = this.registerHelpBubble(TITLE_NATIVE_ID, '#title');
p1Bubble = this.registerHelpBubble(PARAGRAPH_NATIVE_ID, '#p1');
bulletListBubble = this.registerHelpBubble(LIST_NATIVE_ID, '#bulletList');
spanBubble = this.registerHelpBubble(SPAN_NATIVE_ID, spanEl);
}
}

Expand Down Expand Up @@ -241,6 +248,15 @@ suite('CrComponentsHelpBubbleMixinTest', () => {
assertTrue(container.isHelpBubbleShowingForTesting('p1'));
});

test(
'help bubble mixin shows bubble anchored to arbitrary HTMLElment', () => {
assertFalse(container.isHelpBubbleShowing());
assertFalse(spanBubble.isShowing());
container.showHelpBubble(spanBubble, defaultParams);
assertTrue(container.isHelpBubbleShowing());
assertTrue(spanBubble.isShowing());
});

test('help bubble mixin reports not open for other elements', () => {
// Valid but not open.
assertFalse(container.isHelpBubbleShowingForTesting('title'));
Expand Down Expand Up @@ -290,11 +306,13 @@ suite('CrComponentsHelpBubbleMixinTest', () => {
'help bubble mixin shows help bubble when called via proxy', async () => {
testProxy.getCallbackRouterRemote().showHelpBubble(defaultParams);
await waitAfterNextRender(container);
assertTrue(container.isHelpBubbleShowing(), 'here');
assertTrue(container.isHelpBubbleShowing(), 'a bubble is showing');
const bubble = container.getHelpBubbleForTesting('p1');
assertTrue(!!bubble, 'now here');
assertEquals(container.$.p1, bubble.getAnchorElement(), 'f');
assertTrue(isVisible(bubble), 'now f');
assertTrue(!!bubble, 'bubble exists');
assertEquals(
container.$.p1, bubble.getAnchorElement(),
'bubble has correct anchor');
assertTrue(isVisible(bubble), 'bubble is visible');
});

test('help bubble mixin uses close button alt text', async () => {
Expand Down Expand Up @@ -392,16 +410,17 @@ suite('CrComponentsHelpBubbleMixinTest', () => {

test('help bubble mixin sends events on initially visible', async () => {
await waitAfterNextRender(container);
// Since we're watching three elements, we get events for all three.
// Since we're watching four elements, we get events for all four.
assertEquals(
3,
4,
testProxy.getHandler().getCallCount(
'helpBubbleAnchorVisibilityChanged'));
assertDeepEquals(
[
[TITLE_NATIVE_ID, true],
[PARAGRAPH_NATIVE_ID, true],
[LIST_NATIVE_ID, true],
[SPAN_NATIVE_ID, true],
],
testProxy.getHandler().getArgs('helpBubbleAnchorVisibilityChanged'));
});
Expand All @@ -410,17 +429,19 @@ suite('CrComponentsHelpBubbleMixinTest', () => {
container.style.display = 'none';
await waitForVisibilityEvents();
assertEquals(
6,
8,
testProxy.getHandler().getCallCount(
'helpBubbleAnchorVisibilityChanged'));
assertDeepEquals(
[
[TITLE_NATIVE_ID, true],
[PARAGRAPH_NATIVE_ID, true],
[LIST_NATIVE_ID, true],
[SPAN_NATIVE_ID, true],
[TITLE_NATIVE_ID, false],
[PARAGRAPH_NATIVE_ID, false],
[LIST_NATIVE_ID, false],
[SPAN_NATIVE_ID, false],
],
testProxy.getHandler().getArgs('helpBubbleAnchorVisibilityChanged'));
});
Expand Down Expand Up @@ -686,7 +707,7 @@ suite('CrComponentsHelpBubbleMixinTest', () => {
await waitAfterNextRender(container);
assertEquals(
0, testProxy.getHandler().getCallCount('helpBubbleClosed'),
'helpBubbleClosed should not be called');
'helpBubbleClosed has not be called');
assertTrue(container.isHelpBubbleShowing());
});

Expand All @@ -708,11 +729,12 @@ suite('CrComponentsHelpBubbleMixinTest', () => {
totalMs: 1500,
assertionFn: () => assertEquals(
1, testProxy.getHandler().getCallCount('helpBubbleClosed'),
'helpBubbleClosed should be called called'),
'helpBubbleClosed has been called'),
}) as number;
assertDeepEquals(
[[PARAGRAPH_NATIVE_ID, HelpBubbleClosedReason.kTimedOut]],
testProxy.getHandler().getArgs('helpBubbleClosed'), 'im here');
assertFalse(container.isHelpBubbleShowing(), 'or here');
testProxy.getHandler().getArgs('helpBubbleClosed'),
'helpBubbleClosed is called with correct arguments');
assertFalse(container.isHelpBubbleShowing(), 'no bubbles are showing');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {assert} from 'chrome://resources/js/assert_ts.js';
import {assert, assertNotReached} from 'chrome://resources/js/assert_ts.js';

import {HelpBubbleElement} from './help_bubble.js';
import {HelpBubbleParams} from './help_bubble.mojom-webui.js';

export type Trackable = string|HTMLElement;

/**
* HelpBubble controller class
* - There should exist only one HelpBubble instance for each nativeId
Expand Down Expand Up @@ -47,13 +49,22 @@ export class HelpBubbleController {
return this.nativeId_;
}

trackId(idString: string): boolean {
track(trackable: Trackable): boolean {
assert(!this.anchor_);

const anchor = this.root_.querySelector<HTMLElement>(`#${idString}`);
let anchor: HTMLElement|null = null;
if (typeof trackable === 'string') {
anchor = this.root_.querySelector<HTMLElement>(trackable);
} else if (trackable instanceof HTMLElement) {
anchor = trackable;
} else {
assertNotReached('HelpBubbleController.track() - anchor is unrecognized');
}

if (!anchor) {
return false;
}

anchor.dataset['nativeId'] = this.nativeId_;
this.anchor_ = anchor;
return true;
Expand Down
20 changes: 13 additions & 7 deletions ui/webui/resources/cr_components/help_bubble/help_bubble_mixin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {dedupingMixin, PolymerElement} from 'chrome://resources/polymer/v3_0/pol

import {HELP_BUBBLE_DISMISSED_EVENT, HELP_BUBBLE_TIMED_OUT_EVENT, HelpBubbleDismissedEvent, HelpBubbleElement} from './help_bubble.js';
import {HelpBubbleClientCallbackRouter, HelpBubbleClosedReason, HelpBubbleHandlerInterface, HelpBubbleParams} from './help_bubble.mojom-webui.js';
import {HelpBubbleController} from './help_bubble_controller.js';
import {HelpBubbleController, Trackable} from './help_bubble_controller.js';
import {HelpBubbleProxyImpl} from './help_bubble_proxy.js';

type Constructor<T> = new (...args: any[]) => T;
Expand Down Expand Up @@ -102,22 +102,28 @@ export const HelpBubbleMixin = dedupingMixin(

/**
* Maps `nativeId`, which should be the name of a ui::ElementIdentifier
* referenced by the WebUIController, with the `htmlId` of an element in
* this component.
* referenced by the WebUIController, with either an `htmlId` of
* an element in this component or an arbitrary HTMLElement.
*
* Example:
* registerHelpBubbleIdentifier(
* 'kMyComponentTitleLabelElementIdentifier',
* 'title');
* '#title');
*
* Example:
* registerHelpBubbleIdentifier(
* 'kMyComponentTitleLabelElementIdentifier',
* this.$.list.childNodes[0]);
*
*
* See README.md for full instructions.
*/
registerHelpBubbleIdentifier(nativeId: string, htmlId: string):
registerHelpBubble(nativeId: string, trackable: Trackable):
HelpBubbleController {
assert(!this.helpBubbleControllerById_.has(nativeId));
const controller =
new HelpBubbleController(nativeId, this.shadowRoot!);
controller.trackId(htmlId);
controller.track(trackable);
this.helpBubbleControllerById_.set(nativeId, controller);
// This can be called before or after `connectedCallback()`, so if the
// component isn't connected and the observer set up yet, delay
Expand Down Expand Up @@ -358,7 +364,7 @@ export const HelpBubbleMixin = dedupingMixin(
});

export interface HelpBubbleMixinInterface {
registerHelpBubbleIdentifier(nativeId: string, htmlId: string):
registerHelpBubble(nativeId: string, trackable: Trackable):
HelpBubbleController;
isHelpBubbleShowing(): boolean;
isHelpBubbleShowingForTesting(id: string): boolean;
Expand Down

0 comments on commit 311f123

Please sign in to comment.