UNPKG

@hashicorp/design-system-components

Version:
208 lines (205 loc) 8.34 kB
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