Skip to content

Commit

Permalink
AppSideNav - Component branch (HDS-3800) (#2384)
Browse files Browse the repository at this point in the history
Co-authored-by: Alex <[email protected]>
Co-authored-by: Jory Tindall <[email protected]>
Co-authored-by: Cristiano Rastelli <[email protected]>
Co-authored-by: Lee White <[email protected]>
  • Loading branch information
5 people authored Oct 30, 2024
1 parent 9130bb8 commit 70489d6
Show file tree
Hide file tree
Showing 107 changed files with 5,098 additions and 81 deletions.
3 changes: 0 additions & 3 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,6 @@
"./components/hds/app-frame/parts/main.js": "./dist/_app_/components/hds/app-frame/parts/main.js",
"./components/hds/app-frame/parts/modals.js": "./dist/_app_/components/hds/app-frame/parts/modals.js",
"./components/hds/app-frame/parts/sidebar.js": "./dist/_app_/components/hds/app-frame/parts/sidebar.js",
"./components/hds/app-header/home-link.js": "./dist/_app_/components/hds/app-header/home-link.js",
"./components/hds/app-header/index.js": "./dist/_app_/components/hds/app-header/index.js",
"./components/hds/app-header/menu-button.js": "./dist/_app_/components/hds/app-header/menu-button.js",
"./components/hds/application-state/body.js": "./dist/_app_/components/hds/application-state/body.js",
"./components/hds/application-state/footer.js": "./dist/_app_/components/hds/application-state/footer.js",
"./components/hds/application-state/header.js": "./dist/_app_/components/hds/application-state/header.js",
Expand Down
10 changes: 8 additions & 2 deletions packages/components/rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,14 @@ const plugins = [
'components/**/!(*types).js',
'helpers/**/*.js',
'modifiers/**/*.js',
'instance-initializers/**/*.js',
]),
'instance-initializers/**/*.js'],
{
exclude: [
'components/**/app-header/**/*.js',
'components/**/app-side-nav/**/*.js',
],
}
),

