Skip to content

feat(select): add alternate rendering strategy for improved accessibility #14430

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion src/cdk-experimental/dialog/dialog-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import {
BasePortalOutlet,
ComponentPortal,
PortalHostDirective,
TemplatePortal
TemplatePortal,
DomPortal
} from '@angular/cdk/portal';
import {DOCUMENT} from '@angular/common';
import {
Expand Down Expand Up @@ -176,6 +177,19 @@ export class CdkDialogContainer extends BasePortalOutlet implements OnDestroy {
return this._portalHost.attachTemplatePortal(portal);
}

/**
* Attaches a DOM portal to the dialog container.
* @param portal Portal to be attached.
*/
attachDomPortal(portal: DomPortal) {
if (this._portalHost.hasAttached()) {
throwDialogContentAlreadyAttachedError();
}

this._savePreviouslyFocusedElement();
return this._portalHost.attachDomPortal(portal);
}

/** Emit lifecycle events based on animation `start` callback. */
_onAnimationStart(event: AnimationEvent) {
if (event.toState === 'enter') {
Expand Down
20 changes: 14 additions & 6 deletions src/cdk/overlay/overlay-directives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import {Direction, Directionality} from '@angular/cdk/bidi';
import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {ESCAPE} from '@angular/cdk/keycodes';
import {TemplatePortal} from '@angular/cdk/portal';
import {TemplatePortal, Portal} from '@angular/cdk/portal';
import {
Directive,
ElementRef,
Expand Down Expand Up @@ -104,7 +104,6 @@ export class CdkOverlayOrigin {
})
export class CdkConnectedOverlay implements OnDestroy, OnChanges {
private _overlayRef: OverlayRef;
private _templatePortal: TemplatePortal;
private _hasBackdrop = false;
private _lockPosition = false;
private _growAfterOpen = false;
Expand Down Expand Up @@ -198,6 +197,12 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
get push() { return this._push; }
set push(value: boolean) { this._push = coerceBooleanProperty(value); }

/**
* Portal that should be projected into the overlay. If none is provided, one will be
* created automatically from the overlay element's content.
*/
@Input('cdkConnectedOverlayPortal') portal: Portal<any>;

/** Event emitted when the backdrop is clicked. */
@Output() backdropClick = new EventEmitter<MouseEvent>();

Expand All @@ -217,11 +222,10 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {

constructor(
private _overlay: Overlay,
templateRef: TemplateRef<any>,
viewContainerRef: ViewContainerRef,
private _templateRef: TemplateRef<any>,
private _viewContainerRef: ViewContainerRef,
@Inject(CDK_CONNECTED_OVERLAY_SCROLL_STRATEGY) scrollStrategyFactory: any,
@Optional() private _dir: Directionality) {
this._templatePortal = new TemplatePortal(templateRef, viewContainerRef);
this._scrollStrategyFactory = scrollStrategyFactory;
this.scrollStrategy = this._scrollStrategyFactory();
}
Expand Down Expand Up @@ -359,8 +363,12 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
this._overlayRef.getConfig().hasBackdrop = this.hasBackdrop;
}

if (!this.portal) {
this.portal = new TemplatePortal(this._templateRef, this._viewContainerRef);
}

if (!this._overlayRef.hasAttached()) {
this._overlayRef.attach(this._templatePortal);
this._overlayRef.attach(this.portal);
this.attach.emit();
}

Expand Down
24 changes: 23 additions & 1 deletion src/cdk/portal/dom-portal-outlet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
ApplicationRef,
Injector,
} from '@angular/core';
import {BasePortalOutlet, ComponentPortal, TemplatePortal} from './portal';
import {BasePortalOutlet, ComponentPortal, TemplatePortal, DomPortal} from './portal';


/**
Expand Down Expand Up @@ -93,6 +93,28 @@ export class DomPortalOutlet extends BasePortalOutlet {
return viewRef;
}

/**
* Attaches a DOM portal by transferring its content into the outlet.
* @param portal Portal to be attached.
*/
attachDomPortal(portal: DomPortal) {
// Note that we need to convert this into an array, because `childNodes`
// is a live collection which will be updated as we add/remove nodes.
let transferredNodes = Array.from(portal.element.childNodes);

for (let i = 0; i < transferredNodes.length; i++) {
this.outletElement.appendChild(transferredNodes[i]);
}

super.setDisposeFn(() => {
for (let i = 0; i < transferredNodes.length; i++) {
portal.element.appendChild(transferredNodes[i]);
}

transferredNodes = null!;
});
}

/**
* Clears out a portal from the DOM.
*/
Expand Down
27 changes: 25 additions & 2 deletions src/cdk/portal/portal-directives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
TemplateRef,
ViewContainerRef,
} from '@angular/core';
import {BasePortalOutlet, ComponentPortal, Portal, TemplatePortal} from './portal';
import {BasePortalOutlet, ComponentPortal, Portal, TemplatePortal, DomPortal} from './portal';


/**
Expand Down Expand Up @@ -141,7 +141,7 @@ export class CdkPortalOutlet extends BasePortalOutlet implements OnInit, OnDestr
}

/**
* Attach the given TemplatePortal to this PortlHost as an embedded View.
* Attach the given TemplatePortal to this PortalHost as an embedded View.
* @param portal Portal to be attached.
* @returns Reference to the created embedded view.
*/
Expand All @@ -156,6 +156,29 @@ export class CdkPortalOutlet extends BasePortalOutlet implements OnInit, OnDestr

return viewRef;
}

/**
* Attaches the given DomPortal to this PortalHost by moving all of the portal content into it.
* @param portal Portal to be attached.
*/
attachDomPortal(portal: DomPortal) {
portal.setAttachedHost(this);

const origin = portal.element;
const transferredNodes: Node[] = [];
const nativeElement: Node = this._viewContainerRef.element.nativeElement;
const rootNode = nativeElement.nodeType === nativeElement.ELEMENT_NODE ?
nativeElement : nativeElement.parentNode!;

while (origin.firstChild) {
transferredNodes.push(rootNode.appendChild(origin.firstChild));
}

super.setDisposeFn(() => {
transferredNodes.forEach(node => portal.element.appendChild(node));
transferredNodes.length = 0;
});
}
}


