diff --git a/.parity-check-data.json b/.parity-check-data.json index 011dd1a60..37a94d95b 100644 --- a/.parity-check-data.json +++ b/.parity-check-data.json @@ -572,7 +572,7 @@ }, "UIShell": { "lastCheckedCommit": "5102bd860864c2580048183ffaf50d1ffd4400b5", - "lastSyncedCommit": "N/A", + "lastSyncedCommit": "5102bd860864c2580048183ffaf50d1ffd4400b5", "hasChanges": false, "changeCount": 0, "lastUpdate": null diff --git a/carbon-components-ember/package.json b/carbon-components-ember/package.json index d4d2dc17b..7cf59f98c 100644 --- a/carbon-components-ember/package.json +++ b/carbon-components-ember/package.json @@ -2968,15 +2968,26 @@ "./components/carbon/tooltip.js": "./dist/_app_/components/carbon/tooltip.js", "./components/carbon/tree-view.js": "./dist/_app_/components/carbon/tree-view.js", "./components/carbon/ui-shell.js": "./dist/_app_/components/carbon/ui-shell.js", + "./components/carbon/ui-shell/-header-container.js": "./dist/_app_/components/carbon/ui-shell/-header-container.js", "./components/carbon/ui-shell/-header.js": "./dist/_app_/components/carbon/ui-shell/-header.js", "./components/carbon/ui-shell/-header/-global-action.js": "./dist/_app_/components/carbon/ui-shell/-header/-global-action.js", + "./components/carbon/ui-shell/-header/-menu-item.js": "./dist/_app_/components/carbon/ui-shell/-header/-menu-item.js", + "./components/carbon/ui-shell/-header/-menu.js": "./dist/_app_/components/carbon/ui-shell/-header/-menu.js", + "./components/carbon/ui-shell/-header/-panel.js": "./dist/_app_/components/carbon/ui-shell/-header/-panel.js", + "./components/carbon/ui-shell/-header/-side-nav-items.js": "./dist/_app_/components/carbon/ui-shell/-header/-side-nav-items.js", "./components/carbon/ui-shell/-nav.js": "./dist/_app_/components/carbon/ui-shell/-nav.js", "./components/carbon/ui-shell/-nav/-item.js": "./dist/_app_/components/carbon/ui-shell/-nav/-item.js", "./components/carbon/ui-shell/-sidenav.js": "./dist/_app_/components/carbon/ui-shell/-sidenav.js", + "./components/carbon/ui-shell/-sidenav/-details.js": "./dist/_app_/components/carbon/ui-shell/-sidenav/-details.js", "./components/carbon/ui-shell/-sidenav/-divider.js": "./dist/_app_/components/carbon/ui-shell/-sidenav/-divider.js", "./components/carbon/ui-shell/-sidenav/-footer.js": "./dist/_app_/components/carbon/ui-shell/-sidenav/-footer.js", + "./components/carbon/ui-shell/-sidenav/-header.js": "./dist/_app_/components/carbon/ui-shell/-sidenav/-header.js", + "./components/carbon/ui-shell/-sidenav/-icon.js": "./dist/_app_/components/carbon/ui-shell/-sidenav/-icon.js", "./components/carbon/ui-shell/-sidenav/-menu.js": "./dist/_app_/components/carbon/ui-shell/-sidenav/-menu.js", "./components/carbon/ui-shell/-sidenav/-sub-menu.js": "./dist/_app_/components/carbon/ui-shell/-sidenav/-sub-menu.js", + "./components/carbon/ui-shell/-switcher.js": "./dist/_app_/components/carbon/ui-shell/-switcher.js", + "./components/carbon/ui-shell/-switcher/-divider.js": "./dist/_app_/components/carbon/ui-shell/-switcher/-divider.js", + "./components/carbon/ui-shell/-switcher/-item.js": "./dist/_app_/components/carbon/ui-shell/-switcher/-item.js", "./components/carbon/unordered-list.js": "./dist/_app_/components/carbon/unordered-list.js", "./helpers/carbon/default-to.js": "./dist/_app_/helpers/carbon/default-to.js", "./helpers/carbon/generic.js": "./dist/_app_/helpers/carbon/generic.js", diff --git a/carbon-components-ember/src/components/ui-shell.gts b/carbon-components-ember/src/components/ui-shell.gts index e718bb61b..df6d6d15d 100644 --- a/carbon-components-ember/src/components/ui-shell.gts +++ b/carbon-components-ember/src/components/ui-shell.gts @@ -1,6 +1,8 @@ import { default as Header } from './ui-shell/-header.gts'; import { default as Sidenav } from './ui-shell/-sidenav.gts'; import { default as Nav } from './ui-shell/-nav.gts'; +import { default as Switcher } from './ui-shell/-switcher.gts'; +import { default as HeaderContainer } from './ui-shell/-header-container.gts'; import Component from '@glimmer/component'; import UIShellHeader from './ui-shell/-header.gts'; import { hash } from '@ember/helper'; @@ -12,6 +14,8 @@ export interface UIShellSignature { Header: typeof UIShellHeader; Sidenav: typeof Sidenav; Nav: typeof Nav; + Switcher: typeof Switcher; + HeaderContainer: typeof HeaderContainer; }, ]; content: []; @@ -20,7 +24,16 @@ export interface UIShellSignature { export default class UIShell extends Component { } diff --git a/carbon-components-ember/src/components/ui-shell/-header/-global-action.gts b/carbon-components-ember/src/components/ui-shell/-header/-global-action.gts index 6207d85f4..e1f98a35f 100644 --- a/carbon-components-ember/src/components/ui-shell/-header/-global-action.gts +++ b/carbon-components-ember/src/components/ui-shell/-header/-global-action.gts @@ -16,7 +16,7 @@ export default class UIShellHeaderGlobalAction extends Component { + +} diff --git a/carbon-components-ember/src/components/ui-shell/-header/-menu.gts b/carbon-components-ember/src/components/ui-shell/-header/-menu.gts new file mode 100644 index 000000000..f11a3fc2c --- /dev/null +++ b/carbon-components-ember/src/components/ui-shell/-header/-menu.gts @@ -0,0 +1,56 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { on } from '@ember/modifier'; +import { ChevronDown } from '../../../icons.ts'; +import UIShellHeaderMenuItem from './-menu-item.gts'; + +export interface UIShellHeaderMenuSignature { + Element: HTMLLIElement; + Args: { + 'aria-label'?: string; + 'aria-labelledby'?: string; + menuLinkName: string; + isActive?: boolean; + }; + Blocks: { + default: [typeof UIShellHeaderMenuItem]; + }; +} + +export default class UIShellHeaderMenu extends Component { + @tracked expanded = false; + + toggleExpanded = () => { + this.expanded = !this.expanded; + }; + + +} diff --git a/carbon-components-ember/src/components/ui-shell/-header/-panel.gts b/carbon-components-ember/src/components/ui-shell/-header/-panel.gts new file mode 100644 index 000000000..f5eb90caa --- /dev/null +++ b/carbon-components-ember/src/components/ui-shell/-header/-panel.gts @@ -0,0 +1,33 @@ +import Component from '@glimmer/component'; +import { on } from '@ember/modifier'; + +export interface UIShellHeaderPanelSignature { + Element: HTMLDivElement; + Args: { + expanded?: boolean; + onToggle?: (value: boolean) => void; + }; + Blocks: { + default: []; + }; +} + +export default class UIShellHeaderPanel extends Component { + handleKeydown = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + this.args.onToggle?.(false); + } + }; + + +} diff --git a/carbon-components-ember/src/components/ui-shell/-header/-side-nav-items.gts b/carbon-components-ember/src/components/ui-shell/-header/-side-nav-items.gts new file mode 100644 index 000000000..d24664173 --- /dev/null +++ b/carbon-components-ember/src/components/ui-shell/-header/-side-nav-items.gts @@ -0,0 +1,23 @@ +import Component from '@glimmer/component'; + +export interface UIShellHeaderSideNavItemsSignature { + Element: HTMLUListElement; + Args: { + hasDivider?: boolean; + }; + Blocks: { + default: []; + }; +} + +export default class UIShellHeaderSideNavItems extends Component { + +} diff --git a/carbon-components-ember/src/components/ui-shell/-nav.gts b/carbon-components-ember/src/components/ui-shell/-nav.gts index c7193bf78..f48b4bd40 100644 --- a/carbon-components-ember/src/components/ui-shell/-nav.gts +++ b/carbon-components-ember/src/components/ui-shell/-nav.gts @@ -1,10 +1,11 @@ import Component from '@glimmer/component'; import UIShellNavItem from '../../components/ui-shell/-nav/-item.gts'; +import UIShellHeaderMenu from '../../components/ui-shell/-header/-menu.gts'; export interface Signature { Element: null; Blocks: { - default: [typeof UIShellNavItem]; + default: [typeof UIShellNavItem, typeof UIShellHeaderMenu]; }; } @@ -12,7 +13,7 @@ export default class InnerClass extends Component { diff --git a/carbon-components-ember/src/components/ui-shell/-sidenav.gts b/carbon-components-ember/src/components/ui-shell/-sidenav.gts index 7b6ced226..714572ab0 100644 --- a/carbon-components-ember/src/components/ui-shell/-sidenav.gts +++ b/carbon-components-ember/src/components/ui-shell/-sidenav.gts @@ -4,6 +4,10 @@ import Component from '@glimmer/component'; import NavMenuComponent, { type SubMenu } from './-sidenav/-menu.gts'; import Divider from './-sidenav/-divider.gts'; import Footer from './-sidenav/-footer.gts'; +import SideNavHeader from './-sidenav/-header.gts'; +import SideNavDetails from './-sidenav/-details.gts'; +import SideNavIcon from './-sidenav/-icon.gts'; +import HeaderSideNavItems from './-header/-side-nav-items.gts'; import { fn } from '@ember/helper'; import { stylesheet } from 'astroturf'; import type Icon from '../icon'; @@ -22,7 +26,14 @@ export interface UIShellNavSignature { transitionTo: (menu: MenuItem | SubMenu) => void; }; Blocks: { - default: [typeof NavMenuComponent, typeof Divider]; + default: [ + typeof NavMenuComponent, + typeof Divider, + typeof SideNavHeader, + typeof SideNavDetails, + typeof SideNavIcon, + typeof HeaderSideNavItems, + ]; content: []; footer: [typeof Footer]; }; @@ -51,7 +62,7 @@ export default class UIShellNav extends Component { >
    {{#unless @menuItems}} - {{yield Menu Divider}} + {{yield Menu Divider SideNavHeader SideNavDetails SideNavIcon HeaderSideNavItems}} {{/unless}} {{#each @menuItems as |menu|}} { + +} diff --git a/carbon-components-ember/src/components/ui-shell/-sidenav/-header.gts b/carbon-components-ember/src/components/ui-shell/-sidenav/-header.gts new file mode 100644 index 000000000..465bc5924 --- /dev/null +++ b/carbon-components-ember/src/components/ui-shell/-sidenav/-header.gts @@ -0,0 +1,24 @@ +import Component from '@glimmer/component'; +import UIShellSideNavIcon from './-icon.gts'; +import type Icon from '../../icon.gts'; + +export interface UIShellSideNavHeaderSignature { + Element: HTMLElement; + Args: { + icon: typeof Icon; + }; + Blocks: { + default: []; + }; +} + +export default class UIShellSideNavHeader extends Component { + +} diff --git a/carbon-components-ember/src/components/ui-shell/-sidenav/-icon.gts b/carbon-components-ember/src/components/ui-shell/-sidenav/-icon.gts new file mode 100644 index 000000000..4d8117097 --- /dev/null +++ b/carbon-components-ember/src/components/ui-shell/-sidenav/-icon.gts @@ -0,0 +1,22 @@ +import Component from '@glimmer/component'; + +export interface UIShellSideNavIconSignature { + Element: HTMLDivElement; + Args: { + small?: boolean; + }; + Blocks: { + default: []; + }; +} + +export default class UIShellSideNavIcon extends Component { + +} diff --git a/carbon-components-ember/src/components/ui-shell/-switcher.gts b/carbon-components-ember/src/components/ui-shell/-switcher.gts new file mode 100644 index 000000000..722952629 --- /dev/null +++ b/carbon-components-ember/src/components/ui-shell/-switcher.gts @@ -0,0 +1,27 @@ +import Component from '@glimmer/component'; +import UIShellSwitcherItem from './-switcher/-item.gts'; +import UIShellSwitcherDivider from './-switcher/-divider.gts'; + +export interface UIShellSwitcherSignature { + Element: HTMLUListElement; + Args: { + 'aria-label'?: string; + 'aria-labelledby'?: string; + }; + Blocks: { + default: [typeof UIShellSwitcherItem, typeof UIShellSwitcherDivider]; + }; +} + +export default class UIShellSwitcher extends Component { + +} diff --git a/carbon-components-ember/src/components/ui-shell/-switcher/-divider.gts b/carbon-components-ember/src/components/ui-shell/-switcher/-divider.gts new file mode 100644 index 000000000..6bda17870 --- /dev/null +++ b/carbon-components-ember/src/components/ui-shell/-switcher/-divider.gts @@ -0,0 +1,13 @@ +import Component from '@glimmer/component'; + +export interface UIShellSwitcherDividerSignature { + Element: HTMLLIElement; +} + +export default class UIShellSwitcherDivider extends Component { + +} diff --git a/carbon-components-ember/src/components/ui-shell/-switcher/-item.gts b/carbon-components-ember/src/components/ui-shell/-switcher/-item.gts new file mode 100644 index 000000000..b02e3c033 --- /dev/null +++ b/carbon-components-ember/src/components/ui-shell/-switcher/-item.gts @@ -0,0 +1,29 @@ +import Component from '@glimmer/component'; + +export interface UIShellSwitcherItemSignature { + Element: HTMLAnchorElement; + Args: { + 'aria-label'?: string; + isSelected?: boolean; + href?: string; + }; + Blocks: { + default: []; + }; +} + +export default class UIShellSwitcherItem extends Component { + +} diff --git a/docs-app/app/templates/2-components/ui-shell.md b/docs-app/app/templates/2-components/ui-shell.md index 8d101a9b9..587489785 100644 --- a/docs-app/app/templates/2-components/ui-shell.md +++ b/docs-app/app/templates/2-components/ui-shell.md @@ -80,6 +80,163 @@ navigation items, and a `footer` named block that yields a `Footer` toggle control for expanding/collapsing the rail — wire both to the same `@open`/`@onToggle` state used by the `Header`'s menu button. +## Header dropdown menus + +The `Nav` block yields a second component, `Menu` (`HeaderMenu`), for +top-level items that expand to show a list of `HeaderMenuItem`s. + +```gjs live preview +import { UIShell } from 'carbon-components-ember/components'; +import { ThemeSupport } from 'docs-support'; + + +``` + +## Header panel and switcher + +`Header` yields a `HeaderPanel` component into the `headerPanel` named +block for building panels such as an application switcher — toggle its +`@expanded` argument from a `GlobalAction`'s `onClick`. The top-level +`UIShell` yields a `Switcher` component (with `Item` and `Divider`) for +rendering the switcher's contents. + +```gjs live preview +import { UIShell } from 'carbon-components-ember/components'; +import { UserAvatar } from 'carbon-components-ember/icons'; +import { ThemeSupport } from 'docs-support'; +import { cell } from 'ember-resources'; + +const switcherOpen = cell(false); +const toggleSwitcher = () => (switcherOpen.current = !switcherOpen.current); + + +``` + +## Side navigation header, details, and mirrored header items + +The `Sidenav` block also yields `SideNavHeader` (an icon + heading for the +rail), `SideNavDetails` (a titled block, useful for account info), and +`HeaderSideNavItems` (mirrors top `Header` nav items into the rail for +smaller viewports). + +```gjs live preview +import { UIShell } from 'carbon-components-ember/components'; +import { UserAvatar } from 'carbon-components-ember/icons'; +import { ThemeSupport } from 'docs-support'; + + +``` + +## HeaderContainer + +`HeaderContainer` (yielded from the top-level `shell` block) manages the +`isSideNavExpanded` state for you and collapses it when the user presses +Escape, so you don't have to keep your own `@open`/`@onToggle` +cell. It yields `isSideNavExpanded` and `onClickSideNavExpand` for wiring up +the `Header`'s menu toggle and the `Sidenav`'s `@open` argument. + +```gjs live preview +import { UIShell } from 'carbon-components-ember/components'; +import { ThemeSupport } from 'docs-support'; + + +``` + ## API Reference
    diff --git a/test-app/tests/components/ui-shell-test.gts b/test-app/tests/components/ui-shell-test.gts index 1d17f1089..ab1a0497f 100644 --- a/test-app/tests/components/ui-shell-test.gts +++ b/test-app/tests/components/ui-shell-test.gts @@ -1,6 +1,7 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; -import { render, click } from '@ember/test-helpers'; +import { render, click, find, waitUntil } from '@ember/test-helpers'; +import { on } from '@ember/modifier'; import UIShell from 'carbon-components-ember/components/ui-shell'; import { Notification } from 'carbon-components-ember/icons'; import { cell } from 'ember-resources'; @@ -49,8 +50,19 @@ module('Integration | Component | UIShell', (hooks) => { ); assert.dom('.cds--header__menu-toggle').exists(); + await waitUntil(() => find('.cds--header__menu-toggle svg')); + assert.dom('.cds--header__menu-toggle svg').exists(); + const beforeIcon = find('.cds--header__menu-toggle svg')?.innerHTML; + await click('.cds--header__menu-toggle'); assert.true(open.current); + await waitUntil(() => find('.cds--header__menu-toggle svg')); + const afterIcon = find('.cds--header__menu-toggle svg')?.innerHTML; + assert.notStrictEqual( + beforeIcon, + afterIcon, + 'icon swaps between Menu and Close based on @open', + ); }); test('header global action renders a button and calls onClick', async function (assert) { @@ -77,6 +89,11 @@ module('Integration | Component | UIShell', (hooks) => { ); assert.dom('.cds--header__global .cds--header__action').exists(); + assert + .dom('.cds--header__global .cds--header__action') + .hasClass('cds--btn--icon-only'); + await waitUntil(() => find('.cds--header__global .cds--header__action svg')); + assert.dom('.cds--header__global .cds--header__action svg').exists(); await click('.cds--header__global .cds--header__action'); assert.true(clicked.current); }); @@ -140,6 +157,134 @@ module('Integration | Component | UIShell', (hooks) => { assert.dom('.cds--side-nav__menu').doesNotExist(); }); + test('nav renders a HeaderMenu that expands to show HeaderMenuItems', async function (assert) { + await render( + , + ); + + assert.dom('.cds--header__submenu').exists(); + assert.dom('.cds--header__menu-title').hasAria('expanded', 'false'); + assert.dom('.cds--header__menu .cds--header__menu-item').exists({ count: 2 }); + assert + .dom('.cds--header__menu .cds--header__menu-item--current') + .exists({ count: 1 }); + + await click('.cds--header__menu-title'); + assert.dom('.cds--header__menu-title').hasAria('expanded', 'true'); + }); + + test('header panel renders content and toggles the expanded class', async function (assert) { + await render( + , + ); + + assert.dom('.cds--header-panel').hasClass('cds--header-panel--expanded'); + assert.dom('.cds--header-panel').hasText('Panel content'); + }); + + test('switcher renders items and a divider', async function (assert) { + await render( + , + ); + + assert.dom('.cds--switcher').exists(); + assert.dom('.cds--switcher__item').exists({ count: 2 }); + assert + .dom('.cds--switcher__item-link--selected') + .hasText('App 1'); + assert.dom('.cds--switcher__item--divider').exists(); + }); + + test('sidenav renders SideNavHeader, SideNavDetails and HeaderSideNavItems', async function (assert) { + await render( + , + ); + + assert.dom('.cds--side-nav__header').exists(); + assert.dom('.cds--side-nav__header .cds--side-nav__icon').exists(); + assert.dom('.cds--side-nav__details .cds--side-nav__title').hasText('Account'); + assert + .dom('.cds--side-nav__header-navigation.cds--side-nav__header-divider') + .exists(); + }); + + test('HeaderContainer yields expansion state and a toggle action', async function (assert) { + await render( + , + ); + + assert.dom('button').hasText('collapsed'); + await click('button'); + assert.dom('button').hasText('expanded'); + }); + test('sidenav footer toggles open state', async function (assert) { const open = cell(false); const onToggle = (value: boolean) => (open.current = value);