// Follow the V2 Addon rules about dependencies. Your code can import from
// `dependencies` and `peerDependencies` as well as standard Ember-provided
Expand Down
34 changes: 34 additions & 0 deletions packages/components/src/components/hds/app-side-nav/index.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: MPL-2.0
}}
{{! IMPORTANT: we need to add "squishies" here (~) because otherwise the whitespace added by Ember causes the empty element to still have visible padding - See https://handlebarsjs.com/guide/expressions.html#whitespace-control }}
<div
class={{this.classNames}}
...attributes
{{on "transitionstart" (fn this.setTransition "start")}}
{{on "transitionend" (fn this.setTransition "end")}}
{{! @glint-expect-error - https://github.com/josemarluedke/ember-focus-trap/issues/86 }}
{{focus-trap isActive=this.shouldTrapFocus}}
{{did-insert this.didInsert}}
>
<h2 class="sr-only" id="hds-app-side-nav-header">Application local navigation</h2>

<div class="hds-app-side-nav__wrapper">
{{#if this.showToggleButton}}
{{! template-lint-disable no-invalid-interactive}}
<div class="hds-app-side-nav__overlay" {{on "click" this.toggleMinimizedStatus}} />
{{! template-lint-enable no-invalid-interactive}}
<Hds::AppSideNav::ToggleButton
aria-labelledby="hds-app-side-nav-header"
aria-expanded={{if this.isMinimized "false" "true"}}
@icon={{if this.isMinimized "chevrons-right" "chevrons-left"}}
{{on "click" this.toggleMinimizedStatus}}
/>
{{/if}}

<div class="hds-app-side-nav__wrapper-body">
{{~yield~}}
</div>
</div>
</div>
210 changes: 210 additions & 0 deletions packages/components/src/components/hds/app-side-nav/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/

import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { registerDestructor } from '@ember/destroyable';

export interface HdsAppSideNavSignature {
Args: {
isResponsive?: boolean;
isCollapsible?: boolean;
isMinimized?: boolean;
onToggleMinimizedStatus?: (arg: boolean) => void;
onDesktopViewportChange?: (arg: boolean) => void;
};
Blocks: {
default?: [];
};
Element: HTMLDivElement;
}

export default class HdsAppSideNav extends Component<HdsAppSideNavSignature> {
@tracked isMinimized;
@tracked isAnimating = false;
@tracked isDesktop = true;

body!: HTMLElement;
bodyInitialOverflowValue = '';
desktopMQ: MediaQueryList;
containersToHide!: NodeListOf<Element>;

desktopMQVal = getComputedStyle(document.documentElement).getPropertyValue(
'--hds-app-desktop-breakpoint'
);

constructor(owner: unknown, args: HdsAppSideNavSignature['Args']) {
super(owner, args);
this.isMinimized = this.args.isMinimized ?? false; // sets the default state on 'desktop' viewports
this.desktopMQ = window.matchMedia(`(min-width:${this.desktopMQVal})`);
this.addEventListeners();
registerDestructor(this, (): void => {
this.removeEventListeners();
});
}

addEventListeners(): void {
document.addEventListener('keydown', this.escapePress, true);
this.desktopMQ.addEventListener('change', this.updateDesktopVariable, true);
// if not instantiated as minimized via arguments
if (!this.args.isMinimized) {
// set initial state based on viewport using a "synthetic" event
const syntheticEvent = new MediaQueryListEvent('change', {
matches: this.desktopMQ.matches,
media: this.desktopMQ.media,
});
this.updateDesktopVariable(syntheticEvent);
}
}

removeEventListeners(): void {
document.removeEventListener('keydown', this.escapePress, true);
this.desktopMQ.removeEventListener(
'change',
this.updateDesktopVariable,
true
);
}

// controls if the component reacts to viewport changes
get isResponsive(): boolean {
return this.args.isResponsive ?? true;
}

// controls if users can collapse the appsidenav on 'desktop' viewports
get isCollapsible(): boolean {
return this.args.isCollapsible ?? false;
}

get shouldTrapFocus(): boolean {
return this.isResponsive && !this.isDesktop && !this.isMinimized;
}

get showToggleButton(): boolean {
return (this.isResponsive && !this.isDesktop) || this.isCollapsible;
}

get classNames(): string {
const classes = [`hds-app-side-nav`];

// add specific class names for the different possible states
if (this.isResponsive) {
classes.push('hds-app-side-nav--is-responsive');
}
if (!this.isDesktop && this.isResponsive) {
classes.push('hds-app-side-nav--is-mobile');
} else {
classes.push('hds-app-side-nav--is-desktop');
}
if (this.isMinimized && this.isResponsive) {
classes.push('hds-app-side-nav--is-minimized');
} else {
classes.push('hds-app-side-nav--is-not-minimized');
}
if (this.isAnimating) {
classes.push('hds-app-side-nav--is-animating');
}

return classes.join(' ');
}

synchronizeInert(): void {
this.containersToHide?.forEach((element): void => {
if (this.isMinimized) {
element.setAttribute('inert', '');
} else {
element.removeAttribute('inert');
}
});
}

lockBodyScroll(): void {
if (this.body) {
// Prevent page from scrolling when the dialog is open
this.body.style.setProperty('overflow', 'hidden');
}
}

unlockBodyScroll(): void {
// Reset page `overflow` property
if (this.body) {
this.body.style.removeProperty('overflow');
if (this.bodyInitialOverflowValue === '') {
if (this.body.style.length === 0) {
this.body.removeAttribute('style');
}
} else {
this.body.style.setProperty('overflow', this.bodyInitialOverflowValue);
}
}
}

@action
escapePress(event: KeyboardEvent): void {
if (event.key === 'Escape' && !this.isMinimized && !this.isDesktop) {
this.isMinimized = true;
this.synchronizeInert();
}
}

@action
toggleMinimizedStatus(): void {
this.isMinimized = !this.isMinimized;
this.synchronizeInert();

const { onToggleMinimizedStatus } = this.args;

if (typeof onToggleMinimizedStatus === 'function') {
onToggleMinimizedStatus(this.isMinimized);
}

if (this.isMinimized) {
this.unlockBodyScroll();
} else {
this.lockBodyScroll();
}
}

@action
didInsert(element: HTMLElement): void {
this.containersToHide = element.querySelectorAll(
'.hds-app-side-nav-hide-when-minimized'
);
this.body = document.body;
// Store the initial `overflow` value of `<body>` so we can reset to it
this.bodyInitialOverflowValue =
this.body.style.getPropertyValue('overflow');
}

@action
setTransition(phase: string, event: TransitionEvent): void {
// we only want to respond to `width` animation/transitions
if (event.propertyName !== 'width') {
return;
}
if (phase === 'start') {
this.isAnimating = true;
} else {
this.isAnimating = false;
}
}

@action
updateDesktopVariable(event: MediaQueryListEvent): void {
this.isDesktop = event.matches;

// automatically minimize on narrow viewports (when not in desktop mode)
this.isMinimized = !this.isDesktop;

this.synchronizeInert();

const { onDesktopViewportChange } = this.args;

if (typeof onDesktopViewportChange === 'function') {
onDesktopViewportChange(this.isDesktop);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: MPL-2.0
}}

<Hds::AppSideNav::List::Item>
<Hds::Interactive
class="hds-app-side-nav__list-item-link hds-app-side-nav__list-item-link--back-link"
@current-when={{@current-when}}
@models={{hds-link-to-models @model @models}}
@query={{hds-link-to-query @query}}
@replace={{@replace}}
@route={{@route}}
@isRouteExternal={{@isRouteExternal}}
@href={{@href}}
@isHrefExternal={{@isHrefExternal}}
...attributes
>
<Hds::Icon class="hds-app-side-nav__list-item-icon-leading" @name="chevron-left" />
<span class="hds-app-side-nav__list-item-text hds-typography-body-200 hds-font-weight-medium">
{{@text}}
</span>
</Hds::Interactive>
</Hds::AppSideNav::List::Item>
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/

import TemplateOnlyComponent from '@ember/component/template-only';

import type { HdsInteractiveSignature } from '../../interactive';

export interface HdsAppSideNavListBackLinkSignature {
Args: HdsInteractiveSignature['Args'] & {
text: string;
};
Element: HdsInteractiveSignature['Element'];
}

const HdsAppSideNavListBackLink =
TemplateOnlyComponent<HdsAppSideNavListBackLinkSignature>();

export default HdsAppSideNavListBackLink;
19 changes: 19 additions & 0 deletions packages/components/src/components/hds/app-side-nav/list/index.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: MPL-2.0
}}

<nav class="hds-app-side-nav__list-wrapper" aria-labelledby="hds-app-side-nav-header" ...attributes>
{{yield (hash ExtraBefore=(component "hds/yield"))}}
<ul class="hds-app-side-nav__list" role="list" aria-labelledby={{this.titleIds}}>
{{yield
(hash
Item=(component "hds/app-side-nav/list/item")
BackLink=(component "hds/app-side-nav/list/back-link")
Title=(component "hds/app-side-nav/list/title" didInsertTitle=this.didInsertTitle)
Link=(component "hds/app-side-nav/list/link")
)
}}
</ul>
{{yield (hash ExtraAfter=(component "hds/yield"))}}
</nav>
43 changes: 43 additions & 0 deletions packages/components/src/components/hds/app-side-nav/list/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/

import Component from '@glimmer/component';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import type { ComponentLike } from '@glint/template';
import type { HdsYieldSignature } from '../../yield';
import type { HdsAppSideNavListItemSignature } from './item';
import type { HdsAppSideNavListBackLinkSignature } from './back-link';
import type { HdsAppSideNavListTitleSignature } from './title';
import type { HdsAppSideNavListLinkSignature } from './link';

export interface HdsAppSideNavListSignature {
Blocks: {
default: [
{
ExtraBefore?: ComponentLike<HdsYieldSignature>;
Item?: ComponentLike<HdsAppSideNavListItemSignature>;
BackLink?: ComponentLike<HdsAppSideNavListBackLinkSignature>;
Title?: ComponentLike<HdsAppSideNavListTitleSignature>;
Link?: ComponentLike<HdsAppSideNavListLinkSignature>;
ExtraAfter?: ComponentLike<HdsYieldSignature>;
},
];
};
Element: HTMLElement;
}

export default class HdsAppSideNavList extends Component<HdsAppSideNavListSignature> {
@tracked _titleIds: string[] = [];

get titleIds(): string {
return this._titleIds.join(' ');
}

@action
didInsertTitle(titleId: string): void {
this._titleIds = [...this._titleIds, titleId];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: MPL-2.0
}}

<li class="hds-app-side-nav__list-item" ...attributes>
{{yield}}
</li>
Loading

0 comments on commit 70489d6

Please sign in to comment.