Expand Down
49 changes: 47 additions & 2 deletions src/cdk/portal/portal.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ import {
ApplicationRef,
TemplateRef,
ComponentRef,
ElementRef,
} from '@angular/core';
import {CommonModule} from '@angular/common';
import {CdkPortal, CdkPortalOutlet, PortalModule} from './portal-directives';
import {Portal, ComponentPortal, TemplatePortal} from './portal';
import {Portal, ComponentPortal, TemplatePortal, DomPortal} from './portal';
import {DomPortalOutlet} from './dom-portal-outlet';


Expand Down Expand Up @@ -76,6 +77,35 @@ describe('Portals', () => {
.toHaveBeenCalledWith(testAppComponent.portalOutlet.attachedRef);
});

it('should load a DOM portal', () => {
const testAppComponent = fixture.componentInstance;
const hostContainer = fixture.nativeElement.querySelector('.portal-container');
const innerContent = fixture.nativeElement.querySelector('.dom-portal-inner-content');
const domPortal = new DomPortal(testAppComponent.domPortalContent);

expect(innerContent).toBeTruthy('Expected portal content to be rendered.');
expect(domPortal.element.contains(innerContent))
.toBe(true, 'Expected content to be inside portal on init.');
expect(hostContainer.contains(innerContent))
.toBe(false, 'Expected content to be outside of portal outlet.');

testAppComponent.selectedPortal = domPortal;
fixture.detectChanges();

expect(domPortal.element.contains(innerContent))
.toBe(false, 'Expected content to be out of the portal on attach.');
expect(hostContainer.contains(innerContent))
.toBe(true, 'Expected content to be inside the outlet on attach.');

testAppComponent.selectedPortal = undefined;
fixture.detectChanges();

expect(domPortal.element.contains(innerContent))
.toBe(true, 'Expected content to be at initial position on detach.');
expect(hostContainer.contains(innerContent))
.toBe(false, 'Expected content to be removed from outlet on detach.');
});

it('should project template context bindings in the portal', () => {
let testAppComponent = fixture.componentInstance;
let hostContainer = fixture.nativeElement.querySelector('.portal-container');
Expand Down Expand Up @@ -502,6 +532,16 @@ describe('Portals', () => {
expect(spy).toHaveBeenCalled();
});

it('should attach and detach a DOM portal', () => {
const fixture = TestBed.createComponent(PortalTestApp);
fixture.detectChanges();
const portal = new DomPortal(fixture.componentInstance.domPortalContent);

portal.attach(host);

expect(someDomElement.textContent).toContain('Hello there');
});

});
});

