+
+ Add your favorite thing
+
+
+ Tap the add icon to add your favorites
+
+
+
+ `,
+})
+class TestComponent {
+ @ViewChild('calloutTemplate', {static: true})
+ calloutTemplateRef!: TemplateRef<{}>;
+
+ @ViewChild('target', {read: ViewContainerRef, static: true})
+ targetViewContainerRef!: ViewContainerRef;
+
+ enabled = true;
+}
+
+@NgModule({
+ declarations: [
+ TestComponent,
+ ],
+ imports: [
+ FeatureHighlightModule,
+ NoopAnimationsModule,
+ ],
+ entryComponents: [
+ TestComponent,
+ ],
+})
+class FeatureHighlightContentDirectivesTestModule {
+}
diff --git a/src/material-experimental/mat-feature-highlight/feature-highlight-content-directives.ts b/src/material-experimental/mat-feature-highlight/feature-highlight-content-directives.ts
new file mode 100644
index 000000000000..1f3e53a475e7
--- /dev/null
+++ b/src/material-experimental/mat-feature-highlight/feature-highlight-content-directives.ts
@@ -0,0 +1,141 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {Directive, Input, OnInit, Optional} from '@angular/core';
+
+import {FeatureHighlight} from './feature-highlight';
+import {FeatureHighlightRef} from './feature-highlight-ref';
+
+/**
+ * Target featured element to be highlighted. Use it to style the z-index and
+ * position correctly so that the target element is displayed on top of other
+ * feature highlight elements.
+ */
+@Directive({
+ selector: `[feature-highlight-target],
+ [featureHighlightTarget],
+ feature-highlight-target`,
+ exportAs: 'featureHighlightTarget',
+ host: {
+ '[style.zIndex]': 'zIndex',
+ '[style.position]': 'position',
+ },
+})
+export class FeatureHighlightTarget {
+ @Input() featureHighlightEnabled = true;
+
+ /**
+ * Return the zIndex value for the target element. When outer circle is not
+ * bounded, zIndex of the target element needs to be 1001 as the overlay
+ * container has zIndex of 1000. When outer circle is bounded, zIndex only
+ * needs to be 3.
+ */
+ get zIndex(): string|null {
+ return this.featureHighlightEnabled ? '1001' : null;
+ }
+
+ /**
+ * Return the position value for the target element. This is a key hack to
+ * make sure that our target element shows above the inner circle, as z-index
+ * only works for non static positioned elements.
+ */
+ get position(): string|null {
+ return this.featureHighlightEnabled ? 'relative' : null;
+ }
+}
+
+/** Button that closes the feature highlight. */
+@Directive({
+ selector: 'button[feature-highlight-close], button[featureHighlightClose]',
+ exportAs: 'featureHighlightClose',
+ host: {
+ '(click)': 'featureHighlightRef.dismiss()',
+ 'type': 'button',
+ }
+})
+export class FeatureHighlightClose implements OnInit {
+ constructor(
+ @Optional() private _featureHighlightRef: FeatureHighlightRef,
+ private readonly _featureHighlight: FeatureHighlight) {}
+
+
+ ngOnInit() {
+ this._featureHighlightRef = this._featureHighlightRef ||
+ this._featureHighlight.getFeatureHighlightRef();
+ }
+}
+
+/**
+ * Title of the feature highlight. Use it to assign aria-labelledby attribute
+ * to the container element.
+ */
+@Directive({
+ selector: `[feature-highlight-title],
+ [featureHighlightTitle],
+ feature-highlight-title`,
+ exportAs: 'featureHighlightTitle',
+ host: {
+ 'class': 'feature-highlight-title',
+ '[id]': 'id',
+ },
+})
+export class FeatureHighlightTitle implements OnInit {
+ @Input() id: string|null = 'feature-highlight-title-id';
+
+ constructor(
+ @Optional() private readonly _featureHighlightRef: FeatureHighlightRef,
+ private readonly _featureHighlight: FeatureHighlight) {}
+
+
+ ngOnInit() {
+ const ref = this._featureHighlightRef ||
+ this._featureHighlight.getFeatureHighlightRef();
+
+ if (ref) {
+ // Use Promise.resolve() to avoid setting aria attributes multile times
+ // in the same life cycle hook.
+ Promise.resolve().then(() => {
+ ref.containerInstance.updateConfig({ariaLabelledBy: this.id});
+ });
+ }
+ }
+}
+
+/**
+ * Content of the feature highlight. Use it to assign aria-describedby attribute
+ * to the container element.
+ */
+@Directive({
+ selector: `[feature-highlight-content],
+ [featureHighlightContent],
+ feature-highlight-content`,
+ host: {
+ 'class': 'feature-highlight-content',
+ '[id]': 'id',
+ },
+})
+export class FeatureHighlightContent implements OnInit {
+ @Input() id: string|null = 'feature-highlight-content-id';
+
+ constructor(
+ @Optional() private readonly _featureHighlightRef: FeatureHighlightRef,
+ private readonly _featureHighlight: FeatureHighlight) {}
+
+ /** @override */
+ ngOnInit() {
+ const ref = this._featureHighlightRef ||
+ this._featureHighlight.getFeatureHighlightRef();
+ if (ref) {
+ // Use Promise.resolve() to avoid setting aria attributes multile times
+ // in the same life cycle hook.
+ Promise.resolve().then(() => {
+ ref.containerInstance.updateConfig({ariaDescribedBy: this.id});
+ });
+ }
+ }
+}
diff --git a/src/material-experimental/mat-feature-highlight/feature-highlight-overlay-container.spec.ts b/src/material-experimental/mat-feature-highlight/feature-highlight-overlay-container.spec.ts
new file mode 100644
index 000000000000..2292082537b6
--- /dev/null
+++ b/src/material-experimental/mat-feature-highlight/feature-highlight-overlay-container.spec.ts
@@ -0,0 +1,50 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {OverlayModule} from '@angular/cdk/overlay';
+import {Component} from '@angular/core';
+import {async, TestBed} from '@angular/core/testing';
+
+import {FeatureHighlightOverlayContainer} from './feature-highlight-overlay-container';
+
+describe('FeatureHighlightOverlayContainer', () => {
+ let comp: TestComponent;
+
+ beforeEach(async(() => {
+ TestBed
+ .configureTestingModule({
+ declarations: [
+ TestComponent,
+ ],
+ imports: [
+ OverlayModule,
+ ],
+ })
+ .compileComponents();
+
+ const fixture = TestBed.createComponent(TestComponent);
+
+ comp = fixture.debugElement.componentInstance;
+ }));
+
+ it('does not create a container element by default', () => {
+ expect(comp.overlayContainer.getContainerElement()).toBeUndefined();
+ });
+
+ it('can pass a container element', () => {
+ const element = document.createElement('div');
+ comp.overlayContainer.setContainerElement(element);
+
+ expect(comp.overlayContainer.getContainerElement()).toEqual(element);
+ });
+});
+
+@Component({template: ''})
+class TestComponent {
+ constructor(readonly overlayContainer: FeatureHighlightOverlayContainer) {}
+}
diff --git a/src/material-experimental/mat-feature-highlight/feature-highlight-overlay-container.ts b/src/material-experimental/mat-feature-highlight/feature-highlight-overlay-container.ts
new file mode 100644
index 000000000000..38ca5c88e7cb
--- /dev/null
+++ b/src/material-experimental/mat-feature-highlight/feature-highlight-overlay-container.ts
@@ -0,0 +1,43 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {OverlayContainer} from '@angular/cdk/overlay';
+import {DOCUMENT} from '@angular/common';
+import {Inject, Injectable} from '@angular/core';
+
+/**
+ * An overlay container for hosting feature highlight component. Allow a
+ * container element to be set instead of creating a container element under
+ * which is done in {@link OverlayContainer}.
+ */
+@Injectable({providedIn: 'root'})
+export class FeatureHighlightOverlayContainer extends OverlayContainer {
+ constructor(
+ @Inject(DOCUMENT) document: Document,
+ ) {
+ super(document);
+ }
+
+ setContainerElement(element: HTMLElement) {
+ this._containerElement = element;
+ }
+
+ /** @override */
+ getContainerElement(): HTMLElement {
+ return this._containerElement;
+ }
+
+ /**
+ * Prevent a cdk overlay container element being created under body tag.
+ * @override
+ */
+ // tslint:disable-next-line:enforce-name-casing function is overridden.
+ _createContainer() {
+ return;
+ }
+}
diff --git a/src/material-experimental/mat-feature-highlight/feature-highlight-overlay.spec.ts b/src/material-experimental/mat-feature-highlight/feature-highlight-overlay.spec.ts
new file mode 100644
index 000000000000..653db37538bf
--- /dev/null
+++ b/src/material-experimental/mat-feature-highlight/feature-highlight-overlay.spec.ts
@@ -0,0 +1,45 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {OverlayModule} from '@angular/cdk/overlay';
+import {Component} from '@angular/core';
+import {async, TestBed} from '@angular/core/testing';
+
+import {FeatureHighlightOverlay} from './feature-highlight-overlay';
+
+describe('FeatureHighlightOverlay', () => {
+ let comp: TestComponent;
+
+ beforeEach(async(() => {
+ TestBed
+ .configureTestingModule({
+ declarations: [
+ TestComponent,
+ ],
+ imports: [
+ OverlayModule,
+ ],
+ })
+ .compileComponents();
+
+ const fixture = TestBed.createComponent(TestComponent);
+ comp = fixture.debugElement.componentInstance;
+ }));
+
+ it('can pass a container element', () => {
+ const element = document.createElement('div');
+ comp.overlay.setContainerElement(element);
+
+ expect(comp.overlay.getContainerElement()).toBe(element);
+ });
+});
+
+@Component({template: ''})
+class TestComponent {
+ constructor(readonly overlay: FeatureHighlightOverlay) {}
+}
diff --git a/src/material-experimental/mat-feature-highlight/feature-highlight-overlay.ts b/src/material-experimental/mat-feature-highlight/feature-highlight-overlay.ts
new file mode 100644
index 000000000000..2b7fd3957ec0
--- /dev/null
+++ b/src/material-experimental/mat-feature-highlight/feature-highlight-overlay.ts
@@ -0,0 +1,54 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {Directionality} from '@angular/cdk/bidi';
+import {Overlay, OverlayKeyboardDispatcher, OverlayPositionBuilder, ScrollStrategyOptions} from '@angular/cdk/overlay';
+import {DOCUMENT} from '@angular/common';
+import {ComponentFactoryResolver, Inject, Injectable, Injector, NgZone} from '@angular/core';
+
+import {FeatureHighlightOverlayContainer} from './feature-highlight-overlay-container';
+
+/**
+ * An overlay for hosting feature highlight component. Allow a container
+ * element to be passed into {@link FeatureHighlightOverlayContainer}.
+ */
+@Injectable({providedIn: 'root'})
+export class FeatureHighlightOverlay extends Overlay {
+ constructor(
+ readonly scrollStrategies: ScrollStrategyOptions,
+ private readonly _featureHighlightOverlayContainer:
+ FeatureHighlightOverlayContainer,
+ readonly componentFactoryResolver: ComponentFactoryResolver,
+ readonly positionBuilder: OverlayPositionBuilder,
+ readonly keyboardDispatcher: OverlayKeyboardDispatcher,
+ readonly injector: Injector,
+ readonly ngZone: NgZone,
+ @Inject(DOCUMENT) readonly document: Document,
+ readonly directionality: Directionality,
+ ) {
+ super(
+ scrollStrategies,
+ _featureHighlightOverlayContainer,
+ componentFactoryResolver,
+ positionBuilder,
+ keyboardDispatcher,
+ injector,
+ ngZone,
+ document,
+ directionality,
+ );
+ }
+
+ setContainerElement(element: HTMLElement) {
+ this._featureHighlightOverlayContainer.setContainerElement(element);
+ }
+
+ getContainerElement(): HTMLElement {
+ return this._featureHighlightOverlayContainer.getContainerElement();
+ }
+}
diff --git a/src/material-experimental/mat-feature-highlight/feature-highlight-ref.spec.ts b/src/material-experimental/mat-feature-highlight/feature-highlight-ref.spec.ts
new file mode 100644
index 000000000000..13a0f6c1290e
--- /dev/null
+++ b/src/material-experimental/mat-feature-highlight/feature-highlight-ref.spec.ts
@@ -0,0 +1,285 @@
+import {Component, Inject, Injector, NgModule, TemplateRef, Type, ViewChild, ViewContainerRef} from '@angular/core';
+import {async, TestBed} from '@angular/core/testing';
+import {By} from '@angular/platform-browser';
+import {NoopAnimationsModule} from '@angular/platform-browser/animations';
+
+import {FEATURE_HIGHLIGHT_DATA, FeatureHighlight} from './feature-highlight';
+import {FeatureHighlightOverlayContainer} from './feature-highlight-overlay-container';
+import {FeatureHighlightRef} from './feature-highlight-ref';
+import {FeatureHighlightModule} from './module';
+
+describe('FeatureHighlightRef', () => {
+ let featureHighlight: FeatureHighlight;
+ let overlayContainer: FeatureHighlightOverlayContainer;
+
+ beforeEach(async(() => {
+ TestBed
+ .configureTestingModule({
+ imports: [
+ FeatureHighlightTestModule,
+ ],
+ })
+ .compileComponents();
+
+ featureHighlight = TestBed.inject(FeatureHighlight);
+ overlayContainer = TestBed.inject(FeatureHighlightOverlayContainer);
+ }));
+
+ it('opens feature highlight with a template', () => {
+ const fixture = TestBed.createComponent(ComponentWithCalloutTemplate);
+ const ref = featureHighlight.open(
+ fixture.debugElement.componentInstance.calloutTemplateRef, {
+ targetViewContainerRef:
+ fixture.debugElement.componentInstance.targetViewContainerRef,
+ });
+ fixture.detectChanges();
+
+ const calloutElement = ref.containerInstance.callout.nativeElement;
+ expect(calloutElement.textContent).toContain('Add your favorite thing');
+ });
+
+ it('opens feature highlight with a component', () => {
+ const fixture = TestBed.createComponent(ComponentWithTargetViewContainer);
+ const ref = featureHighlight.open(CalloutComponent, {
+ targetViewContainerRef:
+ fixture.debugElement.componentInstance.targetViewContainerRef,
+ });
+ fixture.detectChanges();
+
+ const calloutElement = ref.containerInstance.callout.nativeElement;
+ expect(calloutElement.textContent).toContain('Add your favorite thing');
+ expect(ref.calloutInstance instanceof CalloutComponent).toBe(true);
+ });
+
+ it('opens feature highlight with overlay if outer circle is not bounded',
+ () => {
+ const fixture = TestBed.createComponent(ComponentWithCalloutTemplate);
+ featureHighlight.open(
+ fixture.debugElement.componentInstance.calloutTemplateRef, {
+ targetViewContainerRef:
+ fixture.debugElement.componentInstance.targetViewContainerRef,
+ isOuterCircleBounded: false,
+ });
+ fixture.detectChanges();
+
+ const overlayContainerElement = overlayContainer.getContainerElement();
+
+ expect(
+ overlayContainerElement.classList.contains('cdk-overlay-container'))
+ .toBe(true);
+ expect(overlayContainerElement.textContent)
+ .toContain('Add your favorite thing');
+ });
+
+ it('removes overlay container after feature highlight is dismissed', () => {
+ const fixture = TestBed.createComponent(ComponentWithTargetViewContainer);
+ const ref = featureHighlight.open(CalloutComponent, {
+ targetViewContainerRef:
+ fixture.debugElement.componentInstance.targetViewContainerRef,
+ isOuterCircleBounded: false,
+ });
+
+ ref.dismiss();
+ fixture.detectChanges();
+
+ expect(fixture.debugElement.query(By.css('.cdk-overlay-container')))
+ .toBeNull();
+ });
+
+ it('removes overlay container after feature highlight is accepted', () => {
+ const fixture = TestBed.createComponent(ComponentWithTargetViewContainer);
+ const ref = featureHighlight.open(CalloutComponent, {
+ targetViewContainerRef:
+ fixture.debugElement.componentInstance.targetViewContainerRef,
+ isOuterCircleBounded: false,
+ });
+
+ ref.accept();
+ fixture.detectChanges();
+
+ expect(fixture.debugElement.query(By.css('.cdk-overlay-container')))
+ .toBeNull();
+ });
+
+ it('opens feature highlight without overlay if outer circle is bounded',
+ () => {
+ const fixture = TestBed.createComponent(ComponentWithCalloutTemplate);
+ featureHighlight.open(
+ fixture.debugElement.componentInstance.calloutTemplateRef, {
+ targetViewContainerRef:
+ fixture.debugElement.componentInstance.targetViewContainerRef,
+ isOuterCircleBounded: true,
+ });
+ fixture.detectChanges();
+
+ expect(overlayContainer.getContainerElement()).toBeUndefined();
+ });
+
+ it('uses injector from target view container ref', () => {
+ const fixture = TestBed.createComponent(ComponentWithTargetViewContainer);
+ const ref = featureHighlight.open(CalloutComponent, {
+ targetViewContainerRef:
+ fixture.debugElement.componentInstance.targetViewContainerRef,
+ });
+ fixture.detectChanges();
+
+ expect(ref.calloutInstance.featureHighlightRef).toBe(ref);
+
+ const calloutInjector = ref.calloutInstance.calloutInjector;
+ expect(calloutInjector.get