fix(aio): show embedded ToC (#23944)

On narrow screens (where there is not enough room on the right to show
the floating ToC), an embedded ToC is shown (via an `<aio-toc embedded>`
element in the document). Since ToC was not a custom element, the
component was not instantiated for the embedded element.

This commit fixes it by making `aio-toc` a custom element and loading it
manually for the floating ToC (if necessary).

PR Close #23944
This commit is contained in:
George Kalpakas 2018-05-16 20:24:24 +03:00 committed by Matias Niemelä
parent 431a42a238
commit 6e05ae02a2
9 changed files with 45 additions and 21 deletions

View File

@ -2,8 +2,8 @@
"aio": { "aio": {
"master": { "master": {
"uncompressed": { "uncompressed": {
"runtime": 2712, "runtime": 2768,
"main": 479729, "main": 475857,
"polyfills": 38453, "polyfills": 38453,
"prettify": 14913 "prettify": 14913
} }

View File

@ -58,7 +58,7 @@
</mat-sidenav-container> </mat-sidenav-container>
<div *ngIf="hasFloatingToc" class="toc-container no-print" [style.max-height.px]="tocMaxHeight" (mousewheel)="restrainScrolling($event)"> <div *ngIf="hasFloatingToc" class="toc-container no-print" [style.max-height.px]="tocMaxHeight" (mousewheel)="restrainScrolling($event)">
<aio-toc></aio-toc> <aio-lazy-ce selector="aio-toc"></aio-lazy-ce>
</div> </div>
<footer class="no-print"> <footer class="no-print">

View File

@ -6,7 +6,7 @@ import { HttpClient } from '@angular/common/http';
import { MatProgressBar, MatSidenav } from '@angular/material'; import { MatProgressBar, MatSidenav } from '@angular/material';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { timer } from 'rxjs'; import { of, timer } from 'rxjs';
import { first, mapTo } from 'rxjs/operators'; import { first, mapTo } from 'rxjs/operators';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
@ -14,6 +14,7 @@ import { AppModule } from './app.module';
import { DocumentService } from 'app/documents/document.service'; import { DocumentService } from 'app/documents/document.service';
import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component'; import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
import { Deployment } from 'app/shared/deployment.service'; import { Deployment } from 'app/shared/deployment.service';
import { ElementsLoader } from 'app/custom-elements/elements-loader';
import { GaService } from 'app/shared/ga.service'; import { GaService } from 'app/shared/ga.service';
import { LocationService } from 'app/shared/location.service'; import { LocationService } from 'app/shared/location.service';
import { Logger } from 'app/shared/logger.service'; import { Logger } from 'app/shared/logger.service';
@ -26,7 +27,6 @@ import { SearchBoxComponent } from 'app/search/search-box/search-box.component';
import { SearchResultsComponent } from 'app/shared/search-results/search-results.component'; import { SearchResultsComponent } from 'app/shared/search-results/search-results.component';
import { SearchService } from 'app/search/search.service'; import { SearchService } from 'app/search/search.service';
import { SelectComponent } from 'app/shared/select/select.component'; import { SelectComponent } from 'app/shared/select/select.component';
import { TocComponent } from 'app/layout/toc/toc.component';
import { TocItem, TocService } from 'app/shared/toc.service'; import { TocItem, TocService } from 'app/shared/toc.service';
const sideBySideBreakPoint = 992; const sideBySideBreakPoint = 992;
@ -621,53 +621,53 @@ describe('AppComponent', () => {
}); });
describe('aio-toc', () => { describe('aio-toc', () => {
let tocDebugElement: DebugElement; let tocContainer: HTMLElement|null;
let tocContainer: DebugElement|null; let toc: HTMLElement|null;
const setHasFloatingToc = (hasFloatingToc: boolean) => { const setHasFloatingToc = (hasFloatingToc: boolean) => {
component.hasFloatingToc = hasFloatingToc; component.hasFloatingToc = hasFloatingToc;
fixture.detectChanges(); fixture.detectChanges();
tocDebugElement = fixture.debugElement.query(By.directive(TocComponent)); tocContainer = fixture.debugElement.nativeElement.querySelector('.toc-container');
tocContainer = tocDebugElement && tocDebugElement.parent; toc = tocContainer && tocContainer.querySelector('aio-toc');
}; };
beforeEach(() => setHasFloatingToc(true)); beforeEach(() => setHasFloatingToc(true));
it('should show/hide `<aio-toc>` based on `hasFloatingToc`', () => { it('should show/hide `<aio-toc>` based on `hasFloatingToc`', () => {
expect(tocDebugElement).toBeTruthy();
expect(tocContainer).toBeTruthy(); expect(tocContainer).toBeTruthy();
expect(toc).toBeTruthy();
setHasFloatingToc(false); setHasFloatingToc(false);
expect(tocDebugElement).toBeFalsy();
expect(tocContainer).toBeFalsy(); expect(tocContainer).toBeFalsy();
expect(toc).toBeFalsy();
setHasFloatingToc(true); setHasFloatingToc(true);
expect(tocDebugElement).toBeTruthy();
expect(tocContainer).toBeTruthy(); expect(tocContainer).toBeTruthy();
expect(toc).toBeTruthy();
}); });
it('should have a non-embedded `<aio-toc>` element', () => { it('should have a non-embedded `<aio-toc>` element', () => {
expect(tocDebugElement.classes['embedded']).toBeFalsy(); expect(toc!.classList.contains('embedded')).toBe(false);
}); });
it('should update the TOC container\'s `maxHeight` based on `tocMaxHeight`', () => { it('should update the TOC container\'s `maxHeight` based on `tocMaxHeight`', () => {
expect(tocContainer!.styles['max-height']).toBeNull(); expect(tocContainer!.style['max-height']).toBe('');
component.tocMaxHeight = '100'; component.tocMaxHeight = '100';
fixture.detectChanges(); fixture.detectChanges();
expect(tocContainer!.styles['max-height']).toBe('100px'); expect(tocContainer!.style['max-height']).toBe('100px');
}); });
it('should restrain scrolling inside the ToC container', () => { it('should restrain scrolling inside the ToC container', () => {
const restrainScrolling = spyOn(component, 'restrainScrolling'); const restrainScrolling = spyOn(component, 'restrainScrolling');
const evt = {}; const evt = new MouseEvent('mousewheel');
expect(restrainScrolling).not.toHaveBeenCalled(); expect(restrainScrolling).not.toHaveBeenCalled();
tocContainer!.triggerEventHandler('mousewheel', evt); tocContainer!.dispatchEvent(evt);
expect(restrainScrolling).toHaveBeenCalledWith(evt); expect(restrainScrolling).toHaveBeenCalledWith(evt);
}); });
}); });
@ -1280,6 +1280,7 @@ function createTestingModule(initialUrl: string, mode: string = 'stable') {
imports: [ AppModule ], imports: [ AppModule ],
providers: [ providers: [
{ provide: APP_BASE_HREF, useValue: '/' }, { provide: APP_BASE_HREF, useValue: '/' },
{ provide: ElementsLoader, useClass: TestElementsLoader },
{ provide: GaService, useClass: TestGaService }, { provide: GaService, useClass: TestGaService },
{ provide: HttpClient, useClass: TestHttpClient }, { provide: HttpClient, useClass: TestHttpClient },
{ provide: LocationService, useFactory: () => mockLocationService }, { provide: LocationService, useFactory: () => mockLocationService },
@ -1294,6 +1295,14 @@ function createTestingModule(initialUrl: string, mode: string = 'stable') {
}); });
} }
class TestElementsLoader {
loadContainingCustomElements = jasmine.createSpy('loadContainingCustomElements')
.and.returnValue(of(undefined));
loadCustomElement = jasmine.createSpy('loadCustomElement')
.and.returnValue(Promise.resolve());
}
class TestGaService { class TestGaService {
locationChanged = jasmine.createSpy('locationChanged'); locationChanged = jasmine.createSpy('locationChanged');
} }

View File

@ -32,7 +32,6 @@ import { ScrollService } from 'app/shared/scroll.service';
import { ScrollSpyService } from 'app/shared/scroll-spy.service'; import { ScrollSpyService } from 'app/shared/scroll-spy.service';
import { SearchBoxComponent } from 'app/search/search-box/search-box.component'; import { SearchBoxComponent } from 'app/search/search-box/search-box.component';
import { NotificationComponent } from 'app/layout/notification/notification.component'; import { NotificationComponent } from 'app/layout/notification/notification.component';
import { TocComponent } from 'app/layout/toc/toc.component';
import { TocService } from 'app/shared/toc.service'; import { TocService } from 'app/shared/toc.service';
import { CurrentDateToken, currentDateProvider } from 'app/shared/current-date'; import { CurrentDateToken, currentDateProvider } from 'app/shared/current-date';
import { WindowToken, windowProvider } from 'app/shared/window'; import { WindowToken, windowProvider } from 'app/shared/window';
@ -111,7 +110,6 @@ export const svgIconProviders = [
NavItemComponent, NavItemComponent,
SearchBoxComponent, SearchBoxComponent,
NotificationComponent, NotificationComponent,
TocComponent,
TopMenuComponent, TopMenuComponent,
], ],
providers: [ providers: [
@ -133,7 +131,6 @@ export const svgIconProviders = [
{ provide: CurrentDateToken, useFactory: currentDateProvider }, { provide: CurrentDateToken, useFactory: currentDateProvider },
{ provide: WindowToken, useFactory: windowProvider }, { provide: WindowToken, useFactory: windowProvider },
], ],
entryComponents: [ TocComponent ],
bootstrap: [ AppComponent ] bootstrap: [ AppComponent ]
}) })
export class AppModule { } export class AppModule { }