Expand Down Expand Up @@ -559,12 +599,17 @@ class ArbitraryViewContainerRefComponent {
</ng-template>

<ng-template #templateRef let-data> {{fruit}} - {{ data?.status }}!</ng-template>

<div #domPortalContent>
<p class="dom-portal-inner-content">Hello there</p>
</div>
`,
})
class PortalTestApp {
@ViewChildren(CdkPortal) portals: QueryList<CdkPortal>;
@ViewChild(CdkPortalOutlet, {static: true}) portalOutlet: CdkPortalOutlet;
@ViewChild('templateRef', { read: TemplateRef , static: true}) templateRef: TemplateRef<any>;
@ViewChild('templateRef', {read: TemplateRef, static: true}) templateRef: TemplateRef<any>;
@ViewChild('domPortalContent', {static: true}) domPortalContent: ElementRef<HTMLElement>;

selectedPortal: Portal<any>|undefined;
fruit: string = 'Banana';
Expand Down
23 changes: 23 additions & 0 deletions src/cdk/portal/portal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,22 @@ export class TemplatePortal<C = any> extends Portal<EmbeddedViewRef<C>> {
}
}

/**
* A `DomPortal` is a portal whose content will be taken from its current position
* in the DOM and moved into a portal outlet, when it is attached. On detach, the content
* will be restored to its original position.
*/
export class DomPortal extends Portal<HTMLElement> {
constructor(private _element: HTMLElement | ElementRef<HTMLElement>) {
super();
}

/** DOM node hosting the portal's content. */
get element(): HTMLElement {
return this._element instanceof ElementRef ? this._element.nativeElement : this._element;
}
}


/** A `PortalOutlet` is an space that can contain a single `Portal`. */
export interface PortalOutlet {
Expand Down Expand Up @@ -213,6 +229,10 @@ export abstract class BasePortalOutlet implements PortalOutlet {
} else if (portal instanceof TemplatePortal) {
this._attachedPortal = portal;
return this.attachTemplatePortal(portal);
// @breaking-change 8.0.0 remove null check for `this.attachDomPortal`.
} else if (this.attachDomPortal && portal instanceof DomPortal) {
this._attachedPortal = portal;
return this.attachDomPortal(portal);
}

throwUnknownPortalTypeError();
Expand All @@ -222,6 +242,9 @@ export abstract class BasePortalOutlet implements PortalOutlet {

abstract attachTemplatePortal<C>(portal: TemplatePortal<C>): EmbeddedViewRef<C>;

// @breaking-change 8.0.0 `attachDomPortal` to become a required method.
abstract attachDomPortal?(portal: DomPortal): any;

/** Detaches a previously attached portal. */
detach(): void {
if (this._attachedPortal) {
Expand Down
9 changes: 9 additions & 0 deletions src/dev-app/portal/portal-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ <h2> The portal outlet is here: </h2>
Science joke
</button>

<button type="button" (click)="selectedPortal = dadJoke">
Dad joke
</button>

<!-- Template vars on <ng-template> elements can't be accessed _in_ the template because Angular
doesn't support grabbing the instance / TemplateRef this way because the variable may be
referring to something *in* the template (such as #item in ngFor). As such, the component
Expand All @@ -29,3 +33,8 @@ <h2> The portal outlet is here: </h2>
<p> - Did you hear about this year's Fibonacci Conference? </p>
<p> - It's going to be as big as the last two put together. </p>
</div>

<div class="demo-dad-joke" #domPortalSource>
<p> - Scientists got bored of watching the moon for 24 hours </p>
<p> - So they called it a day. </p>
</div>
4 changes: 4 additions & 0 deletions src/dev-app/portal/portal-demo.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@
width: 500px;
height: 100px;
}

.demo-dad-joke {
opacity: 0.25;
}
9 changes: 7 additions & 2 deletions src/dev-app/portal/portal-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/

import {ComponentPortal, Portal, CdkPortal} from '@angular/cdk/portal';
import {Component, QueryList, ViewChildren} from '@angular/core';
import {ComponentPortal, Portal, CdkPortal, DomPortal} from '@angular/cdk/portal';
import {Component, QueryList, ViewChildren, ElementRef, ViewChild} from '@angular/core';


@Component({
Expand All @@ -18,6 +18,7 @@ import {Component, QueryList, ViewChildren} from '@angular/core';
})
export class PortalDemo {
@ViewChildren(CdkPortal) templatePortals: QueryList<Portal<any>>;
@ViewChild('domPortalSource', {static: false}) domPortalSource: ElementRef<HTMLElement>;

selectedPortal: Portal<any>;

Expand All @@ -32,6 +33,10 @@ export class PortalDemo {
get scienceJoke() {
return new ComponentPortal(ScienceJoke);
}

get dadJoke() {
return new DomPortal(this.domPortalSource);
}
}


Expand Down
9 changes: 9 additions & 0 deletions src/material/bottom-sheet/bottom-sheet-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
ComponentPortal,
TemplatePortal,
CdkPortalOutlet,
DomPortal,
} from '@angular/cdk/portal';
import {BreakpointObserver, Breakpoints} from '@angular/cdk/layout';
import {MatBottomSheetConfig} from './bottom-sheet-config';
Expand Down Expand Up @@ -122,6 +123,14 @@ export class MatBottomSheetContainer extends BasePortalOutlet implements OnDestr
return this._portalOutlet.attachTemplatePortal(portal);
}

/** Attaches a DOM portal to the bottom sheet container. */
attachDomPortal(portal: DomPortal) {
this._validatePortalAttached();
this._setPanelClass();
this._savePreviouslyFocusedElement();
return this._portalOutlet.attachDomPortal(portal);
}

/** Begin animation of bottom sheet entrance into view. */
enter(): void {
if (!this._destroyed) {
Expand Down
Loading