@hashicorp/design-system-components
Version:
Helios Design System Components
208 lines (205 loc) • 8.34 kB
JavaScript
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { registerDestructor } from '@ember/destroyable';
import { modifier } from 'ember-modifier';
import { on } from '@ember/modifier';
import { fn } from '@ember/helper';
import focusTrap from 'ember-focus-trap/modifiers/focus-trap';
import { hdsBreakpoints } from '../../../utils/hds-breakpoints.js';
import HdsTHelper from '../../../helpers/hds-t.js';
import HdsAppSideNavToggleButton from './toggle-button.js';
import { precompileTemplate } from '@ember/template-compilation';
import { setComponentTemplate } from '@ember/component';
import { g, i } from 'decorator-transforms/runtime';
/**
* Copyright IBM Corp. 2021, 2025
* SPDX-License-Identifier: MPL-2.0
*/
class HdsAppSideNav extends Component {
static {
g(this.prototype, "_isMinimized", [tracked]);
}
#_isMinimized = (i(this, "_isMinimized"), void 0);
static {
g(this.prototype, "_isAnimating", [tracked], function () {
return false;
});
}
#_isAnimating = (i(this, "_isAnimating"), void 0);
static {
g(this.prototype, "_isDesktop", [tracked], function () {
return true;
});
}
#_isDesktop = (i(this, "_isDesktop"), void 0);
_body;
_bodyInitialOverflowValue = '';
_desktopMQ;
_navWrapperBody;
// we use the `lg` breakpoint for `desktop` viewports, but consumers can override its value
_desktopMQVal = this.args.breakpoint ?? hdsBreakpoints['lg'].px;
constructor(owner, 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, () => {
this.removeEventListeners();
});
}
_setUpBodyElement = modifier(() => {
this._body = document.body;
// Store the initial `overflow` value of `<body>` so we can reset to it
this._bodyInitialOverflowValue = this._body.style.getPropertyValue('overflow');
});
_setUpNavWrapperBody = modifier(element => {
this._navWrapperBody = element;
});
addEventListeners() {
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() {
document.removeEventListener('keydown', this.escapePress, true);
this._desktopMQ.removeEventListener('change', this.updateDesktopVariable, true);
}
// controls if the component reacts to viewport changes
get isResponsive() {
return this.args.isResponsive ?? true;
}
// controls if users can collapse the appsidenav on 'desktop' viewports
get isCollapsible() {
return this.args.isCollapsible ?? false;
}
get isMobileCollapsible() {
return this.isResponsive && !this._isDesktop;
}
// traps focus if isResponsive is enabled and it's in mobile view with side nav expanded (overlaying content)
get shouldTrapFocus() {
return this.isResponsive && !this._isDesktop && !this._isMinimized;
}
get showToggleButton() {
return this.isResponsive && !this._isDesktop || this.isCollapsible;
}
get classNames() {
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 = () => {
if (this._isMinimized) {
this._navWrapperBody?.setAttribute('inert', '');
} else {
this._navWrapperBody?.removeAttribute('inert');
}
};
lockBodyScroll = () => {
if (this._body) {
// Prevent page from scrolling when the dialog is open
this._body.style.setProperty('overflow', 'hidden');
}
};
unlockBodyScroll = () => {
// 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);
}
}
};
escapePress = event => {
if (event.key === 'Escape' && !this._isMinimized && !this._isDesktop) {
this._isMinimized = true;
this.synchronizeInert();
this.unlockBodyScroll();
}
};
toggleMinimizedStatus = () => {
this._isMinimized = !this._isMinimized;
this.synchronizeInert();
const {
onToggleMinimizedStatus
} = this.args;
if (typeof onToggleMinimizedStatus === 'function') {
onToggleMinimizedStatus(this._isMinimized);
}
if (!this._isDesktop) {
if (this._isMinimized) {
this.unlockBodyScroll();
} else {
this.lockBodyScroll();
}
}
};
setTransition = (phase, event) => {
// we only want to respond to `width` animation/transitions
if (event.propertyName !== 'width') {
return;
}
if (phase === 'start') {
this._isAnimating = true;
} else {
this._isAnimating = false;
}
};
updateDesktopVariable = event => {
this._isDesktop = event.matches;
// automatically minimize on narrow viewports (when not in desktop mode)
this._isMinimized = !this._isDesktop;
this.synchronizeInert();
if (this._isDesktop) {
// make sure scrolling is enabled if the user resizes the window from mobile to desktop
this.unlockBodyScroll();
}
const {
onDesktopViewportChange
} = this.args;
if (typeof onDesktopViewportChange === 'function') {
onDesktopViewportChange(this._isDesktop);
}
};
static {
setComponentTemplate(precompileTemplate("{{!-- 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 --}}\n<div class={{this.classNames}} ...attributes role={{if this.isMobileCollapsible \"dialog\"}} aria-labelledby={{if this.isMobileCollapsible \"hds-app-side-nav-header\"}} aria-modal={{if this.isMobileCollapsible \"true\"}} {{on \"transitionstart\" (fn this.setTransition \"start\")}} {{on \"transitionend\" (fn this.setTransition \"end\")}} {{focusTrap isActive=this.shouldTrapFocus}} {{this._setUpBodyElement}}>\n <h2 class=\"sr-only\" id=\"hds-app-side-nav-header\">\n {{hdsT \"hds.components.app-side-nav.screen-reader-label\" default=\"Application local navigation\"}}\n </h2>\n\n <div class=\"hds-app-side-nav__wrapper\">\n {{#if this.showToggleButton}}\n {{!-- template-lint-disable no-invalid-interactive--}}\n <div class=\"hds-app-side-nav__overlay\" {{on \"click\" this.toggleMinimizedStatus}} />\n {{!-- template-lint-enable no-invalid-interactive--}}\n <HdsAppSideNavToggleButton 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}} />\n {{/if}}\n\n <div class=\"hds-app-side-nav__wrapper-body\" {{this._setUpNavWrapperBody}}>\n {{~yield~}}\n </div>\n </div>\n</div>", {
strictMode: true,
scope: () => ({
on,
fn,
focusTrap,
hdsT: HdsTHelper,
HdsAppSideNavToggleButton
})
}), this);
}
}
export { HdsAppSideNav as default };
//# sourceMappingURL=index.js.map