diff --git a/packages/components/package.json b/packages/components/package.json index 5797ebcbdb..2588930663 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -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", diff --git a/packages/components/rollup.config.mjs b/packages/components/rollup.config.mjs index 76932ea53c..b75d264796 100644 --- a/packages/components/rollup.config.mjs +++ b/packages/components/rollup.config.mjs @@ -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 diff --git a/packages/components/src/components/hds/app-side-nav/index.hbs b/packages/components/src/components/hds/app-side-nav/index.hbs new file mode 100644 index 0000000000..6e3bec268a --- /dev/null +++ b/packages/components/src/components/hds/app-side-nav/index.hbs @@ -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 }} +
+

Application local navigation

+ +
+ {{#if this.showToggleButton}} + {{! template-lint-disable no-invalid-interactive}} +
+ {{! template-lint-enable no-invalid-interactive}} + + {{/if}} + +
+ {{~yield~}} +
+
+
\ No newline at end of file diff --git a/packages/components/src/components/hds/app-side-nav/index.ts b/packages/components/src/components/hds/app-side-nav/index.ts new file mode 100644 index 0000000000..ee0038d41d --- /dev/null +++ b/packages/components/src/components/hds/app-side-nav/index.ts @@ -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 { + @tracked isMinimized; + @tracked isAnimating = false; + @tracked isDesktop = true; + + body!: HTMLElement; + bodyInitialOverflowValue = ''; + desktopMQ: MediaQueryList; + containersToHide!: NodeListOf; + + 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 `` 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); + } + } +} diff --git a/packages/components/src/components/hds/app-side-nav/list/back-link.hbs b/packages/components/src/components/hds/app-side-nav/list/back-link.hbs new file mode 100644 index 0000000000..91da72b719 --- /dev/null +++ b/packages/components/src/components/hds/app-side-nav/list/back-link.hbs @@ -0,0 +1,24 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: MPL-2.0 +}} + + + + + + {{@text}} + + + \ No newline at end of file diff --git a/packages/components/src/components/hds/app-side-nav/list/back-link.ts b/packages/components/src/components/hds/app-side-nav/list/back-link.ts new file mode 100644 index 0000000000..4cd0e21d8e --- /dev/null +++ b/packages/components/src/components/hds/app-side-nav/list/back-link.ts @@ -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(); + +export default HdsAppSideNavListBackLink; diff --git a/packages/components/src/components/hds/app-side-nav/list/index.hbs b/packages/components/src/components/hds/app-side-nav/list/index.hbs new file mode 100644 index 0000000000..c82c85d635 --- /dev/null +++ b/packages/components/src/components/hds/app-side-nav/list/index.hbs @@ -0,0 +1,19 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: MPL-2.0 +}} + + \ No newline at end of file diff --git a/packages/components/src/components/hds/app-side-nav/list/index.ts b/packages/components/src/components/hds/app-side-nav/list/index.ts new file mode 100644 index 0000000000..7d9f3a5c80 --- /dev/null +++ b/packages/components/src/components/hds/app-side-nav/list/index.ts @@ -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; + Item?: ComponentLike; + BackLink?: ComponentLike; + Title?: ComponentLike; + Link?: ComponentLike; + ExtraAfter?: ComponentLike; + }, + ]; + }; + Element: HTMLElement; +} + +export default class HdsAppSideNavList extends Component { + @tracked _titleIds: string[] = []; + + get titleIds(): string { + return this._titleIds.join(' '); + } + + @action + didInsertTitle(titleId: string): void { + this._titleIds = [...this._titleIds, titleId]; + } +} diff --git a/packages/components/src/components/hds/app-side-nav/list/item.hbs b/packages/components/src/components/hds/app-side-nav/list/item.hbs new file mode 100644 index 0000000000..1a7773a5d3 --- /dev/null +++ b/packages/components/src/components/hds/app-side-nav/list/item.hbs @@ -0,0 +1,8 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: MPL-2.0 +}} + +
  • + {{yield}} +
  • \ No newline at end of file diff --git a/packages/components/src/components/hds/app-side-nav/list/item.ts b/packages/components/src/components/hds/app-side-nav/list/item.ts new file mode 100644 index 0000000000..4dcd422618 --- /dev/null +++ b/packages/components/src/components/hds/app-side-nav/list/item.ts @@ -0,0 +1,18 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import TemplateOnlyComponent from '@ember/component/template-only'; + +export interface HdsAppSideNavListItemSignature { + Blocks: { + default: []; + }; + Element: HTMLLIElement; +} + +const HdsAppSideNavListItem = + TemplateOnlyComponent(); + +export default HdsAppSideNavListItem; diff --git a/packages/components/src/components/hds/app-side-nav/list/link.hbs b/packages/components/src/components/hds/app-side-nav/list/link.hbs new file mode 100644 index 0000000000..1c959972de --- /dev/null +++ b/packages/components/src/components/hds/app-side-nav/list/link.hbs @@ -0,0 +1,51 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: MPL-2.0 +}} + + + + {{#if @icon}} + + {{/if}} + + {{#if @text}} + + {{@text}} + + {{/if}} + + {{#if @count}} + + {{/if}} + + {{#if @badge}} + + {{/if}} + + {{yield}} + + {{#if @hasSubItems}} + + + + {{/if}} + {{#if @isHrefExternal}} + + + + {{/if}} + + \ No newline at end of file diff --git a/packages/components/src/components/hds/app-side-nav/list/link.ts b/packages/components/src/components/hds/app-side-nav/list/link.ts new file mode 100644 index 0000000000..6d22928fe1 --- /dev/null +++ b/packages/components/src/components/hds/app-side-nav/list/link.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import TemplateOnlyComponent from '@ember/component/template-only'; + +import type { HdsIconSignature } from '../../icon'; +import type { HdsInteractiveSignature } from '../../interactive'; + +export interface HdsAppSideNavListLinkSignature { + Args: HdsInteractiveSignature['Args'] & { + icon?: HdsIconSignature['Args']['name']; + text?: string; + badge?: string; + count?: string; + hasSubItems?: boolean; + isActive?: boolean; + }; + Blocks: { + default: []; + }; + Element: HdsInteractiveSignature['Element']; +} + +const HdsAppSideNavListLink = + TemplateOnlyComponent(); + +export default HdsAppSideNavListLink; diff --git a/packages/components/src/components/hds/app-side-nav/list/title.hbs b/packages/components/src/components/hds/app-side-nav/list/title.hbs new file mode 100644 index 0000000000..91530a0d5d --- /dev/null +++ b/packages/components/src/components/hds/app-side-nav/list/title.hbs @@ -0,0 +1,13 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: MPL-2.0 +}} + + +
    {{~yield~}}
    +
    \ No newline at end of file diff --git a/packages/components/src/components/hds/app-side-nav/list/title.ts b/packages/components/src/components/hds/app-side-nav/list/title.ts new file mode 100644 index 0000000000..31bc0232dd --- /dev/null +++ b/packages/components/src/components/hds/app-side-nav/list/title.ts @@ -0,0 +1,32 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import { guidFor } from '@ember/object/internals'; +import { action } from '@ember/object'; +import Component from '@glimmer/component'; + +export interface HdsAppSideNavListTitleSignature { + Args: { + didInsertTitle?: (titleId: string) => void; + }; + Blocks: { + default: []; + }; + Element: HTMLDivElement; +} + +export default class HdsAppSideNavListTitle extends Component { + /* Generate a unique ID for each Title */ + titleId = 'title-' + guidFor(this); + + @action + didInsertTitle(element: HTMLElement): void { + const { didInsertTitle } = this.args; + + if (typeof didInsertTitle === 'function') { + didInsertTitle(element.id); + } + } +} diff --git a/packages/components/src/components/hds/app-side-nav/portal/index.hbs b/packages/components/src/components/hds/app-side-nav/portal/index.hbs new file mode 100644 index 0000000000..1b560ca4ec --- /dev/null +++ b/packages/components/src/components/hds/app-side-nav/portal/index.hbs @@ -0,0 +1,12 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: MPL-2.0 +}} + + +
    + + {{yield ListElements}} + +
    +
    \ No newline at end of file diff --git a/packages/components/src/components/hds/app-side-nav/portal/index.ts b/packages/components/src/components/hds/app-side-nav/portal/index.ts new file mode 100644 index 0000000000..10367dc5d9 --- /dev/null +++ b/packages/components/src/components/hds/app-side-nav/portal/index.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import TemplateOnlyComponent from '@ember/component/template-only'; + +import type { HdsAppSideNavListSignature } from '../list/index'; + +// TODO! understand how this should be done "correctly" +// import type { PortalSignature } from 'ember-stargate/components/portal'; +interface PortalSignature { + Args: { + target: string; + renderInPlace?: boolean; + fallback?: 'inplace'; + }; + Blocks: { + default: []; + }; +} + +export interface HdsAppSideNavPortalSignature { + Args: PortalSignature['Args'] & { + ariaLabel?: string; + targetName?: string; + }; + Blocks: HdsAppSideNavListSignature['Blocks']; + Element: HTMLDivElement; +} + +const HdsAppSideNavPortal = + TemplateOnlyComponent(); + +export default HdsAppSideNavPortal; diff --git a/packages/components/src/components/hds/app-side-nav/portal/target.hbs b/packages/components/src/components/hds/app-side-nav/portal/target.hbs new file mode 100644 index 0000000000..33384679e6 --- /dev/null +++ b/packages/components/src/components/hds/app-side-nav/portal/target.hbs @@ -0,0 +1,14 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: MPL-2.0 +}} + +
    + +
    \ No newline at end of file diff --git a/packages/components/src/components/hds/app-side-nav/portal/target.ts b/packages/components/src/components/hds/app-side-nav/portal/target.ts new file mode 100644 index 0000000000..c2b15882b2 --- /dev/null +++ b/packages/components/src/components/hds/app-side-nav/portal/target.ts @@ -0,0 +1,193 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import { DEBUG } from '@glimmer/env'; +import { macroCondition, isTesting } from '@embroider/macros'; + +import type { HdsAppSideNavPortalSignature } from './index'; + +// import { PortalTargetSignature } from 'ember-stargate/components/portal-target'; +interface PortalTargetSignature { + Element: HTMLDivElement; + Args: { + name: string; + multiple?: boolean; + onChange?: (count: number) => void; + }; + Blocks: { + default: [number]; + }; +} + +import type { Registry as Services } from '@ember/service'; + +interface HdsAppSideNavPortalTargetSignature { + Args: PortalTargetSignature['Args'] & { + targetName?: HdsAppSideNavPortalSignature['Args']['targetName']; + }; + Element: HTMLDivElement; +} + +export default class HdsAppSideNavPortalTarget extends Component { + @service router!: Services['router']; + + @tracked numSubnavs = 0; + @tracked lastPanelEl: Element | undefined; + + static get prefersReducedMotionOverride(): boolean { + return macroCondition(isTesting()) ? true : false; + } + + prefersReducedMotionMQ = window.matchMedia( + '(prefers-reduced-motion: reduce)' + ); + + get prefersReducedMotion(): boolean { + return ( + HdsAppSideNavPortalTarget.prefersReducedMotionOverride || + (this.prefersReducedMotionMQ && this.prefersReducedMotionMQ.matches) + ); + } + + @action + panelsChanged(portalCount: number): void { + this.numSubnavs = portalCount; + } + + @action + didUpdateSubnav(element: HTMLElement, [count]: [number]): void { + this.animateSubnav(element, [count]); + } + + @action + animateSubnav(element: HTMLElement, [count]: [number]): void { + /* + * Here is ascii art of what the layout looks like for this setup + * + + AppSideNav + +----------------------+ + | +------------------+ | + | | ("header") | | + | +------------------+ | + | | + | +------------------+ | + | | ("body") | | + (PortalTarget) | | | | + +----------------------------------------------+ | | + | +----------+ +----------+ | +----------+ | | | + | | (Portal) | | (Portal) | | (Portal) | | | | + | | | | | | | | | | | + | | hidden | | hidden | | *active* | | | | + | | panel | | panel | | | panel | | | | + | | | | | | | | | | + | | | | | | | | | | | + | | | | | | | | | | + | | | | | | | | | | | + | | | | | | | | | | + | | | | | | | | | | | + | | | | | | | | | | + | +----------+ +----------+ | +----------+ | | | + +----------------------------------------------+ | | + | | | | + | +------------------+ | + | | + | +------------------+ | + | | ("footer") | | + | +------------------+ | + +----------------------+ + + * + * every time `HcAppFrame::SideNav::Portal` renders, it contains a portaled "panel" + * that is rendered into the `hds-app-side-nav__content-panels` (inside the PortalTarget). + * + * Rendering or unrendering other `HcAppFrame::SideNav::Portal`s triggers the number of + * subnavs to change (via `numSubnavs`), so this function runs and slides + * `hds-app-side-nav__content-panels` left or right using the `element.animate` api. + * + * */ + + const activeIndex = count - 1; + const targetElement = element; + const { prefersReducedMotion } = this; + + const styles = getComputedStyle(targetElement); + const columnWidth = styles.getPropertyValue( + '--hds-app-sidenav-width-expanded' + ); + const slideDuration = prefersReducedMotion ? 0 : 150; + let fadeDuration = prefersReducedMotion ? 0 : 175; + let fadeDelay = prefersReducedMotion ? 0 : 50; + + // slide entire parent panel + const start = styles.transform; + const end = `translateX(-${activeIndex * parseInt(columnWidth, 10)}px)`; + const anim = targetElement.animate( + [{ transform: start }, { transform: end }], + { + duration: slideDuration, + easing: 'cubic-bezier(0.65, 0, 0.35, 1)', + fill: 'forwards', + } + ); + + anim.finished.then((): void => { + // uncomment this if we need/want to scroll the element to the top + // targetElement.scrollIntoView(true); + if (activeIndex > 0) { + const allPrev = Array.from(targetElement.children).slice( + 0, + activeIndex + ) as HTMLElement[]; + for (const ele of allPrev) { + ele.ariaHidden = 'true'; + ele.style.setProperty('visibility', 'hidden'); + ele.style.setProperty('opacity', '0'); + } + } + // Notice: we don't add the styles by default because it writes a `style` attribute to the element and it causes an additional re-render + if (DEBUG) { + // Check the visibility of the element before attempting to commitStyles. + if (targetElement.offsetParent !== null) { + anim.commitStyles(); + } + } + }); + + // fade in next panel + const nextPanelEl = targetElement.children[activeIndex] as HTMLElement; + + // get reference to last child panel + const lastPanelEl = targetElement.children[ + targetElement.children.length - 1 + ] as HTMLElement; + + if (nextPanelEl) { + nextPanelEl.ariaHidden = 'false'; + nextPanelEl.style.setProperty('visibility', 'visible'); + // this eliminates a flicker if there's only one subnav rendering or if we + // already just rendered this panel. + if (this.lastPanelEl) { + if (activeIndex === 0 || nextPanelEl.isSameNode(this.lastPanelEl)) { + fadeDelay = 0; + fadeDuration = 0; + } + } + + // remember the last panel + this.lastPanelEl = lastPanelEl; + + nextPanelEl.animate([{ opacity: '0' }, { opacity: '1' }], { + delay: fadeDelay, + duration: fadeDuration, + fill: 'forwards', + }); + } + } +} diff --git a/packages/components/src/components/hds/app-side-nav/toggle-button.hbs b/packages/components/src/components/hds/app-side-nav/toggle-button.hbs new file mode 100644 index 0000000000..aaf35b42d7 --- /dev/null +++ b/packages/components/src/components/hds/app-side-nav/toggle-button.hbs @@ -0,0 +1,7 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: MPL-2.0 +}} + \ No newline at end of file diff --git a/packages/components/src/components/hds/app-side-nav/toggle-button.ts b/packages/components/src/components/hds/app-side-nav/toggle-button.ts new file mode 100644 index 0000000000..11dbe1f0f0 --- /dev/null +++ b/packages/components/src/components/hds/app-side-nav/toggle-button.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import TemplateOnlyComponent from '@ember/component/template-only'; + +import type { HdsIconSignature } from '../icon'; + +interface HdsAppSideNavToggleButtonSignature { + Args: { + icon: HdsIconSignature['Args']['name']; + }; + Element: HTMLButtonElement; +} + +const HdsAppSideNavToggleButton = + TemplateOnlyComponent(); + +export default HdsAppSideNavToggleButton; diff --git a/packages/components/src/components/hds/side-nav/index.hbs b/packages/components/src/components/hds/side-nav/index.hbs index 8581d193a4..2a7df14374 100644 --- a/packages/components/src/components/hds/side-nav/index.hbs +++ b/packages/components/src/components/hds/side-nav/index.hbs @@ -2,6 +2,7 @@ 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 }} { constructor(owner: unknown, args: HdsSideNavSignature['Args']) { super(owner, args); + this.desktopMQ = window.matchMedia(`(min-width:${this.desktopMQVal})`); this.addEventListeners(); registerDestructor(this, (): void => { diff --git a/packages/components/src/styles/@hashicorp/design-system-components.scss b/packages/components/src/styles/@hashicorp/design-system-components.scss index 9f67d4d0ec..8688eaeff6 100644 --- a/packages/components/src/styles/@hashicorp/design-system-components.scss +++ b/packages/components/src/styles/@hashicorp/design-system-components.scss @@ -16,7 +16,8 @@ @use "../components/alert"; @use "../components/app-footer"; @use "../components/app-frame"; -@use "../components/app-header"; +// @use "../components/app-header"; +// @use "../components/app-side-nav"; @use "../components/application-state"; @use "../components/badge"; @use "../components/badge-count"; diff --git a/packages/components/src/styles/components/app-side-nav/content.scss b/packages/components/src/styles/components/app-side-nav/content.scss new file mode 100644 index 0000000000..ac86e2c92e --- /dev/null +++ b/packages/components/src/styles/components/app-side-nav/content.scss @@ -0,0 +1,182 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +// +// SIDE-NAV > CONTENT (PORTALS + LISTS OF ITEMS/LINKS) +// + +@use "../../mixins/focus-ring" as *; + + +// PANELS (wrappers used in conjunction with the portal elements) + +.hds-app-side-nav__content { + // we use this trick (increasing the container size here, and reducing it at single panel level) + // to have the panels width match the sidebar extended width (it's used in the animated sliding of the panels) + margin: 0 calc(var(--token-app-side-nav-wrapper-padding-horizontal) * -1); + + // we hide the content when the SideNav is collapsed to prevent the vertical scrollbar from being visible + // when the scrollbar is set to be always visible or a mouse or trackpad force it to be always visible. + // ideally we would use `display: none` but doing so would disable the fade-in transition when expanding + .hds-app-side-nav--is-minimized & { + height: 0; + overflow: hidden; + } +} + +.hds-app-side-nav__content-panels { + // see https://codepen.io/didoo/pen/YzOeRPr + display: grid; + grid-template-columns: repeat(5, var(--hds-app-side-nav-width-expanded)); + width: 100%; +} + +.hds-app-side-nav__content-panel { + padding: 0 var(--token-app-side-nav-wrapper-padding-horizontal); + overflow: hidden; // the panel itself does not need to be scrollable + + &[aria-hidden="true"] { + max-height: 0; // prevents hidden panels from causing scrolling + } +} + +// (LIST) TITLE + +.hds-app-side-nav__list-title { + min-height: var(--token-app-side-nav-body-list-item-height); + margin-top: var(--token-app-side-nav-body-list-margin-vertical); + padding: 9px var(--token-app-side-nav-body-list-item-padding-horizontal); // 8px = (min-height - body-100-line-height) / 2 + color: var(--token-app-side-nav-color-foreground-faint); + overflow-wrap: break-word; + + // Remove margin from title at top of all list-items & lists + .hds-app-side-nav__list-wrapper:first-child .hds-app-side-nav__list-item:first-child > & { + margin-top: 0; + } +} + +// LIST (root elements) + +.hds-app-side-nav__list-wrapper, //
    - + --}} \ No newline at end of file diff --git a/showcase/app/templates/components/app-side-nav.hbs b/showcase/app/templates/components/app-side-nav.hbs new file mode 100644 index 0000000000..23df92fa2d --- /dev/null +++ b/showcase/app/templates/components/app-side-nav.hbs @@ -0,0 +1,525 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: MPL-2.0 +}} + +{{page-title "AppSideNav Component"}} + + + AppSideNav: removed from rollup, examples have been commented out + + +
    + Content + + {{!-- With generic content + + + +
    + + + +
    +
    + + +
    + + + +
    +
    +
    + + With content injected via "portal" + + + +
    + + + +
    +
    +
    + + + + + + +
    + + + +
    + Examples of sidebar navigation + + + + Content injected via portal + + +
    + + + +
    +
    +
    + + + Services + + + + + + + + + Default Org + + + + + + +
    + + + Yielded content + + + +
    + + + + + + + Services + + + + + + + + + + + + Default Org + + + + + + + +
    +
    +
    +
    + + + Yielded content + + + +
    + + + + A section title + + + + + + + + +
    +
    +
    +
    +
    +
    + + + +
    + Hds::AppSideNav::List + + Content + + + +
    + + Services + + + + + + + + + + + Default Org + + + + + + +
    +
    + + +
    + + + + + Services + + + + + + + + + + + + Default Org + + + + + + +
    +
    +
    + + + + AppSideNav::List::Title + + + +
      + Group title + + + +
    +
    + +
      + This is a long text that should go on two lines + + + +
    +
    + +
      + ThisIsLongTextThatShouldWrapToTwoLinesAndNotOverflow + + + +
    +
    +
    + + + + AppSideNav::List::Item + +
      + + + +
    + + + + AppSideNav::List::Link + + Content + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{! we've added an empty label on purpose, to test the layout in this case }} + + + + + + + + + + + + + + + + + This text needs local styling + + + + + + This text is locally styled + + + + + + + + + States + + + {{#let (array "default" "hover" "active" "focus") as |states|}} + {{#each states as |state|}} + + + + + + + + + + + + + + {{/each}} + {{/let}} + + + + {{#let (array "default" "hover" "active" "focus") as |states|}} + {{#each states as |state|}} + + + + {{/each}} + {{/let}} + + + + + AppSideNav::List::BackLink + + Content + + + +
      + +
    +
    + +
      + +
    +
    + +
      + +
    +
    +
    + + States + + + {{#let (array "default" "hover" "active" "focus") as |states|}} + {{#each states as |state|}} + + + +
      + +
    +
    + +
      + +
    +
    +
    +
    + {{/each}} + {{/let}} +
    + + + + AppSideNav::ToggleButton + + Content + + + +
    + +
    +
    + +
    + +
    +
    + +
    + +
    +
    +
    + + States + + + {{#let (array "default" "hover" "active" "focus") as |states|}} + {{#each states as |state|}} + +
    + +
    +
    + {{/each}} + {{/let}} +
    --}} +
    \ No newline at end of file diff --git a/showcase/app/templates/index.hbs b/showcase/app/templates/index.hbs index 4616f41871..7d5689a96b 100644 --- a/showcase/app/templates/index.hbs +++ b/showcase/app/templates/index.hbs @@ -24,6 +24,7 @@ +
    Components
      @@ -42,11 +43,16 @@ AppFooter -
    1. + {{!
    2. AppHeader
    3. +
    4. + + AppSideNav + +
    5. }}
    6. Application State @@ -259,6 +265,7 @@
    +
    Layouts
      @@ -276,6 +283,7 @@
    + Utilities
    1. diff --git a/showcase/app/templates/layouts/app-frame/frameless/demo-full-app-frame-with-app-header-and-app-side-nav.hbs b/showcase/app/templates/layouts/app-frame/frameless/demo-full-app-frame-with-app-header-and-app-side-nav.hbs new file mode 100644 index 0000000000..3228f9d341 --- /dev/null +++ b/showcase/app/templates/layouts/app-frame/frameless/demo-full-app-frame-with-app-header-and-app-side-nav.hbs @@ -0,0 +1,262 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: MPL-2.0 +}} + +{{page-title "AppFrame Component - Frameless"}} + +{{! NOTE: AppHeader & AppSideNav components are not published so components are commented out for now to satisfy linter }} + + + + {{!-- + <:logo> + + + <:globalActions> + + + + organizationName + + + + <:utilityActions> + + + + Americas + + + + + + + + + + + + + + + + + + + + + + + --}} + + + + {{!-- + + + + + Services + + + + + + + + + + + Default Org + + + + + + + --}} + + + +
      + + Page title + + + + + + + + + + + + Project dashboard + + An overview of all resources in the project. + Learn more. + + + + + + + +

      Lorem ipsum dolor sit amet, consectetur adipisicing elit. Excepturi aperiam a molestias quisquam sapiente + alias corporis sit aliquid similique esse illum at itaque ducimus, eligendi eos. Iure dolor eos cumque autem + placeat pariatur voluptate deserunt quas, iste quo alias? Sequi, qui ipsa. Laborum, ipsa atque alias nostrum + nihil repudiandae ratione inventore, qui impedit obcaecati facilis quaerat aliquam omnis consequuntur. Ab, + deleniti vel. Optio consequuntur sint officiis distinctio dolorem nobis porro ipsum natus hic debitis nihil at + nostrum, reiciendis exercitationem quod deserunt inventore, repellendus officia cum temporibus. Molestias + voluptate magni earum unde officia illum doloribus facere natus molestiae quisquam nobis adipisci non + distinctio quam asperiores saepe, ab veniam dolor animi sed accusamus nulla nam dolorem. Quos reprehenderit + molestiae veritatis fugiat eligendi temporibus fuga modi, id recusandae dolor mollitia accusantium soluta + cumque corporis ipsa tempore amet dolorum. Velit nemo voluptatum, culpa libero assumenda ea quae dolorem + molestias fugiat, maxime eveniet ipsum, et facere aut animi praesentium. Eum voluptatum eaque fugit aspernatur + voluptas maxime, iste blanditiis doloribus amet repellendus aut cupiditate beatae aperiam molestiae hic saepe + optio. Neque voluptates quidem, tempore harum aliquid esse perferendis officiis repudiandae dicta? Excepturi + temporibus molestias sit consectetur consequatur exercitationem et necessitatibus cumque aliquam, quisquam + dolores alias reiciendis.

      +

      Dicta ab ut, facilis aliquid sit praesentium nesciunt molestias consequatur doloribus fugiat eius laborum aut + ullam animi excepturi. Recusandae ipsum, quia quis nesciunt asperiores, distinctio aut eligendi facilis ex + pariatur consectetur, maxime sunt omnis natus vitae amet soluta exercitationem deserunt maiores labore + corporis eum a porro! Cupiditate fugit architecto reprehenderit pariatur sed rerum enim repellat consequuntur + ducimus. Aspernatur, quia eligendi? Dignissimos commodi, voluptatem aliquid inventore nobis non qui + repellendus earum fugit harum a eum eligendi? Id, excepturi! Illum obcaecati maiores, expedita assumenda + perspiciatis vero dolor quis similique in? Tempora numquam, ipsum ut odio cupiditate soluta aliquid saepe + necessitatibus explicabo repellat animi! Ut optio adipisci rem ab harum eaque dolores, et animi magnam + commodi? Quisquam, autem, eum enim quae quam tempore aperiam libero velit adipisci illum tempora ad, at + deserunt. Est eum molestiae illum possimus nesciunt, impedit culpa, non minus vero labore fuga, nemo id velit + cumque! Nemo, nesciunt eaque provident consequatur recusandae quisquam illum quidem obcaecati praesentium + magnam delectus architecto modi neque iure, qui eveniet perferendis iusto soluta quasi laudantium accusamus! + Possimus, mollitia aut quibusdam voluptatibus dolore accusantium omnis, odio dolores enim vitae dicta rerum + sit obcaecati modi iste odit repellendus officia quam. Cupiditate ipsum fuga, vitae cumque tempora + perspiciatis.

      +

      Quisquam est tenetur eius cum voluptate magnam odit ipsum iusto officia quis iste, dignissimos autem + accusantium aspernatur ea in vel minima nihil commodi aperiam tempora, ullam illum. Ea in molestiae ducimus + saepe natus minima fugit numquam cupiditate mollitia, ipsa deserunt accusamus quas nobis fuga modi aspernatur + iure. Similique inventore totam fugit nobis quaerat, numquam rem quos labore cupiditate est. Ratione incidunt + voluptas minus ipsam necessitatibus aperiam rerum, sapiente explicabo laudantium dicta rem praesentium. + Repudiandae tenetur velit eligendi odio exercitationem eum natus voluptas? Minus, suscipit! Id, dignissimos. + Maiores labore voluptatibus repellendus earum beatae quae qui, repellat, voluptas commodi hic enim voluptate + obcaecati iste. Praesentium qui explicabo nemo, modi nihil asperiores molestias nobis vitae alias nesciunt + mollitia. Illo labore fuga quia unde consectetur ut cum, beatae tempora vitae nihil aperiam recusandae dolor + facilis quod repudiandae ratione? Consequuntur illum, autem nulla et neque deserunt at quo reprehenderit in + voluptatibus rerum, iste, maxime cum eius aperiam eos laborum accusantium dolore laboriosam est sunt assumenda + nemo sint. Ex officia quam iusto modi facere molestiae reprehenderit beatae ducimus voluptate provident? + Saepe, voluptate magnam nemo, corrupti, rem ea sequi sunt aut commodi nobis quia. Voluptatem aliquam, + obcaecati id nemo expedita incidunt culpa minus praesentium, rerum porro quos?

      +

      Voluptate est quibusdam nostrum blanditiis, voluptatibus neque officia eum similique laboriosam voluptates + hic in obcaecati aliquid aperiam excepturi saepe dolorem illum! Aperiam accusamus id, libero et veritatis in + quidem aliquam amet exercitationem architecto voluptatum voluptates pariatur sapiente assumenda dolores eius + repellendus excepturi asperiores molestias natus recusandae. Incidunt similique consequuntur, cum nostrum + quidem fugit tenetur. Earum ipsa reprehenderit rerum ab, sed omnis dignissimos ea explicabo illo incidunt + delectus odio modi consequuntur provident magni non, eos voluptatum! Temporibus accusantium pariatur cum qui + saepe neque, hic accusamus sequi velit distinctio! Quidem eos recusandae assumenda eum veniam repellat culpa + atque cumque architecto. Deserunt nam omnis cupiditate corrupti est ipsa nesciunt, maxime accusantium numquam + velit modi, commodi libero placeat ipsum? Iusto harum modi voluptas provident, impedit, tempora quas, + quibusdam voluptatem facilis debitis repellat quae. Itaque laboriosam nobis quia assumenda tempora + reprehenderit distinctio odio accusamus omnis beatae ipsum id reiciendis quod est illo modi facere sapiente + doloremque, maxime libero laborum blanditiis! Architecto numquam cupiditate, ratione ipsum necessitatibus + reiciendis temporibus accusamus eius iure unde eveniet minima obcaecati veniam ad vero sint eos voluptatem + ipsa sit amet labore voluptates facilis nobis doloribus! Nobis ex alias ducimus quia numquam voluptas + possimus. Esse neque consequatur nobis ipsum facere corporis quidem.

      +

      Quam fugiat nihil quasi porro dolores nemo? Asperiores consequuntur rerum eaque perspiciatis ab, quaerat + debitis totam animi vero officiis cumque ipsa minus tempore cum dolores. Obcaecati, quis officiis! Incidunt + quas non sed cumque earum sunt optio aspernatur ut quibusdam commodi quia quos ea similique enim consequuntur + eos dolorum cupiditate quaerat totam modi ratione, voluptate quidem. Inventore libero nisi corrupti. Quibusdam + ex quidem hic perspiciatis assumenda magnam, labore architecto veritatis eos numquam, expedita aperiam. + Ratione architecto quia beatae temporibus incidunt neque illo voluptatum aperiam, rem eos deleniti aspernatur + impedit animi. Facilis voluptas eligendi iste saepe provident sed libero laboriosam at nesciunt voluptate + dolore modi, ut tenetur? Necessitatibus recusandae nihil voluptas pariatur veritatis nesciunt a vero quae, + nemo molestiae tempore quasi eius provident ipsum tenetur labore autem ratione fuga perferendis earum quia? At + neque incidunt, temporibus ducimus nam magni minima eum in esse labore, alias, odio voluptatibus ipsam + repellendus praesentium quae! Ipsum, reprehenderit aliquid. Sapiente hic blanditiis illum perferendis minus + beatae nisi rerum qui voluptatum eos quos cupiditate, commodi unde aperiam tenetur in tempora laudantium + explicabo consequatur autem labore quasi omnis, ut deserunt. Excepturi non maiores laboriosam, magni commodi + accusantium mollitia! Quos ut dolore esse! Officiis dolor laboriosam impedit. Fuga, error fugiat?

      +

      Animi nulla nisi quia quis molestias omnis odit repellat consectetur soluta et, vel modi voluptatibus! Ut ad + facere ea debitis. Tenetur explicabo, omnis amet aspernatur atque tempora accusamus sint aperiam obcaecati + alias! Perferendis, libero, reprehenderit nostrum esse quaerat perspiciatis harum suscipit earum ipsa omnis + dolores nam natus error placeat tempora exercitationem recusandae. Repudiandae veritatis nulla consequatur + dolor! Numquam enim at tempore odio voluptatum. Esse, impedit? Perspiciatis delectus illo cupiditate. Vitae + est cumque sed possimus magni alias voluptas, quisquam, temporibus, obcaecati optio assumenda recusandae + similique nulla doloribus quas expedita rem. Consequuntur similique neque magni doloremque voluptatibus ut + voluptas mollitia corporis aliquid? Hic unde quam harum expedita beatae quis, fugit quae. Atque et velit nisi + iure, asperiores ratione provident laboriosam aut odit deleniti unde sit eligendi dignissimos reprehenderit, + vel quas magnam incidunt, dolore magni suscipit cumque adipisci accusamus placeat dicta. Labore autem veniam + ex doloremque quaerat hic voluptas facere ipsa natus, excepturi inventore optio quidem deserunt repudiandae + possimus. Neque, molestias blanditiis nisi cum praesentium nostrum. Officiis repellat totam deleniti in, + doloribus animi vero, exercitationem culpa molestias quasi at molestiae quas. Aliquam laboriosam error dolorem + magnam ad earum nisi fugit asperiores! Sint minima similique delectus sed ratione numquam natus dolore + praesentium facilis deserunt.

      +

      Nemo ratione quos, consequatur magni libero, incidunt provident, cumque quod dolorum numquam debitis earum + minima? Quae, in, officia optio minus neque est dolores commodi, atque dolorum consectetur deserunt culpa + tempora numquam iusto ab magnam nesciunt ratione! Animi quisquam aliquam praesentium officia dolores, + necessitatibus distinctio aliquid placeat quasi! Et dolorum tempore explicabo iste amet non at qui aut + temporibus eum reiciendis fuga, similique ipsa illum ipsam! Veritatis adipisci totam sunt architecto + repudiandae nemo perferendis dolorem? Veritatis perferendis, cupiditate, architecto harum consequatur + excepturi adipisci temporibus doloribus porro iste beatae dicta at sit qui voluptatibus ipsa fugit quod ipsum + ab, libero soluta? Hic distinctio tenetur itaque provident praesentium aspernatur architecto officia ea + assumenda exercitationem, eius dolorum quod, optio porro. Dolore delectus illum accusantium quibusdam quos non + autem voluptatum cumque cum accusamus alias, beatae tenetur velit illo nesciunt doloremque ipsum minima ad, + voluptatem quae! Quod labore iusto omnis, vitae eum animi impedit cum porro dicta consequatur! Quisquam, quo + asperiores in voluptates, odit, autem culpa sunt est architecto fuga aperiam perferendis quia nostrum. Non eos + pariatur nesciunt! Dolorum voluptatibus, dicta quos sapiente dolor autem doloremque cupiditate commodi totam + quisquam unde ab iste molestias exercitationem delectus illo laboriosam illum distinctio officia? Sed + blanditiis temporibus natus repellendus?

      +

      Esse consectetur maxime unde voluptates rem est minus cum soluta aut quisquam eum omnis corrupti rerum + voluptatum autem molestiae quidem magnam, exercitationem quas odit placeat fugit fugiat! Modi adipisci + molestiae rem voluptatum eos perferendis autem distinctio quam, officiis ipsa voluptas earum qui illum iste + aliquid porro praesentium laborum dicta enim optio suscipit repellendus mollitia consequuntur ea? Nesciunt + odit exercitationem ab pariatur, est vel! Odio quidem eaque aut cum ea officiis ducimus nisi error a hic magni + voluptatibus sit pariatur similique minima mollitia nam eius iste, itaque ipsam. Iure, aperiam molestiae! + Molestias magni eos ipsa fugiat perferendis quae odio ipsum explicabo maiores porro, nobis facere nisi + recusandae laboriosam, vel necessitatibus impedit modi praesentium dolores consectetur hic quam sint. Commodi + iusto vitae quia aliquid officia unde, est rem possimus in cupiditate! Qui odio impedit suscipit, ipsa animi + molestias dolores enim ea a optio quo ex magnam! Possimus quibusdam, laborum atque consequatur unde quasi aut + pariatur voluptas ab itaque, nisi debitis molestiae neque. Nisi, provident rem. Fugit, dolore. Eos corporis + quae minus magni in consequatur provident voluptate. Quis maxime sequi nostrum optio recusandae hic veniam + earum dolorum, ipsam illo error molestiae, doloremque esse! Repellendus perferendis error ad dolor, suscipit + cumque? Ex, non quas.

      +

      Neque voluptate itaque eaque sit nulla repellendus officiis odit similique laudantium, ducimus veritatis sed + eius provident, sunt corrupti illum accusamus pariatur corporis reprehenderit ipsum, culpa voluptatibus quis + ad! Earum vitae necessitatibus tempora sapiente odit eos suscipit impedit, aut natus eum distinctio. Alias + fuga rerum quibusdam, qui placeat enim pariatur tempore consectetur? Repellendus excepturi, fuga incidunt + itaque enim aut provident ut laboriosam minima optio error in, inventore officia. Ab quia neque laudantium + debitis expedita ut ad vitae necessitatibus velit, nisi, inventore eos excepturi fuga reiciendis error alias! + Corporis dolor aut laboriosam est provident magni sequi, modi quibusdam voluptatibus similique cum expedita + dolore beatae rem officia labore fugiat libero totam maxime illum, debitis culpa ex soluta autem! Esse + consequuntur quisquam dolor nisi aperiam hic consectetur sint, voluptas repellendus vitae error, veritatis + harum enim! Porro cumque eum maiores nam aspernatur odit voluptate saepe similique cupiditate, molestias + consequatur ex expedita non amet sint, id nemo tempore deserunt reiciendis! Consectetur adipisci iste cumque + ut modi voluptate impedit, ab, accusamus hic dolor pariatur eligendi perspiciatis explicabo ipsam nostrum + nesciunt harum voluptatum. Illo, molestiae voluptatum? Cum, accusantium amet facilis eveniet, temporibus in + autem distinctio ratione alias voluptas omnis, sequi magnam provident? Vitae, omnis! Delectus nobis magni ut.

      +

      Corrupti consequatur, velit tenetur excepturi ea quod voluptatem nisi commodi expedita reprehenderit. Minus + est amet, repellat rem molestiae ratione doloremque nihil temporibus voluptatibus veritatis suscipit soluta + tenetur, nam accusamus quasi unde saepe a modi excepturi nisi sapiente asperiores voluptas. Libero vero, illo + qui harum maiores illum voluptas natus accusantium quasi quae ut consequuntur nostrum error saepe quod fugit + possimus nulla magni porro! Autem reiciendis amet sunt nesciunt, natus ullam saepe nihil, quo eius corrupti + dolorum fugit omnis beatae dicta excepturi nulla quis laudantium ratione, nisi a dolor voluptatum? Amet, + repellendus dolor. Perferendis praesentium eaque assumenda iusto corporis, nulla accusamus recusandae, facere + rerum, voluptatem modi labore quae! Ratione culpa quod autem quia ipsa optio natus quos nisi dignissimos + perspiciatis, laborum adipisci recusandae voluptate! Nihil dolorem doloremque iste quia quasi illo voluptatum, + eveniet natus voluptatibus, asperiores sequi nesciunt explicabo quidem atque sit suscipit blanditiis + accusantium laboriosam deleniti mollitia. Quia saepe repellendus dolore officia dignissimos impedit vitae sed + quam placeat vero tempora, at quod earum magni praesentium eius iure eligendi. Illo rem sint fugit ullam + pariatur voluptatum est culpa ratione ab quaerat doloribus accusantium ducimus similique asperiores + dignissimos, perspiciatis deleniti hic quam laudantium unde harum nobis eos, nemo sit! Quasi explicabo itaque + animi!

      +
      +
      +
      + + + + + + +
      \ No newline at end of file diff --git a/showcase/app/templates/layouts/app-frame/index.hbs b/showcase/app/templates/layouts/app-frame/index.hbs index cc791e4139..af824a57ab 100644 --- a/showcase/app/templates/layouts/app-frame/index.hbs +++ b/showcase/app/templates/layouts/app-frame/index.hbs @@ -359,12 +359,21 @@ Framed + {{! NOTE: AppHeader & AppSideNav components are not published so examples are commented out for now + + + }} ` ); @@ -24,7 +24,7 @@ module('Integration | Component | hds/app-header/home-link', function (hooks) { // CONTENT - test('it renders the passed in args', async function (assert) { + skip('it renders the passed in args', async function (assert) { await render( hbs`` ); @@ -35,7 +35,7 @@ module('Integration | Component | hds/app-header/home-link', function (hooks) { .hasAttribute('aria-label', 'HashiCorp'); }); - test('it renders the logo with a custom passed in color', async function (assert) { + skip('it renders the logo with a custom passed in color', async function (assert) { await render( hbs`` ); @@ -46,7 +46,7 @@ module('Integration | Component | hds/app-header/home-link', function (hooks) { // ASSERTIONS - test('it should throw an assertion if @ariaLabel is missing/has no value', async function (assert) { + skip('it should throw an assertion if @ariaLabel is missing/has no value', async function (assert) { const errorMessage = '@ariaLabel for "Hds::AppHeader::HomeLink" ("Logo") must have a valid value'; assert.expect(2); diff --git a/showcase/tests/integration/components/hds/app-header/index-test.js b/showcase/tests/integration/components/hds/app-header/index-test.js index 8ec77d9612..2d4c5a8be5 100644 --- a/showcase/tests/integration/components/hds/app-header/index-test.js +++ b/showcase/tests/integration/components/hds/app-header/index-test.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: MPL-2.0 */ -import { module, test } from 'qunit'; +import { module, skip } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { render, click, triggerKeyEvent } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; @@ -11,14 +11,14 @@ import { hbs } from 'ember-cli-htmlbars'; module('Integration | Component | hds/app-header/index', function (hooks) { setupRenderingTest(hooks); - test('it should render the component with a CSS class that matches the component name', async function (assert) { + skip('it should render the component with a CSS class that matches the component name', async function (assert) { await render(hbs``); assert.dom('#test-app-header').hasClass('hds-app-header'); }); // CONTENT - test('it renders content passed into the globalActions and utilityActions named blocks', async function (assert) { + skip('it renders content passed into the globalActions and utilityActions named blocks', async function (assert) { await render(hbs` <:logo> @@ -39,12 +39,12 @@ module('Integration | Component | hds/app-header/index', function (hooks) { // RESPONSIVENESS - test('it is "desktop" by default', async function (assert) { + skip('it is "desktop" by default', async function (assert) { await render(hbs``); assert.dom('#test-app-header').hasClass('hds-app-header--is-desktop'); }); - test('it does not show a menu button on wide viewports', async function (assert) { + skip('it does not show a menu button on wide viewports', async function (assert) { await render(hbs` `); @@ -55,7 +55,7 @@ module('Integration | Component | hds/app-header/index', function (hooks) { // Note: We set a high breakpoint to force the component to render as "mobile" - test('it is "mobile" on narrow viewports', async function (assert) { + skip('it is "mobile" on narrow viewports', async function (assert) { await render(hbs` @@ -63,7 +63,7 @@ module('Integration | Component | hds/app-header/index', function (hooks) { assert.dom('#test-app-header').hasClass('hds-app-header--is-mobile'); }); - test('it shows a menu button on narrow viewports', async function (assert) { + skip('it shows a menu button on narrow viewports', async function (assert) { await render(hbs` @@ -72,7 +72,7 @@ module('Integration | Component | hds/app-header/index', function (hooks) { }); // Mobile menu functionality - test(`the actions do not display by default on narrow viewports`, async function (assert) { + skip(`the actions do not display by default on narrow viewports`, async function (assert) { await render(hbs` @@ -80,7 +80,7 @@ module('Integration | Component | hds/app-header/index', function (hooks) { assert.dom('#test-app-header').hasClass('hds-app-header--menu-is-closed'); }); - test(`the actions show/hide when the menu button is pressed on narrow viewports`, async function (assert) { + skip(`the actions show/hide when the menu button is pressed on narrow viewports`, async function (assert) { await render(hbs` @@ -99,7 +99,7 @@ module('Integration | Component | hds/app-header/index', function (hooks) { // Breakpoint // Note: We pass in a high custom breakpoint to force the component to render as "mobile" - test('it uses the custom passed in breakpoint to control menu display', async function (assert) { + skip('it uses the custom passed in breakpoint to control menu display', async function (assert) { await render(hbs` `); @@ -108,7 +108,7 @@ module('Integration | Component | hds/app-header/index', function (hooks) { // A11Y - test(`it displays the correct value for aria-expanded when actions are displayed vs hidden`, async function (assert) { + skip(`it displays the correct value for aria-expanded when actions are displayed vs hidden`, async function (assert) { await render(hbs` @@ -124,7 +124,7 @@ module('Integration | Component | hds/app-header/index', function (hooks) { .hasAttribute('aria-expanded', 'false'); }); - test('the actions menu collapses when the ESC key is pressed on narrow viewports', async function (assert) { + skip('the actions menu collapses when the ESC key is pressed on narrow viewports', async function (assert) { await render(hbs` @@ -138,7 +138,7 @@ module('Integration | Component | hds/app-header/index', function (hooks) { assert.dom('#test-app-header').hasClass('hds-app-header--menu-is-closed'); }); - test('the menu button has an aria-controls attribute with a value matching the menu id', async function (assert) { + skip('the menu button has an aria-controls attribute with a value matching the menu id', async function (assert) { await render(hbs` @@ -157,7 +157,7 @@ module('Integration | Component | hds/app-header/index', function (hooks) { // A11Y Refocus - test('it renders the `a11y-refocus` elements by default with a default skip link href value of "#hds-main', async function (assert) { + skip('it renders the `a11y-refocus` elements by default with a default skip link href value of "#hds-main', async function (assert) { await render(hbs``); assert.dom('#ember-a11y-refocus-nav-message').exists(); assert @@ -166,7 +166,7 @@ module('Integration | Component | hds/app-header/index', function (hooks) { .hasAttribute('href', '#hds-main'); }); - test('it renders the `a11y-refocus` elements with the right properties provided as arguments', async function (assert) { + skip('it renders the `a11y-refocus` elements with the right properties provided as arguments', async function (assert) { await render(hbs` `); assert.dom('#ember-a11y-refocus-nav-message').doesNotExist(); assert.dom('#ember-a11y-refocus-skip-link').doesNotExist(); diff --git a/showcase/tests/integration/components/hds/app-side-nav/index-test.js b/showcase/tests/integration/components/hds/app-side-nav/index-test.js new file mode 100644 index 0000000000..a85ffbe765 --- /dev/null +++ b/showcase/tests/integration/components/hds/app-side-nav/index-test.js @@ -0,0 +1,296 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import { module, skip } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { + render, + click, + resetOnerror, + settled, + triggerKeyEvent, +} from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +class MockEventTarget extends EventTarget {} + +module('Integration | Component | hds/app-side-nav/index', function (hooks) { + setupRenderingTest(hooks); + + hooks.beforeEach(function () { + // Mock window.matchMedia to control media query events + let mockMedia = new MockEventTarget(); + mockMedia.matches = true; + + this.__matchMedia = window.matchMedia; + + this.mockMedia = () => { + window.matchMedia = () => mockMedia; + }; + + this.changeBrowserSize = async (isDesktop) => { + mockMedia.matches = isDesktop; + mockMedia.dispatchEvent( + new MediaQueryListEvent('change', { + matches: isDesktop, + }) + ); + await settled(); + }; + }); + + hooks.afterEach(function () { + resetOnerror(); + window.matchMedia = this.__matchMedia; + }); + + skip('it should render the component with a CSS class that matches the component name', async function (assert) { + await render( + hbs`` + ); + assert.dom('#test-app-side-nav').hasClass('hds-app-side-nav'); + }); + + // CONTENT + + skip('it renders content passed to the named blocks', async function (assert) { + await render(hbs` + + + + `); + assert.dom('#test-app-side-nav-body').exists(); + }); + + // RESPONSIVENESS + + skip('it is "desktop" by default', async function (assert) { + await render(hbs``); + assert.dom('#test-app-side-nav').hasClass('hds-app-side-nav--is-desktop'); + }); + + skip('it is "responsive" by default', async function (assert) { + await render(hbs``); + assert + .dom('#test-app-side-nav') + .hasClass('hds-app-side-nav--is-responsive'); + }); + + skip('it is not "responsive" if `isResponsive` is false', async function (assert) { + await render( + hbs`` + ); + assert + .dom('#test-app-side-nav') + .doesNotHaveClass('hds-app-side-nav--is-responsive'); + }); + + // MOBILE + + skip('it is "mobile" on narrow viewports', async function (assert) { + await render(hbs` + + + `); + assert.dom('#test-app-side-nav').hasClass('hds-app-side-nav--is-mobile'); + }); + + skip('it is minimized/collapsed on narrow viewports by default', async function (assert) { + await render(hbs` + + + `); + assert.dom('#test-app-side-nav').hasClass('hds-app-side-nav--is-minimized'); + }); + + skip('it is not minimized/collapsed on narrow viewports if `isResponsive` is false', async function (assert) { + await render(hbs` + + + `); + assert + .dom('#test-app-side-nav') + .hasClass('hds-app-side-nav--is-not-minimized'); + }); + + skip('it shows a toggle button on narrow viewports by default', async function (assert) { + await render(hbs` + + + `); + assert.dom('.hds-app-side-nav__toggle-button').exists(); + }); + + skip('it does not show a toggle button on narrow viewports if `isResponsive` is false', async function (assert) { + await render(hbs` + + + `); + assert.dom('.hds-app-side-nav__toggle-button').doesNotExist(); + }); + + skip('it expands/collapses when the toggle button is pressed on narrow viewports', async function (assert) { + await render(hbs` + + + `); + assert.dom('#test-app-side-nav').hasClass('hds-app-side-nav--is-minimized'); + + await click('.hds-app-side-nav__toggle-button'); + assert + .dom('#test-app-side-nav') + .hasClass('hds-app-side-nav--is-not-minimized'); + await click('.hds-app-side-nav__toggle-button'); + assert.dom('#test-app-side-nav').hasClass('hds-app-side-nav--is-minimized'); + }); + + skip('it collapses when the ESC key is pressed on narrow viewports', async function (assert) { + await render(hbs` + + + + + + `); + assert.dom('#test-app-side-nav').hasClass('hds-app-side-nav--is-minimized'); + await click('.hds-app-side-nav__toggle-button'); + assert + .dom('#test-app-side-nav') + .hasClass('hds-app-side-nav--is-not-minimized'); + + await triggerKeyEvent('#test-app-side-nav', 'keydown', 'Escape'); + assert.dom('#test-app-side-nav').hasClass('hds-app-side-nav--is-minimized'); + assert.dom('.hds-app-side-nav-hide-when-minimized').hasAttribute('inert'); + }); + + // COLLAPSIBLE + + skip('it responds to different events to toggle between "non-minimized" (by default) and "mimimized" states', async function (assert) { + await render( + hbs`` + ); + assert + .dom('#test-app-side-nav') + .hasClass('hds-app-side-nav--is-not-minimized'); + + await click('.hds-app-side-nav__toggle-button'); + assert.dom('#test-app-side-nav').hasClass('hds-app-side-nav--is-minimized'); + + await click('.hds-app-side-nav__toggle-button'); + assert + .dom('#test-app-side-nav') + .hasClass('hds-app-side-nav--is-not-minimized'); + }); + + skip('the "non-minimized" and "minimized" states have impact on its internal properties', async function (assert) { + await render(hbs` + + + + + `); + assert + .dom('#test-app-side-nav') + .hasClass('hds-app-side-nav--is-not-minimized'); + assert + .dom('.hds-app-side-nav__toggle-button') + .hasAttribute('aria-expanded', 'true'); + assert + .dom('.hds-app-side-nav__toggle-button .hds-icon') + .hasClass('hds-icon-chevrons-left'); + assert + .dom('.hds-app-side-nav-hide-when-minimized') + .doesNotHaveAttribute('inert'); + assert.dom('#test-app-side-nav-body').doesNotHaveAttribute('inert'); + + await click('.hds-app-side-nav__toggle-button'); + + assert.dom('#test-app-side-nav').hasClass('hds-app-side-nav--is-minimized'); + assert + .dom('.hds-app-side-nav__toggle-button') + .hasAttribute('aria-expanded', 'false'); + assert + .dom('.hds-app-side-nav__toggle-button .hds-icon') + .hasClass('hds-icon-chevrons-right'); + assert.dom('.hds-app-side-nav-hide-when-minimized').hasAttribute('inert'); + assert.dom('#test-app-side-nav-body').doesNotHaveAttribute('inert'); + }); + + skip('when the viewport changes from desktop to mobile, it automatically collapses and becomes inert', async function (assert) { + this.mockMedia(); + + let calls = []; + this.setProperties({ + onDesktopViewportChange: (...args) => calls.push(args), + }); + + await render(hbs` + + + + + `); + + assert.strictEqual(calls.length, 1, 'called with initial viewport'); + + await this.changeBrowserSize(false); + assert.deepEqual( + calls[1], + [false], + 'resizing to mobile triggers a false event' + ); + + assert.dom('.hds-app-side-nav-hide-when-minimized').hasAttribute('inert'); + }); + + skip('when collapsed and the viewport changes from mobile to desktop, it automatically expands and is no longer inert', async function (assert) { + this.mockMedia(); + + let calls = []; + this.setProperties({ + onDesktopViewportChange: (...args) => calls.push(args), + }); + + await render(hbs` + + + + + `); + + await click('.hds-app-side-nav__toggle-button'); + assert.dom('.hds-app-side-nav-hide-when-minimized').hasAttribute('inert'); + + await this.changeBrowserSize(false); + assert.deepEqual( + calls[1], + [false], + 'resizing to mobile triggers a false event' + ); + assert.dom('.hds-app-side-nav-hide-when-minimized').hasAttribute('inert'); + + await this.changeBrowserSize(true); + assert.deepEqual( + calls[2], + [true], + 'resizing to desktop triggers a true event' + ); + assert + .dom('.hds-app-side-nav-hide-when-minimized') + .doesNotHaveAttribute('inert'); + }); + + // CALLBACKS + + skip('it should call `onToggleMinimizedStatus` function if provided', async function (assert) { + let toggled = false; + this.set('onToggleMinimizedStatus', () => (toggled = true)); + await render( + hbs`` + ); + await click('.hds-app-side-nav__toggle-button'); + assert.ok(toggled); + }); +}); diff --git a/showcase/tests/integration/components/hds/app-side-nav/list/back-link-test.js b/showcase/tests/integration/components/hds/app-side-nav/list/back-link-test.js new file mode 100644 index 0000000000..41ac1b04e3 --- /dev/null +++ b/showcase/tests/integration/components/hds/app-side-nav/list/back-link-test.js @@ -0,0 +1,68 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import { module, skip } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module( + 'Integration | Component | hds/app-side-nav/list/back-link', + function (hooks) { + setupRenderingTest(hooks); + + // Basic + + skip('it should render the component with a CSS class that matches the component name', async function (assert) { + await render( + hbs`` + ); + assert + .dom('#test-app-side-nav-list-item-link-back-link') + .hasClass('hds-app-side-nav__list-item-link--back-link'); + }); + + // Test Content / Args + + skip('it renders the passed in args', async function (assert) { + await render( + hbs`` + ); + assert.dom('.hds-icon-chevron-left').exists(); + assert + .dom('.hds-app-side-nav__list-item-text') + .hasText('Back to parent page'); + }); + + // GENERATED ELEMENTS + + skip('it should render a