From 560a7530b98d5546d9f716e12e4d82efdcf1775d Mon Sep 17 00:00:00 2001 From: Anna Wen <54281166+annawen1@users.noreply.github.com> Date: Wed, 14 Jun 2023 11:36:31 -0400 Subject: [PATCH] feat(ui-shell): sync with @carbon/react version (#10526) * feat(ui-shell): ui-shell carbon v11 sync * feat(ui-shell): header side nav items and new props * feat(ui-shell): side-nav overlay * feat(ui-shell): active header menu * feat(ui-shell): add box-sizing to switcher link * feat(ui-shell): import grid styles * Update packages/carbon-web-components/src/components/ui-shell/header-side-nav-items.ts Co-authored-by: Ignacio Becerra * feat(ui-shell): address some of the feedback * feat(ui-shell): header styles * feat(ui-shell): set correct tabbing order for sidenav * feat(ui-shell): fix rail focus and overlay * feat(ui-shell): story book content * feat(ui-shell): remove unnecessary test * feat(ui-shell): add elements to index * feat(ui-shell): adjust focus order for sidenav,actions,nav --------- Co-authored-by: Ignacio Becerra --- .../.storybook/_container.scss | 17 +- .../ui-shell/header-global-action.ts | 55 + .../components/ui-shell/header-menu-button.ts | 17 +- .../components/ui-shell/header-menu-item.ts | 6 +- .../src/components/ui-shell/header-menu.ts | 38 +- .../src/components/ui-shell/header-name.ts | 4 +- .../components/ui-shell/header-nav-item.ts | 27 +- .../src/components/ui-shell/header-nav.ts | 4 +- .../src/components/ui-shell/header-panel.ts | 40 + .../ui-shell/header-side-nav-items.ts | 43 + .../src/components/ui-shell/header.scss | 69 ++ .../src/components/ui-shell/header.ts | 4 +- .../src/components/ui-shell/index.ts | 5 + .../components/ui-shell/side-nav-divider.ts | 4 +- .../src/components/ui-shell/side-nav-items.ts | 6 +- .../src/components/ui-shell/side-nav-link.ts | 10 +- .../components/ui-shell/side-nav-menu-item.ts | 10 +- .../src/components/ui-shell/side-nav-menu.ts | 18 +- .../src/components/ui-shell/side-nav.scss | 76 +- .../src/components/ui-shell/side-nav.ts | 177 ++- .../components/ui-shell/switcher-divider.ts | 32 + .../src/components/ui-shell/switcher-item.ts | 89 ++ .../src/components/ui-shell/switcher.ts | 53 + .../components/ui-shell/ui-shell-story.scss | 18 - .../src/components/ui-shell/ui-shell-story.ts | 1050 +++++++++++++---- .../carbon-web-components/src/index.ts | 5 + .../src/typings/jsx-elements.d.ts | 5 + .../tests/spec/ui-shell_spec.ts | 20 +- 28 files changed, 1505 insertions(+), 397 deletions(-) create mode 100644 web-components/packages/carbon-web-components/src/components/ui-shell/header-global-action.ts create mode 100644 web-components/packages/carbon-web-components/src/components/ui-shell/header-panel.ts create mode 100644 web-components/packages/carbon-web-components/src/components/ui-shell/header-side-nav-items.ts create mode 100644 web-components/packages/carbon-web-components/src/components/ui-shell/switcher-divider.ts create mode 100644 web-components/packages/carbon-web-components/src/components/ui-shell/switcher-item.ts create mode 100644 web-components/packages/carbon-web-components/src/components/ui-shell/switcher.ts diff --git a/web-components/packages/carbon-web-components/.storybook/_container.scss b/web-components/packages/carbon-web-components/.storybook/_container.scss index 819ad6afcc13..a61307acdb96 100644 --- a/web-components/packages/carbon-web-components/.storybook/_container.scss +++ b/web-components/packages/carbon-web-components/.storybook/_container.scss @@ -7,7 +7,10 @@ // LICENSE file in the root directory of this source tree. // -@use '@carbon/styles' as *; +@use '@carbon/styles' as * with ( + $use-flexbox-grid: true +); + @include reset(); *, @@ -53,19 +56,7 @@ body { } .#{$prefix}--content.#{$prefix}-ce-demo-devenv--ui-shell-content { - background-color: $layer-01; margin: 0; height: 100vh; width: 100%; - - h2 { - font-weight: 800; - /* stylelint-disable declaration-property-unit-whitelist */ - margin: 30px 0; - font-size: 20px; - } - - p { - line-height: 20px; - } } diff --git a/web-components/packages/carbon-web-components/src/components/ui-shell/header-global-action.ts b/web-components/packages/carbon-web-components/src/components/ui-shell/header-global-action.ts new file mode 100644 index 000000000000..0d4b94398712 --- /dev/null +++ b/web-components/packages/carbon-web-components/src/components/ui-shell/header-global-action.ts @@ -0,0 +1,55 @@ +/** + * @license + * + * Copyright IBM Corp. 2023 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { LitElement } from 'lit'; +import { property, customElement } from 'lit/decorators.js'; +import { BUTTON_TOOLTIP_POSITION } from '../button/button'; +import CDSButton from '../button/button'; +import styles from './header.scss'; +import { prefix } from '../../globals/settings'; + +/** + * Header global action button + * + * @element cds-header-global-action + */ +@customElement(`${prefix}-header-global-action`) +class CDSHeaderGlobalAction extends CDSButton { + /** + * Specify whether the action is currently active + */ + @property({ type: Boolean, reflect: true }) + active; + + connectedCallback() { + this.tooltipPosition = BUTTON_TOOLTIP_POSITION.BOTTOM; + super.connectedCallback(); + } + + firstUpdated() { + const button = this.shadowRoot?.querySelector('button'); + + if (button) { + button?.classList.add(`${prefix}--header__action`); + + if (this.active) { + button?.classList.add(`${prefix}--header__action--active`); + } + } + } + + static shadowRootOptions = { + ...LitElement.shadowRootOptions, + delegatesFocus: true, + }; + + static styles = styles; // `styles` here is a `CSSResult` generated by custom WebPack loader +} + +export default CDSHeaderGlobalAction; diff --git a/web-components/packages/carbon-web-components/src/components/ui-shell/header-menu-button.ts b/web-components/packages/carbon-web-components/src/components/ui-shell/header-menu-button.ts index a62c32b53b76..78e1962fb173 100644 --- a/web-components/packages/carbon-web-components/src/components/ui-shell/header-menu-button.ts +++ b/web-components/packages/carbon-web-components/src/components/ui-shell/header-menu-button.ts @@ -15,7 +15,7 @@ import Menu20 from '@carbon/icons/lib/menu/20'; import { ifDefined } from 'lit/directives/if-defined.js'; import { prefix } from '../../globals/settings'; import FocusMixin from '../../globals/mixins/focus'; -import { SIDE_NAV_COLLAPSE_MODE, SIDE_NAV_USAGE_MODE } from './side-nav'; +import { SIDE_NAV_COLLAPSE_MODE } from './side-nav'; import styles from './header.scss'; /** @@ -27,13 +27,13 @@ import styles from './header.scss'; * @fires cds-header-menu-button-toggled - The custom event fired after this header menu button is toggled upon a user gesture. */ @customElement(`${prefix}-header-menu-button`) -class BXHeaderMenuButton extends FocusMixin(LitElement) { +class CDSHeaderMenuButton extends FocusMixin(LitElement) { private _handleClick() { const active = !this.active; this.active = active; this.dispatchEvent( new CustomEvent( - (this.constructor as typeof BXHeaderMenuButton).eventToggle, + (this.constructor as typeof CDSHeaderMenuButton).eventToggle, { bubbles: true, cancelable: true, @@ -77,10 +77,13 @@ class BXHeaderMenuButton extends FocusMixin(LitElement) { disabled = false; /** - * Usage mode of the side nav. + * If `true` will style the side nav to sit below the header */ - @property({ reflect: true, attribute: 'usage-mode' }) - usageMode = SIDE_NAV_USAGE_MODE.REGULAR; + @property({ + type: Boolean, + attribute: 'is-not-child-of-header', + }) + isNotChildOfHeader = false; render() { const { @@ -123,4 +126,4 @@ class BXHeaderMenuButton extends FocusMixin(LitElement) { static styles = styles; // `styles` here is a `CSSResult` generated by custom WebPack loader } -export default BXHeaderMenuButton; +export default CDSHeaderMenuButton; diff --git a/web-components/packages/carbon-web-components/src/components/ui-shell/header-menu-item.ts b/web-components/packages/carbon-web-components/src/components/ui-shell/header-menu-item.ts index 1ecac7bc66e8..4e1886207808 100644 --- a/web-components/packages/carbon-web-components/src/components/ui-shell/header-menu-item.ts +++ b/web-components/packages/carbon-web-components/src/components/ui-shell/header-menu-item.ts @@ -9,7 +9,7 @@ import { customElement } from 'lit/decorators.js'; import { prefix } from '../../globals/settings'; -import BXHeaderNavItem from './header-nav-item'; +import CDSHeaderNavItem from './header-nav-item'; /** * Header submenu item. @@ -17,6 +17,6 @@ import BXHeaderNavItem from './header-nav-item'; * @element cds-header-menu-item */ @customElement(`${prefix}-header-menu-item`) -class BXHeaderMenuItem extends BXHeaderNavItem {} +class CDSHeaderMenuItem extends CDSHeaderNavItem {} -export default BXHeaderMenuItem; +export default CDSHeaderMenuItem; diff --git a/web-components/packages/carbon-web-components/src/components/ui-shell/header-menu.ts b/web-components/packages/carbon-web-components/src/components/ui-shell/header-menu.ts index 3924bc3d0dc6..3e99b8758649 100644 --- a/web-components/packages/carbon-web-components/src/components/ui-shell/header-menu.ts +++ b/web-components/packages/carbon-web-components/src/components/ui-shell/header-menu.ts @@ -10,12 +10,14 @@ import { ifDefined } from 'lit/directives/if-defined.js'; import { LitElement, html } from 'lit'; import { property, customElement, query } from 'lit/decorators.js'; +import { classMap } from 'lit/directives/class-map.js'; import ChevronDownGlyph from '@carbon/icons/lib/chevron--down/16'; import { prefix } from '../../globals/settings'; import FocusMixin from '../../globals/mixins/focus'; import HostListenerMixin from '../../globals/mixins/host-listener'; import HostListener from '../../globals/decorators/host-listener'; import { forEach } from '../../globals/internal/collection-helpers'; +import CDSHeaderMenuItem from './header-menu-item'; import styles from './header.scss'; /** @@ -27,13 +29,18 @@ import styles from './header.scss'; * @csspart menu-body The menu body. */ @customElement(`${prefix}-header-menu`) -class BXHeaderMenu extends HostListenerMixin(FocusMixin(LitElement)) { +class CDSHeaderMenu extends HostListenerMixin(FocusMixin(LitElement)) { /** * The trigger button. */ @query('a') private _trigger!: HTMLElement; + /** + * keeps track if header menu has any active submenus + */ + private _hasActiveChildren = false; + /** * Handles `click` event handler on this element. */ @@ -79,6 +86,12 @@ class BXHeaderMenu extends HostListenerMixin(FocusMixin(LitElement)) { @property({ type: Boolean, reflect: true }) expanded = false; + /** + * Applies selected styles to the item if a user sets this to true and `aria-current !== 'page'`. + */ + @property({ type: Boolean, attribute: 'is-active', reflect: true }) + isActive = false; + /** * The content of the trigger button. */ @@ -95,12 +108,19 @@ class BXHeaderMenu extends HostListenerMixin(FocusMixin(LitElement)) { if (!this.hasAttribute('role')) { this.setAttribute('role', 'listitem'); } + const { selectorItem } = this.constructor as typeof CDSHeaderMenu; + forEach(this.querySelectorAll(selectorItem), (elem) => { + if ((elem as CDSHeaderMenuItem).isActive === true) { + this._hasActiveChildren = true; + } + }); + super.connectedCallback(); } updated(changedProperties) { if (changedProperties.has('expanded')) { - const { selectorItem } = this.constructor as typeof BXHeaderMenu; + const { selectorItem } = this.constructor as typeof CDSHeaderMenu; const { expanded } = this; forEach(this.querySelectorAll(selectorItem), (elem) => { (elem as HTMLElement).tabIndex = expanded ? 0 : -1; @@ -111,16 +131,26 @@ class BXHeaderMenu extends HostListenerMixin(FocusMixin(LitElement)) { render() { const { expanded, + isActive, triggerContent, menuLabel, + _hasActiveChildren, _handleClick: handleClick, _handleKeydownTrigger: handleKeydownTrigger, } = this; + + const linkClasses = classMap({ + [`${prefix}--header__menu-item`]: true, + [`${prefix}--header__menu-title`]: true, + [`${prefix}--header__menu-item--current`]: + isActive || (_hasActiveChildren && !expanded), + }); + return html` , this element must have role of listitem */ @@ -42,11 +55,17 @@ class BXHeaderNavItem extends FocusMixin(LitElement) { role: string = 'listitem'; render() { - const { href, title } = this; + const { ariaCurrent, href, isActive, title } = this; + const linkClass = classMap({ + [`${prefix}--header__menu-item`]: true, + [`${prefix}--header__menu-item--current`]: + isActive && ariaCurrent !== 'page', + }); + return html` `; + } + + static shadowRootOptions = { + ...LitElement.shadowRootOptions, + delegatesFocus: true, + }; + + static styles = styles; // `styles` here is a `CSSResult` generated by custom WebPack loader +} + +export default CDSHeaderPanel; diff --git a/web-components/packages/carbon-web-components/src/components/ui-shell/header-side-nav-items.ts b/web-components/packages/carbon-web-components/src/components/ui-shell/header-side-nav-items.ts new file mode 100644 index 000000000000..cc57a28581a0 --- /dev/null +++ b/web-components/packages/carbon-web-components/src/components/ui-shell/header-side-nav-items.ts @@ -0,0 +1,43 @@ +/** + * @license + * + * Copyright IBM Corp. 2023 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { LitElement, html } from 'lit'; +import { property, customElement } from 'lit/decorators.js'; +import { prefix } from '../../globals/settings'; +import styles from './side-nav.scss'; + +/** + * Header Side Nav Items section + * + * @element cds-header-side-nav-items + */ +@customElement(`${prefix}-header-side-nav-items`) +class CDSHeaderSideNavItems extends LitElement { + /** + * Optionally specify if container will have a bottom divider to differentiate + * between original sidenav items and header menu items. False by default. + */ + @property({ type: Boolean, attribute: 'has-divider' }) + hasDivider = false; + + connectedCallback() { + if (!this.hasAttribute('role')) { + this.setAttribute('role', 'list'); + } + super.connectedCallback(); + } + + render() { + return html``; + } + + static styles = styles; // `styles` here is a `CSSResult` generated by custom WebPack loader +} + +export default CDSHeaderSideNavItems; diff --git a/web-components/packages/carbon-web-components/src/components/ui-shell/header.scss b/web-components/packages/carbon-web-components/src/components/ui-shell/header.scss index 56207f4a9a23..4289c87add66 100644 --- a/web-components/packages/carbon-web-components/src/components/ui-shell/header.scss +++ b/web-components/packages/carbon-web-components/src/components/ui-shell/header.scss @@ -11,7 +11,12 @@ $css--plex: true !default; @use '@carbon/styles/scss/breakpoint' as *; @use '@carbon/styles/scss/spacing' as *; @use '@carbon/styles/scss/theme' as *; +@use '@carbon/styles/scss/components/button'; +@use '@carbon/styles/scss/components/popover/index'; +@use '@carbon/styles/scss/components/tooltip'; @use '@carbon/styles/scss/components/ui-shell/header'; +@use '@carbon/styles/scss/components/ui-shell/header-panel'; +@use '@carbon/styles/scss/components/ui-shell/switcher'; :host(#{$prefix}-header) { @extend .#{$prefix}--header; @@ -46,6 +51,7 @@ $css--plex: true !default; a.#{$prefix}--header__menu-item { height: $spacing-09; + background-color: $layer; &:hover { background-color: $background-hover; @@ -56,6 +62,41 @@ $css--plex: true !default; background-color: $background-active; } } + + a.#{$prefix}--header__menu-item--current { + background-color: $layer-selected; + + &:hover { + background-color: $layer-selected-hover; + } + + &::after { + position: absolute; + top: -2px; + left: -2px; + bottom: -2px; + width: 3px; + height: 100%; + background-color: $border-interactive; + content: ''; + } + } +} + +:host(#{$prefix}-header-global-action) { + ::slotted(svg) { + fill: $icon-secondary; + } + &:hover ::slotted(svg) { + fill: $icon-primary; + } +} + +:host(#{$prefix}-header-nav-item), +:host(#{$prefix}-header-menu) { + a.#{$prefix}--header__menu-item { + box-sizing: border-box; + } } :host(#{$prefix}-header-menu-button) { @@ -80,4 +121,32 @@ $css--plex: true !default; :host(#{$prefix}-header-name) { display: content; height: 100%; + + a { + box-sizing: border-box; + } +} + +:host(#{$prefix}-header-panel) { + @extend .#{$prefix}--header-panel; +} + +:host(#{$prefix}-header-panel) { + @extend .#{$prefix}--header-panel--expanded; +} + +:host(#{$prefix}-switcher) { + @extend .#{$prefix}--switcher; +} + +:host(#{$prefix}-switcher-item) { + @extend .#{$prefix}--switcher__item; + + a { + box-sizing: border-box; + } +} + +:host(#{$prefix}-switcher-divider) { + @extend .#{$prefix}--switcher__item--divider; } diff --git a/web-components/packages/carbon-web-components/src/components/ui-shell/header.ts b/web-components/packages/carbon-web-components/src/components/ui-shell/header.ts index 0feed39cdf8a..e66341d732dc 100644 --- a/web-components/packages/carbon-web-components/src/components/ui-shell/header.ts +++ b/web-components/packages/carbon-web-components/src/components/ui-shell/header.ts @@ -18,7 +18,7 @@ import styles from './header.scss'; * @element cds-header */ @customElement(`${prefix}-header`) -class BXHeader extends LitElement { +class CDSHeader extends LitElement { connectedCallback() { if (!this.hasAttribute('role')) { this.setAttribute('role', 'banner'); @@ -33,4 +33,4 @@ class BXHeader extends LitElement { static styles = styles; // `styles` here is a `CSSResult` generated by custom WebPack loader } -export default BXHeader; +export default CDSHeader; diff --git a/web-components/packages/carbon-web-components/src/components/ui-shell/index.ts b/web-components/packages/carbon-web-components/src/components/ui-shell/index.ts index 0d6365ea4509..b5168f4696e4 100644 --- a/web-components/packages/carbon-web-components/src/components/ui-shell/index.ts +++ b/web-components/packages/carbon-web-components/src/components/ui-shell/index.ts @@ -19,4 +19,9 @@ import './header-menu-item'; import './side-nav-items'; import './header-menu-button'; import './header-nav-item'; +import './header-global-action'; +import './header-panel'; +import './switcher'; +import './switcher-item'; +import './switcher-divider'; import './side-nav-menu'; diff --git a/web-components/packages/carbon-web-components/src/components/ui-shell/side-nav-divider.ts b/web-components/packages/carbon-web-components/src/components/ui-shell/side-nav-divider.ts index f07b34cc2b9f..c704aa084c6b 100644 --- a/web-components/packages/carbon-web-components/src/components/ui-shell/side-nav-divider.ts +++ b/web-components/packages/carbon-web-components/src/components/ui-shell/side-nav-divider.ts @@ -18,7 +18,7 @@ import styles from './side-nav.scss'; * @element cds-side-nav-divider */ @customElement(`${prefix}-side-nav-divider`) -class BXSideNavDivider extends LitElement { +class CDSSideNavDivider extends LitElement { connectedCallback() { if (!this.hasAttribute('role')) { this.setAttribute('role', 'separator'); @@ -29,4 +29,4 @@ class BXSideNavDivider extends LitElement { static styles = styles; // `styles` here is a `CSSResult` generated by custom WebPack loader } -export default BXSideNavDivider; +export default CDSSideNavDivider; diff --git a/web-components/packages/carbon-web-components/src/components/ui-shell/side-nav-items.ts b/web-components/packages/carbon-web-components/src/components/ui-shell/side-nav-items.ts index 6350cd2a3582..2715b32229a1 100644 --- a/web-components/packages/carbon-web-components/src/components/ui-shell/side-nav-items.ts +++ b/web-components/packages/carbon-web-components/src/components/ui-shell/side-nav-items.ts @@ -18,7 +18,7 @@ import styles from './side-nav.scss'; * @element cds-side-nav-items */ @customElement(`${prefix}-side-nav-items`) -class BXSideNavItems extends LitElement { +class CDSSideNavItems extends LitElement { connectedCallback() { if (!this.hasAttribute('role')) { this.setAttribute('role', 'list'); @@ -27,10 +27,10 @@ class BXSideNavItems extends LitElement { } render() { - return html` `; + return html``; } static styles = styles; // `styles` here is a `CSSResult` generated by custom WebPack loader } -export default BXSideNavItems; +export default CDSSideNavItems; diff --git a/web-components/packages/carbon-web-components/src/components/ui-shell/side-nav-link.ts b/web-components/packages/carbon-web-components/src/components/ui-shell/side-nav-link.ts index 5740800f39d4..4432fa3d7e2b 100644 --- a/web-components/packages/carbon-web-components/src/components/ui-shell/side-nav-link.ts +++ b/web-components/packages/carbon-web-components/src/components/ui-shell/side-nav-link.ts @@ -23,7 +23,7 @@ import styles from './side-nav.scss'; * @slot title-icon-container - The title icon container. */ @customElement(`${prefix}-side-nav-link`) -class BXSideNavLink extends FocusMixin(LitElement) { +class CDSSideNavLink extends FocusMixin(LitElement) { /** * The container for the title icon. */ @@ -52,6 +52,12 @@ class BXSideNavLink extends FocusMixin(LitElement) { @property() href = ''; + /** + * Specify if this is a large variation of the side nav link + */ + @property({ type: Boolean, reflect: true }) + large = false; + /** * The title. */ @@ -101,4 +107,4 @@ class BXSideNavLink extends FocusMixin(LitElement) { static styles = styles; // `styles` here is a `CSSResult` generated by custom WebPack loader } -export default BXSideNavLink; +export default CDSSideNavLink; diff --git a/web-components/packages/carbon-web-components/src/components/ui-shell/side-nav-menu-item.ts b/web-components/packages/carbon-web-components/src/components/ui-shell/side-nav-menu-item.ts index 453f276524fe..00570977f1bc 100644 --- a/web-components/packages/carbon-web-components/src/components/ui-shell/side-nav-menu-item.ts +++ b/web-components/packages/carbon-web-components/src/components/ui-shell/side-nav-menu-item.ts @@ -12,7 +12,7 @@ import { LitElement, html } from 'lit'; import { property, customElement } from 'lit/decorators.js'; import { prefix } from '../../globals/settings'; import FocusMixin from '../../globals/mixins/focus'; -import BXSideNavMenu from './side-nav-menu'; +import CDSSideNavMenu from './side-nav-menu'; import styles from './side-nav.scss'; /** @@ -23,7 +23,7 @@ import styles from './side-nav.scss'; * @csspart title The title. */ @customElement(`${prefix}-side-nav-menu-item`) -class BXSideNavMenuItem extends FocusMixin(LitElement) { +class CDSSideNavMenuItem extends FocusMixin(LitElement) { /** * `true` if the menu item should be active. */ @@ -44,10 +44,10 @@ class BXSideNavMenuItem extends FocusMixin(LitElement) { shouldUpdate(changedProperties) { if (changedProperties.has('active') && this.active) { - const { selectorMenu } = this.constructor as typeof BXSideNavMenuItem; + const { selectorMenu } = this.constructor as typeof CDSSideNavMenuItem; const parent = this.closest(selectorMenu); if (parent) { - (parent as BXSideNavMenu).active = true; + (parent as CDSSideNavMenu).active = true; } } return true; @@ -82,4 +82,4 @@ class BXSideNavMenuItem extends FocusMixin(LitElement) { static styles = styles; // `styles` here is a `CSSResult` generated by custom WebPack loader } -export default BXSideNavMenuItem; +export default CDSSideNavMenuItem; diff --git a/web-components/packages/carbon-web-components/src/components/ui-shell/side-nav-menu.ts b/web-components/packages/carbon-web-components/src/components/ui-shell/side-nav-menu.ts index 54c9d2e4b9b3..cdecda4fbf0c 100644 --- a/web-components/packages/carbon-web-components/src/components/ui-shell/side-nav-menu.ts +++ b/web-components/packages/carbon-web-components/src/components/ui-shell/side-nav-menu.ts @@ -28,7 +28,7 @@ import styles from './side-nav.scss'; * @csspart menu-body The menu body. */ @customElement(`${prefix}-side-nav-menu`) -class BXSideNavMenu extends FocusMixin(LitElement) { +class CDSSideNavMenu extends FocusMixin(LitElement) { /** * `true` if this menu has an icon. */ @@ -47,7 +47,7 @@ class BXSideNavMenu extends FocusMixin(LitElement) { */ private _handleUserInitiatedToggle(expanded = !this.expanded) { const { eventBeforeToggle, eventToggle } = this - .constructor as typeof BXSideNavMenu; + .constructor as typeof CDSSideNavMenu; const init = { bubbles: true, cancelable: true, @@ -77,7 +77,7 @@ class BXSideNavMenu extends FocusMixin(LitElement) { forEach(target.assignedNodes(), (item) => { if (item.nodeType === Node.ELEMENT_NODE) { item.toggleAttribute( - (this.constructor as typeof BXSideNavMenu).attribItemHasIcon, + (this.constructor as typeof CDSSideNavMenu).attribItemHasIcon, hasIcon ); } @@ -88,7 +88,7 @@ class BXSideNavMenu extends FocusMixin(LitElement) { * Handles `slotchange` event on the `` for the title icon. */ private _handleSlotChangeTitleIcon({ target }) { - const constructor = this.constructor as typeof BXSideNavMenu; + const constructor = this.constructor as typeof CDSSideNavMenu; const hasIcon = target.assignedNodes().length > 0; this._hasIcon = hasIcon; this._titleIconContainerNode?.toggleAttribute('hidden', !hasIcon); @@ -109,6 +109,12 @@ class BXSideNavMenu extends FocusMixin(LitElement) { @property({ type: Boolean, reflect: true }) expanded = false; + /** + * Specify if this is a large variation of the side nav menu + */ + @property({ type: Boolean, reflect: true }) + large = false; + /** * `true` if the menu should be forced collapsed upon side nav's expanded state. */ @@ -130,7 +136,7 @@ class BXSideNavMenu extends FocusMixin(LitElement) { updated(changedProperties) { if (changedProperties.has('expanded')) { - const { selectorItem } = this.constructor as typeof BXSideNavMenu; + const { selectorItem } = this.constructor as typeof CDSSideNavMenu; const { expanded } = this; forEach(this.querySelectorAll(selectorItem), (elem) => { (elem as HTMLElement).tabIndex = expanded ? 0 : -1; @@ -213,4 +219,4 @@ class BXSideNavMenu extends FocusMixin(LitElement) { static styles = styles; // `styles` here is a `CSSResult` generated by custom WebPack loader } -export default BXSideNavMenu; +export default CDSSideNavMenu; diff --git a/web-components/packages/carbon-web-components/src/components/ui-shell/side-nav.scss b/web-components/packages/carbon-web-components/src/components/ui-shell/side-nav.scss index 95e313370aaf..a1fa6b141c31 100644 --- a/web-components/packages/carbon-web-components/src/components/ui-shell/side-nav.scss +++ b/web-components/packages/carbon-web-components/src/components/ui-shell/side-nav.scss @@ -13,45 +13,8 @@ $css--plex: true !default; @use '@carbon/styles/scss/theme' as *; @use '@carbon/styles/scss/components/ui-shell/side-nav'; -:host(#{$prefix}-side-nav) { - @extend .#{$prefix}--side-nav; - @extend .#{$prefix}--side-nav--ux; - @extend .#{$prefix}--side-nav__navigation; - - top: 0; -} - -:host(#{$prefix}-side-nav[collapse-mode='fixed']) { - // TODO: Consider making `@extend .#{$prefix}--side-nav--fixed` work, possibly caused by `:not()` styles - width: 16rem; -} - -:host(#{$prefix}-side-nav[collapse-mode='rail']) { - // TODO: audit - width: $spacing-09; - - &:hover { - // TODO: audit - width: 16rem; - } -} - -:host(#{$prefix}-side-nav[expanded]), -:host(#{$prefix}-side-nav[collapse-mode][expanded]) { - @extend .#{$prefix}--side-nav--expanded; -} - -:host(#{$prefix}-side-nav[usage-mode='header-nav']), -:host(#{$prefix}-side-nav[collapse-mode][usage-mode='header-nav']) { - width: 0; -} - -:host(#{$prefix}-side-nav[expanded][usage-mode='header-nav']), -:host(#{$prefix}-side-nav[collapse-mode][expanded][usage-mode='header-nav']) { - @include breakpoint-down('lg') { - // TODO: audit - width: 16rem; - } +:host(#{$prefix}-side-nav[expanded]) ::slotted(#{$prefix}-side-nav-items) { + overflow-y: auto; } :host(#{$prefix}-side-nav-items) { @@ -74,6 +37,10 @@ $css--plex: true !default; display: none; } } + + &[large] { + @extend .#{$prefix}--side-nav__item--large; + } } :host(#{$prefix}-side-nav-divider) { @@ -86,20 +53,21 @@ $css--plex: true !default; @extend .#{$prefix}--side-nav__item; display: block; - outline: none; - width: auto; - height: auto; .#{$prefix}--side-nav__icon[hidden] { display: none; } + + .#{$prefix}--side-nav__menu { + margin: 0; + padding: 0; + } } :host(#{$prefix}-side-nav-menu[active]) { @extend .#{$prefix}--side-nav__item--active; // TODO: audit - background-color: $background-selected; color: $text-primary; position: relative; @@ -128,6 +96,10 @@ $css--plex: true !default; @extend .#{$prefix}--side-nav__item--icon; } +:host(#{$prefix}-side-nav-menu[large]) { + @extend .#{$prefix}--side-nav__item--large; +} + :host(#{$prefix}-side-nav-menu-item) { @extend .#{$prefix}--side-nav__menu-item; @@ -137,18 +109,16 @@ $css--plex: true !default; height: auto; a.#{$prefix}--side-nav__link { - // TODO: audit - height: $spacing-05; - min-height: $spacing-05; - padding-left: $spacing-05; + height: $spacing-07; + min-height: $spacing-07; + padding-left: $spacing-07; font-weight: 400; } } :host(#{$prefix}-side-nav-menu-item[parent-has-icon]) a.#{$prefix}--side-nav__link { - // TODO: audit - padding-left: rem(72px); + padding-left: 4.5rem; } :host(#{$prefix}-side-nav-item) .#{$prefix}--side-nav__link:hover, @@ -158,3 +128,11 @@ $css--plex: true !default; background-color: $background-hover; color: $text-primary; } + +:host(#{$prefix}-header-side-nav-items) { + @extend .#{$prefix}--side-nav__header-navigation; + + &[has-divider] { + @extend .#{$prefix}--side-nav__header-divider; + } +} diff --git a/web-components/packages/carbon-web-components/src/components/ui-shell/side-nav.ts b/web-components/packages/carbon-web-components/src/components/ui-shell/side-nav.ts index b7fa81b53104..9b25dfdfecc1 100644 --- a/web-components/packages/carbon-web-components/src/components/ui-shell/side-nav.ts +++ b/web-components/packages/carbon-web-components/src/components/ui-shell/side-nav.ts @@ -9,6 +9,7 @@ import { LitElement, html } from 'lit'; import { property, customElement } from 'lit/decorators.js'; +import { classMap } from 'lit/directives/class-map.js'; import on from '../../globals/mixins/on'; import { prefix } from '../../globals/settings'; import HostListenerMixin from '../../globals/mixins/host-listener'; @@ -16,8 +17,8 @@ import HostListener from '../../globals/decorators/host-listener'; import { forEach } from '../../globals/internal/collection-helpers'; import Handle from '../../globals/internal/handle'; import { SIDE_NAV_COLLAPSE_MODE, SIDE_NAV_USAGE_MODE } from './defs'; -import BXHeaderMenuButton from './header-menu-button'; -import BXSideNavMenu from './side-nav-menu'; +import CDSHeaderMenuButton from './header-menu-button'; +import CDSSideNavMenu from './side-nav-menu'; import styles from './side-nav.scss'; export { SIDE_NAV_COLLAPSE_MODE, SIDE_NAV_USAGE_MODE }; @@ -28,7 +29,7 @@ export { SIDE_NAV_COLLAPSE_MODE, SIDE_NAV_USAGE_MODE }; * @element cds-side-nav */ @customElement(`${prefix}-side-nav`) -class BXSideNav extends HostListenerMixin(LitElement) { +class CDSSideNav extends HostListenerMixin(LitElement) { /** * `true` if this side nav is hovered. */ @@ -73,7 +74,7 @@ class BXSideNav extends HostListenerMixin(LitElement) { if (this.expanded) { ( this.querySelector( - (this.constructor as typeof BXSideNav).selectorNavItems + (this.constructor as typeof CDSSideNav).selectorNavItems ) as HTMLElement )?.focus(); } @@ -87,34 +88,14 @@ class BXSideNav extends HostListenerMixin(LitElement) { const { expanded, _hovered: hovered } = this; forEach( this.querySelectorAll( - (this.constructor as typeof BXSideNav).selectorMenu + (this.constructor as typeof CDSSideNav).selectorMenu ), (item) => { - (item as BXSideNavMenu).forceCollapsed = !expanded && !hovered; + (item as CDSSideNavMenu).forceCollapsed = !expanded && !hovered; } ); } - /** - * Handles `mouseover` event handler. - */ - @HostListener('mouseover') - // @ts-ignore: The decorator refers to this method but TS thinks this method is not referred to - private _handleMouseover() { - this._hovered = true; - this._updatedSideNavMenuForceCollapsedState(); - } - - /** - * Handles `mouseout` event handler. - */ - @HostListener('mouseout') - // @ts-ignore: The decorator refers to this method but TS thinks this method is not referred to - private _handleMouseout() { - this._hovered = false; - this._updatedSideNavMenuForceCollapsedState(); - } - /** * Collapse mode of the side nav. */ @@ -128,10 +109,19 @@ class BXSideNav extends HostListenerMixin(LitElement) { expanded = false; /** - * Usage mode of the side nav. + * If `true` will style the side nav to sit below the header + */ + @property({ + type: Boolean, + attribute: 'is-not-child-of-header', + }) + isNotChildOfHeader = false; + + /** + * Specify if the side-nav will be persistent above the lg breakpoint */ - @property({ reflect: true, attribute: 'usage-mode' }) - usageMode = SIDE_NAV_USAGE_MODE.REGULAR; + @property({ type: Boolean, reflect: true, attribute: 'is-not-persistent' }) + isNotPersistent = false; connectedCallback() { if (!this.hasAttribute('role')) { @@ -159,57 +149,131 @@ class BXSideNav extends HostListenerMixin(LitElement) { } updated(changedProperties) { - if ( - changedProperties.has('collapseMode') || - changedProperties.has('usageMode') - ) { - const { collapseMode, usageMode } = this; - if ( - (collapseMode === SIDE_NAV_COLLAPSE_MODE.FIXED || - collapseMode === SIDE_NAV_COLLAPSE_MODE.RAIL) && - usageMode === SIDE_NAV_USAGE_MODE.HEADER_NAV - ) { - console.warn( - 'Fixed/rail modes of side nav cannot be used with header nav mode.' - ); // eslint-disable-line no-console - } - } const doc = this.getRootNode() as Document; if (changedProperties.has('collapseMode')) { forEach( doc.querySelectorAll( - (this.constructor as typeof BXSideNav).selectorButtonToggle + (this.constructor as typeof CDSSideNav).selectorButtonToggle ), (item) => { - (item as BXHeaderMenuButton).collapseMode = this.collapseMode; + (item as CDSHeaderMenuButton).collapseMode = this.collapseMode; } ); } if (changedProperties.has('expanded')) { + const headerItems = doc.querySelectorAll( + (this.constructor as typeof CDSSideNav).selectorHeaderItems + ); this._updatedSideNavMenuForceCollapsedState(); forEach( doc.querySelectorAll( - (this.constructor as typeof BXSideNav).selectorButtonToggle + (this.constructor as typeof CDSSideNav).selectorButtonToggle ), (item) => { - (item as BXHeaderMenuButton).active = this.expanded; + (item as CDSHeaderMenuButton).active = this.expanded; } ); + if (this.expanded) { + forEach(headerItems, (item) => { + item.setAttribute('tabindex', '-1'); + }); + } else { + forEach(headerItems, (item) => { + item.removeAttribute('tabindex'); + }); + } } - if (changedProperties.has('usageMode')) { + if (changedProperties.has('isNotChildOfHeader')) { forEach( doc.querySelectorAll( - (this.constructor as typeof BXSideNav).selectorButtonToggle + (this.constructor as typeof CDSSideNav).selectorButtonToggle ), (item) => { - (item as BXHeaderMenuButton).usageMode = this.usageMode; + (item as CDSHeaderMenuButton).isNotChildOfHeader = + this.isNotChildOfHeader; } ); } } + /** + * Handles `blur` event handler on this element. + * + * @param event The event. + */ + @HostListener('focusout') + // @ts-ignore: The decorator refers to this method but TS thinks this method is not referred to + private _handleFocusOut({ relatedTarget }: FocusEvent) { + if (!this.contains(relatedTarget as Node)) { + this.expanded = false; + } + } + + /** + * Handles `focus` event handler on this element. + * + * @param event The event. + */ + @HostListener('focusin') + // @ts-ignore: The decorator refers to this method but TS thinks this method is not referred to + private _handleFocusIn() { + this.expanded = true; + } + + /** + * Handles the `mouseover` event for the side nav in rail mode. + * + */ + private _handleNavMouseOver() { + const { collapseMode } = this; + if (collapseMode === SIDE_NAV_COLLAPSE_MODE.RAIL) { + this.expanded = true; + this._hovered = true; + this._updatedSideNavMenuForceCollapsedState(); + } + } + + /** + * Handles the `mouseout` event for the side nav in rail mode. + * + */ + private _handleNavMouseOut() { + const { collapseMode } = this; + if (collapseMode === SIDE_NAV_COLLAPSE_MODE.RAIL) { + this.expanded = false; + this._hovered = false; + this._updatedSideNavMenuForceCollapsedState(); + } + } + render() { - return html` `; + const { collapseMode, expanded, isNotChildOfHeader, isNotPersistent } = + this; + const classes = classMap({ + [`${prefix}--side-nav__navigation`]: true, + [`${prefix}--side-nav`]: true, + [`${prefix}--side-nav--expanded`]: expanded, + [`${prefix}--side-nav--collapsed`]: + !expanded && collapseMode === SIDE_NAV_COLLAPSE_MODE.FIXED, + [`${prefix}--side-nav--rail`]: + collapseMode === SIDE_NAV_COLLAPSE_MODE.RAIL, + [`${prefix}--side-nav--ux`]: !isNotChildOfHeader, + [`${prefix}--side-nav--hidden`]: isNotPersistent, + }); + + const overlayClasses = classMap({ + [`${prefix}--side-nav__overlay`]: true, + [`${prefix}--side-nav__overlay-active`]: expanded, + }); + return html`${this.collapseMode === SIDE_NAV_COLLAPSE_MODE.FIXED + ? null + : html`
`} +
+ +
`; } /** @@ -219,6 +283,13 @@ class BXSideNav extends HostListenerMixin(LitElement) { return `${prefix}-header-menu-button`; } + /** + * A selector that will return the header name + global action elements. + */ + static get selectorHeaderItems() { + return `${prefix}-header-name, ${prefix}-header-global-action`; + } + /** * A selector that will return side nav focusable items. */ @@ -243,4 +314,4 @@ class BXSideNav extends HostListenerMixin(LitElement) { static styles = styles; // `styles` here is a `CSSResult` generated by custom WebPack loader } -export default BXSideNav; +export default CDSSideNav; diff --git a/web-components/packages/carbon-web-components/src/components/ui-shell/switcher-divider.ts b/web-components/packages/carbon-web-components/src/components/ui-shell/switcher-divider.ts new file mode 100644 index 000000000000..d04e6a55ea05 --- /dev/null +++ b/web-components/packages/carbon-web-components/src/components/ui-shell/switcher-divider.ts @@ -0,0 +1,32 @@ +/** + * @license + * + * Copyright IBM Corp. 2023 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { LitElement } from 'lit'; +import { customElement } from 'lit/decorators.js'; +import { prefix } from '../../globals/settings'; +import styles from './header.scss'; + +/** + * A divider in switcher. + * + * @element cds-switcher-divider + */ +@customElement(`${prefix}-switcher-divider`) +class CDSSwitcherDivider extends LitElement { + connectedCallback() { + if (!this.hasAttribute('role')) { + this.setAttribute('role', 'separator'); + } + super.connectedCallback(); + } + + static styles = styles; // `styles` here is a `CSSResult` generated by custom WebPack loader +} + +export default CDSSwitcherDivider; diff --git a/web-components/packages/carbon-web-components/src/components/ui-shell/switcher-item.ts b/web-components/packages/carbon-web-components/src/components/ui-shell/switcher-item.ts new file mode 100644 index 000000000000..00a58384ca1b --- /dev/null +++ b/web-components/packages/carbon-web-components/src/components/ui-shell/switcher-item.ts @@ -0,0 +1,89 @@ +/** + * @license + * + * Copyright IBM Corp. 2019, 2023 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { classMap } from 'lit/directives/class-map.js'; +import { LitElement, html } from 'lit'; +import { property, customElement } from 'lit/decorators.js'; +import { prefix } from '../../globals/settings'; +import FocusMixin from '../../globals/mixins/focus'; +import styles from './header.scss'; + +/** + * Switcher menu item. + * + * @element cds-switcher-item + */ +@customElement(`${prefix}-switcher-item`) +class CDSSwitcherItem extends FocusMixin(LitElement) { + /** + * Required props for accessibility label + */ + @property({ type: String, attribute: 'aria-label' }) + ariaLabel; + + /** + * Props for accessibility labelled by + */ + @property({ type: String, attribute: 'aria-labelledby' }) + ariaLabelledBy; + + /** + * Link `href`. + */ + @property() + href = ''; + + /** + * Specify if this is a large variation of the side nav link + */ + @property({ type: Boolean, reflect: true }) + selected = false; + + /** + * Specify if this is a large variation of the side nav link + */ + @property({ type: Number, reflect: true, attribute: 'tab-index' }) + tabIndex = 0; + + connectedCallback() { + if (!this.hasAttribute('role')) { + this.setAttribute('role', 'listitem'); + } + super.connectedCallback(); + } + + render() { + const { href, selected, ariaLabel, ariaLabelledBy, tabIndex } = this; + + const classes = classMap({ + [`${prefix}--switcher__item-link`]: true, + [`${prefix}--switcher__item-link--selected`]: selected, + }); + + return html` +
+ + + `; + } + + static shadowRootOptions = { + ...LitElement.shadowRootOptions, + delegatesFocus: true, + }; + static styles = styles; // `styles` here is a `CSSResult` generated by custom WebPack loader +} + +export default CDSSwitcherItem; diff --git a/web-components/packages/carbon-web-components/src/components/ui-shell/switcher.ts b/web-components/packages/carbon-web-components/src/components/ui-shell/switcher.ts new file mode 100644 index 000000000000..30e2898dc301 --- /dev/null +++ b/web-components/packages/carbon-web-components/src/components/ui-shell/switcher.ts @@ -0,0 +1,53 @@ +/** + * @license + * + * Copyright IBM Corp. 2023 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { LitElement, html } from 'lit'; +import { property, customElement } from 'lit/decorators.js'; +import styles from './header.scss'; +import { prefix } from '../../globals/settings'; + +/** + * Switcher + * + * @element cds-switcher + */ +@customElement(`${prefix}-switcher`) +class CDSSwitcher extends LitElement { + /** + * Required props for accessibility label on the underlying menu + */ + @property({ type: String, attribute: 'aria-label' }) + ariaLabel; + + /** + * Prop for accessibility labelled by on the underlying menu + */ + @property({ type: String, attribute: 'aria-labelledby' }) + ariaLabelledBy; + + connectedCallback() { + if (!this.hasAttribute('role')) { + this.setAttribute('role', 'list'); + } + super.connectedCallback(); + } + + render() { + return html``; + } + + static shadowRootOptions = { + ...LitElement.shadowRootOptions, + delegatesFocus: true, + }; + + static styles = styles; // `styles` here is a `CSSResult` generated by custom WebPack loader +} + +export default CDSSwitcher; diff --git a/web-components/packages/carbon-web-components/src/components/ui-shell/ui-shell-story.scss b/web-components/packages/carbon-web-components/src/components/ui-shell/ui-shell-story.scss index 4c5501feb58d..90d5eb4c67c1 100644 --- a/web-components/packages/carbon-web-components/src/components/ui-shell/ui-shell-story.scss +++ b/web-components/packages/carbon-web-components/src/components/ui-shell/ui-shell-story.scss @@ -25,21 +25,3 @@ margin-left: 16rem; } } - -.#{$prefix}-ce-demo-devenv--with-rail .#{$prefix}-ce-demo-devenv--container { - margin-left: $spacing-09; -} - -.#{$prefix}-ce-demo-devenv--rail-expanded - .#{$prefix}-ce-demo-devenv--container { - @include breakpoint('lg') { - margin-left: 16rem; - } -} - -.#{$prefix}-ce-demo-devenv--with-side-nav-for-header - .#{$prefix}-ce-demo-devenv--container { - @include breakpoint('lg') { - margin-left: 0; - } -} diff --git a/web-components/packages/carbon-web-components/src/components/ui-shell/ui-shell-story.ts b/web-components/packages/carbon-web-components/src/components/ui-shell/ui-shell-story.ts index 110ce2df1439..0aa5dd2e96fd 100644 --- a/web-components/packages/carbon-web-components/src/components/ui-shell/ui-shell-story.ts +++ b/web-components/packages/carbon-web-components/src/components/ui-shell/ui-shell-story.ts @@ -8,15 +8,16 @@ */ import { html } from 'lit'; -import { boolean, select } from '@storybook/addon-knobs'; // Below path will be there when an application installs `@carbon/web-components` package. // In our dev env, we auto-generate the file and re-map below path to to point to the generated file. // @ts-ignore import Fade16 from '@carbon/web-components/es/icons/fade/16'; +import Search20 from '@carbon/web-components/es/icons/search/20'; +import Notification20 from '@carbon/web-components/es/icons/notification/20'; +import SwitcherIcon20 from '@carbon/web-components/es/icons/switcher/20'; import contentStyles from '@carbon/styles/scss/components/ui-shell/content/_content.scss'; -import textNullable from '../../../.storybook/knob-text-nullable'; -import { ifDefined } from 'lit/directives/if-defined.js'; import { SIDE_NAV_COLLAPSE_MODE, SIDE_NAV_USAGE_MODE } from './side-nav'; +import { classMap } from 'lit/directives/class-map.js'; import './side-nav-items'; import './side-nav-link'; import './side-nav-divider'; @@ -29,270 +30,426 @@ import './header-menu'; import './header-menu-item'; import './header-menu-button'; import './header-name'; +import './header-global-action'; +import './header-panel'; +import './header-side-nav-items'; +import './switcher'; +import './switcher-item'; +import './switcher-divider'; +import '../skip-to-content'; +import '../modal/modal'; +import '../button/button'; import styles from './ui-shell-story.scss'; import storyDocs from './ui-shell-story.mdx'; import { prefix } from '../../globals/settings'; -const collapseModes = { - Responsive: null, - [`Fixed (${SIDE_NAV_COLLAPSE_MODE.FIXED})`]: SIDE_NAV_COLLAPSE_MODE.FIXED, - [`Rail (${SIDE_NAV_COLLAPSE_MODE.RAIL})`]: SIDE_NAV_COLLAPSE_MODE.RAIL, -}; - -const usageModes = { - Regular: null, - [`For header nav (${SIDE_NAV_USAGE_MODE.HEADER_NAV})`]: - SIDE_NAV_USAGE_MODE.HEADER_NAV, -}; - -const updateRailExpanded = ({ - collapseMode, - expanded, - usageMode = SIDE_NAV_USAGE_MODE.REGULAR, -}) => { - document.body.classList.toggle( - `${prefix}-ce-demo-devenv--with-rail`, - collapseMode === SIDE_NAV_COLLAPSE_MODE.RAIL - ); - document.body.classList.toggle( - `${prefix}-ce-demo-devenv--rail-expanded`, - collapseMode === SIDE_NAV_COLLAPSE_MODE.RAIL && expanded - ); - document.body.classList.toggle( - `${prefix}-ce-demo-devenv--with-side-nav-for-header`, - usageMode === SIDE_NAV_USAGE_MODE.HEADER_NAV - ); -}; +const linksHref = 'https://www.carbondesignsystem.com/'; -const StoryContent = () => html` - -
-
-
-
-

Purpose and function

-

- The shell is perhaps the most crucial piece of any UI built with - Carbon. It contains the shared navigation framework for the entire - design system and ties the products in IBM’s portfolio together in a - cohesive and elegant way. The shell is the home of the topmost - navigation, where users can quickly and dependably gain their - bearings and move between pages. -
-
- The shell was designed with maximum flexibility built in, to serve - the needs of a broad range of products and users. Adopting the shell - ensures compliance with IBM design standards, simplifies development - efforts, and provides great user experiences. All IBM products built - with Carbon are required to use the shell’s header. -
-
- To better understand the purpose and function of the UI shell, - consider the “shell” of MacOS, which contains the Apple menu, - top-level navigation, and universal, OS-level controls at the top of - the screen, as well as a universal dock along the bottom or side of - the screen. The Carbon UI shell is roughly analogous in function to - these parts of the Mac UI. For example, the app switcher portion of - the shell can be compared to the dock in MacOS. -

-

Header responsive behavior

-

- As a header scales down to fit smaller screen sizes, headers with - persistent side nav menus should have the side nav collapse into - “hamburger” menu. See the example to better understand responsive - behavior of the header. -

-

Secondary navigation

-

- The side-nav contains secondary navigation and fits below the - header. It can be configured to be either fixed-width or flexible, - with only one level of nested items allowed. Both links and category - lists can be used in the side-nav and may be mixed together. There - are several configurations of the side-nav, but only one - configuration should be used per product section. If tabs are needed - on a page when using a side-nav, then the tabs are secondary in - hierarchy to the side-nav. -

+const StoryContent = ({ useResponsiveOffset = true }) => { + const firstColumnClasses = classMap({ + [`${prefix}--col-lg-13`]: true, + [`${prefix}--offset-lg-3`]: useResponsiveOffset, + }); + const toggleButton = () => { + document.querySelector('cds-modal')?.toggleAttribute('open'); + }; + return html` + +
+
+
+
+

Purpose and function

+

+ The shell is perhaps the most crucial piece of any UI built with + Carbon. It contains the + shared navigation framework for the entire design system and ties + the products in IBM’s portfolio together in a cohesive and elegant + way. The shell is the home of the topmost navigation, where users + can quickly and dependably gain their bearings and move between + pages. +
+
+ The shell was designed with maximum flexibility built in, to serve + the needs of a broad range of products and users. Adopting the + shell ensures compliance with IBM design standards, simplifies + development efforts, and provides great user experiences. All IBM + products built with Carbon are required to use the shell’s header. +
+
+ To better understand the purpose and function of the UI shell, + consider the “shell” of MacOS, which contains the Apple menu, + top-level navigation, and universal, OS-level controls at the top + of the screen, as well as a universal dock along the bottom or + side of the screen. The Carbon UI shell is roughly analogous in + function to these parts of the Mac UI. For example, the app + switcher portion of the shell can be compared to the dock in + MacOS. +

+

Header responsive behavior

+

+ As a header scales down to fit smaller screen sizes, headers with + persistent side nav menus should have the side nav collapse into + “hamburger” menu. See the example to better understand responsive + behavior of the header. +

+

Secondary navigation

+

+ The side-nav contains secondary navigation and fits below the + header. It can be configured to be either fixed-width or flexible, + with only one level of nested items allowed. Both links and + category lists can be used in the side-nav and may be mixed + together. There are several configurations of the side-nav, but + only one configuration should be used per product section. If tabs + are needed on a page when using a side-nav, then the tabs are + secondary in hierarchy to the side-nav. +

+ + + + Account resources + Add a custom domain + + + + Custom domains direct requests for your apps in this Cloud + Foundry organization to a URL that you own. A custom domain + can be a shared domain, a shared subdomain, or a shared domain + and host. + + + + Cancel + Add + + + Launch modal +
-
-
-`; - -export const sideNav = (args) => { - const { collapseMode, expanded } = args?.[`${prefix}-side-nav`] ?? {}; - const { href } = args?.[`${prefix}-side-nav-menu-item`] ?? {}; - updateRailExpanded({ collapseMode, expanded }); + + `; +}; + +export const FixedSideNav = () => { const result = html` + collapse-mode="${SIDE_NAV_COLLAPSE_MODE.FIXED}" + expanded> - + L0 menu item - + L0 menu item - + L0 menu item - + L0 menu item + href="${linksHref}"> L0 menu item - + L0 menu item - + L0 menu item - + L0 menu item - + L0 menu item - L0 link L0 link - ${StoryContent()} + ${StoryContent({ useResponsiveOffset: false })} `; (result as any).hasMainTag = true; return result; }; -sideNav.storyName = 'Side nav'; - -sideNav.parameters = { - knobs: { - [`${prefix}-side-nav`]: () => ({ - expanded: boolean('Expanded (expanded)', true), - collapseMode: select( - 'Collapse mode (collapse-mode)', - collapseModes, - null - ), - }), - [`${prefix}-side-nav-menu-item`]: () => ({ - href: textNullable('Link href (href)', 'javascript:void 0'), // eslint-disable-line no-script-url - }), - }, -}; +FixedSideNav.storyName = 'Fixed SideNav'; -export const sideNavWithIcons = (args) => { - const { collapseMode, expanded } = args?.[`${prefix}-side-nav`] ?? {}; - const { href } = args?.[`${prefix}-side-nav-menu-item`] ?? {}; - updateRailExpanded({ collapseMode, expanded }); +export const FixedSideNavDivider = () => { const result = html` + collapse-mode="${SIDE_NAV_COLLAPSE_MODE.FIXED}" + expanded> - ${Fade16({ slot: 'title-icon' })} - + L0 menu item - + L0 menu item - + L0 menu item - ${Fade16({ slot: 'title-icon' })} - + L0 menu item + href="${linksHref}"> L0 menu item - + L0 menu item - ${Fade16({ slot: 'title-icon' })} - + L0 menu item - + L0 menu item - + L0 menu item + L0 link + L0 link + + + ${StoryContent({ useResponsiveOffset: false })} + `; + (result as any).hasMainTag = true; + return result; +}; + +FixedSideNavDivider.storyName = 'Fixed SideNav w/ Divider'; + +export const FixedSideNavIcons = () => { + const result = html` + + + + + ${Fade16({ slot: 'title-icon' })} + + Link + + + Link + + + Link + + + + ${Fade16({ slot: 'title-icon' })} + + Link + + + Link + + + Link + + + + ${Fade16({ slot: 'title-icon' })} + + Link + + + Link + + + Link + + ${Fade16({ slot: 'title-icon' })}L0 link${Fade16({ slot: 'title-icon' })}Link ${Fade16({ slot: 'title-icon' })}L0 link${Fade16({ slot: 'title-icon' })}Link - ${StoryContent()} + ${StoryContent({ useResponsiveOffset: false })} `; (result as any).hasMainTag = true; return result; }; -sideNavWithIcons.storyName = 'Side nav with icons'; +FixedSideNavIcons.storyName = 'Fixed SideNav w/ Icons'; -sideNavWithIcons.parameters = { - knobs: sideNav.parameters.knobs, +export const HeaderBase = () => { + return html` + + [Platform] + `; }; -export const header = (args) => { - const { collapseMode, expanded, usageMode } = - args?.[`${prefix}-side-nav`] ?? {}; - const { href } = args?.[`${prefix}-side-nav-menu-item`] ?? {}; - updateRailExpanded({ collapseMode, expanded, usageMode }); - const handleButtonToggle = (event) => { - updateRailExpanded({ - collapseMode, - expanded: event.detail.active, - usageMode, - }); - }; - const result = html` - + + [Platform] +
+ + ${Search20({ slot: 'icon' })} + + + ${Notification20({ slot: 'icon' })} + + + ${SwitcherIcon20({ slot: 'icon' })} + +
+
`; +}; + +HeaderBaseWActions.storyName = 'Header Base w/ Actions'; + +export const HeaderBaseWActionsRightPanel = () => { + return html` + + [Platform] +
+ + ${Search20({ slot: 'icon' })} + + + ${Notification20({ slot: 'icon' })} + + + ${SwitcherIcon20({ slot: 'icon' })} + +
+ +
`; +}; + +HeaderBaseWActionsRightPanel.storyName = + 'Header Base w/ Actions and Right Panel'; + +export const HeaderBaseWActionsSwitcher = () => { + return html` + + [Platform] +
+ + ${Search20({ slot: 'icon' })} + + + ${Notification20({ slot: 'icon' })} + + + ${SwitcherIcon20({ slot: 'icon' })} + +
+ + + Link 1 + + Link 2 + Link 3 + Link 4 + Link 5 + + Link 6 + + +
`; +}; + +HeaderBaseWActionsSwitcher.storyName = 'Header Base w/ Actions and Switcher'; + +export const HeaderBaseWNavigationActionsAndSideNav = () => { + return html` + button-label-inactive="Open menu"> [Platform] @@ -318,77 +475,564 @@ export const header = (args) => { > +
+ + ${Search20({ slot: 'icon' })} + + + ${Notification20({ slot: 'icon' })} + + + ${SwitcherIcon20({ slot: 'icon' })} + +
+ + + + + Link 1 + + + Link 2 + + + Link 3 + + + + Sub-link 1 + + + Sub-link 2 + + + Sub-link 3 + + + + + ${Fade16({ slot: 'title-icon' })} + + Link + + + Link + + + Link + + + + ${Fade16({ slot: 'title-icon' })} + + Link + + + Link + + + Link + + + + ${Fade16({ slot: 'title-icon' })} + + Link + + + Link + + + Link + + + ${Fade16({ slot: 'title-icon' })}Link + ${Fade16({ slot: 'title-icon' })}Link + +
+ ${StoryContent({ useResponsiveOffset: true })}`; +}; + +HeaderBaseWNavigationActionsAndSideNav.storyName = + 'Header Base w/ Navigation, Actions and SideNav'; + +export const HeaderBaseWNavigationActions = () => { + return html` + + + [Platform] + + Link 1 + Link 2 + Link 3 + + Sub-link 1 + Sub-link 2 + Sub-link 3 + + +
+ + ${Search20({ slot: 'icon' })} + + + ${Notification20({ slot: 'icon' })} + + + ${SwitcherIcon20({ slot: 'icon' })} + +
+ + + + Link 1 + + + Link 2 + + + Link 3 + + + + Sub-link 1 + + + Sub-link 2 + + + Sub-link 3 + + + + +
`; +}; + +HeaderBaseWNavigationActions.storyName = + 'Header Base w/ Navigation and Actions'; + +export const HeaderBaseWNavigation = () => { + return html` + + + [Platform] + + Link 1 + Link 2 + Link 3 + + Sub-link 1 + Sub-link 2 + Sub-link 3 + + + + + + Link 1 + + + Link 2 + + + Link 3 + + + + Sub-link 1 + + + Sub-link 2 + + + Sub-link 3 + + + + + `; +}; + +HeaderBaseWNavigation.storyName = 'Header Base w/ Navigation'; + +export const HeaderBaseWSideNav = () => { + const result = html` + + + + [Platform] + + + + ${Fade16({ slot: 'title-icon' })} + + Link + + + Link + + + Link + + + + ${Fade16({ slot: 'title-icon' })} + + Link + + + Link + + + Link + + + + ${Fade16({ slot: 'title-icon' })} + + Link + + + Link + + + Link + + + ${Fade16({ slot: 'title-icon' })}Link + ${Fade16({ slot: 'title-icon' })}Link + + + + ${StoryContent({ useResponsiveOffset: true })} + `; + (result as any).hasMainTag = true; + return result; +}; + +HeaderBaseWSideNav.storyName = 'Header Base w/ SideNav'; + +export const HeaderBaseWSkipToContent = () => { + return html` + + + [Platform] +
+ + ${Search20({ slot: 'icon' })} + + + ${Notification20({ slot: 'icon' })} + + + ${SwitcherIcon20({ slot: 'icon' })} + +
+
+ ${StoryContent({ useResponsiveOffset: true })}`; +}; + +HeaderBaseWSkipToContent.storyName = 'Header Base w/ SkipToContent'; + +export const SideNavRail = () => { + return html` + collapse-mode="${SIDE_NAV_COLLAPSE_MODE.RAIL}"> - + ${Fade16({ slot: 'title-icon' })} - - L0 menu item + + Link - - L0 menu item + + Link - - L0 menu item + + Link - + ${Fade16({ slot: 'title-icon' })} - - L0 menu item + + Link - - L0 menu item + + Link - - L0 menu item + + Link - + ${Fade16({ slot: 'title-icon' })} - - L0 menu item + + Link - - L0 menu item + + Link - - L0 menu item + + Link - ${Fade16({ slot: 'title-icon' })}L0 link${Fade16({ slot: 'title-icon' })}Link ${Fade16({ slot: 'title-icon' })}L0 link${Fade16({ slot: 'title-icon' })}Link - ${StoryContent()} + ${StoryContent({ useResponsiveOffset: true })}`; +}; + +SideNavRail.storyName = 'SideNav Rail'; + +export const SideNavRailWHeader = () => { + return html` + + + [Platform] + + Link 1 + Link 2 + Link 3 + + Sub-link 1 + Sub-link 2 + Sub-link 3 + + +
+ + ${Search20({ slot: 'icon' })} + + + ${Notification20({ slot: 'icon' })} + + + ${SwitcherIcon20({ slot: 'icon' })} + +
+ + + + ${Fade16({ slot: 'title-icon' })} + + Link + + + Link + + + Link + + + + ${Fade16({ slot: 'title-icon' })} + + Link + + + Link + + + Link + + + + ${Fade16({ slot: 'title-icon' })} + + Link + + + Link + + + Link + + + ${Fade16({ slot: 'title-icon' })}Link + ${Fade16({ slot: 'title-icon' })}Link + + +
+ ${StoryContent({ useResponsiveOffset: true })}`; +}; + +SideNavRailWHeader.storyName = 'SideNav Rail w/ Header'; + +export const SideNavWLargeSideNavItems = () => { + const result = html` + + + + + + Menu 1 + + + Menu 2 + + + Menu 3 + + + Large link + ${Fade16({ slot: 'title-icon' })} + + Menu 1 + + + Menu 2 + + + Menu 3 + + + + ${Fade16({ slot: 'title-icon' })} Large link w/icon + + + ${StoryContent({ useResponsiveOffset: true })} `; (result as any).hasMainTag = true; return result; }; -header.parameters = { - knobs: { - [`${prefix}-side-nav`]: () => ({ - ...sideNav.parameters.knobs[`${prefix}-side-nav`](), - usageMode: select('Usage mode (usage-mode)', usageModes, null), - }), - [`${prefix}-side-nav-menu-item`]: - sideNav.parameters.knobs[`${prefix}-side-nav-menu-item`], - }, -}; +SideNavWLargeSideNavItems.storyName = 'SideNav w/ large side nav items'; export default { title: 'Components/UI Shell', diff --git a/web-components/packages/carbon-web-components/src/index.ts b/web-components/packages/carbon-web-components/src/index.ts index cc78125426fb..9c234f0c1155 100644 --- a/web-components/packages/carbon-web-components/src/index.ts +++ b/web-components/packages/carbon-web-components/src/index.ts @@ -118,9 +118,14 @@ export { default as CDSHeaderMenuItem } from './components/ui-shell/header-menu- export { default as CDSHeaderName } from './components/ui-shell/header-name'; export { default as CDSHeaderNav } from './components/ui-shell/header-nav'; export { default as CDSHeaderNavItem } from './components/ui-shell/header-nav-item'; +export { default as CDSHeaderSideNavItems } from './components/ui-shell/header-side-nav-items'; +export { default as CDSHeaderPanel } from './components/ui-shell/header-panel'; export { default as CDSSideNav } from './components/ui-shell/side-nav'; export { default as CDSSideNavItems } from './components/ui-shell/side-nav-items'; export { default as CDSSideNavLink } from './components/ui-shell/side-nav-link'; export { default as CDSSideNavMenu } from './components/ui-shell/side-nav-menu'; export { default as CDSSideNavMenuItem } from './components/ui-shell/side-nav-menu-item'; +export { default as CDSSwitcher } from './components/ui-shell/switcher'; +export { default as CDSSwitcherItem } from './components/ui-shell/switcher-item'; +export { default as CDSSwitcherDivider } from './components/ui-shell/switcher-divider'; export { default as CDSStack } from './components/stack/stack'; diff --git a/web-components/packages/carbon-web-components/src/typings/jsx-elements.d.ts b/web-components/packages/carbon-web-components/src/typings/jsx-elements.d.ts index cf1734742757..c0b4ae202e1e 100644 --- a/web-components/packages/carbon-web-components/src/typings/jsx-elements.d.ts +++ b/web-components/packages/carbon-web-components/src/typings/jsx-elements.d.ts @@ -112,6 +112,11 @@ declare global { 'cds-header-menu-item': any; 'cds-header-menu-button': any; 'cds-header-name': any; + 'cds-header-panel': any; + 'cds-header-side-nav-items': any; + 'cds-switcher': any; + 'cds-switcher-divider': any; + 'cds-switcher-item': any; 'cds-side-nav': any; 'cds-side-nav-items': any; 'cds-side-nav-menu': any; diff --git a/web-components/packages/carbon-web-components/tests/spec/ui-shell_spec.ts b/web-components/packages/carbon-web-components/tests/spec/ui-shell_spec.ts index 5f68f7d0082f..86fee26cfa2b 100644 --- a/web-components/packages/carbon-web-components/tests/spec/ui-shell_spec.ts +++ b/web-components/packages/carbon-web-components/tests/spec/ui-shell_spec.ts @@ -40,7 +40,6 @@ const headerMenuButtonTemplate = (props?) => { buttonLabelInactive, collapseMode, disabled, - usageMode, } = props ?? {}; return html` { button-label-active="${ifDefined(buttonLabelActive)}" button-label-inactive="${ifDefined(buttonLabelInactive)}" collapse-mode="${ifDefined(collapseMode)}" - ?disabled="${disabled}" - usage-mode="${ifDefined(usageMode)}"> + ?disabled="${disabled}"> `; }; @@ -441,22 +439,6 @@ describe('ui-shell', function () { ); expect((menuButton as BXSideNavMenuButton).active).toBe(true); }); - - it('should propagate usage mode to header menu button', async function () { - render( - sideNavTemplate({ - usageMode: SIDE_NAV_USAGE_MODE.HEADER_NAV, - }), - document.body - ); - await Promise.resolve(); - const menuButton = document.body.querySelector( - 'cds-header-menu-button' - ); - expect((menuButton as BXSideNavMenuButton).usageMode).toBe( - SIDE_NAV_USAGE_MODE.HEADER_NAV - ); - }); }); });