View File

@ -24,6 +24,10 @@ export const ELEMENT_MODULE_PATHS_AS_ROUTES = [
selector: 'aio-resource-list', selector: 'aio-resource-list',
loadChildren: './resource/resource-list.module#ResourceListModule' loadChildren: './resource/resource-list.module#ResourceListModule'
}, },
{
selector: 'aio-toc',
loadChildren: './toc/toc.module#TocModule'
},
{ {
selector: 'code-example', selector: 'code-example',
loadChildren: './code/code-example.module#CodeExampleModule' loadChildren: './code/code-example.module#CodeExampleModule'

View File

@ -4,8 +4,8 @@ import { By } from '@angular/platform-browser';
import { asapScheduler as asap, BehaviorSubject } from 'rxjs'; import { asapScheduler as asap, BehaviorSubject } from 'rxjs';
import { ScrollService } from 'app/shared/scroll.service'; import { ScrollService } from 'app/shared/scroll.service';
import { TocComponent } from './toc.component';
import { TocItem, TocService } from 'app/shared/toc.service'; import { TocItem, TocService } from 'app/shared/toc.service';
import { TocComponent } from './toc.component';
describe('TocComponent', () => { describe('TocComponent', () => {
let tocComponentDe: DebugElement; let tocComponentDe: DebugElement;

View File

@ -0,0 +1,14 @@
import { NgModule, Type } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatIconModule } from '@angular/material/icon';
import { WithCustomElementComponent } from '../element-registry';
import { TocComponent } from './toc.component';
@NgModule({
imports: [ CommonModule, MatIconModule ],
declarations: [ TocComponent ],
entryComponents: [ TocComponent ],
})
export class TocModule implements WithCustomElementComponent {
customElementComponent: Type<any> = TocComponent;
}