diff --git a/apps/design-land/src/app/app-routing.module.ts b/apps/design-land/src/app/app-routing.module.ts index 37417739a0..c73b260175 100644 --- a/apps/design-land/src/app/app-routing.module.ts +++ b/apps/design-land/src/app/app-routing.module.ts @@ -4,42 +4,46 @@ import { RouterModule, } from '@angular/router'; +import { DesignLandTemplateComponent } from './core/template/template.component'; + export const appRoutes: Routes = [ { path: '', - redirectTo: '/button', - pathMatch: 'full', + component: DesignLandTemplateComponent, + children: [ + { path: '', pathMatch: 'full', loadChildren: () => import('./foundations/color/color.module').then(m => m.DesignLandColorModule) }, + { path: 'accordion', loadChildren: () => import('./accordion/accordion.module').then(m => m.DesignLandAccordionModule) }, + { path: 'article', loadChildren: () => import('./article/article.module').then(m => m.DesignLandArticleModule) }, + { path: 'button', loadChildren: () => import('./button/button.module').then(m => m.DesignLandButtonModule) }, + { path: 'callout', loadChildren: () => import('./callout/callout.module').then(m => m.DesignLandCalloutModule) }, + { path: 'card', loadChildren: () => import('./card/card.module').then(m => m.DesignLandCardModule) }, + { path: 'checkbox', loadChildren: () => import('./checkbox/checkbox.module').then(m => m.DesignLandCheckboxModule) }, + { path: 'color', loadChildren: () => import('./foundations/color/color.module').then(m => m.DesignLandColorModule) }, + { path: 'container', loadChildren: () => import('./container/container.module').then(m => m.DesignLandContainerModule) }, + { path: 'feature', loadChildren: () => import('./feature/feature.module').then(m => m.DesignLandFeatureModule) }, + { path: 'form', loadChildren: () => import('./form/form.module').then(m => m.DesignLandFormModule) }, + { path: 'hero', loadChildren: () => import('./hero/hero.module').then(m => m.DesignLandHeroModule) }, + { path: 'link-set', loadChildren: () => import('./link-set/link-set.module').then(m => m.DesignLandLinkSetModule) }, + { path: 'list', loadChildren: () => import('./list/list.module').then(m => m.DesignLandListModule) }, + { path: 'loading-icon', loadChildren: () => import('./loading-icon/loading-icon.module').then(m => m.DesignLandLoadingIconModule) }, + { path: 'image', loadChildren: () => import('./image/image.module').then(m => m.DesignLandImageModule) }, + { path: 'image-gallery', loadChildren: () => import('./image-gallery/image-gallery.module').then(m => m.DesignLandImageGalleryModule) }, + { path: 'input', loadChildren: () => import('./input/input.module').then(m => m.DesignLandInputModule) }, + { path: 'navbar', loadChildren: () => import('./navbar/navbar.module').then(m => m.DesignLandNavbarModule) }, + { path: 'media-gallery', loadChildren: () => import('./media-gallery/media-gallery.module').then(m => m.DesignLandMediaGalleryModule) }, + { path: 'menu', loadChildren: () => import('./menu/menu.module').then(m => m.DesignLandMenuModule) }, + { path: 'modal', loadChildren: () => import('./modal/modal.module').then(m => m.DesignLandModalModule) }, + { path: 'notification', loadChildren: () => import('./notification/notification.module').then(m => m.DesignLandNotificationModule) }, + { path: 'paginator', loadChildren: () => import('./paginator/paginator.module').then(m => m.DesignLandPaginatorModule) }, + { path: 'progress-indicator', loadChildren: () => import('./progress-indicator/progress-indicator.module').then(m => m.DesignLandProgressIndicatorModule) }, + { path: 'qty-dropdown', loadChildren: () => import('./qty-dropdown/qty-dropdown.module').then(m => m.DesignLandQtyDropdownModule) }, + { path: 'quantity-field', loadChildren: () => import('./quantity-field/quantity-field.module').then(m => m.DesignLandQuantityFieldModule) }, + { path: 'sidebar', loadChildren: () => import('./sidebar/sidebar.module').then(m => m.DesignLandSidebarModule) }, + { path: 'radio', loadChildren: () => import('./radio/radio.module').then(m => m.DesignLandRadioModule) }, + { path: 'typography', loadChildren: () => import('./typography/typography.module').then(m => m.DesignLandTypographyModule) }, + { path: 'variables', loadChildren: () => import('./foundations/variables/variables.module').then(m => m.DesignLandVariablesModule) }, + ], }, - { path: 'accordion', loadChildren: () => import('./accordion/accordion.module').then(m => m.DesignLandAccordionModule) }, - { path: 'article', loadChildren: () => import('./article/article.module').then(m => m.DesignLandArticleModule) }, - { path: 'button', loadChildren: () => import('./button/button.module').then(m => m.DesignLandButtonModule) }, - { path: 'callout', loadChildren: () => import('./callout/callout.module').then(m => m.DesignLandCalloutModule) }, - { path: 'card', loadChildren: () => import('./card/card.module').then(m => m.DesignLandCardModule) }, - { path: 'checkbox', loadChildren: () => import('./checkbox/checkbox.module').then(m => m.DesignLandCheckboxModule) }, - { path: 'color', loadChildren: () => import('./foundations/color/color.module').then(m => m.DesignLandColorModule) }, - { path: 'container', loadChildren: () => import('./container/container.module').then(m => m.DesignLandContainerModule) }, - { path: 'feature', loadChildren: () => import('./feature/feature.module').then(m => m.DesignLandFeatureModule) }, - { path: 'form', loadChildren: () => import('./form/form.module').then(m => m.DesignLandFormModule) }, - { path: 'hero', loadChildren: () => import('./hero/hero.module').then(m => m.DesignLandHeroModule) }, - { path: 'link-set', loadChildren: () => import('./link-set/link-set.module').then(m => m.DesignLandLinkSetModule) }, - { path: 'list', loadChildren: () => import('./list/list.module').then(m => m.DesignLandListModule) }, - { path: 'loading-icon', loadChildren: () => import('./loading-icon/loading-icon.module').then(m => m.DesignLandLoadingIconModule) }, - { path: 'image', loadChildren: () => import('./image/image.module').then(m => m.DesignLandImageModule) }, - { path: 'image-gallery', loadChildren: () => import('./image-gallery/image-gallery.module').then(m => m.DesignLandImageGalleryModule) }, - { path: 'input', loadChildren: () => import('./input/input.module').then(m => m.DesignLandInputModule) }, - { path: 'navbar', loadChildren: () => import('./navbar/navbar.module').then(m => m.DesignLandNavbarModule) }, - { path: 'media-gallery', loadChildren: () => import('./media-gallery/media-gallery.module').then(m => m.DesignLandMediaGalleryModule) }, - { path: 'menu', loadChildren: () => import('./menu/menu.module').then(m => m.DesignLandMenuModule) }, - { path: 'modal', loadChildren: () => import('./modal/modal.module').then(m => m.DesignLandModalModule) }, - { path: 'notification', loadChildren: () => import('./notification/notification.module').then(m => m.DesignLandNotificationModule) }, - { path: 'paginator', loadChildren: () => import('./paginator/paginator.module').then(m => m.DesignLandPaginatorModule) }, - { path: 'progress-indicator', loadChildren: () => import('./progress-indicator/progress-indicator.module').then(m => m.DesignLandProgressIndicatorModule) }, - { path: 'qty-dropdown', loadChildren: () => import('./qty-dropdown/qty-dropdown.module').then(m => m.DesignLandQtyDropdownModule) }, - { path: 'quantity-field', loadChildren: () => import('./quantity-field/quantity-field.module').then(m => m.DesignLandQuantityFieldModule) }, - { path: 'sidebar', loadChildren: () => import('./sidebar/sidebar.module').then(m => m.DesignLandSidebarModule) }, - { path: 'radio', loadChildren: () => import('./radio/radio.module').then(m => m.DesignLandRadioModule) }, - { path: 'typography', loadChildren: () => import('./typography/typography.module').then(m => m.DesignLandTypographyModule) }, - { path: 'variables', loadChildren: () => import('./foundations/variables/variables.module').then(m => m.DesignLandVariablesModule) }, ]; @NgModule({ diff --git a/apps/design-land/src/app/app.component.html b/apps/design-land/src/app/app.component.html index 6829e246cc..90c6b64632 100644 --- a/apps/design-land/src/app/app.component.html +++ b/apps/design-land/src/app/app.component.html @@ -1,16 +1 @@ - - - - - -
- - - -
-
\ No newline at end of file + \ No newline at end of file diff --git a/apps/design-land/src/app/app.component.scss b/apps/design-land/src/app/app.component.scss index 373c449e8e..e69de29bb2 100644 --- a/apps/design-land/src/app/app.component.scss +++ b/apps/design-land/src/app/app.component.scss @@ -1,22 +0,0 @@ -:host { - --daff-sidebar-side-fixed-top-shift: 64px; -} - -.design-land { - - &__theme-switch { - margin-left: auto; - } - - &__content { - padding: 24px 48px 48px; - } -} - -daff-link-set { - margin: 0 0 24px 0; - - &:last-child { - margin: 0; - } -} \ No newline at end of file diff --git a/apps/design-land/src/app/app.component.ts b/apps/design-land/src/app/app.component.ts index a03ce148f2..6463107f40 100644 --- a/apps/design-land/src/app/app.component.ts +++ b/apps/design-land/src/app/app.component.ts @@ -1,21 +1,9 @@ -import { BreakpointObserver } from '@angular/cdk/layout'; import { Component, Injector, ComponentFactoryResolver, } from '@angular/core'; -import { faBars } from '@fortawesome/free-solid-svg-icons'; -import { - Observable, - map, - tap, -} from 'rxjs'; -import { - DaffBreakpoints, - DaffSidebarMode, - DaffSidebarModeEnum, -} from '@daffodil/design'; import { ACCORDION_EXAMPLES } from '@daffodil/design/accordion/examples'; import { ARTICLE_EXAMPLES } from '@daffodil/design/article/examples'; import { BUTTON_EXAMPLES } from '@daffodil/design/button/examples'; @@ -40,24 +28,15 @@ import { SIDEBAR_EXAMPLES } from '@daffodil/design/sidebar/examples'; import { createCustomElementFromExample } from './core/elements/create-element-from-example'; - - @Component({ selector: 'design-land-app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'], }) export class DesignLandAppComponent { - faBars = faBars; - - public mode$: Observable; - - public open = false; - constructor( injector: Injector, private componentFactoryResolver: ComponentFactoryResolver, - private breakpoint: BreakpointObserver, ) { [ ...ARTICLE_EXAMPLES, @@ -89,13 +68,5 @@ export class DesignLandAppComponent { customElement.element, ); }); - this.open = this.breakpoint.isMatched(DaffBreakpoints.BIG_TABLET); - this.mode$ = this.breakpoint.observe(DaffBreakpoints.BIG_TABLET).pipe( - map((match) => match.matches ? DaffSidebarModeEnum.SideFixed : DaffSidebarModeEnum.Under), - ); - } - - toggleOpen() { - this.open = !this.open; } } diff --git a/apps/design-land/src/app/app.module.ts b/apps/design-land/src/app/app.module.ts index 861d14eab5..4ed52bbd92 100644 --- a/apps/design-land/src/app/app.module.ts +++ b/apps/design-land/src/app/app.module.ts @@ -17,6 +17,7 @@ import { DaffThemeSwitchButtonModule } from '@daffodil/theme-switch'; import { DesignLandAppRoutingModule } from './app-routing.module'; import { DesignLandAppComponent } from './app.component'; import { DesignLandNavModule } from './core/nav/nav.module'; +import { DesignLandTemplateModule } from './core/template/template.module'; @NgModule({ imports: [ @@ -32,6 +33,7 @@ import { DesignLandNavModule } from './core/nav/nav.module'; DaffButtonModule, FontAwesomeModule, DesignLandNavModule, + DesignLandTemplateModule, ], declarations: [ DesignLandAppComponent, diff --git a/apps/design-land/src/app/core/sidebar-viewport/sidebar-viewport-theme.scss b/apps/design-land/src/app/core/sidebar-viewport/sidebar-viewport-theme.scss new file mode 100644 index 0000000000..942a8c01d8 --- /dev/null +++ b/apps/design-land/src/app/core/sidebar-viewport/sidebar-viewport-theme.scss @@ -0,0 +1,28 @@ +@use 'sass:map'; +@use 'theme' as daff-theme; + +@mixin sidebar-viewport-theme($theme) { + $gray: daff-theme.daff-map-deep-get($theme, 'core.gray'); + $base: daff-theme.daff-map-deep-get($theme, 'core.base'); + $base-contrast: daff-theme.daff-map-deep-get($theme, 'core.base-contrast'); + $primary: map.get($theme, primary); + + .design-land-sidebar-viewport { + &__sidebar { + border-right: 1px solid daff-theme.daff-illuminate($base, $gray, 2); + } + + &__get-started-button { + background: daff-theme.daff-illuminate($base, $gray, 1); + + &:hover { + background: daff-theme.daff-illuminate($base, $gray, 2); + } + } + + &__docs-tag { + background: $base-contrast; + color: $base; + } + } +} \ No newline at end of file diff --git a/apps/design-land/src/app/core/sidebar-viewport/sidebar-viewport.component.html b/apps/design-land/src/app/core/sidebar-viewport/sidebar-viewport.component.html new file mode 100644 index 0000000000..2b604acd39 --- /dev/null +++ b/apps/design-land/src/app/core/sidebar-viewport/sidebar-viewport.component.html @@ -0,0 +1,25 @@ + + + + + + + + Get Started + + + +
+ + + +
+
\ No newline at end of file diff --git a/apps/design-land/src/app/core/sidebar-viewport/sidebar-viewport.component.scss b/apps/design-land/src/app/core/sidebar-viewport/sidebar-viewport.component.scss new file mode 100644 index 0000000000..90195ee310 --- /dev/null +++ b/apps/design-land/src/app/core/sidebar-viewport/sidebar-viewport.component.scss @@ -0,0 +1,52 @@ +@use 'utilities' as daff; + +:host { + --daff-sidebar-side-fixed-top-shift: 64px; +} + +.design-land-sidebar-viewport { + &__theme-switch { + margin-left: auto; + } + + &__content { + padding: 24px; + + @include daff.breakpoint(mobile) { + padding: 48px; + } + } + + &__sidebar { + width: 288px; + } + + &__get-started-button { + display: block; + font-weight: 500; + padding: 16px; + text-align: center; + text-decoration: none; + } + + &__docs-logo { + display: flex; + align-items: center; + gap: 8px; + } + + &__logo { + display: flex; + align-items: center; + max-width: 144px; + } + + &__docs-tag { + border-radius: 4px; + font-size: 0.875rem; + font-weight: 600; + line-height: 1rem; + padding: 4px; + text-transform: uppercase; + } +} diff --git a/apps/design-land/src/app/core/sidebar-viewport/sidebar-viewport.component.spec.ts b/apps/design-land/src/app/core/sidebar-viewport/sidebar-viewport.component.spec.ts new file mode 100644 index 0000000000..e45e24a77b --- /dev/null +++ b/apps/design-land/src/app/core/sidebar-viewport/sidebar-viewport.component.spec.ts @@ -0,0 +1,56 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { RouterTestingModule } from '@angular/router/testing'; +import { DaffioGuidesNavModule } from 'apps/daffio/src/app/guides/components/guides-nav/guides-nav.module'; + +import { + DaffSidebarModule, + DaffSidebarViewportComponent, + DaffSidebarComponent, +} from '@daffodil/design'; + +import { DesignLandSidebarViewportComponent } from './sidebar-viewport.component'; + +describe('DesignLandSidebarViewportComponent', () => { + let component: DesignLandSidebarViewportComponent; + let fixture: ComponentFixture; + + let daffSidebarViewport: DaffSidebarViewportComponent; + let daffSidebar: DaffSidebarComponent; + + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NoopAnimationsModule, + DaffSidebarModule, + DaffioGuidesNavModule, + HttpClientTestingModule, + ], + declarations: [ + DesignLandSidebarViewportComponent, + ], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DesignLandSidebarViewportComponent); + component = fixture.componentInstance; + + fixture.detectChanges(); + daffSidebar = fixture.debugElement.query(By.css('daff-sidebar')).componentInstance; + daffSidebarViewport = fixture.debugElement.query(By.css('daff-sidebar-viewport')).componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/design-land/src/app/core/sidebar-viewport/sidebar-viewport.component.ts b/apps/design-land/src/app/core/sidebar-viewport/sidebar-viewport.component.ts new file mode 100644 index 0000000000..f9ec5a610e --- /dev/null +++ b/apps/design-land/src/app/core/sidebar-viewport/sidebar-viewport.component.ts @@ -0,0 +1,46 @@ +import { BreakpointObserver } from '@angular/cdk/layout'; +import { + ChangeDetectionStrategy, + Component, + Input, +} from '@angular/core'; +import { faBars } from '@fortawesome/free-solid-svg-icons'; +import { + map, + Observable, +} from 'rxjs'; + +import { + DaffBreakpoints, + DaffSidebarMode, + DaffSidebarModeEnum, +} from '@daffodil/design'; + +@Component({ + selector: 'design-land-sidebar-viewport', + templateUrl: './sidebar-viewport.component.html', + styleUrls: ['./sidebar-viewport.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DesignLandSidebarViewportComponent { + faBars = faBars; + + public mode$: Observable; + + public open = false; + + constructor(private breakpoint: BreakpointObserver) { + this.open = this.breakpoint.isMatched(DaffBreakpoints.BIG_TABLET); + this.mode$ = this.breakpoint.observe(DaffBreakpoints.BIG_TABLET).pipe( + map((match) => match.matches ? DaffSidebarModeEnum.SideFixed : DaffSidebarModeEnum.Under), + ); + } + + toggleOpen() { + this.open = !this.open; + } + + get ariaLabel() { + return this.open ? 'close docs menu' : 'open docs menu'; + } +} diff --git a/apps/design-land/src/app/core/sidebar-viewport/sidebar-viewport.module.ts b/apps/design-land/src/app/core/sidebar-viewport/sidebar-viewport.module.ts new file mode 100644 index 0000000000..428a5eef8e --- /dev/null +++ b/apps/design-land/src/app/core/sidebar-viewport/sidebar-viewport.module.ts @@ -0,0 +1,39 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; + +import { DaffLogoModule } from '@daffodil/branding'; +import { + DaffNavbarModule, + DaffSidebarModule, +} from '@daffodil/design'; +import { DaffArticleModule } from '@daffodil/design/article'; +import { DaffButtonModule } from '@daffodil/design/button'; +import { DaffThemeSwitchButtonModule } from '@daffodil/theme-switch'; + +import { DesignLandSidebarViewportComponent } from './sidebar-viewport.component'; +import { DesignLandNavModule } from '../nav/nav.module'; + +@NgModule({ + imports: [ + RouterModule, + CommonModule, + FontAwesomeModule, + + DaffSidebarModule, + DaffNavbarModule, + DaffButtonModule, + DaffArticleModule, + DesignLandNavModule, + DaffThemeSwitchButtonModule, + DaffLogoModule, + ], + declarations: [ + DesignLandSidebarViewportComponent, + ], + exports: [ + DesignLandSidebarViewportComponent, + ], +}) +export class DesignLandSidebarViewportModule { } diff --git a/apps/design-land/src/app/core/template/template.component.html b/apps/design-land/src/app/core/template/template.component.html new file mode 100644 index 0000000000..f9f2840d2a --- /dev/null +++ b/apps/design-land/src/app/core/template/template.component.html @@ -0,0 +1,3 @@ + + + diff --git a/apps/design-land/src/app/core/template/template.component.spec.ts b/apps/design-land/src/app/core/template/template.component.spec.ts new file mode 100644 index 0000000000..6448a9c2b0 --- /dev/null +++ b/apps/design-land/src/app/core/template/template.component.spec.ts @@ -0,0 +1,50 @@ +import { + CUSTOM_ELEMENTS_SCHEMA, + DebugElement, +} from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { DesignLandTemplateComponent } from './template.component'; + +describe('DesignLandTemplateComponent', () => { + let component: DesignLandTemplateComponent; + let fixture: ComponentFixture; + let sidebarViewport: DebugElement; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + schemas: [ + CUSTOM_ELEMENTS_SCHEMA, + ], + imports: [ + RouterTestingModule, + ], + declarations: [ + DesignLandTemplateComponent, + ], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DesignLandTemplateComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + sidebarViewport = fixture.debugElement.query(By.css('design-land-sidebar-viewport')); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should render a inside a ', () => { + expect(sidebarViewport.query(By.css('router-outlet'))).not.toBeNull(); + }); +}); diff --git a/apps/design-land/src/app/core/template/template.component.ts b/apps/design-land/src/app/core/template/template.component.ts new file mode 100644 index 0000000000..26b1eec585 --- /dev/null +++ b/apps/design-land/src/app/core/template/template.component.ts @@ -0,0 +1,10 @@ +import { + ChangeDetectionStrategy, + Component, +} from '@angular/core'; + +@Component({ + templateUrl: './template.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DesignLandTemplateComponent {} diff --git a/apps/design-land/src/app/core/template/template.module.ts b/apps/design-land/src/app/core/template/template.module.ts new file mode 100644 index 0000000000..954caafd84 --- /dev/null +++ b/apps/design-land/src/app/core/template/template.module.ts @@ -0,0 +1,21 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { DesignLandTemplateComponent } from './template.component'; +import { DesignLandSidebarViewportModule } from '../sidebar-viewport/sidebar-viewport.module'; + +@NgModule({ + imports: [ + RouterModule, + CommonModule, + DesignLandSidebarViewportModule, + ], + declarations: [ + DesignLandTemplateComponent, + ], + exports: [ + DesignLandTemplateComponent, + ], +}) +export class DesignLandTemplateModule { } diff --git a/apps/design-land/src/app/sidebar/sidebar.component.html b/apps/design-land/src/app/sidebar/sidebar.component.html index 51bf1aa0c7..4a5a4c56a7 100644 --- a/apps/design-land/src/app/sidebar/sidebar.component.html +++ b/apps/design-land/src/app/sidebar/sidebar.component.html @@ -1,21 +1,178 @@

Sidebar

+

Sidebar is a component used to display additional information to the side of a page that may be fixed or collapsible.

+ +

Overview

+

Sidebars are often used for navigation, but it's built to be flexible and extensible so that it can be used for any content. Sidebar supports a header and footer component with minimal default styling.

+ +

Basic sidebar

+

The default setting for sidebar is mode="side" and side="left". +

+

Implementing the main and sidebar content

+

The main and sidebar content should be placed inside of the <daff-sidebar-viewport>. The sidebar content should be placed inside of the <daff-sidebar>.

+ +

A viewport navigation can be:

+ +

Placed alongside the <daff-sidebar> using the [daff-sidebar-viewport-nav] selector.

+
<daff-sidebar-viewport (backdropClicked)="toggleOpen()">
+  <nav daff-sidebar-viewport-nav daff-navbar>
+    Nav content
+  </nav>    
+  <daff-sidebar mode="over" [open]="open">
+    <div class="sidebar-content">
+      Sidebar content
+    </div>
+  </daff-sidebar>
+  <div class="page-content">
+    Page content
+  </div>
+</daff-sidebar-viewport>
+ +

Additionally, a [daff-sidebar-viewport-nav] that is paired with a side-fixed sidebar can be placed above or beside to the sidebar. It's placed above by default but can be updated by using the navPlacement property on the <daff-sidebar-viewport>.

+ +

Placed inside of the viewport content by omitting the [daff-sidebar-viewport-nav] selector from the nav component.

+ +
<daff-sidebar-viewport (backdropClicked)="toggleOpen()">
+  <nav daff-navbar>
+    Nav content
+  </nav>    
+  <daff-sidebar mode="over" [open]="open">
+    <div class="sidebar-content">
+      Sidebar content
+    </div>
+  </daff-sidebar>
+  <div class="page-content">
+    Page content
+  </div>
+</daff-sidebar-viewport>
+ +

Required imports

+

The BrowserAnimationsModule or NoopAnimationsModule must be imported in the particular Angular @NgModule the sidebar is used in for the sidebar to render and work properly.

+ +

Header and footer

+

The <daff-sidebar-header> includes optional title ([daffSidebarHeaderTitle]) and action ([daffSidebarHeaderAction]) selectors, and a slot to render custom content. The action selector should be used along with the <daff-icon-button> (view Button Documentation) to make sure that the action is positioned correctly and it passes WCAG guidelines.

+ +

The <daff-sidebar-footer> is a "holder" component with minimal default styling. Its main purpose is to position the footer at the bottom of the sidebar, allowing the sidebar's content to overflow and scroll while ensuring that the footer remains constantly visible.

+ +

Both the header and footer are optional components that will not render in the DOM if they are not used.

+ +

Opening and closing a sidebar

+

The open property is used to set the open state for a sidebar.

+ +

By default, sidebar supports two methods of closing itself: clicking on the backdrop of the sidebar viewport or pressing ESC when the sidebar has focus.

+ + + + + + + + + + + + + + + + + + +
Method
+ (backdropClicked) + Set on the <daff-sidebar-viewport>
+ (escapePressed) + Set on the <daff-sidebar>
+ +

Modes

+

<daff-sidebar> can be rendered four different ways by using the mode property. If mode is not specified, side is used by default.

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
ModeDescription
sideSidebar is placed alongside the content
side-fixedSidebar is placed alongside the content and will scroll separately from the content
overSidebar slides over the rest of the content in the viewport and covering it with a backdrop
underSidebar freezes in place and and the content slides above it, while also being covered by a backdrop
+ +

Sides

+

<daff-sidebar> can be positioned on either side of a screen by using the side property. If side is not specified, left is used by default.

+ + + + + + + + + + + + + + + + + + +
SideDescription
leftPlaces sidebar on the left side of the screen
rightPlaces sidebar on the right side of the screen
+ +

Custom styles

+

Changing a sidebar's width

+

The default width of a sidebar is 240px. It can be changed by setting it via CSS:

+ +
daff-sidebar {
+  width: 320px;
+}
+ +

Changing a side-fixed sidebar's top offset position

+

The default offset position of a sidebar is 0px. The --daff-sidebar-side-fixed-top-shift variable can be used to adjust the top offset position for a primary sidebar and its viewport content.

+ +
body {
+  --daff-sidebar-side-fixed-top-shift: 64px;
+}
+ +

Examples

+

Over and under sidebars

- + +

Side fixed sidebar

- + +

Sidebar with sticky content

- + - - - \ No newline at end of file +

Accessibility

+

A meaningful role should be set on all sidebars depending on how they are used. When the <daff-sidebar-header> is not used or there is no title for the sidebar, a meaningful aria-label should be set to describe the sidebar.

+ +

Focus

+

Focus trapping is enabled for over and under modes, and disabled for side and side-fixed modes. When a sidebar is opened, the first tabbable element within the will receive focus. When a sidebar is closed, the element that was focused before the sidebar was opened will be re-focused.

\ No newline at end of file diff --git a/apps/design-land/src/app/sidebar/sidebar.component.ts b/apps/design-land/src/app/sidebar/sidebar.component.ts index 96ae6feaf7..c713f74d5a 100644 --- a/apps/design-land/src/app/sidebar/sidebar.component.ts +++ b/apps/design-land/src/app/sidebar/sidebar.component.ts @@ -1,8 +1,12 @@ -import { Component } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, +} from '@angular/core'; @Component({ selector: 'design-land-sidebar', templateUrl: './sidebar.component.html', styleUrls: ['./sidebar.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DesignLandSidebarComponent {} diff --git a/apps/design-land/src/app/sidebar/sidebar.module.ts b/apps/design-land/src/app/sidebar/sidebar.module.ts index 41a4d102d5..c521915a44 100644 --- a/apps/design-land/src/app/sidebar/sidebar.module.ts +++ b/apps/design-land/src/app/sidebar/sidebar.module.ts @@ -1,6 +1,8 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { DaffArticleModule } from '@daffodil/design/article'; + import { DesignLandSidebarRoutingModule } from './sidebar-routing.module'; import { DesignLandSidebarComponent } from './sidebar.component'; import { DesignLandArticleEncapsulatedModule } from '../core/article-encapsulated/article-encapsulated.module'; @@ -12,6 +14,7 @@ import { DesignLandExampleViewerModule } from '../core/code-preview/container/ex ], imports: [ CommonModule, + DaffArticleModule, DesignLandExampleViewerModule, DesignLandSidebarRoutingModule, DesignLandArticleEncapsulatedModule, diff --git a/apps/design-land/src/scss/component-themes.scss b/apps/design-land/src/scss/component-themes.scss index deeff0d7b0..89625c8a00 100644 --- a/apps/design-land/src/scss/component-themes.scss +++ b/apps/design-land/src/scss/component-themes.scss @@ -1,9 +1,11 @@ @use '../app/core/code-preview/component/code-preview-theme' as code-preview; @use '../app/typography/typography-theme' as typography; @use '../app/foundations/color/color-theme' as color; +@use '../app/core/sidebar-viewport/sidebar-viewport-theme' as sidebar-viewport; @mixin component-themes($theme) { @include code-preview.code-preview-theme($theme); - @include typography.typography-theme($theme); - @include color.color-theme($theme); -} \ No newline at end of file + @include typography.typography-theme($theme); + @include color.color-theme($theme); + @include sidebar-viewport.sidebar-viewport-theme($theme); +} diff --git a/libs/design/sidebar/examples/src/basic-sidebar/basic-sidebar.component.html b/libs/design/sidebar/examples/src/basic-sidebar/basic-sidebar.component.html index 3f0f5f6299..7eec23ab12 100644 --- a/libs/design/sidebar/examples/src/basic-sidebar/basic-sidebar.component.html +++ b/libs/design/sidebar/examples/src/basic-sidebar/basic-sidebar.component.html @@ -1,11 +1,8 @@ - - Accordion
- Article
+ + Sidebar content -
- +
+ Page content
\ No newline at end of file diff --git a/libs/design/sidebar/examples/src/basic-sidebar/basic-sidebar.component.scss b/libs/design/sidebar/examples/src/basic-sidebar/basic-sidebar.component.scss new file mode 100644 index 0000000000..5e4783b017 --- /dev/null +++ b/libs/design/sidebar/examples/src/basic-sidebar/basic-sidebar.component.scss @@ -0,0 +1,7 @@ +daff-sidebar-viewport { + height: 288px; +} + +.basic-sidebar { + border-right: 1px solid rgb(var(--daff-theme-contrast-rgb), 0.1); +} \ No newline at end of file diff --git a/libs/design/sidebar/examples/src/basic-sidebar/basic-sidebar.component.ts b/libs/design/sidebar/examples/src/basic-sidebar/basic-sidebar.component.ts index 224221771e..a69233bfd5 100644 --- a/libs/design/sidebar/examples/src/basic-sidebar/basic-sidebar.component.ts +++ b/libs/design/sidebar/examples/src/basic-sidebar/basic-sidebar.component.ts @@ -7,7 +7,7 @@ import { // eslint-disable-next-line @angular-eslint/component-selector selector: 'basic-sidebar', templateUrl: './basic-sidebar.component.html', - styles: ['daff-sidebar-viewport { height: 300px }'], + styleUrls: ['./basic-sidebar.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) export class BasicSidebarComponent { diff --git a/libs/design/sidebar/examples/src/basic-sidebar/basic-sidebar.module.ts b/libs/design/sidebar/examples/src/basic-sidebar/basic-sidebar.module.ts index 8fd4158eb1..8f99ec5dfa 100644 --- a/libs/design/sidebar/examples/src/basic-sidebar/basic-sidebar.module.ts +++ b/libs/design/sidebar/examples/src/basic-sidebar/basic-sidebar.module.ts @@ -3,6 +3,7 @@ import { NgModule } from '@angular/core'; import { DaffSidebarModule, DaffNavbarModule, + DaffLinkSetModule, } from '@daffodil/design'; import { BasicSidebarComponent } from './basic-sidebar.component'; @@ -11,6 +12,7 @@ import { BasicSidebarComponent } from './basic-sidebar.component'; imports: [ DaffSidebarModule, DaffNavbarModule, + DaffLinkSetModule, ], declarations: [ BasicSidebarComponent, diff --git a/libs/design/sidebar/examples/src/fixed-and-over-sidebar/fixed-and-over-sidebar.component.html b/libs/design/sidebar/examples/src/fixed-and-over-sidebar/fixed-and-over-sidebar.component.html deleted file mode 100644 index 8cacd9570b..0000000000 --- a/libs/design/sidebar/examples/src/fixed-and-over-sidebar/fixed-and-over-sidebar.component.html +++ /dev/null @@ -1,21 +0,0 @@ - - - Accordion
- Article
-
- - Accordion
- Article
-
-
- -
-
Content
-
Some Other Content
-
-
-
\ No newline at end of file diff --git a/libs/design/sidebar/examples/src/fixed-and-over-sidebar/fixed-and-over-sidebar.component.scss b/libs/design/sidebar/examples/src/fixed-and-over-sidebar/fixed-and-over-sidebar.component.scss deleted file mode 100644 index 0432c3119a..0000000000 --- a/libs/design/sidebar/examples/src/fixed-and-over-sidebar/fixed-and-over-sidebar.component.scss +++ /dev/null @@ -1,24 +0,0 @@ -nav { - justify-content: flex-end; -} -daff-sidebar-viewport { height: 300px } - - -.inner-layout { - position: relative; - display: flex; -} - -.filler { - flex-grow: 1; - height: 600px; - padding: 0 16px; -} - -.inner-sticky { - position: sticky; - padding: 20px; - top: 0; - height: 200px; - width: 300px; -} \ No newline at end of file diff --git a/libs/design/sidebar/examples/src/fixed-and-over-sidebar/fixed-and-over-sidebar.component.ts b/libs/design/sidebar/examples/src/fixed-and-over-sidebar/fixed-and-over-sidebar.component.ts deleted file mode 100644 index ae74c004fb..0000000000 --- a/libs/design/sidebar/examples/src/fixed-and-over-sidebar/fixed-and-over-sidebar.component.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, -} from '@angular/core'; - -@Component({ - // eslint-disable-next-line @angular-eslint/component-selector - selector: 'fixed-and-over-sidebar', - templateUrl: './fixed-and-over-sidebar.component.html', - styleUrls: [ - 'fixed-and-over-sidebar.component.scss', - ], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class FixedAndOverSidebarComponent { - overOpen = false; - - openOverSidebar(){ - this.overOpen = !this.overOpen; - } - - closeOverSidebar(){ - this.overOpen = false; - } -} diff --git a/libs/design/sidebar/examples/src/fixed-and-over-sidebar/fixed-and-over-sidebar.module.ts b/libs/design/sidebar/examples/src/fixed-and-over-sidebar/fixed-and-over-sidebar.module.ts deleted file mode 100644 index 9c3b84fc8d..0000000000 --- a/libs/design/sidebar/examples/src/fixed-and-over-sidebar/fixed-and-over-sidebar.module.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { NgModule } from '@angular/core'; - -import { - DaffSidebarModule, - DaffNavbarModule, -} from '@daffodil/design'; -import { DaffButtonModule } from '@daffodil/design/button'; - -import { FixedAndOverSidebarComponent } from './fixed-and-over-sidebar.component'; - -@NgModule({ - imports: [ - DaffSidebarModule, - DaffNavbarModule, - DaffButtonModule, - ], - declarations: [ - FixedAndOverSidebarComponent, - ], - exports: [ - FixedAndOverSidebarComponent, - ], -}) -export class FixedAndOverSidebarModule { } diff --git a/libs/design/sidebar/examples/src/over-and-under-sidebars/over-and-under-sidebars.component.html b/libs/design/sidebar/examples/src/over-and-under-sidebars/over-and-under-sidebars.component.html new file mode 100644 index 0000000000..e176000f14 --- /dev/null +++ b/libs/design/sidebar/examples/src/over-and-under-sidebars/over-and-under-sidebars.component.html @@ -0,0 +1,39 @@ + + + + +
Title
+
+
+ Sidebar content +
+ + + +
+
+
+
Side:
+ +
+ +
+
Mode:
+ +
+ + +
+
diff --git a/libs/design/sidebar/examples/src/over-and-under-sidebars/over-and-under-sidebars.component.scss b/libs/design/sidebar/examples/src/over-and-under-sidebars/over-and-under-sidebars.component.scss new file mode 100644 index 0000000000..38a30e01ea --- /dev/null +++ b/libs/design/sidebar/examples/src/over-and-under-sidebars/over-and-under-sidebars.component.scss @@ -0,0 +1,16 @@ +daff-sidebar-viewport { + height: 288px; +} + +.over-and-under-sidebars { + &__options-container { + display: flex; + gap: 8px; + margin: 0 0 16px; + } + + &__footer, + &__sidebar-content { + padding: 16px; + } +} diff --git a/libs/design/sidebar/examples/src/over-and-under-sidebars/over-and-under-sidebars.component.ts b/libs/design/sidebar/examples/src/over-and-under-sidebars/over-and-under-sidebars.component.ts new file mode 100644 index 0000000000..1cca23a6a9 --- /dev/null +++ b/libs/design/sidebar/examples/src/over-and-under-sidebars/over-and-under-sidebars.component.ts @@ -0,0 +1,30 @@ +import { + ChangeDetectionStrategy, + Component, +} from '@angular/core'; +import { FormControl } from '@angular/forms'; +import { faTimes } from '@fortawesome/free-solid-svg-icons'; + +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'over-and-under-sidebars', + templateUrl: './over-and-under-sidebars.component.html', + styleUrls: ['over-and-under-sidebars.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class OverandUnderSidebarsComponent { + faTimes = faTimes; + + open = false; + + sideControl: FormControl = new FormControl('left'); + modeControl: FormControl = new FormControl('over'); + + openSidebar() { + this.open = !this.open; + } + + closeSidebar() { + this.open = false; + } +} diff --git a/libs/design/sidebar/examples/src/over-and-under-sidebars/over-and-under-sidebars.module.ts b/libs/design/sidebar/examples/src/over-and-under-sidebars/over-and-under-sidebars.module.ts new file mode 100644 index 0000000000..606d10845e --- /dev/null +++ b/libs/design/sidebar/examples/src/over-and-under-sidebars/over-and-under-sidebars.module.ts @@ -0,0 +1,31 @@ +import { NgModule } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; + +import { + DaffSidebarModule, + DaffNavbarModule, + DaffLinkSetModule, +} from '@daffodil/design'; +import { DaffButtonModule } from '@daffodil/design/button'; + +import { OverandUnderSidebarsComponent } from './over-and-under-sidebars.component'; + +@NgModule({ + imports: [ + ReactiveFormsModule, + FontAwesomeModule, + + DaffSidebarModule, + DaffNavbarModule, + DaffButtonModule, + DaffLinkSetModule, + ], + declarations: [ + OverandUnderSidebarsComponent, + ], + exports: [ + OverandUnderSidebarsComponent, + ], +}) +export class OverandUnderSidebarsModule { } diff --git a/libs/design/sidebar/examples/src/public_api.ts b/libs/design/sidebar/examples/src/public_api.ts index dbac9a0fae..c798d6c82c 100644 --- a/libs/design/sidebar/examples/src/public_api.ts +++ b/libs/design/sidebar/examples/src/public_api.ts @@ -2,19 +2,16 @@ import { ComponentExample } from '@daffodil/design'; import { BasicSidebarComponent } from './basic-sidebar/basic-sidebar.component'; import { BasicSidebarModule } from './basic-sidebar/basic-sidebar.module'; -import { FixedAndOverSidebarComponent } from './fixed-and-over-sidebar/fixed-and-over-sidebar.component'; -import { FixedAndOverSidebarModule } from './fixed-and-over-sidebar/fixed-and-over-sidebar.module'; -import { SidebarWithStickyComponent } from './sidebar-with-sticky/sidebar-with-sticky.component'; -import { SidebarWithStickyModule } from './sidebar-with-sticky/sidebar-with-sticky.module'; -import { TwoFixedSidebarsEitherSideComponent } from './two-fixed-sidebars-either-side/two-fixed-sidebars-either-side.component'; -import { TwoFixedSidebarsEitherSideModule } from './two-fixed-sidebars-either-side/two-fixed-sidebars-either-side.module'; -import { UnderSidebarComponent } from './under-sidebar/under-sidebar.component'; -import { UnderSidebarModule } from './under-sidebar/under-sidebar.module'; +import { OverandUnderSidebarsComponent } from './over-and-under-sidebars/over-and-under-sidebars.component'; +import { OverandUnderSidebarsModule } from './over-and-under-sidebars/over-and-under-sidebars.module'; +import { SideFixedSidebarComponent } from './side-fixed-sidebar/side-fixed-sidebar.component'; +import { SideFixedSidebarModule } from './side-fixed-sidebar/side-fixed-sidebar.module'; +import { SidebarWithStickyContentComponent } from './sidebar-with-sticky-content/sidebar-with-sticky-content.component'; +import { SidebarWithStickyContentModule } from './sidebar-with-sticky-content/sidebar-with-sticky-content.module'; export const SIDEBAR_EXAMPLES: ComponentExample[] = [ { component: BasicSidebarComponent, module: BasicSidebarModule }, - { component: SidebarWithStickyComponent, module: SidebarWithStickyModule }, - { component: TwoFixedSidebarsEitherSideComponent, module: TwoFixedSidebarsEitherSideModule }, - { component: FixedAndOverSidebarComponent, module: FixedAndOverSidebarModule }, - { component: UnderSidebarComponent, module: UnderSidebarModule }, + { component: SideFixedSidebarComponent, module: SideFixedSidebarModule }, + { component: OverandUnderSidebarsComponent, module: OverandUnderSidebarsModule }, + { component: SidebarWithStickyContentComponent, module: SidebarWithStickyContentModule }, ]; diff --git a/libs/design/sidebar/examples/src/side-fixed-sidebar/side-fixed-sidebar.component.html b/libs/design/sidebar/examples/src/side-fixed-sidebar/side-fixed-sidebar.component.html new file mode 100644 index 0000000000..b431268a48 --- /dev/null +++ b/libs/design/sidebar/examples/src/side-fixed-sidebar/side-fixed-sidebar.component.html @@ -0,0 +1,9 @@ + + + Fixed sidebar content + + +
+ Page content +
+
\ No newline at end of file diff --git a/libs/design/sidebar/examples/src/side-fixed-sidebar/side-fixed-sidebar.component.scss b/libs/design/sidebar/examples/src/side-fixed-sidebar/side-fixed-sidebar.component.scss new file mode 100644 index 0000000000..5704561afa --- /dev/null +++ b/libs/design/sidebar/examples/src/side-fixed-sidebar/side-fixed-sidebar.component.scss @@ -0,0 +1,12 @@ +daff-sidebar-viewport { + height: 288px; +} + +.side-fixed-sidebar { + border-right: 1px solid rgb(var(--daff-theme-contrast-rgb), 0.1); +} + +.content { + padding: 24px; + height: 480px; +} diff --git a/libs/design/sidebar/examples/src/sidebar-with-sticky/sidebar-with-sticky.component.ts b/libs/design/sidebar/examples/src/side-fixed-sidebar/side-fixed-sidebar.component.ts similarity index 51% rename from libs/design/sidebar/examples/src/sidebar-with-sticky/sidebar-with-sticky.component.ts rename to libs/design/sidebar/examples/src/side-fixed-sidebar/side-fixed-sidebar.component.ts index d230845afb..56795bfe4d 100644 --- a/libs/design/sidebar/examples/src/sidebar-with-sticky/sidebar-with-sticky.component.ts +++ b/libs/design/sidebar/examples/src/side-fixed-sidebar/side-fixed-sidebar.component.ts @@ -5,13 +5,9 @@ import { @Component({ // eslint-disable-next-line @angular-eslint/component-selector - selector: 'sidebar-with-sticky', - templateUrl: './sidebar-with-sticky.component.html', - styleUrls: [ - 'sidebar-with-sticky.component.scss', - ], + selector: 'side-fixed-sidebar', + templateUrl: './side-fixed-sidebar.component.html', + styleUrls: ['side-fixed-sidebar.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class SidebarWithStickyComponent { - -} +export class SideFixedSidebarComponent {} diff --git a/libs/design/sidebar/examples/src/side-fixed-sidebar/side-fixed-sidebar.module.ts b/libs/design/sidebar/examples/src/side-fixed-sidebar/side-fixed-sidebar.module.ts new file mode 100644 index 0000000000..4217e13ea2 --- /dev/null +++ b/libs/design/sidebar/examples/src/side-fixed-sidebar/side-fixed-sidebar.module.ts @@ -0,0 +1,25 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { + DaffNavbarModule, + DaffSidebarModule, +} from '@daffodil/design'; + +import { SideFixedSidebarComponent } from './side-fixed-sidebar.component'; + +@NgModule({ + imports: [ + CommonModule, + + DaffSidebarModule, + DaffNavbarModule, + ], + declarations: [ + SideFixedSidebarComponent, + ], + exports: [ + SideFixedSidebarComponent, + ], +}) +export class SideFixedSidebarModule { } diff --git a/libs/design/sidebar/examples/src/sidebar-with-sticky-content/sidebar-with-sticky-content.component.html b/libs/design/sidebar/examples/src/sidebar-with-sticky-content/sidebar-with-sticky-content.component.html new file mode 100644 index 0000000000..0c2bd5e297 --- /dev/null +++ b/libs/design/sidebar/examples/src/sidebar-with-sticky-content/sidebar-with-sticky-content.component.html @@ -0,0 +1,11 @@ + + + Fixed sidebar content + +
+
+
Sticky content
+
+
Inner content
+
+
\ No newline at end of file diff --git a/libs/design/sidebar/examples/src/sidebar-with-sticky-content/sidebar-with-sticky-content.component.scss b/libs/design/sidebar/examples/src/sidebar-with-sticky-content/sidebar-with-sticky-content.component.scss new file mode 100644 index 0000000000..a38bf98874 --- /dev/null +++ b/libs/design/sidebar/examples/src/sidebar-with-sticky-content/sidebar-with-sticky-content.component.scss @@ -0,0 +1,22 @@ +daff-sidebar-viewport { + height: 288px; +} + +.sidebar-with-sticky-content { + border-right: 1px solid rgb(var(--daff-theme-contrast-rgb), 0.1); +} + +.content { + height: 480px; +} + +.sticky { + position: sticky; + background: #fafafa; + padding: 16px; + top: 0; +} + +.inner-content { + padding: 16px; +} \ No newline at end of file diff --git a/libs/design/sidebar/examples/src/sidebar-with-sticky-content/sidebar-with-sticky-content.component.ts b/libs/design/sidebar/examples/src/sidebar-with-sticky-content/sidebar-with-sticky-content.component.ts new file mode 100644 index 0000000000..72674da813 --- /dev/null +++ b/libs/design/sidebar/examples/src/sidebar-with-sticky-content/sidebar-with-sticky-content.component.ts @@ -0,0 +1,13 @@ +import { + ChangeDetectionStrategy, + Component, +} from '@angular/core'; + +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'sidebar-with-sticky-content', + templateUrl: './sidebar-with-sticky-content.component.html', + styleUrls: ['sidebar-with-sticky-content.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SidebarWithStickyContentComponent {} diff --git a/libs/design/sidebar/examples/src/sidebar-with-sticky/sidebar-with-sticky.module.ts b/libs/design/sidebar/examples/src/sidebar-with-sticky-content/sidebar-with-sticky-content.module.ts similarity index 52% rename from libs/design/sidebar/examples/src/sidebar-with-sticky/sidebar-with-sticky.module.ts rename to libs/design/sidebar/examples/src/sidebar-with-sticky-content/sidebar-with-sticky-content.module.ts index 716b4f79fc..76447dc951 100644 --- a/libs/design/sidebar/examples/src/sidebar-with-sticky/sidebar-with-sticky.module.ts +++ b/libs/design/sidebar/examples/src/sidebar-with-sticky-content/sidebar-with-sticky-content.module.ts @@ -1,11 +1,11 @@ import { NgModule } from '@angular/core'; import { - DaffSidebarModule, DaffNavbarModule, + DaffSidebarModule, } from '@daffodil/design'; -import { SidebarWithStickyComponent } from './sidebar-with-sticky.component'; +import { SidebarWithStickyContentComponent } from './sidebar-with-sticky-content.component'; @NgModule({ imports: [ @@ -13,10 +13,10 @@ import { SidebarWithStickyComponent } from './sidebar-with-sticky.component'; DaffNavbarModule, ], declarations: [ - SidebarWithStickyComponent, + SidebarWithStickyContentComponent, ], exports: [ - SidebarWithStickyComponent, + SidebarWithStickyContentComponent, ], }) -export class SidebarWithStickyModule { } +export class SidebarWithStickyContentModule { } diff --git a/libs/design/sidebar/examples/src/sidebar-with-sticky/sidebar-with-sticky.component.html b/libs/design/sidebar/examples/src/sidebar-with-sticky/sidebar-with-sticky.component.html deleted file mode 100644 index 52cf7d5fb8..0000000000 --- a/libs/design/sidebar/examples/src/sidebar-with-sticky/sidebar-with-sticky.component.html +++ /dev/null @@ -1,15 +0,0 @@ - - - Accordion
- Article
-
-
- -
-
Content
-
Some Other Content
-
-
-
\ No newline at end of file diff --git a/libs/design/sidebar/examples/src/sidebar-with-sticky/sidebar-with-sticky.component.scss b/libs/design/sidebar/examples/src/sidebar-with-sticky/sidebar-with-sticky.component.scss deleted file mode 100644 index 6d3aecb002..0000000000 --- a/libs/design/sidebar/examples/src/sidebar-with-sticky/sidebar-with-sticky.component.scss +++ /dev/null @@ -1,20 +0,0 @@ -daff-sidebar-viewport { height: 300px } - -.inner-layout { - position: relative; - display: flex; -} - -.filler { - flex-grow: 1; - height: 400px; - padding: 0 16px; -} - -.inner-sticky { - position: sticky; - padding: 20px; - top: 0; - height: 200px; - width: 300px; -} \ No newline at end of file diff --git a/libs/design/sidebar/examples/src/two-fixed-sidebars-either-side/two-fixed-sidebars-either-side.component.html b/libs/design/sidebar/examples/src/two-fixed-sidebars-either-side/two-fixed-sidebars-either-side.component.html deleted file mode 100644 index 6770581456..0000000000 --- a/libs/design/sidebar/examples/src/two-fixed-sidebars-either-side/two-fixed-sidebars-either-side.component.html +++ /dev/null @@ -1,18 +0,0 @@ - - - Accordion
- Article
-
-
- -
-

Content

-
-
- - Accordion
- Article
-
-
\ No newline at end of file diff --git a/libs/design/sidebar/examples/src/two-fixed-sidebars-either-side/two-fixed-sidebars-either-side.component.scss b/libs/design/sidebar/examples/src/two-fixed-sidebars-either-side/two-fixed-sidebars-either-side.component.scss deleted file mode 100644 index 10a2138289..0000000000 --- a/libs/design/sidebar/examples/src/two-fixed-sidebars-either-side/two-fixed-sidebars-either-side.component.scss +++ /dev/null @@ -1,6 +0,0 @@ -daff-sidebar-viewport { height: 300px } - -.filler { - height: 400px; - padding: 0 16px; -} diff --git a/libs/design/sidebar/examples/src/two-fixed-sidebars-either-side/two-fixed-sidebars-either-side.component.ts b/libs/design/sidebar/examples/src/two-fixed-sidebars-either-side/two-fixed-sidebars-either-side.component.ts deleted file mode 100644 index 09d430848f..0000000000 --- a/libs/design/sidebar/examples/src/two-fixed-sidebars-either-side/two-fixed-sidebars-either-side.component.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, -} from '@angular/core'; - -@Component({ - // eslint-disable-next-line @angular-eslint/component-selector - selector: 'two-fixed-sidebars-either-side', - templateUrl: './two-fixed-sidebars-either-side.component.html', - styleUrls: ['./two-fixed-sidebars-either-side.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class TwoFixedSidebarsEitherSideComponent { - -} diff --git a/libs/design/sidebar/examples/src/two-fixed-sidebars-either-side/two-fixed-sidebars-either-side.module.ts b/libs/design/sidebar/examples/src/two-fixed-sidebars-either-side/two-fixed-sidebars-either-side.module.ts deleted file mode 100644 index 1d4b592d91..0000000000 --- a/libs/design/sidebar/examples/src/two-fixed-sidebars-either-side/two-fixed-sidebars-either-side.module.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { NgModule } from '@angular/core'; - -import { - DaffNavbarModule, - DaffSidebarModule, -} from '@daffodil/design'; - -import { TwoFixedSidebarsEitherSideComponent } from './two-fixed-sidebars-either-side.component'; - -@NgModule({ - imports: [ - DaffSidebarModule, - DaffNavbarModule, - ], - declarations: [ - TwoFixedSidebarsEitherSideComponent, - ], - exports: [ - TwoFixedSidebarsEitherSideComponent, - ], -}) -export class TwoFixedSidebarsEitherSideModule { } - diff --git a/libs/design/sidebar/examples/src/under-sidebar/under-sidebar.component.html b/libs/design/sidebar/examples/src/under-sidebar/under-sidebar.component.html deleted file mode 100644 index 7bafd212e1..0000000000 --- a/libs/design/sidebar/examples/src/under-sidebar/under-sidebar.component.html +++ /dev/null @@ -1,20 +0,0 @@ - - - Accordion
- Article
-
- - Accordion
- Article
-
-
- -
-
\ No newline at end of file diff --git a/libs/design/sidebar/examples/src/under-sidebar/under-sidebar.component.scss b/libs/design/sidebar/examples/src/under-sidebar/under-sidebar.component.scss deleted file mode 100644 index 3751e38a2e..0000000000 --- a/libs/design/sidebar/examples/src/under-sidebar/under-sidebar.component.scss +++ /dev/null @@ -1,23 +0,0 @@ -nav { - justify-content: space-between; -} -daff-sidebar-viewport { height: 300px } - -.inner-layout { - position: relative; - display: flex; -} - -.filler { - flex-grow: 1; - height: 600px; - padding: 0 16px; -} - -.inner-sticky { - position: sticky; - padding: 20px; - top: 0; - height: 200px; - width: 300px; -} \ No newline at end of file diff --git a/libs/design/sidebar/examples/src/under-sidebar/under-sidebar.component.ts b/libs/design/sidebar/examples/src/under-sidebar/under-sidebar.component.ts deleted file mode 100644 index 6b575e5ae0..0000000000 --- a/libs/design/sidebar/examples/src/under-sidebar/under-sidebar.component.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, -} from '@angular/core'; - -@Component({ - // eslint-disable-next-line @angular-eslint/component-selector - selector: 'under-sidebar', - templateUrl: './under-sidebar.component.html', - styleUrls: [ - 'under-sidebar.component.scss', - ], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class UnderSidebarComponent { - openLeft = false; - openRight = false; - - openLeftSidebar(){ - this.openLeft = !this.openLeft; - } - - openRightSidebar(){ - this.openRight = !this.openRight; - } - - closeSidebar(){ - this.openLeft = false; - this.openRight = false; - } -} diff --git a/libs/design/sidebar/examples/src/under-sidebar/under-sidebar.module.ts b/libs/design/sidebar/examples/src/under-sidebar/under-sidebar.module.ts deleted file mode 100644 index cec5fee30c..0000000000 --- a/libs/design/sidebar/examples/src/under-sidebar/under-sidebar.module.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { NgModule } from '@angular/core'; - -import { - DaffSidebarModule, - DaffNavbarModule, -} from '@daffodil/design'; -import { DaffButtonModule } from '@daffodil/design/button'; - -import { UnderSidebarComponent } from './under-sidebar.component'; - -@NgModule({ - imports: [ - DaffSidebarModule, - DaffNavbarModule, - DaffButtonModule, - ], - declarations: [ - UnderSidebarComponent, - ], - exports: [ - UnderSidebarComponent, - ], -}) -export class UnderSidebarModule { } diff --git a/libs/design/src/molecules/sidebar/README.md b/libs/design/src/molecules/sidebar/README.md index a8d0b15e4d..66496b9da0 100644 --- a/libs/design/src/molecules/sidebar/README.md +++ b/libs/design/src/molecules/sidebar/README.md @@ -1,20 +1,135 @@ -# Sidebar Component +# Sidebar +Sidebar is a component used to display additional information to the side of a page that may be fixed or collapsible. -## Usage +## Overview +Sidebars are often used for navigation, but it's built to be flexible and extensible so that it can be used for any content. Sidebar supports a header and footer component with minimal default styling. +### Basic sidebar +The default setting for sidebar is `mode="side"` and `side="left"`. + + + +### Implementing the main and sidebar content +The main and sidebar content should be placed inside of the ``. The sidebar content should be placed inside of the ``. + +A viewport navigation can either be: + +- Placed alongside the `` using the `[daff-sidebar-viewport-nav]` selector. + +```html + + + + + +
+ Page content +
+
``` - - -

Some Content

+ +- Placed inside of the viewport content by **omitting** the `[daff-sidebar-viewport-nav]` selector from the nav component. + +```html + + + + + +
+ Page content +
``` +#### Required Imports +The `BrowserAnimationsModule` or `NoopAnimationsModule` must be imported in the particular Angular `@NgModule` the sidebar is used in for the sidebar to render and work properly. + +### Header and footer +The `` includes optional title (`[daffSidebarHeaderTitle]`) and action (`[daffSidebarHeaderAction]`) selectors, and a slot to render custom content. The action selector should be used along with the `` (view [Button Documentation](../../atoms//button//README.md)) to make sure that the action is positioned correctly and it passes WCAG guidelines. + +The `` is a "holder" component with minimal default styling. Its main purpose is to position the footer at the bottom of the sidebar, allowing the sidebar's content to overflow and scroll while ensuring that the footer remains constantly visible. + +Both the header and footer are optional components that will not render in the DOM if they are not used. + +### Opening and closing a sidebar +THe `open` property is used to set the open state for a sidebar. + +By default, sidebar supports two methods of closing itself: clicking on the backdrop of the sidebar viewport or pressing `ESC` when the sidebar has focus. + +| Method | ------------------------------------ | +| `(backdropClicked)` | Set on the `` | +| `(escapePressed)` | Set on the `` | + +### Modes +`` can be rendered four different ways by using the `mode` property. If `mode` is not specified, `side` is used by default. + +| Mode | Description | +| ---------- | ---------------------------------------------------------------------------------------------------- | +| side | Sidebar is placed alongside the content | +| side-fixed | Sidebar is placed alongside the content and will scroll separately from the content | +| over | Sidebar slides over the rest of the content in the viewport and covering it with a backdrop | +| under | Sidebar freezes in place and and the content slides above it, while also being covered by a backdrop | + +#### Over sidebar + + +#### Under sidebar + + +#### Two fixed sidebars on either side + + +#### Fixed and over sidebar + + +### Sides +`` can be positioned on either side of a screen by using the `side` property. If `side` is not specificed, `left` is used by default. + +| Side | Description | +| ----- | ---------------------------------------------- | +| left | Places sidebar on the left side of the screen | +| right | Places sidebar on the right side of the screen | + +### Custom styles + +#### Setting a sidebar's width +The default size of a sidebar is `240px`. The width can be changed by setting it via CSS: + +```scss +daff-sidebar { + width: 240px; +} +``` + +### Changing a side-fixed sidebar's top offset position +The default offset position of a sidebar is `0px`. The `--daff-sidebar-side-fixed-top-shift` variable can be used to adjust the top offset position for a primary sidebar and its viewport content. + +```scss +body { + --daff-sidebar-side-fixed-top-shift: 64px; +} +``` + +### Examples +#### Over and under sidebars + + +#### Side fixed sidebar + + +### Accessibility +A meaningful `role` should be set on all sidebars depending on how they are used. -### Goals +When the `` is not used or there is no title for the sidebar, a meaningful `aria-label` should be set to describe the sidebar. -1. Support position sticky inside sidebar content -2. Support 1 sidebar at a time on each side -3. Support viewports inside of other viewports. -4. A `global` flag that can be set to use `dvh` -5. Sidebars as the window size changes from `side` to `over` and vice-versa by default. -6. Where does the body scrollbar start and end? Does it hit the header of go to the top of the document. \ No newline at end of file +#### Focus +Focus trapping is enabled for `over` and `under` modes, and disabled for `side` and `side-fixed` modes. When a sidebar is opened, the first tabbable element within the will receive focus. When a sidebar is opened, the first tabbable element within the will receive focus. When a sidebar is closed, the element that was focused before the sidebar was opened will be re-focused. \ No newline at end of file diff --git a/libs/design/src/molecules/sidebar/animation/sidebar-animation-state.spec.ts b/libs/design/src/molecules/sidebar/animation/sidebar-animation-state.spec.ts index ae015c0fca..6baac43f83 100644 --- a/libs/design/src/molecules/sidebar/animation/sidebar-animation-state.spec.ts +++ b/libs/design/src/molecules/sidebar/animation/sidebar-animation-state.spec.ts @@ -1,17 +1,31 @@ import { DaffSidebarAnimationStates } from './sidebar-animation'; import { getAnimationState } from './sidebar-animation-state'; +import { DaffSidebarModeEnum } from '../helper/sidebar-mode'; describe('SidebarAnimationState Calculation', () => { - it('should return `open` if it is open', () => { - expect(getAnimationState(true)).toEqual(DaffSidebarAnimationStates.OPEN); + it('should return `none` if the sidebar mode is `side`', () => { + expect(getAnimationState(false, DaffSidebarModeEnum.Side)).toEqual(DaffSidebarAnimationStates.NONE); + expect(getAnimationState(true, DaffSidebarModeEnum.Side)).toEqual(DaffSidebarAnimationStates.NONE); + }); + + it('should return `side-fixed-open` or `side-fixed-closed` if the sidebar mode is `side-fixed`', () => { + expect(getAnimationState(false, DaffSidebarModeEnum.SideFixed)).toEqual(DaffSidebarAnimationStates.SIDEFIXEDCLOSED); + expect(getAnimationState(true, DaffSidebarModeEnum.SideFixed)).toEqual(DaffSidebarAnimationStates.SIDEFIXEDOPEN); + }); + + it('should return `under-open` if it is open and the sidebar mode is `under', () => { + expect(getAnimationState(true, DaffSidebarModeEnum.Under)).toEqual(DaffSidebarAnimationStates.UNDEROPEN); }); - it('should return `none` if it is disabled`', () => { - expect(getAnimationState(false, false)).toEqual(DaffSidebarAnimationStates.NONE); - expect(getAnimationState(true, false)).toEqual(DaffSidebarAnimationStates.NONE); + it('should return `under-closed` if it is not open and the sidebar mode is `under', () => { + expect(getAnimationState(false, DaffSidebarModeEnum.Under)).toEqual(DaffSidebarAnimationStates.UNDERCLOSED); }); it('should return `closed` if it is not open', () => { - expect(getAnimationState(false, true)).toEqual(DaffSidebarAnimationStates.CLOSED); + expect(getAnimationState(false, DaffSidebarModeEnum.Over)).toEqual(DaffSidebarAnimationStates.CLOSED); + }); + + it('should return `open` if it is open', () => { + expect(getAnimationState(true, DaffSidebarModeEnum.Over)).toEqual(DaffSidebarAnimationStates.OPEN); }); }); diff --git a/libs/design/src/molecules/sidebar/animation/sidebar-animation-state.ts b/libs/design/src/molecules/sidebar/animation/sidebar-animation-state.ts index ed6151e2fa..b63c700b58 100644 --- a/libs/design/src/molecules/sidebar/animation/sidebar-animation-state.ts +++ b/libs/design/src/molecules/sidebar/animation/sidebar-animation-state.ts @@ -1,10 +1,30 @@ -export type DaffSidebarAnimationState = 'open' | 'closed' | 'none'; +import { DaffSidebarMode } from '../helper/sidebar-mode'; -export const getAnimationState = (open: boolean, enabled: boolean = true): DaffSidebarAnimationState => { - if(!enabled){ +export type DaffSidebarAnimationState = +'open' | 'closed' | 'under-open' | 'under-closed' | 'side-fixed-open' | 'side-fixed-closed' | 'none'; + +export const getAnimationState = (open: boolean, mode: DaffSidebarMode): DaffSidebarAnimationState => { + if(mode === 'side') { return 'none'; } - if(open && enabled){ + + if(mode === 'side-fixed' && open) { + return 'side-fixed-open'; + } + + if(mode === 'side-fixed' && !open) { + return 'side-fixed-closed'; + } + + if(open && mode === 'under') { + return 'under-open'; + } + + if(!open && mode === 'under') { + return 'under-closed'; + } + + if(open) { return 'open'; } else { return 'closed'; diff --git a/libs/design/src/molecules/sidebar/animation/sidebar-animation.ts b/libs/design/src/molecules/sidebar/animation/sidebar-animation.ts index 628dbf6692..85813faf8b 100644 --- a/libs/design/src/molecules/sidebar/animation/sidebar-animation.ts +++ b/libs/design/src/molecules/sidebar/animation/sidebar-animation.ts @@ -17,8 +17,8 @@ export const daffSidebarAnimations: { readonly backdropTrigger: AnimationTriggerMetadata; } = { transformSidebar: trigger('transformSidebar', [ - // We remove the `transform` here completely, rather than setting it to zero, because: - // 1. 3d transforms causes text to appear blurry on IE and Edge. + // We remove the `transform` here completely, rather than setting it to zero, because + // 3d transforms causes text to appear blurry on IE and Edge. state('open', style({ transform: 'none', })), @@ -29,8 +29,8 @@ export const daffSidebarAnimations: { transition('closed => open', animate(duration + ' ' + sidebarAnimateInTransition)), ]), transformContent: trigger('transformContent', [ - // We remove the `transform` here completely, rather than setting it to zero, because: - // 1. 3d transforms causes text to appear blurry on IE and Edge. + // We remove the `transform` here completely, rather than setting it to zero, because + // 3d transforms causes text to appear blurry on IE and Edge. state('closed', style({ transform: 'none', })), @@ -55,5 +55,9 @@ export const daffSidebarAnimations: { export enum DaffSidebarAnimationStates { OPEN = 'open', CLOSED = 'closed', + UNDEROPEN = 'under-open', + UNDERCLOSED = 'under-closed', + SIDEFIXEDOPEN = 'side-fixed-open', + SIDEFIXEDCLOSED = 'side-fixed-closed', NONE = 'none' } diff --git a/libs/design/src/molecules/sidebar/animation/sidebar-viewport-animation-state.ts b/libs/design/src/molecules/sidebar/animation/sidebar-viewport-animation-state.ts index 96f23bcd9f..e2881e5cc7 100644 --- a/libs/design/src/molecules/sidebar/animation/sidebar-viewport-animation-state.ts +++ b/libs/design/src/molecules/sidebar/animation/sidebar-viewport-animation-state.ts @@ -1,8 +1,16 @@ +import { DaffSidebarAnimationStates } from './sidebar-animation'; import { DaffSidebarAnimationState } from './sidebar-animation-state'; import { AnimationStateWithParams } from '../../../core/public_api'; +export type DaffSidebarViewportAnimationState = DaffSidebarAnimationStates.OPEN | DaffSidebarAnimationStates.CLOSED; + + export interface DaffSidebarAnimationStateParams { shift: string; }; -export type DaffSidebarViewportAnimationState = AnimationStateWithParams; +export type DaffSidebarViewportAnimationStateWithParams = AnimationStateWithParams; + + +export const getSidebarViewportAnimationState = (open: boolean): DaffSidebarViewportAnimationState => + open ? DaffSidebarAnimationStates.OPEN : DaffSidebarAnimationStates.CLOSED; diff --git a/libs/design/src/molecules/sidebar/helper/_variables.scss b/libs/design/src/molecules/sidebar/helper/_variables.scss index dee4faebe2..e82e43d1e8 100644 --- a/libs/design/src/molecules/sidebar/helper/_variables.scss +++ b/libs/design/src/molecules/sidebar/helper/_variables.scss @@ -1,7 +1,8 @@ // Stacking Context Layers -$daff-sidebar-sidebar-over-z-index: 5; -$daff-sidebar-backdrop-z-index: 4; -$daff-sidebar-sidebar-side-fixed-z-index: 4; +$daff-sidebar-sidebar-over-z-index: 7; +$daff-sidebar-backdrop-z-index: 6; +$daff-sidebar-sidebar-side-fixed-z-index: 5; +$daff-sidebar-nav-z-index: 4; $daff-sidebar-content-z-index: 3; $daff-sidebar-sidebar-under-z-index: 2; -$daff-sidebar-viewport-z-index: 1; \ No newline at end of file +$daff-sidebar-viewport-z-index: 1; diff --git a/libs/design/src/molecules/sidebar/public_api.ts b/libs/design/src/molecules/sidebar/public_api.ts index 8fb2e2e649..1d62e5c7cf 100644 --- a/libs/design/src/molecules/sidebar/public_api.ts +++ b/libs/design/src/molecules/sidebar/public_api.ts @@ -1,6 +1,10 @@ export { DaffSidebarModule } from './sidebar.module'; export * from './sidebar-viewport/sidebar-viewport.component'; export * from './sidebar/sidebar.component'; +export * from './sidebar-header/sidebar-header.component'; +export * from './sidebar-footer/sidebar-footer.component'; +export * from './sidebar-header/sidebar-header-title/sidebar-header-title.directive'; +export * from './sidebar-header/sidebar-header-action/sidebar-header-action.directive'; export { DaffSidebarMode, DaffSidebarModeEnum, diff --git a/libs/design/src/molecules/sidebar/sidebar-footer/sidebar-footer.component.scss b/libs/design/src/molecules/sidebar/sidebar-footer/sidebar-footer.component.scss new file mode 100644 index 0000000000..8e4ee52f89 --- /dev/null +++ b/libs/design/src/molecules/sidebar/sidebar-footer/sidebar-footer.component.scss @@ -0,0 +1,5 @@ +:host { + display: block; + width: 100%; + flex-grow: 0; +} diff --git a/libs/design/src/molecules/sidebar/sidebar-footer/sidebar-footer.component.spec.ts b/libs/design/src/molecules/sidebar/sidebar-footer/sidebar-footer.component.spec.ts new file mode 100644 index 0000000000..e0d6d7ed3b --- /dev/null +++ b/libs/design/src/molecules/sidebar/sidebar-footer/sidebar-footer.component.spec.ts @@ -0,0 +1,47 @@ +import { + Component, + DebugElement, +} from '@angular/core'; +import { + waitForAsync, + ComponentFixture, + TestBed, +} from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; + +import { DaffSidebarFooterComponent } from './sidebar-footer.component'; + +@Component({ template: ` + Footer +` }) +class WrapperComponent {} + +describe('DaffSidebarFooterComponent', () => { + let wrapper: WrapperComponent; + let fixture: ComponentFixture; + let sidebarFooter: DaffSidebarFooterComponent; + let sidebarFooterDe: DebugElement; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ + WrapperComponent, + DaffSidebarFooterComponent, + ], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(WrapperComponent); + wrapper = fixture.componentInstance; + + sidebarFooterDe = fixture.debugElement.query(By.css('daff-sidebar-footer')); + sidebarFooter = sidebarFooterDe.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(wrapper).toBeTruthy(); + }); +}); diff --git a/libs/design/src/molecules/sidebar/sidebar-footer/sidebar-footer.component.ts b/libs/design/src/molecules/sidebar/sidebar-footer/sidebar-footer.component.ts new file mode 100644 index 0000000000..8c6596253f --- /dev/null +++ b/libs/design/src/molecules/sidebar/sidebar-footer/sidebar-footer.component.ts @@ -0,0 +1,15 @@ +import { + Component, + HostBinding, + ChangeDetectionStrategy, +} from '@angular/core'; + +@Component({ + selector: 'daff-sidebar-footer', + template: '', + styleUrls: ['./sidebar-footer.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DaffSidebarFooterComponent { + @HostBinding('class.daff-sidebar-footer') class = true; +} diff --git a/libs/design/src/molecules/sidebar/sidebar-header/sidebar-header-action/sidebar-header-action.directive.spec.ts b/libs/design/src/molecules/sidebar/sidebar-header/sidebar-header-action/sidebar-header-action.directive.spec.ts new file mode 100644 index 0000000000..96a5d61d20 --- /dev/null +++ b/libs/design/src/molecules/sidebar/sidebar-header/sidebar-header-action/sidebar-header-action.directive.spec.ts @@ -0,0 +1,54 @@ +import { + Component, + DebugElement, +} from '@angular/core'; +import { + waitForAsync, + ComponentFixture, + TestBed, +} from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; + +import { DaffSidebarHeaderActionDirective } from './sidebar-header-action.directive'; + +@Component({ + template: ` +
Action
+ `, +}) +class WrapperComponent {} + +describe('DaffSidebarHeaderActionDirective', () => { + let wrapper: WrapperComponent; + let de: DebugElement; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ + DaffSidebarHeaderActionDirective, + WrapperComponent, + ], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(WrapperComponent); + wrapper = fixture.componentInstance; + de = fixture.debugElement.query(By.css('[daffSidebarHeaderAction]')); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(wrapper).toBeTruthy(); + }); + + describe('[daffSidebarHeaderAction]',() => { + it('should add a class of `daff-sidebar-header__action` to its host element', () => { + expect(de.classes).toEqual(jasmine.objectContaining({ + 'daff-sidebar-header__action': true, + })); + }); + }); +}); diff --git a/libs/design/src/molecules/sidebar/sidebar-header/sidebar-header-action/sidebar-header-action.directive.ts b/libs/design/src/molecules/sidebar/sidebar-header/sidebar-header-action/sidebar-header-action.directive.ts new file mode 100644 index 0000000000..29b3829af6 --- /dev/null +++ b/libs/design/src/molecules/sidebar/sidebar-header/sidebar-header-action/sidebar-header-action.directive.ts @@ -0,0 +1,14 @@ +import { + Directive, + HostBinding, +} from '@angular/core'; + +@Directive({ + selector: '[daffSidebarHeaderAction]', +}) +export class DaffSidebarHeaderActionDirective { + /** + * @docs-private + */ + @HostBinding('class.daff-sidebar-header__action') class = true; +} diff --git a/libs/design/src/molecules/sidebar/sidebar-header/sidebar-header-title/sidebar-header-title.directive.spec.ts b/libs/design/src/molecules/sidebar/sidebar-header/sidebar-header-title/sidebar-header-title.directive.spec.ts new file mode 100644 index 0000000000..01a9a0c390 --- /dev/null +++ b/libs/design/src/molecules/sidebar/sidebar-header/sidebar-header-title/sidebar-header-title.directive.spec.ts @@ -0,0 +1,54 @@ +import { + Component, + DebugElement, +} from '@angular/core'; +import { + waitForAsync, + ComponentFixture, + TestBed, +} from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; + +import { DaffSidebarHeaderTitleDirective } from './sidebar-header-title.directive'; + +@Component({ + template: ` +

Title

+ `, +}) +class WrapperComponent {} + +describe('DaffSidebarHeaderTitleDirective', () => { + let wrapper: WrapperComponent; + let de: DebugElement; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ + DaffSidebarHeaderTitleDirective, + WrapperComponent, + ], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(WrapperComponent); + wrapper = fixture.componentInstance; + de = fixture.debugElement.query(By.css('[daffSidebarHeaderTitle]')); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(wrapper).toBeTruthy(); + }); + + describe('[daffSidebarHeaderTitle]',() => { + it('should add a class of `daff-sidebar-header__title` to its host element', () => { + expect(de.classes).toEqual(jasmine.objectContaining({ + 'daff-sidebar-header__title': true, + })); + }); + }); +}); diff --git a/libs/design/src/molecules/sidebar/sidebar-header/sidebar-header-title/sidebar-header-title.directive.ts b/libs/design/src/molecules/sidebar/sidebar-header/sidebar-header-title/sidebar-header-title.directive.ts new file mode 100644 index 0000000000..bf4430e160 --- /dev/null +++ b/libs/design/src/molecules/sidebar/sidebar-header/sidebar-header-title/sidebar-header-title.directive.ts @@ -0,0 +1,14 @@ +import { + Directive, + HostBinding, +} from '@angular/core'; + +@Directive({ + selector: '[daffSidebarHeaderTitle]', +}) +export class DaffSidebarHeaderTitleDirective { + /** + * @docs-private + */ + @HostBinding('class.daff-sidebar-header__title') class = true; +} diff --git a/libs/design/src/molecules/sidebar/sidebar-header/sidebar-header.component.html b/libs/design/src/molecules/sidebar/sidebar-header/sidebar-header.component.html new file mode 100644 index 0000000000..28d3dcc5fb --- /dev/null +++ b/libs/design/src/molecules/sidebar/sidebar-header/sidebar-header.component.html @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/libs/design/src/molecules/sidebar/sidebar-header/sidebar-header.component.scss b/libs/design/src/molecules/sidebar/sidebar-header/sidebar-header.component.scss new file mode 100644 index 0000000000..52d9774d90 --- /dev/null +++ b/libs/design/src/molecules/sidebar/sidebar-header/sidebar-header.component.scss @@ -0,0 +1,48 @@ +@use '../../../../scss/typography' as t; + +.daff-sidebar-header { + display: flex; + align-items: center; + position: relative; + width: 100%; + + &__action { + position: absolute; + } + + &__title { + @include t.text-truncate(); + font-size: 1rem; + line-height: 1rem; + font-weight: 500; + } + + &__action + &__title { + margin: 0 0 0 29px; + } +} + +.daff-sidebar { + .daff-sidebar-header { + padding: 16px; + + &__action { + left: 0; + right: initial; + top: 0; + } + } + + &.right { + .daff-sidebar-header { + &__action { + left: initial; + right: 0; + } + + &__title { + margin: 0 29px 0 0; + } + } + } +} diff --git a/libs/design/src/molecules/sidebar/sidebar-header/sidebar-header.component.spec.ts b/libs/design/src/molecules/sidebar/sidebar-header/sidebar-header.component.spec.ts new file mode 100644 index 0000000000..f93df53c33 --- /dev/null +++ b/libs/design/src/molecules/sidebar/sidebar-header/sidebar-header.component.spec.ts @@ -0,0 +1,53 @@ +import { + Component, + DebugElement, +} from '@angular/core'; +import { + waitForAsync, + ComponentFixture, + TestBed, +} from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; + +import { DaffSidebarHeaderComponent } from './sidebar-header.component'; + +@Component({ template: ` + Header +` }) +class WrapperComponent {} + +describe('DaffSidebarHeaderComponent', () => { + let wrapper: WrapperComponent; + let fixture: ComponentFixture; + let component: DaffSidebarHeaderComponent; + let de: DebugElement; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ + WrapperComponent, + DaffSidebarHeaderComponent, + ], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(WrapperComponent); + wrapper = fixture.componentInstance; + + de = fixture.debugElement.query(By.css('daff-sidebar-header')); + component = de.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(wrapper).toBeTruthy(); + }); + + it('should add a class of "daff-sidebar-header" to the host element', () => { + expect(de.classes).toEqual(jasmine.objectContaining({ + 'daff-sidebar-header': true, + })); + }); +}); diff --git a/libs/design/src/molecules/sidebar/sidebar-header/sidebar-header.component.ts b/libs/design/src/molecules/sidebar/sidebar-header/sidebar-header.component.ts new file mode 100644 index 0000000000..58c7d09280 --- /dev/null +++ b/libs/design/src/molecules/sidebar/sidebar-header/sidebar-header.component.ts @@ -0,0 +1,24 @@ +import { + Component, + HostBinding, + ChangeDetectionStrategy, + Output, + EventEmitter, + Input, + ViewEncapsulation, + ContentChild, + ElementRef, +} from '@angular/core'; +import { faTimes } from '@fortawesome/free-solid-svg-icons'; + +@Component({ + selector: 'daff-sidebar-header', + templateUrl: './sidebar-header.component.html', + styleUrls: ['./sidebar-header.component.scss'], + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + +}) +export class DaffSidebarHeaderComponent { + @HostBinding('class.daff-sidebar-header') class = true; +} diff --git a/libs/design/src/molecules/sidebar/sidebar-viewport/helper/has-parent-viewport.ts b/libs/design/src/molecules/sidebar/sidebar-viewport/helper/has-parent-viewport.ts new file mode 100644 index 0000000000..9e08f2dc4c --- /dev/null +++ b/libs/design/src/molecules/sidebar/sidebar-viewport/helper/has-parent-viewport.ts @@ -0,0 +1,17 @@ +/** + * Fallback function for determining whether or not a viewport has a parent viewport. + * This is really only used in the Daffodil docs when the injector heirarchy + * doesn't act as anticipated as a result of custom elements. + */ +export const hasParentViewport = (element: HTMLElement) => { + let currentElement = element.parentElement; + + while (currentElement !== null) { + if (currentElement.tagName === 'DAFF-SIDEBAR-VIEWPORT') { + return true; + } + currentElement = currentElement.parentElement; + } + + return false; +}; diff --git a/libs/design/src/molecules/sidebar/sidebar-viewport/nav-placement.ts b/libs/design/src/molecules/sidebar/sidebar-viewport/nav-placement.ts new file mode 100644 index 0000000000..871bfb505c --- /dev/null +++ b/libs/design/src/molecules/sidebar/sidebar-viewport/nav-placement.ts @@ -0,0 +1,14 @@ +/** + * The placement of the nav in relation to the sidebar. + * See {@link DaffNavPlacementEnum } + */ +export type DaffNavPlacement = 'above' | 'beside'; + +/** + * The placement of the nav in relation to the sidebar. + * See {@link DaffNavPlacement } + */ +export enum DaffNavPlacementEnum { + ABOVE = 'above', + BESIDE = 'beside', +} diff --git a/libs/design/src/molecules/sidebar/sidebar-viewport/scroll-token/scroll.token.ts b/libs/design/src/molecules/sidebar/sidebar-viewport/scroll-token/scroll.token.ts new file mode 100644 index 0000000000..3c6486b271 --- /dev/null +++ b/libs/design/src/molecules/sidebar/sidebar-viewport/scroll-token/scroll.token.ts @@ -0,0 +1,55 @@ +import { DOCUMENT } from '@angular/common'; +import { + ElementRef, + InjectionToken, + inject, +} from '@angular/core'; + +/** + * An interface that enables a user to enable or disable scrolling on sidebars. + * + * See {@link DAFF_SIDEBAR_SCROLL_TOKEN} + */ +export interface DaffSidebarScroll { + enable(): void; + disable(): void; +} + +/** + * An injection token that can be used within a sidebar to determine + * what to do enabling and disabling scrolling. By default, the body + * is the element where scrolling is controlled. + */ +export const DAFF_SIDEBAR_SCROLL_TOKEN = new InjectionToken('DAFF_SIDEBAR_SCROLL_TOKEN', { + providedIn: 'root', + factory: () => { + const document = inject(DOCUMENT); + return { + enable: () => { + document.body.style.overflow = null; + }, + disable: () => { + document.body.style.overflow = 'hidden'; + }, + }; + }, +}); + + +/** + * A factory function that return a DaffSidebarScroll + * for the current sidebar viewport. + * + * See the providers of {@link DaffSidebarViewportComponent} + */ +export const daffSidebarViewportScrollFactory = (): DaffSidebarScroll => { + const element = inject(ElementRef).nativeElement; + return { + enable: () => { + element.style.overflow = null; + }, + disable: () => { + element.style.overflow = 'hidden'; + }, + }; +}; diff --git a/libs/design/src/molecules/sidebar/sidebar-viewport/sidebar-viewport.component.html b/libs/design/src/molecules/sidebar/sidebar-viewport/sidebar-viewport.component.html index 41d5a17ae5..45333ae46a 100644 --- a/libs/design/src/molecules/sidebar/sidebar-viewport/sidebar-viewport.component.html +++ b/libs/design/src/molecules/sidebar/sidebar-viewport/sidebar-viewport.component.html @@ -1,16 +1,18 @@ - + + (backdropClicked)="_backdropClicked()"> + + +
+ +
-
- - \ No newline at end of file diff --git a/libs/design/src/molecules/sidebar/sidebar-viewport/sidebar-viewport.component.scss b/libs/design/src/molecules/sidebar/sidebar-viewport/sidebar-viewport.component.scss index 4cccdb81ac..41722eb34d 100644 --- a/libs/design/src/molecules/sidebar/sidebar-viewport/sidebar-viewport.component.scss +++ b/libs/design/src/molecules/sidebar/sidebar-viewport/sidebar-viewport.component.scss @@ -1,15 +1,12 @@ @use '../helper/variables'; :host { - overflow: hidden; // fixes a bug where certain elements show in front of backdrop for a second. display: flex; - min-height: 100%; + overflow: hidden; position: relative; width: 100%; z-index: variables.$daff-sidebar-viewport-z-index; - height: 100vh; - height: 100dvh; } .daff-sidebar-viewport { @@ -23,22 +20,18 @@ height: 100%; } - &__sidebar { - flex-shrink: 0; - } - &__nav { position: fixed; top: 0; width: 100%; - z-index: variables.$daff-sidebar-content-z-index; + z-index: variables.$daff-sidebar-nav-z-index; &:empty { display: none; } &:not(:empty) { - + #{$root}__inner { + + #{$root}__content { margin-top: var(--daff-sidebar-side-fixed-top-shift); } } @@ -48,11 +41,14 @@ height: 100%; position: absolute; width: 100%; + pointer-events: auto; z-index: variables.$daff-sidebar-backdrop-z-index; } } :host-context(daff-sidebar-viewport daff-sidebar-viewport) { + transform: translateX(0px); + .daff-sidebar-viewport__inner { padding-left: 0px !important; padding-right: 0px !important; diff --git a/libs/design/src/molecules/sidebar/sidebar-viewport/sidebar-viewport.component.spec.ts b/libs/design/src/molecules/sidebar/sidebar-viewport/sidebar-viewport.component.spec.ts index 40025209d7..e3db70f728 100644 --- a/libs/design/src/molecules/sidebar/sidebar-viewport/sidebar-viewport.component.spec.ts +++ b/libs/design/src/molecules/sidebar/sidebar-viewport/sidebar-viewport.component.spec.ts @@ -1,6 +1,7 @@ import { CommonModule } from '@angular/common'; import { Component, + DebugElement, Input, } from '@angular/core'; import { @@ -11,6 +12,7 @@ import { import { By } from '@angular/platform-browser'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { DaffNavPlacement } from './nav-placement'; import { DaffSidebarViewportComponent } from './sidebar-viewport.component'; import { DaffBackdropComponent, @@ -20,7 +22,7 @@ import { DaffSidebarComponent } from '../sidebar/sidebar.component'; @Component({ template: ` ` }) @@ -34,6 +36,7 @@ class WrapperComponent { reset() { this.backdropClickedCounter = 0; } + navPlacement: DaffNavPlacement = 'above'; } describe('DaffSidebarViewportComponent | Usage', () => { @@ -41,6 +44,7 @@ describe('DaffSidebarViewportComponent | Usage', () => { let fixture: ComponentFixture; let component: DaffSidebarViewportComponent; let backdrop: DaffBackdropComponent; + let de: DebugElement; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ @@ -61,7 +65,9 @@ describe('DaffSidebarViewportComponent | Usage', () => { fixture = TestBed.createComponent(WrapperComponent); wrapper = fixture.componentInstance; - component = fixture.debugElement.query(By.css('daff-sidebar-viewport')).componentInstance; + de = fixture.debugElement.query(By.css('daff-sidebar-viewport')); + + component = de.componentInstance; fixture.detectChanges(); }); @@ -70,6 +76,29 @@ describe('DaffSidebarViewportComponent | Usage', () => { expect(wrapper).toBeTruthy(); }); + describe('', () => { + it('should add a class of "daff-sidebar-viewport" to the host element', () => { + expect(de.nativeElement.classList.contains('daff-sidebar-viewport')).toBeTruthy(); + }); + }); + + describe('navPlacement', () => { + it('should be able to take `navPlacement` as an input', () => { + expect(component.navPlacement).toEqual(wrapper.navPlacement); + }); + + it('should set the default navPlacement to above', () => { + expect(component.navPlacement).toEqual('above'); + }); + + it('should add a class of `.beside` if navPlacement="beside"', () => { + wrapper.navPlacement = 'beside'; + fixture.detectChanges(); + + expect(de.nativeElement.classList.contains('beside')).toBeTruthy(); + }); + }); + describe('when emits backdropClicked', () => { beforeEach(() => { fixture.detectChanges(); diff --git a/libs/design/src/molecules/sidebar/sidebar-viewport/sidebar-viewport.component.ts b/libs/design/src/molecules/sidebar/sidebar-viewport/sidebar-viewport.component.ts index 968df51c65..72d7869fc8 100644 --- a/libs/design/src/molecules/sidebar/sidebar-viewport/sidebar-viewport.component.ts +++ b/libs/design/src/molecules/sidebar/sidebar-viewport/sidebar-viewport.component.ts @@ -7,17 +7,38 @@ import { ContentChildren, QueryList, AfterContentChecked, + ElementRef, + Input, + HostBinding, + Inject, + SkipSelf, + Optional, } from '@angular/core'; -import { sidebarViewportBackdropInteractable } from './backdrop-interactable'; -import { sidebarViewportContentPadding } from './content-pad'; +import { hasParentViewport } from './helper/has-parent-viewport'; +import { + DaffNavPlacement, + DaffNavPlacementEnum, +} from './nav-placement'; +import { + DAFF_SIDEBAR_SCROLL_TOKEN, + DaffSidebarScroll, + daffSidebarViewportScrollFactory, +} from './scroll-token/scroll.token'; +import { sidebarViewportBackdropInteractable } from './utils/backdrop-interactable'; +import { sidebarViewportContentPadding } from './utils/content-pad'; import { isViewportContentShifted, sidebarViewportContentShift, -} from './content-shift'; -import { daffSidebarAnimations } from '../animation/sidebar-animation'; -import { getAnimationState } from '../animation/sidebar-animation-state'; -import { DaffSidebarViewportAnimationState } from '../animation/sidebar-viewport-animation-state'; +} from './utils/content-shift'; +import { + DaffSidebarAnimationStates, + daffSidebarAnimations, +} from '../animation/sidebar-animation'; +import { + DaffSidebarViewportAnimationStateWithParams, + getSidebarViewportAnimationState, +} from '../animation/sidebar-viewport-animation-state'; import { DaffSidebarMode } from '../helper/sidebar-mode'; import { DaffSidebarComponent } from '../sidebar/sidebar.component'; @@ -36,11 +57,9 @@ import { DaffSidebarComponent } from '../sidebar/sidebar.component'; * at the same time. @see {@link DaffSidebarMode } * * Since this is a functional component, it's possible to have multiple "open" sidebars - * within at the same time. As a result, this component attempts to - * gracefully handle these situations. However, importantly, this sidebar - * has a constraint, there's only allowed to be one sidebar, - * of each mode, on each side, at any given time. If this is violated, - * this component will throw an exception. + * at the same time. As a result, this component attempts to gracefully handle these situations. + * However, importantly, there can only be one sidebar of each mode, on each side, at any given time. + * If this is violated, this component will throw an exception. */ @Component({ selector: 'daff-sidebar-viewport', @@ -50,10 +69,37 @@ import { DaffSidebarComponent } from '../sidebar/sidebar.component'; animations: [ daffSidebarAnimations.transformContent, ], + providers: [ + { provide: DAFF_SIDEBAR_SCROLL_TOKEN, useFactory: daffSidebarViewportScrollFactory }, + ], }) export class DaffSidebarViewportComponent implements AfterContentChecked { + @HostBinding('class.daff-sidebar-viewport') hostClass = true; - constructor(private cdRef: ChangeDetectorRef) { } + @HostBinding('class') get classes() { + return { + 'daff-sidebar-viewport': true, + [this.navPlacement]: true, + }; + }; + + get isNavOnSide() { + return this.navPlacement === DaffNavPlacementEnum.BESIDE; + } + + /** + * The placement of the nav in relation to the sidebar. The default is set to `top`. + * Note that this is really only available when there is a `side-fixed` sidebar. + */ + @Input() navPlacement: DaffNavPlacement = DaffNavPlacementEnum.ABOVE; + + constructor( + private cdRef: ChangeDetectorRef, + private _elementRef: ElementRef, + @Inject(DAFF_SIDEBAR_SCROLL_TOKEN) @SkipSelf() private bodyScroll: DaffSidebarScroll, + @Inject(DaffSidebarViewportComponent) @SkipSelf() @Optional() private parentViewport, + @Inject(DAFF_SIDEBAR_SCROLL_TOKEN) private scroll: DaffSidebarScroll, + ) { } /** * The list of sidebars in the viewport. @@ -73,11 +119,21 @@ export class DaffSidebarViewportComponent implements AfterContentChecked { */ public _contentPadLeft = 0; + /** + * The left padding on the nav when left side-fixed sidebars are open. + */ + public _navPadLeft = 0; + /** * The right padding on the content when right side-fixed sidebars are open. */ public _contentPadRight = 0; + /** + * The right padding on the content when right side-fixed sidebars are open. + */ + public _navPadRight = 0; + /** * Whether or not the backdrop is interactable */ @@ -86,11 +142,10 @@ export class DaffSidebarViewportComponent implements AfterContentChecked { /** * The animation state */ - _animationState: DaffSidebarViewportAnimationState = { value: 'closed', params: { shift: '0px' }}; + _animationState: DaffSidebarViewportAnimationStateWithParams = { value: DaffSidebarAnimationStates.CLOSED, params: { shift: '0px' }}; /** - * Event fired when the backdrop is clicked - * This is often used to close the sidebar + * Event fired when the backdrop is clicked. This is often used to close the sidebar. */ @Output() backdropClicked: EventEmitter = new EventEmitter(); @@ -107,11 +162,25 @@ export class DaffSidebarViewportComponent implements AfterContentChecked { this._backdropInteractable = nextBackdropInteractable; this.updateAnimationState(); this.cdRef.markForCheck(); + if(nextBackdropInteractable) { + if(!this.parentViewport && !hasParentViewport(this._elementRef.nativeElement)) { + this.bodyScroll.disable(); + } else { + this.scroll.disable(); + } + } else { //if we are hiding the sidebars + if(!this.parentViewport && !hasParentViewport(this._elementRef.nativeElement)) { + this.bodyScroll.enable(); + } else { + this.scroll.enable(); + } + } }; const nextLeftPadding = sidebarViewportContentPadding(this.sidebars, 'left'); if(this._contentPadLeft !== nextLeftPadding) { this._contentPadLeft = nextLeftPadding; + this._navPadLeft = this.isNavOnSide ? this._contentPadLeft : null; this.updateAnimationState(); this.cdRef.markForCheck(); } @@ -119,6 +188,7 @@ export class DaffSidebarViewportComponent implements AfterContentChecked { const nextRightPadding = sidebarViewportContentPadding(this.sidebars, 'right'); if(this._contentPadRight !== nextRightPadding) { this._contentPadRight = nextRightPadding; + this._navPadRight = this.isNavOnSide ? this._contentPadRight : null; this.updateAnimationState(); this.cdRef.markForCheck(); } @@ -132,7 +202,7 @@ export class DaffSidebarViewportComponent implements AfterContentChecked { */ private updateAnimationState() { this._animationState = { - value: getAnimationState( + value: getSidebarViewportAnimationState( this.sidebars.reduce((acc: boolean, sidebar) => acc || isViewportContentShifted(sidebar.mode, sidebar.open), false), ), params: { shift: this._shift }, diff --git a/libs/design/src/molecules/sidebar/sidebar-viewport/specs/defaults.spec.ts b/libs/design/src/molecules/sidebar/sidebar-viewport/specs/defaults.spec.ts index c83374941f..c6e9abe574 100644 --- a/libs/design/src/molecules/sidebar/sidebar-viewport/specs/defaults.spec.ts +++ b/libs/design/src/molecules/sidebar/sidebar-viewport/specs/defaults.spec.ts @@ -8,6 +8,7 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { DaffSidebarViewportComponent } from './../sidebar-viewport.component'; import { DaffBackdropModule } from '../../../backdrop/public_api'; +import { DaffSidebarAnimationStates } from '../../animation/sidebar-animation'; import { DaffSidebarComponent } from '../../sidebar/sidebar.component'; describe('DaffSidebarViewportComponent | Defaults', () => { @@ -43,6 +44,6 @@ describe('DaffSidebarViewportComponent | Defaults', () => { }); it('should have the _animationState should be `open` by default', () => { - expect(component._animationState).toEqual({ value: 'closed', params: { shift: '0px' }}); + expect(component._animationState).toEqual({ value: DaffSidebarAnimationStates.CLOSED, params: { shift: '0px' }}); }); }); diff --git a/libs/design/src/molecules/sidebar/sidebar-viewport/backdrop-interactable.spec.ts b/libs/design/src/molecules/sidebar/sidebar-viewport/utils/backdrop-interactable.spec.ts similarity index 97% rename from libs/design/src/molecules/sidebar/sidebar-viewport/backdrop-interactable.spec.ts rename to libs/design/src/molecules/sidebar/sidebar-viewport/utils/backdrop-interactable.spec.ts index f8da92198c..0cfd3462ec 100644 --- a/libs/design/src/molecules/sidebar/sidebar-viewport/backdrop-interactable.spec.ts +++ b/libs/design/src/molecules/sidebar/sidebar-viewport/utils/backdrop-interactable.spec.ts @@ -6,7 +6,7 @@ import { import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { sidebarViewportBackdropInteractable } from './backdrop-interactable'; -import { DaffSidebarComponent } from '../sidebar/sidebar.component'; +import { DaffSidebarComponent } from '../../sidebar/sidebar.component'; describe('@daffodil/design | sidebar-viewport | backdrop-interactable', () => { beforeEach(waitForAsync(() => { diff --git a/libs/design/src/molecules/sidebar/sidebar-viewport/backdrop-interactable.ts b/libs/design/src/molecules/sidebar/sidebar-viewport/utils/backdrop-interactable.ts similarity index 84% rename from libs/design/src/molecules/sidebar/sidebar-viewport/backdrop-interactable.ts rename to libs/design/src/molecules/sidebar/sidebar-viewport/utils/backdrop-interactable.ts index 16ca9725ad..e20f59b64e 100644 --- a/libs/design/src/molecules/sidebar/sidebar-viewport/backdrop-interactable.ts +++ b/libs/design/src/molecules/sidebar/sidebar-viewport/utils/backdrop-interactable.ts @@ -1,6 +1,6 @@ import { QueryList } from '@angular/core'; -import { DaffSidebarComponent } from '../sidebar/sidebar.component'; +import { DaffSidebarComponent } from '../../sidebar/sidebar.component'; /** * Determines, given a list of sidebars, whether or not the backdrop is interactable (typically clickable). diff --git a/libs/design/src/molecules/sidebar/sidebar-viewport/content-pad.spec.ts b/libs/design/src/molecules/sidebar/sidebar-viewport/utils/content-pad.spec.ts similarity index 97% rename from libs/design/src/molecules/sidebar/sidebar-viewport/content-pad.spec.ts rename to libs/design/src/molecules/sidebar/sidebar-viewport/utils/content-pad.spec.ts index 845e2a4964..68932d7eda 100644 --- a/libs/design/src/molecules/sidebar/sidebar-viewport/content-pad.spec.ts +++ b/libs/design/src/molecules/sidebar/sidebar-viewport/utils/content-pad.spec.ts @@ -13,7 +13,7 @@ import { import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { sidebarViewportContentPadding } from './content-pad'; -import { DaffSidebarComponent } from '../sidebar/sidebar.component'; +import { DaffSidebarComponent } from '../../sidebar/sidebar.component'; @Component({ template: ` diff --git a/libs/design/src/molecules/sidebar/sidebar-viewport/content-pad.ts b/libs/design/src/molecules/sidebar/sidebar-viewport/utils/content-pad.ts similarity index 76% rename from libs/design/src/molecules/sidebar/sidebar-viewport/content-pad.ts rename to libs/design/src/molecules/sidebar/sidebar-viewport/utils/content-pad.ts index bb62c607cf..f5000bdd77 100644 --- a/libs/design/src/molecules/sidebar/sidebar-viewport/content-pad.ts +++ b/libs/design/src/molecules/sidebar/sidebar-viewport/utils/content-pad.ts @@ -1,9 +1,8 @@ import { QueryList } from '@angular/core'; -import { DaffSidebarModeEnum } from '../helper/sidebar-mode'; -import { DaffSidebarSide } from '../helper/sidebar-side'; -import { DaffSidebarComponent } from '../sidebar/sidebar.component'; - +import { DaffSidebarModeEnum } from '../../helper/sidebar-mode'; +import { DaffSidebarSide } from '../../helper/sidebar-side'; +import { DaffSidebarComponent } from '../../sidebar/sidebar.component'; /** * Given a list of sidebars, compute the associated content shift. diff --git a/libs/design/src/molecules/sidebar/sidebar-viewport/content-shift.spec.ts b/libs/design/src/molecules/sidebar/sidebar-viewport/utils/content-shift.spec.ts similarity index 97% rename from libs/design/src/molecules/sidebar/sidebar-viewport/content-shift.spec.ts rename to libs/design/src/molecules/sidebar/sidebar-viewport/utils/content-shift.spec.ts index d441bb4726..43ca080c4c 100644 --- a/libs/design/src/molecules/sidebar/sidebar-viewport/content-shift.spec.ts +++ b/libs/design/src/molecules/sidebar/sidebar-viewport/utils/content-shift.spec.ts @@ -13,7 +13,7 @@ import { import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { sidebarViewportContentShift } from './content-shift'; -import { DaffSidebarComponent } from '../sidebar/sidebar.component'; +import { DaffSidebarComponent } from '../../sidebar/sidebar.component'; @Component({ diff --git a/libs/design/src/molecules/sidebar/sidebar-viewport/content-shift.ts b/libs/design/src/molecules/sidebar/sidebar-viewport/utils/content-shift.ts similarity index 86% rename from libs/design/src/molecules/sidebar/sidebar-viewport/content-shift.ts rename to libs/design/src/molecules/sidebar/sidebar-viewport/utils/content-shift.ts index f9b356f2b4..07da23e326 100644 --- a/libs/design/src/molecules/sidebar/sidebar-viewport/content-shift.ts +++ b/libs/design/src/molecules/sidebar/sidebar-viewport/utils/content-shift.ts @@ -1,7 +1,7 @@ import { QueryList } from '@angular/core'; -import { DaffSidebarMode } from '../helper/sidebar-mode'; -import { DaffSidebarComponent } from '../sidebar/sidebar.component'; +import { DaffSidebarMode } from '../../helper/sidebar-mode'; +import { DaffSidebarComponent } from '../../sidebar/sidebar.component'; export const isViewportContentShifted = (mode: DaffSidebarMode, open: boolean): boolean => (mode === 'under' && open); diff --git a/libs/design/src/molecules/sidebar/sidebar-viewport/utils/viewport-height.spec.ts b/libs/design/src/molecules/sidebar/sidebar-viewport/utils/viewport-height.spec.ts new file mode 100644 index 0000000000..5f7991aee9 --- /dev/null +++ b/libs/design/src/molecules/sidebar/sidebar-viewport/utils/viewport-height.spec.ts @@ -0,0 +1,93 @@ +import { QueryList } from '@angular/core'; +import { + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + +import { sidebarViewportHeight } from './viewport-height'; +import { DaffSidebarComponent } from '../../sidebar/sidebar.component'; + +describe('@daffodil/design | sidebar-viewport | viewport-height', () => { + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + NoopAnimationsModule, + ], + declarations: [ + DaffSidebarComponent, + ], + }).compileComponents(); + })); + + it('should not set a height on the viewport if there are no sidebars', () => { + expect(sidebarViewportHeight(new QueryList())).toEqual(false); + }); + + it('should not set a height on the viewport if there are no open sidebars', () => { + const list = new QueryList(); + list.reset([ + TestBed.createComponent(DaffSidebarComponent).componentInstance, + ]); + expect(sidebarViewportHeight(list)).toEqual(false); + }); + + it('should not set a height on the viewport if there are only "side" or "side-fixed" sidebars', () => { + const sideLeft = TestBed.createComponent(DaffSidebarComponent).componentInstance; + sideLeft.mode = 'side'; + sideLeft.side = 'left'; + const sideFixedLeft = TestBed.createComponent(DaffSidebarComponent).componentInstance; + sideFixedLeft.mode = 'side-fixed'; + sideFixedLeft.side = 'left'; + const sideRight = TestBed.createComponent(DaffSidebarComponent).componentInstance; + sideRight.mode = 'side'; + sideRight.side = 'right'; + const sideFixedRight = TestBed.createComponent(DaffSidebarComponent).componentInstance; + sideFixedRight.mode = 'side-fixed'; + sideFixedRight.side = 'right'; + + const list = new QueryList(); + list.reset([ + sideLeft, + sideFixedLeft, + sideRight, + sideFixedRight, + ]); + expect(sidebarViewportHeight(list)).toEqual(false); + }); + + it('should set height on the viewport if there is at least one open "over" sidebar', () => { + const leftSidebar = TestBed.createComponent(DaffSidebarComponent).componentInstance; + leftSidebar.mode = 'side'; + leftSidebar.side = 'left'; + const rightSidebar = TestBed.createComponent(DaffSidebarComponent).componentInstance; + rightSidebar.mode = 'over'; + rightSidebar.side = 'right'; + rightSidebar.open = true; + + const list = new QueryList(); + list.reset([ + leftSidebar, + rightSidebar, + ]); + expect(sidebarViewportHeight(list)).toEqual(true); + + }); + + it('should set height on the viewport if there is at least one open "under" sidebar', () => { + const leftSidebar = TestBed.createComponent(DaffSidebarComponent).componentInstance; + leftSidebar.mode = 'side'; + leftSidebar.side = 'left'; + const rightSidebar = TestBed.createComponent(DaffSidebarComponent).componentInstance; + rightSidebar.mode = 'under'; + rightSidebar.side = 'right'; + rightSidebar.open = true; + + const list = new QueryList(); + list.reset([ + leftSidebar, + rightSidebar, + ]); + expect(sidebarViewportHeight(list)).toEqual(true); + }); +}); diff --git a/libs/design/src/molecules/sidebar/sidebar-viewport/utils/viewport-height.ts b/libs/design/src/molecules/sidebar/sidebar-viewport/utils/viewport-height.ts new file mode 100644 index 0000000000..76ade5d703 --- /dev/null +++ b/libs/design/src/molecules/sidebar/sidebar-viewport/utils/viewport-height.ts @@ -0,0 +1,10 @@ +import { QueryList } from '@angular/core'; + +import { DaffSidebarComponent } from '../../sidebar/sidebar.component'; + +/** + * Determines whether or not the sidebar viewport should have a pre-defined height, give a list of sidebars. + */ +export const sidebarViewportHeight = (sidebars: QueryList): boolean => sidebars.reduce( + (acc: boolean, sidebar) => ((sidebar.mode === 'over' || sidebar.mode === 'under') && sidebar.open) || acc, + false); diff --git a/libs/design/src/molecules/sidebar/sidebar.module.ts b/libs/design/src/molecules/sidebar/sidebar.module.ts index 08f9bfd13e..d4f234e545 100644 --- a/libs/design/src/molecules/sidebar/sidebar.module.ts +++ b/libs/design/src/molecules/sidebar/sidebar.module.ts @@ -3,23 +3,34 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { DaffSidebarComponent } from './sidebar/sidebar.component'; +import { DaffSidebarFooterComponent } from './sidebar-footer/sidebar-footer.component'; +import { DaffSidebarHeaderActionDirective } from './sidebar-header/sidebar-header-action/sidebar-header-action.directive'; +import { DaffSidebarHeaderTitleDirective } from './sidebar-header/sidebar-header-title/sidebar-header-title.directive'; +import { DaffSidebarHeaderComponent } from './sidebar-header/sidebar-header.component'; import { DaffSidebarViewportComponent } from './sidebar-viewport/sidebar-viewport.component'; import { DaffBackdropModule } from '../backdrop/backdrop.module'; - @NgModule({ imports: [ CommonModule, - DaffBackdropModule, A11yModule, + DaffBackdropModule, ], declarations: [ DaffSidebarComponent, DaffSidebarViewportComponent, + DaffSidebarHeaderComponent, + DaffSidebarFooterComponent, + DaffSidebarHeaderTitleDirective, + DaffSidebarHeaderActionDirective, ], exports: [ DaffSidebarComponent, DaffSidebarViewportComponent, + DaffSidebarHeaderComponent, + DaffSidebarFooterComponent, + DaffSidebarHeaderTitleDirective, + DaffSidebarHeaderActionDirective, ], }) export class DaffSidebarModule { } diff --git a/libs/design/src/molecules/sidebar/sidebar/is-opening.ts b/libs/design/src/molecules/sidebar/sidebar/is-opening.ts new file mode 100644 index 0000000000..5f6b88a3c9 --- /dev/null +++ b/libs/design/src/molecules/sidebar/sidebar/is-opening.ts @@ -0,0 +1,15 @@ +import { DaffSidebarAnimationState } from '../animation/sidebar-animation-state'; + +/** + * Determine whether or animation states are going to an open state from a non-open state. + */ +export const isOpening = (fromState: DaffSidebarAnimationState, toState: DaffSidebarAnimationState): boolean => { + if(fromState === 'under-open' && toState === 'open' || fromState === 'open' && toState === 'under-open') { + return false; + } + + if(toState === 'under-open' || toState === 'open') { + return true; + } + return false; +}; diff --git a/libs/design/src/molecules/sidebar/sidebar/sidebar-theme.scss b/libs/design/src/molecules/sidebar/sidebar/sidebar-theme.scss index cdc515be9d..2f6677d9cd 100644 --- a/libs/design/src/molecules/sidebar/sidebar/sidebar-theme.scss +++ b/libs/design/src/molecules/sidebar/sidebar/sidebar-theme.scss @@ -3,7 +3,6 @@ @mixin daff-sidebar-theme($theme) { $base: core.daff-map-deep-get($theme, 'core.base'); $base-contrast: core.daff-map-deep-get($theme, 'core.base-contrast'); - $black: core.daff-map-deep-get($theme, 'core.black'); $font-color: core.daff-map-deep-get($theme, 'core.font-color'); .daff-sidebar { diff --git a/libs/design/src/molecules/sidebar/sidebar/sidebar.component.html b/libs/design/src/molecules/sidebar/sidebar/sidebar.component.html new file mode 100644 index 0000000000..7184edce2b --- /dev/null +++ b/libs/design/src/molecules/sidebar/sidebar/sidebar.component.html @@ -0,0 +1,5 @@ + +
+ +
+ \ No newline at end of file diff --git a/libs/design/src/molecules/sidebar/sidebar/sidebar.component.scss b/libs/design/src/molecules/sidebar/sidebar/sidebar.component.scss index d163657870..617b11bb6c 100644 --- a/libs/design/src/molecules/sidebar/sidebar/sidebar.component.scss +++ b/libs/design/src/molecules/sidebar/sidebar/sidebar.component.scss @@ -1,16 +1,17 @@ @use '../helper/variables'; :host { - display: block; + display: flex; + flex-direction: column; width: 240px; flex-shrink: 0; - overflow: auto; + overflow-y: auto; &.side-fixed { position: fixed; - top: var(--daff-sidebar-side-fixed-top-shift); bottom: 0; z-index: variables.$daff-sidebar-sidebar-side-fixed-z-index; + top: var(--daff-sidebar-side-fixed-top-shift); height: calc(100dvh - var(--daff-sidebar-side-fixed-top-shift)); &.left { @@ -24,7 +25,7 @@ &.over, &.under { - position: absolute; + position: fixed; top: 0; bottom: 0; @@ -37,7 +38,6 @@ } } - &.over { z-index: variables.$daff-sidebar-sidebar-over-z-index; } @@ -51,4 +51,19 @@ &.side-fixed { position: sticky; } +} + +:host-context(.daff-sidebar-viewport.beside) { + &.side-fixed { + top: 0; + height: 100dvh; + } +} + +.daff-sidebar { + &__body { + flex-grow: 1; + height: 100%; + overflow-y: auto; + } } \ No newline at end of file diff --git a/libs/design/src/molecules/sidebar/sidebar/sidebar.component.spec.ts b/libs/design/src/molecules/sidebar/sidebar/sidebar.component.spec.ts index b1701338ed..69c953a4d3 100644 --- a/libs/design/src/molecules/sidebar/sidebar/sidebar.component.spec.ts +++ b/libs/design/src/molecules/sidebar/sidebar/sidebar.component.spec.ts @@ -1,4 +1,7 @@ -import { Component } from '@angular/core'; +import { + Component, + DebugElement, +} from '@angular/core'; import { waitForAsync, ComponentFixture, @@ -62,33 +65,89 @@ describe('DaffSidebarComponent', () => { @Component({ template: `
- +
` }) -class WrapperComponent { - escapePressedCount = 0; +class DefaultsWrapperComponent { + side = 'left'; + mode = 'side'; +} - open = false; +describe('DaffSidebarComponent | Defaults', () => { + let wrapper: DefaultsWrapperComponent; + let fixture: ComponentFixture; + let component: DaffSidebarComponent; + let de: DebugElement; - side = 'left'; + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [NoopAnimationsModule], + declarations: [ + DefaultsWrapperComponent, + DaffSidebarComponent, + ], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DefaultsWrapperComponent); + wrapper = fixture.componentInstance; + fixture.detectChanges(); + + de = fixture.debugElement.query(By.css('daff-sidebar')); + component = de.componentInstance; + }); + + it('should create', () => { + expect(wrapper).toBeTruthy(); + }); + + it('should add a class of "daff-sidebar" to the host element', () => { + expect(de.classes['daff-sidebar']).toBeTrue(); + }); + + it('should set the default side to left', () => { + expect(de.classes['left']).toBeTrue(); + }); + + it('should set the default mode to side', () => { + expect(de.classes['side']).toBeTrue(); + }); + it('should set the default width to 240px', () => { + expect(component.width).toEqual(240); + }); +}); + +@Component({ template: ` +
+ +
+` }) +class UsageWrapperComponent { + open = false; + side = 'left'; mode = 'side'; + escapePressedCount = 0; + pressed(): void{ this.escapePressedCount++; } } -describe('DaffSidebarComponent | usage', () => { - let wrapper: WrapperComponent; - let fixture: ComponentFixture; +describe('DaffSidebarComponent | Usage', () => { + let wrapper: UsageWrapperComponent; + let fixture: ComponentFixture; let component: DaffSidebarComponent; + let de: DebugElement; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [NoopAnimationsModule], declarations: [ - WrapperComponent, + UsageWrapperComponent, DaffSidebarComponent, ], }) @@ -96,17 +155,38 @@ describe('DaffSidebarComponent | usage', () => { })); beforeEach(() => { - fixture = TestBed.createComponent(WrapperComponent); + fixture = TestBed.createComponent(UsageWrapperComponent); wrapper = fixture.componentInstance; fixture.detectChanges(); - component = fixture.debugElement.query(By.css('daff-sidebar')).componentInstance; + de = fixture.debugElement.query(By.css('daff-sidebar')); + component = de.componentInstance; }); it('should create', () => { expect(wrapper).toBeTruthy(); }); + describe('setting the side', () => { + describe('when side="left"', () => { + it('should add a class of "left" to the host element', () => { + wrapper.side = 'left'; + fixture.detectChanges(); + + expect(de.classes['left']).toBeTrue(); + }); + }); + + describe('when side="right"', () => { + it('should add a class of "right" to the host element', () => { + wrapper.side = 'right'; + fixture.detectChanges(); + + expect(de.classes['right']).toBeTrue(); + }); + }); + }); + it('should be able to bind to the Output `escapePressed`', () => { component.escapePressed.emit(); @@ -126,8 +206,5 @@ describe('DaffSidebarComponent | usage', () => { it('should be able to bind to the input `open`', () => { expect(component.open).toEqual(false); }); - - it('should have a width of 240px by default', () => { - expect(component.width).toEqual(240); - }); }); + diff --git a/libs/design/src/molecules/sidebar/sidebar/sidebar.component.ts b/libs/design/src/molecules/sidebar/sidebar/sidebar.component.ts index adbc8a5a3c..e2607a8861 100644 --- a/libs/design/src/molecules/sidebar/sidebar/sidebar.component.ts +++ b/libs/design/src/molecules/sidebar/sidebar/sidebar.component.ts @@ -1,6 +1,11 @@ +import { AnimationEvent } from '@angular/animations'; +import { + ConfigurableFocusTrap, + ConfigurableFocusTrapFactory, +} from '@angular/cdk/a11y'; +import { DOCUMENT } from '@angular/common'; import { Component, - ViewEncapsulation, NgZone, ElementRef, Output, @@ -8,12 +13,22 @@ import { HostBinding, ChangeDetectionStrategy, Input, + HostListener, + Inject, } from '@angular/core'; import { fromEvent } from 'rxjs'; import { filter } from 'rxjs/operators'; +import { isOpening } from './is-opening'; +import { + daffFocusableElementsSelector, + DaffFocusStackService, +} from '../../../core/focus/public_api'; import { daffSidebarAnimations } from '../animation/sidebar-animation'; -import { getAnimationState } from '../animation/sidebar-animation-state'; +import { + DaffSidebarAnimationState, + getAnimationState, +} from '../animation/sidebar-animation-state'; import { getSidebarAnimationWidth } from '../animation/sidebar-animation-width'; import { DaffSidebarMode } from '../helper/sidebar-mode'; import { DaffSidebarSide } from '../helper/sidebar-side'; @@ -25,8 +40,8 @@ import { DaffSidebarSide } from '../helper/sidebar-side'; */ @Component({ selector: 'daff-sidebar', + templateUrl: './sidebar.component.html', styleUrls: ['./sidebar.component.scss'], - template: '', changeDetection: ChangeDetectionStrategy.OnPush, animations: [ daffSidebarAnimations.transformSidebar, @@ -34,6 +49,8 @@ import { DaffSidebarSide } from '../helper/sidebar-side'; }) export class DaffSidebarComponent { /** + * @docs-private + * * The CSS classes set. */ @HostBinding('class') get classes() { @@ -49,8 +66,8 @@ export class DaffSidebarComponent { */ @HostBinding('@transformSidebar') get transformSidebar() { return { - value: getAnimationState(this.open, this.mode === 'over' || this.mode === 'side-fixed'), - params: { width: getSidebarAnimationWidth(this.side, this.width) }, + value: getAnimationState(this.open, this.mode), + params: { width: getSidebarAnimationWidth(this.side, this.width) }, }; } @@ -84,6 +101,9 @@ export class DaffSidebarComponent { constructor( private _elementRef: ElementRef, private _ngZone: NgZone, + private _focusTrapFactory: ConfigurableFocusTrapFactory, + private _focusStack: DaffFocusStackService, + @Inject(DOCUMENT) private _doc: any, ) { /** @@ -100,4 +120,52 @@ export class DaffSidebarComponent { })); }); } + + private _focusTrap: ConfigurableFocusTrap; + + /** + * Animation event that can be used to hook into when the transformSidebar + * animation begins. This is used in sidebar to determine when to show the + * visibility of the sidebar so that the animation does not jump as the element is shown. + */ + @HostListener('@transformSidebar.start', ['$event']) onAnimationStart(e: AnimationEvent) { + if (e.toState === 'open' || e.toState === 'under-open' || e.toState === 'side-fixed-open') { + this._elementRef.nativeElement.style.visibility = 'visible'; + } + } + + /** + * Animation event that can be used to hook into when the + * transformSidebar animation is complete. + */ + @HostListener('@transformSidebar.done', ['$event']) onAnimationComplete(e: AnimationEvent) { + if(isOpening(e.fromState, e.toState)) { + this._focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement); + + const focusableChild = (this._elementRef.nativeElement.querySelector(daffFocusableElementsSelector)); + + this._focusStack.push(this._doc.activeElement); + + if(focusableChild) { + focusableChild.focus(); + } else { + this._elementRef.nativeElement.tabIndex = 0; + (this._elementRef.nativeElement).focus(); + } + } else { + if(this._focusTrap) { + this._focusTrap.destroy(); + this._focusTrap = undefined; + this._focusStack.pop(); + } + } + + /** + * This is used in sidebar to determine when to hide the visibility + * of the sidebar so that the animation does not jump as the element is hidden. + */ + if (e.toState === 'closed' || e.toState === 'under-closed' || e.toState === 'side-fixed-closed') { + this._elementRef.nativeElement.style.visibility = 'hidden'; + } + } }