From 32ac43e7e7a16e5528eb3778c29f8c1a20c2f626 Mon Sep 17 00:00:00 2001 From: Steven Spriggs Date: Wed, 20 Nov 2024 15:18:56 -0500 Subject: [PATCH 1/4] feat(navigation-secondary): poc for details summary --- .../rh-navigation-secondary-dsd/README.md | 11 + .../demo/rh-navigation-secondary-dsd.html | 93 ++++++ .../docs/00-overview.md | 2 + .../docs/30-code.md | 1 + .../rh-navigation-secondary-dsd---backup.css | 188 ++++++++++++ .../rh-navigation-secondary-dsd-dropdown.css | 93 ++++++ .../rh-navigation-secondary-dsd-dropdown.ts | 94 ++++++ .../rh-navigation-secondary-dsd-lightdom.css | 72 +++++ ...-navigation-secondary-dsd-menu-section.css | 39 +++ ...h-navigation-secondary-dsd-menu-section.ts | 74 +++++ .../rh-navigation-secondary-dsd-menu.css | 95 ++++++ .../rh-navigation-secondary-dsd-menu.ts | 72 +++++ .../rh-navigation-secondary-dsd-overlay.css | 22 ++ .../rh-navigation-secondary-dsd-overlay.ts | 21 ++ .../rh-navigation-secondary-dsd.css | 172 +++++++++++ .../rh-navigation-secondary-dsd.ts | 288 ++++++++++++++++++ .../test/rh-navigation-secondary-dsd.e2e.ts | 25 ++ .../test/rh-navigation-secondary-dsd.spec.ts | 21 ++ eleventy.config.ts | 1 + 19 files changed, 1384 insertions(+) create mode 100644 elements/rh-navigation-secondary-dsd/README.md create mode 100644 elements/rh-navigation-secondary-dsd/demo/rh-navigation-secondary-dsd.html create mode 100644 elements/rh-navigation-secondary-dsd/docs/00-overview.md create mode 100644 elements/rh-navigation-secondary-dsd/docs/30-code.md create mode 100644 elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd---backup.css create mode 100644 elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd-dropdown.css create mode 100644 elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd-dropdown.ts create mode 100644 elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd-lightdom.css create mode 100644 elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd-menu-section.css create mode 100644 elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd-menu-section.ts create mode 100644 elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd-menu.css create mode 100644 elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd-menu.ts create mode 100644 elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd-overlay.css create mode 100644 elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd-overlay.ts create mode 100644 elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd.css create mode 100644 elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd.ts create mode 100644 elements/rh-navigation-secondary-dsd/test/rh-navigation-secondary-dsd.e2e.ts create mode 100644 elements/rh-navigation-secondary-dsd/test/rh-navigation-secondary-dsd.spec.ts diff --git a/elements/rh-navigation-secondary-dsd/README.md b/elements/rh-navigation-secondary-dsd/README.md new file mode 100644 index 0000000000..d95308e5b6 --- /dev/null +++ b/elements/rh-navigation-secondary-dsd/README.md @@ -0,0 +1,11 @@ +# Navigation Secondary Dsd +Add a description of the component here. + +## Usage +Describe how best to use this web component along with best practices. + +```html + + + +``` diff --git a/elements/rh-navigation-secondary-dsd/demo/rh-navigation-secondary-dsd.html b/elements/rh-navigation-secondary-dsd/demo/rh-navigation-secondary-dsd.html new file mode 100644 index 0000000000..2f9e182cc1 --- /dev/null +++ b/elements/rh-navigation-secondary-dsd/demo/rh-navigation-secondary-dsd.html @@ -0,0 +1,93 @@ + + + Red Hat Ansible Automation Platform + + + Call to Action + + +
+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sagittis, ex eget congue consectetur, odio lorem tincidunt risus, eget viverra mauris nunc id quam. In eu risus et tortor luctus vehicula. Suspendisse scelerisque posuere est at imperdiet. Aliquam ut tempor ante. Sed ac egestas mauris, in pretium diam. Nam et faucibus felis. Fusce in porttitor sem. Proin commodo mi enim, sed molestie nunc dictum accumsan. Integer a suscipit lorem, a ornare turpis. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam non ornare arcu. Donec vitae rhoncus metus, eget bibendum diam. Ut tincidunt arcu non risus maximus accumsan. Praesent vel purus purus. +

+

+ Vestibulum fringilla augue a lorem maximus imperdiet. Interdum et malesuada fames ac ante ipsum primis in faucibus. Suspendisse et augue sit amet arcu aliquam fringilla. Mauris vehicula, lacus non ultricies laoreet, purus mauris hendrerit quam, nec tincidunt velit leo id neque. Nam consectetur mauris pulvinar mattis posuere. Proin in nisl sed mauris dignissim vehicula. Nulla ut elit sodales, tempor ante ut, rutrum ipsum. Pellentesque tempus in nisl a egestas. Fusce ornare urna et orci vehicula, in euismod libero cursus. Cras non interdum sem, ut condimentum quam. Nam vestibulum quam massa, cursus interdum diam facilisis quis. Praesent ut quam ut augue varius tempor. +

+

+ Nunc ac hendrerit justo, a placerat urna. Nam finibus posuere leo eget vulputate. In lectus nisi, porta vitae orci in, pharetra consectetur leo. Cras ultricies orci et dolor hendrerit tincidunt. Etiam eget mauris neque. Ut a sollicitudin enim. Curabitur at risus ultrices, faucibus elit sit amet, dictum odio. Mauris tristique viverra massa, at bibendum eros vulputate nec. +

+

+ Nam risus sapien, lacinia id massa sit amet, faucibus finibus nisi. Mauris interdum scelerisque sapien. Vestibulum tincidunt ac turpis sed tristique. Integer ac magna quis neque venenatis tristique. Vivamus elementum dolor quis massa sollicitudin tincidunt. Nulla accumsan, nibh eu rutrum porta, libero leo vehicula odio, ut luctus enim nibh tempor mauris. Suspendisse pellentesque erat varius erat efficitur, id condimentum sapien congue. Quisque sit amet magna rutrum, posuere lorem vel, auctor dolor. Donec et magna libero. Mauris ut lacinia magna. Donec dignissim sapien id sem eleifend fermentum. Nullam semper eros ac mattis lobortis. Nulla facilisi. +

+

+ In hac habitasse platea dictumst. Fusce sed tempor metus. Proin condimentum turpis at eros posuere, sed vestibulum turpis faucibus. Sed ornare urna odio, vitae suscipit enim tempor eu. Fusce vitae magna ac augue accumsan facilisis. In hac habitasse platea dictumst. Proin vehicula risus sed odio finibus, at semper lectus consequat. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Integer neque tortor, lacinia sit amet sagittis eget, scelerisque et nunc. Cras congue luctus cursus. Sed vitae ipsum dui. In non risus porttitor libero dapibus interdum at ut lacus. Sed finibus suscipit rhoncus. Aliquam erat volutpat. Sed nec accumsan dolor. +

+
+ + + + + + + + + diff --git a/elements/rh-navigation-secondary-dsd/docs/00-overview.md b/elements/rh-navigation-secondary-dsd/docs/00-overview.md new file mode 100644 index 0000000000..8a507f46a0 --- /dev/null +++ b/elements/rh-navigation-secondary-dsd/docs/00-overview.md @@ -0,0 +1,2 @@ +## When to use + diff --git a/elements/rh-navigation-secondary-dsd/docs/30-code.md b/elements/rh-navigation-secondary-dsd/docs/30-code.md new file mode 100644 index 0000000000..7d8cdf686d --- /dev/null +++ b/elements/rh-navigation-secondary-dsd/docs/30-code.md @@ -0,0 +1 @@ +### System Integration \ No newline at end of file diff --git a/elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd---backup.css b/elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd---backup.css new file mode 100644 index 0000000000..25d0dc0cac --- /dev/null +++ b/elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd---backup.css @@ -0,0 +1,188 @@ +:host { + --_max-height: max-content; + --_min-height: 80px; + --_nav-max-height: var(--_max-height, max-content); + --_nav-min-height: var(--_min-height, 80px); + --_inline-padding: var(--rh-space-lg); + + display: block; + position: sticky; + inset-block-start: 0; + container: navigation-secondary / inline-size; + border-block-end: var(--rh-border-width-sm, 1px) solid transparent; + box-shadow: var(--rh-box-shadow-sm, 0 2px 4px 0 rgba(21, 21, 21, 0.2)); + width: 100%; + min-height: var(--_min-height); + height: var(--_max-height); +} + +#container { + display: block; + width: -webkit-fill-available; + width: -moz-available; + width: fill-available; + + @container navigation-secondary (min-width: 567px) { + --_inline-padding: var(--rh-space-xl); + } + + @container navigation-secondary (min-width: 567px) { + --_inline-padding: var(--rh-space-2xl); + } +} + +#bar { + position: absolute; + display: grid; + /* stylelint-disable-next-line rhds/no-unknown-token-name */ + z-index: var(--rh-navigation-secondary-z-index, 102); + grid-template-areas: + 'logo hamburger' + 'nav nav'; + grid-template-columns: 1fr min-content; + grid-template-rows: + minmax(var(--_nav-min-height), var(--_nav-max-height)) + max-content + max-content; + background-color: var(--rh-color-surface-lighter); + width: -webkit-fill-available; + width: -moz-available; + width: fill-available; +} + +#logo { + grid-area: logo; + display: flex; + align-items: center; + font-family: var(--rh-font-family-heading, RedHatDisplay, 'Red Hat Display', 'Noto Sans Arabic', 'Noto Sans Hebrew', 'Noto Sans JP', 'Noto Sans KR', 'Noto Sans Malayalam', 'Noto Sans SC', 'Noto Sans TC', 'Noto Sans Thai', Helvetica, Arial, sans-serif); + font-weight: var(--rh-font-weight-heading-medium); + line-height: var(--rh-line-height-heading); + padding-inline-start: var(--_inline-padding); + /* stylelint-disable-next-line rhds/no-unknown-token-name */ + max-width: var(--rh-navigation-secondary-logo-max-width, 10em); + + & slot[name='logo']::slotted(*) { + display: flex; + align-items: center; + height: 100%; + color: var(--rh-color-text-primary) !important; + padding-block: var(--rh-space-lg, 16px); + } + + @container (min-width: 567px) { + padding-inline-start: var(--_inline-padding); + } +} + +#nav-container { + grid-area: nav; +} + +#cta { + grid-area: cta; + display: flex; + gap: var(--rh-space-sm); + height: 100%; + align-items: center; + + @container navigation-secondary (min-width: 992px) { + padding-block: 0; + padding-inline: 0 var(--_inline-padding); + } +} + +#hamburger { + display: contents; + + & summary { + --rh-icon-size: 12px; + + cursor: pointer; + grid-area: hamburger; + display: flex; + gap: var(--rh-space-sm); + height: 100%; + align-items: center; + margin-inline-end: var(--_inline-padding); + padding-inline: 16px; + + &::marker { + display: none; + } + } + + & #nav-container { + display: flex; + flex-direction: column; + gap: var(--rh-space-2xl); + width: -webkit-fill-available; + width: -moz-available; + width: fill-available; + + /* the color-surface token has a fallback for no JS scenarios */ + /* stylelint-disable-next-line rhds/token-values */ + background-color: var(--rh-color-surface, var(--rh-color-surface-lightest, #ffffff)); + + & slot[name='nav']::slotted(ul) { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + height: 100%; + } + } + + &[open] { + & summary { + background-color: var(--rh-color-surface-lightest); + + rh-icon { + rotate: 180deg; + } + } + + & #nav-container { + padding-block: var(--rh-space-2xl); + padding-inline: var(--_inline-padding); + box-shadow: var(--rh-box-shadow-sm); + } + } +} + +/* loads when JS isn't loaded and wide viewport */ +#container:not(.compact) { + #bar { + @container navigation-secondary (min-width: 992px) { + grid-template-areas: 'logo nav cta'; + grid-template-columns: max-content 1fr max-content; + grid-template-rows: minmax(var(--_min-height), var(--_max-height)) max-content max-content; + } + } + + #nav-container { + padding-inline: var(--rh-space-md) 0; + + & > slot[name='nav']::slotted(ul) { + display: flex; + flex-direction: row; + gap: 0; + height: 100%; + align-items: center; + margin: 0; + padding: 0; + } + } + + #hamburger { + & rh-surface { + & > slot[name='nav']::slotted(ul) { + gap: 0; + flex-direction: row; + align-items: center; + margin-block: 0; + margin-inline: var(--rh-space-md) 0; + } + } + } +} diff --git a/elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd-dropdown.css b/elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd-dropdown.css new file mode 100644 index 0000000000..ae9a2ca2fd --- /dev/null +++ b/elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd-dropdown.css @@ -0,0 +1,93 @@ +:host { + display: flex; + height: 100%; + align-items: center; +} + +details { + border-inline-start: var(--rh-border-width-lg) solid transparent; + border-inline-end: var(--rh-border-width-sm) solid transparent; + width: 100%; + + & > summary { + --rh-icon-size: 12px; + + cursor: pointer; + list-style: none; + display: flex; + align-items: center; + height: max-content; + color: var(--_nav-link-color) !important; + font-size: var(--rh-font-size-body-text-md, 1rem); + font-weight: 400; + padding: var(--rh-space-lg, 16px) var(--rh-space-xl, 24px); + text-decoration: none !important; + text-align: center; + justify-content: space-between; + + &::marker { + display: none; + } + } + + #details-content { + background-color: var(--rh-color-surface-lightest); + } + + &[open] { + border-inline-start-color: var(--rh-color-text-brand-on-light); + border-inline-end-color: var(--rh-color-border-subtle-on-light); + box-shadow: var(--rh-box-shadow-sm); + + & summary { + rh-icon { + rotate: 180deg; + } + } + } +} + +:host(:defined) { + & details { + @container (min-width: 992px) { + height: 100%; + } + + & > summary { + @container (min-width: 992px) { + display: flex; + height: 100%; + align-items: center; + padding-block: 0; + padding-inline: var(--rh-space-md); + gap: var(--rh-space-sm); + } + } + + & > #details-content { + @container (min-width: 992px) { + position: absolute; + inset-inline: 0; + padding: 0; + box-shadow: var(--rh-box-shadow-sm); + max-height: calc(100vh - var(--rh-space-4xl) - var(--_nav-min-height)); + /* stylelint-disable-next-line rhds/no-unknown-token-name */ + z-index: var(--rh-navigation-secondary-z-index, 103); + } + } + + &[open] { + @container (min-width: 992px) { + border-inline-start-color: transparent; + border-inline-end-color: transparent; + box-shadow: unset; + } + + & summary { + @container (min-width: 992px) { + background-color: var(--rh-color-surface-lightest); + } + } + } + } +} diff --git a/elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd-dropdown.ts b/elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd-dropdown.ts new file mode 100644 index 0000000000..ee1e5a6e9a --- /dev/null +++ b/elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd-dropdown.ts @@ -0,0 +1,94 @@ +import { LitElement, html, isServer } from 'lit'; +import { customElement } from 'lit/decorators/custom-element.js'; +import { state } from 'lit/decorators/state.js'; +import { query } from 'lit/decorators/query.js'; +import { classMap } from 'lit/directives/class-map.js'; + +import { observes } from '@patternfly/pfe-core/decorators.js'; +import { SlotController } from '@patternfly/pfe-core/controllers/slot-controller.js'; +import { ComposedEvent } from '@patternfly/pfe-core/core.js'; + +import '@rhds/elements/rh-surface/rh-surface.js'; + +import styles from './rh-navigation-secondary-dsd-dropdown.css'; + + +export class NavigationSecondaryDsdDropdownEvent extends ComposedEvent { + constructor( + public open: boolean, + public toggle: RhNavigationSecondaryDsdDropdown, + ) { + super('expand-request'); + } +} + +/* Note breaking changes + - Removal of part[container] as the element has switched to a details > summary + - Changing the slot names to reflect this change to details > summary (removal of menu opting for default slot) + - Changing API of expanded to reflect the details summary open attribute + - Changing the exported Event property to reflect the open attribute instead of reflected + - Addition of a public close method + - Addition of a public open method +*/ + +@customElement('rh-navigation-secondary-dsd-dropdown') +export class RhNavigationSecondaryDsdDropdown extends LitElement { + static readonly styles = [styles]; + + private _slots = new SlotController(this, { slots: ['link', 'menu'] }); + + private _highlight = false; + + private _mo = new MutationObserver(this._mutationsCallback.bind(this)); + + @query('details') _details!: HTMLDetailsElement; + + @state() _open = false; + + connectedCallback(): void { + super.connectedCallback(); + if (!isServer) { + this._mo.observe(this, { attributeFilter: ['aria-current'], childList: true, subtree: true }); + } + } + + render() { + const classes = { 'highlight': this._highlight }; + return html` +
+ + + + +
+
+ `; + } + + + private _detailsToggle() { + this._open = this._details.open; + this.dispatchEvent(new NavigationSecondaryDsdDropdownEvent(this._open, this)); + } + + private _mutationsCallback() { + // if the [aria-current-page] link is a child of the default slot ensure the state of the highlight + const [dropdownMenu] = this._slots.getSlotted(''); + this._highlight = dropdownMenu.querySelector('[aria-current="page"]') ? true : false; + this.requestUpdate(); + } + + public close() { + this._details.open = false; + } + + public open() { + this._details.open = true; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'rh-navigation-secondary-dsd-dropdown': RhNavigationSecondaryDsdDropdown; + } +} diff --git a/elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd-lightdom.css b/elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd-lightdom.css new file mode 100644 index 0000000000..12bdec824b --- /dev/null +++ b/elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd-lightdom.css @@ -0,0 +1,72 @@ +rh-navigation-secondary-dsd { + & > [slot='nav'] { + & > li { + max-width: 100%; + border-block-start: var(--rh-border-width-sm) solid var(--rh-color-border-subtle-on-light); + + &:last-of-type { + border-block-end: var(--rh-border-width-sm) solid var(--rh-color-border-subtle-on-light); + } + + & > a { + list-style: none; + display: flex; + align-items: center; + height: 100%; + color: var(--_nav-link-color) !important; + font-size: var(--rh-font-size-body-text-md); + font-weight: 400; + padding: var(--rh-space-lg) var(--rh-space-xl); + text-decoration: none !important; + text-align: center; + border-inline-start: var(--rh-border-width-lg) solid transparent; + border-inline-end: var(--rh-border-width-sm) solid transparent; + border-block-start: var(--rh-border-width-lg) solid transparent; + + &[aria-current='page'] { + border-block-start-color: var(--rh-color-brand-red); + } + } + + & rh-navigation-secondary-dsd-dropdown { + border-block-start: var(--rh-border-width-lg) solid transparent; + + &[aria-current='page'] { + border-block-start-color: var(--rh-color-brand-red); + } + } + } + } + + & > [slot='logo'] { + border-block-start: var(--rh-border-width-lg) solid transparent; + + &[aria-current='page'] { + border-block-start-color: var(--rh-color-brand-red); + } + } + + /* explict if defined only if js loaded */ + &:defined { + & > [slot='nav'] { + & > li { + @container (min-width: 992px) { + border-block-start: none; + height: 100%; + align-items: center; + display: flex; + + &:last-of-type { + border-block-end: none; + } + } + } + + & > a { + @container (min-width: 992px) { + padding: 0; + } + } + } + } +} diff --git a/elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd-menu-section.css b/elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd-menu-section.css new file mode 100644 index 0000000000..b4a94621ec --- /dev/null +++ b/elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd-menu-section.css @@ -0,0 +1,39 @@ +:host { + display: block; +} + +section { + & slot[name='header']::slotted(:is(h1,h2,h3,h4,h5,h6)) { + font-family: var(--rh-font-family-heading, RedHatDisplay, 'Red Hat Display', 'Noto Sans Arabic', 'Noto Sans Hebrew', 'Noto Sans JP', 'Noto Sans KR', 'Noto Sans Malayalam', 'Noto Sans SC', 'Noto Sans TC', 'Noto Sans Thai', Helvetica, Arial, sans-serif) !important; + padding: 0 !important; + font-size: var(--rh-font-size-body-text-md, 1rem) !important; + font-weight: 500 !important; + margin: 0 0 var(--rh-space-lg, 16px) !important; + + @container (min-width: 992px) { + padding: 0 !important; + } + } + + & slot[name='links']::slotted(:is(ul, ol)) { + list-style: none !important; + margin: 0 !important; + padding: 0 !important; + display: flex !important; + flex-direction: column !important; + gap: var(--rh-font-size-body-text-md, 1rem); + + @container (min-width: 992px) { + padding: 0 !important; + margin: 0 !important; + } + } + + & slot[name='cta']::slotted(*) { + padding: var(--rh-space-xl, 24px) 0 0; + + &:last-of-type { + padding: var(--rh-space-xl, 24px) 0; + } + } +} diff --git a/elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd-menu-section.ts b/elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd-menu-section.ts new file mode 100644 index 0000000000..b09fb12fc7 --- /dev/null +++ b/elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd-menu-section.ts @@ -0,0 +1,74 @@ +import { html, LitElement } from 'lit'; +import { customElement } from 'lit/decorators/custom-element.js'; + +import { getRandomId } from '@patternfly/pfe-core/functions/random.js'; +import { Logger } from '@patternfly/pfe-core/controllers/logger.js'; + +import { isHeadingElement } from '../../lib/functions.js'; + +import styles from './rh-navigation-secondary-dsd-menu-section.css'; + +/** + * A menu section which auto upgrades accessibility for headers and sibling list + * @summary 'A menu section which auto upgrades accessibility for headers and sibling list' + * @slot header - Adds a header tag to section, expects `

|

|

|

|

|
` element + * @slot links - Adds a ul tag to section, expects `
    |
      ` element + * @slot cta - Adds a section level CTA, expects `` element + * @csspart container - container,
      element + */ +@customElement('rh-navigation-secondary-dsd-menu-section') +export class RhNavigationSecondaryDsdMenuSection extends LitElement { + static readonly styles = [styles]; + + #logger = new Logger(this); + + connectedCallback(): void { + super.connectedCallback(); + + this.#updateAccessibility(); + } + + render() { + return html` +
      + + + +
      + `; + } + + /** + * Finds all list elements `
        ,
          ` and if the list does not have an + * `aria-labelledby` attribute finds the previousElementSibling header + * `` tags if available assigns an id or uses preexisting id + * to that header, then uses that id to the list on the `aria-labelledby`. + */ + #updateAccessibility() { + const lists = this.querySelectorAll(':is([slot="links"]):is(ul, ol)'); + + for (const list of lists) { + if (!list.hasAttribute('aria-labelledby')) { + const header = isHeadingElement(list.previousElementSibling) ? + list.previousElementSibling + : null; + if (!header) { + return this.#logger.warn( + 'This links set doesn\'t have a valid header associated with it.' + ); + } else { + // add an ID to the header if we need it + header.id ||= getRandomId('rh-navigation-secondary-menu-section'); + // add that header id to the aria-labelledby tag + list.setAttribute('aria-labelledby', header.id); + } + } + } + } +} + +declare global { + interface HTMLElementTagNameMap { + 'rh-navigation-secondary-dsd-menu-section': RhNavigationSecondaryDsdMenuSection; + } +} diff --git a/elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd-menu.css b/elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd-menu.css new file mode 100644 index 0000000000..8b47a26b02 --- /dev/null +++ b/elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd-menu.css @@ -0,0 +1,95 @@ +:host { + display: block; +} + +#container { + color: var(--rh-color-text-primary-on-light, #151515); + background-color: var(--rh-color-surface-lightest, #ffffff); + + &:not(.visible) { + display: none; + } +} + +#sections { + padding: var(--rh-space-xl, 24px); +} + +:host(:defined) { + #container { + &.visible { + @container (min-width: 992px) { + position: absolute; + inset-inline: 0; + padding: + var(--rh-space-4xl, 64px) + var(--rh-space-2xl, 32px) + var(--rh-space-3xl, 48px); + box-shadow: var(--rh-box-shadow-sm, 0 2px 4px 0 rgba(21, 21, 21, 0.2)); + z-index: -1; + max-height: calc(100vh - (var(--rh-space-4xl, 64px)) - var(--_nav-min-height)); + overflow-y: scroll; + } + + @container (min-width: 1200px) { + padding: var(--rh-space-3xl, 48px) var(--rh-space-2xl, 32px); + } + + @container (min-width: 1440px) { + padding: var(--rh-space-3xl, 48px) var(--rh-space-4xl, 64px); + } + } + } + + #sections { + @container (min-width: 992px) { + padding: 0; + max-width: var(--rh-navigation-secondary-menu-content-max-width, 1136px); + margin: auto; + } + } + + #full-width { + @container (min-width: 1600px) { + margin: auto; + } + } +} + +:host([layout='fixed-width']) { + #sections { + padding: var(--rh-space-2xl, 32px); + } +} + +:host([layout='fixed-width']:defined) { + #container { + @container (min-width: 992px) { + position: absolute; + inset: var(--_nav-height) auto auto auto; + margin-top: 0; + padding: 0; + } + } +} + +:host(:not([type='fixed-width'])) { + #sections { + display: grid; + grid-template-columns: + var(--rh-navigation-secondary-menu-section-grid, + repeat(auto-fit, + minmax(15.5rem, 1fr))); + grid-template-rows: auto; + gap: var(--rh-navigation-secondary-menu-section-grid-gap, var(--rh-space-2xl, 32px)); + } +} + +::slotted(:is(ul, ol)) { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: var(--rh-font-size-body-text-md, 1rem); +} diff --git a/elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd-menu.ts b/elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd-menu.ts new file mode 100644 index 0000000000..4145306a9b --- /dev/null +++ b/elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd-menu.ts @@ -0,0 +1,72 @@ +import { html, LitElement } from 'lit'; +import { customElement } from 'lit/decorators/custom-element.js'; +import { property } from 'lit/decorators/property.js'; +import { classMap } from 'lit/directives/class-map.js'; + +import { getRandomId } from '@patternfly/pfe-core/functions/random.js'; + +import styles from './rh-navigation-secondary-dsd-menu.css'; + +/** + * Dropdown menu for secondary nav, available in full-width and fixed-with sizes + * @summary 'Dropdown menu for secondary nav, available in full-width and fixed-with sizes' + * @slot - Optional `` elements or content following [design guidelines](../guidelines/#expandable-tray) + * @csspart container - container - `
          ` element, wrapper for menus + * @csspart full-width - container - `
          ` element, wrapper for full-width menus + * @csspart fixed-width - container - `
          ` element, wrapper for fixed-width menus + * @csspart sections - container - `
          ` element, wrapper for menu sections + * @cssprop [--rh-navigation-secondary-menu-section-grid=repeat(auto-fit, minmax(15.5rem, 1fr))] + * grid-template-columns for menu sections + * @cssprop {} [--rh-navigation-secondary-menu-section-grid-gap=32px] + * grid-gap for menu sections + * @cssprop {} [--rh-navigation-secondary-menu-content-max-width=1136px] + * max-width for menu content + */ +@customElement('rh-navigation-secondary-dsd-menu') +export class RhNavigationSecondaryDsdMenu extends LitElement { + static readonly styles = [styles]; + + /** + * Layout (default: full-width) + * Secondary nav menus by default are always full-width, but can be set to fixed-width for special cases. + */ + @property({ reflect: true }) layout: 'fixed-width' | 'full-width' = 'full-width'; + + // #screenSize = new ScreenSizeController(this); + + /** + * `visible` toggles on click (default: true) + * @deprecated + */ + @property({ type: Boolean, reflect: false }) visible = false; + + connectedCallback() { + super.connectedCallback(); + this.id ||= getRandomId('rh-navigation-secondary-menu'); + } + + render() { + const { visible } = this; + + return html` +
          ${this.layout === 'full-width' ? html` +
          +
          + +
          +
          ` : html` +
          +
          + +
          +
          `} +
          + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'rh-navigation-secondary-dsd-menu': RhNavigationSecondaryDsdMenu; + } +} diff --git a/elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd-overlay.css b/elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd-overlay.css new file mode 100644 index 0000000000..85a2ebd503 --- /dev/null +++ b/elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd-overlay.css @@ -0,0 +1,22 @@ +:host { + position: fixed; + + --_gray-90-rgb: var(--rh-color-gray-90-rgb, 31 31 31); + + background-color: rgb(var(--_gray-90-rgb) / var(--rh-opacity-80, 80%)); + top: 0; + width: 100vw; + height: 100vh; + + /* TODO remove --rh-secondary-nav-overlay-z-index for v2 */ + /* stylelint-disable-next-line */ + z-index: var(--rh-navigation-secondary-overlay-z-index, var(--rh-secondary-nav-overlay-z-index, -1)); +} + +:host([open]) { + display: block; +} + +:host(:not([open])) { + display: none; +} diff --git a/elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd-overlay.ts b/elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd-overlay.ts new file mode 100644 index 0000000000..fb6ff430f8 --- /dev/null +++ b/elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd-overlay.ts @@ -0,0 +1,21 @@ +import { LitElement } from 'lit'; +import { customElement } from 'lit/decorators/custom-element.js'; +import { property } from 'lit/decorators/property.js'; + +import styles from './rh-navigation-secondary-dsd-overlay.css'; + +/** + * @summary An overlay element to cover content with an opacity when navigation is expanded. + */ +@customElement('rh-navigation-secondary-dsd-overlay') +export class RhNavigationSecondaryDsdOverlay extends LitElement { + static readonly styles = [styles]; + + @property({ type: Boolean, reflect: true }) open = false; +} + +declare global { + interface HTMLElementTagNameMap { + 'rh-navigation-secondary-dsd-overlay': RhNavigationSecondaryDsdOverlay; + } +} diff --git a/elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd.css b/elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd.css new file mode 100644 index 0000000000..48b4f8dbe1 --- /dev/null +++ b/elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd.css @@ -0,0 +1,172 @@ +:host { + --_max-height: max-content; + --_min-height: 80px; + --_nav-max-height: var(--_max-height, max-content); + --_nav-min-height: var(--_min-height, 80px); + --_inline-padding: var(--rh-space-lg); + + display: block; + position: sticky; + inset-block-start: 0; + container: navigation-secondary / inline-size; + border-block-end: var(--rh-border-width-sm, 1px) solid transparent; + box-shadow: var(--rh-box-shadow-sm, 0 2px 4px 0 rgba(21, 21, 21, 0.2)); + width: 100%; + min-height: var(--_min-height); + height: var(--_max-height); +} + +#container { + display: block; + width: -webkit-fill-available; + width: -moz-available; + width: fill-available; + + @container navigation-secondary (min-width: 567px) { + --_inline-padding: var(--rh-space-xl); + } + + @container navigation-secondary (min-width: 567px) { + --_inline-padding: var(--rh-space-2xl); + } + + #bar { + position: absolute; + display: grid; + /* stylelint-disable-next-line rhds/no-unknown-token-name */ + z-index: var(--rh-navigation-secondary-z-index, 102); + grid-template-areas: + 'logo hamburger' + 'nav nav'; + grid-template-columns: 1fr min-content; + grid-template-rows: + minmax(var(--_nav-min-height), var(--_nav-max-height)) + max-content + max-content; + background-color: var(--rh-color-surface-lighter); + width: -webkit-fill-available; + width: -moz-available; + width: fill-available; + + #logo { + grid-area: logo; + display: flex; + align-items: center; + font-family: var(--rh-font-family-heading, RedHatDisplay, 'Red Hat Display', 'Noto Sans Arabic', 'Noto Sans Hebrew', 'Noto Sans JP', 'Noto Sans KR', 'Noto Sans Malayalam', 'Noto Sans SC', 'Noto Sans TC', 'Noto Sans Thai', Helvetica, Arial, sans-serif); + font-weight: var(--rh-font-weight-heading-medium); + line-height: var(--rh-line-height-heading); + padding-inline-start: var(--_inline-padding); + /* stylelint-disable-next-line rhds/no-unknown-token-name */ + max-width: var(--rh-navigation-secondary-logo-max-width, 10em); + + & slot[name='logo']::slotted(*) { + display: flex; + align-items: center; + height: 100%; + color: var(--rh-color-text-primary) !important; + padding-block: var(--rh-space-lg, 16px); + } + + @container (min-width: 567px) { + padding-inline-start: var(--_inline-padding); + } + } + + #hamburger { + display: contents; + + & summary { + --rh-icon-size: 12px; + + cursor: pointer; + grid-area: hamburger; + display: flex; + gap: var(--rh-space-sm); + height: 100%; + align-items: center; + margin-inline-end: var(--_inline-padding); + padding-inline: 16px; + + &::marker { + display: none; + } + } + + & #nav-container { + grid-area: nav; + display: flex; + flex-direction: column; + gap: var(--rh-space-2xl); + width: -webkit-fill-available; + width: -moz-available; + width: fill-available; + background-color: var(--rh-color-surface-lightest); + + & slot[name='nav']::slotted(ul) { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + height: 100%; + } + } + + &[open] { + & summary { + background-color: var(--rh-color-surface-lightest); + + rh-icon { + rotate: 180deg; + } + } + + & #nav-container { + padding-block: var(--rh-space-2xl); + padding-inline: var(--_inline-padding); + box-shadow: var(--rh-box-shadow-sm); + } + } + } + } + + /* wide viewport and js enabled */ + &:not(.compact) { + #bar { + grid-template-areas: 'logo nav'; + grid-template-columns: max-content 1fr; + grid-template-rows: minmax(var(--_min-height), var(--_max-height)) max-content max-content; + column-gap: var(--rh-space-2xl); + + #hamburger { + & summary { + display: none; + } + + #nav-container { + flex-direction: row; + background-color: var(--rh-color-surface-lighter); + justify-content: space-between; + + & slot[name='nav']::slotted(ul) { + flex-direction: row; + } + } + + #cta { + display: flex; + height: 100%; + align-content: center; + padding-inline-end: var(--_inline-padding); + } + + &[open] { + #nav-container { + padding: 0; + box-shadow: none; + } + } + } + } + } +} diff --git a/elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd.ts b/elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd.ts new file mode 100644 index 0000000000..c66ebd7b0a --- /dev/null +++ b/elements/rh-navigation-secondary-dsd/rh-navigation-secondary-dsd.ts @@ -0,0 +1,288 @@ +import { LitElement, html, isServer } from 'lit'; +import { customElement } from 'lit/decorators/custom-element.js'; +import { state } from 'lit/decorators/state.js'; +import { query } from 'lit/decorators/query.js'; +import { property } from 'lit/decorators/property.js'; +import { classMap } from 'lit/directives/class-map.js'; + +import { ComposedEvent } from '@patternfly/pfe-core/core.js'; +import { InternalsController } from '@patternfly/pfe-core/controllers/internals-controller.js'; + +import { DirController } from '../../lib/DirController.js'; +import { colorContextProvider, type ColorPalette } from '../../lib/context/color/provider.js'; +import { colorContextConsumer, type ColorTheme } from '../../lib/context/color/consumer.js'; + +import '@rhds/elements/rh-surface/rh-surface.js'; + +import { + RhNavigationSecondaryDsdDropdown, + NavigationSecondaryDsdDropdownEvent, +} from './rh-navigation-secondary-dsd-dropdown.js'; +import './rh-navigation-secondary-dsd-menu.js'; +import './rh-navigation-secondary-dsd-menu-section.js'; +import './rh-navigation-secondary-dsd-overlay.js'; + +import styles from './rh-navigation-secondary-dsd.css'; + +export class SecondaryNavDsdOverlayChangeEvent extends ComposedEvent { + constructor( + public open: boolean, + public toggle: HTMLElement + ) { + super('overlay-change'); + } +} + +export type NavPalette = Extract; + +/** + * Navigation Secondary Dsd + * @slot - Place element content here + */ +@customElement('rh-navigation-secondary-dsd') +export class RhNavigationSecondaryDsd extends LitElement { + static readonly styles = [styles]; + + private static instances = new Set(); + + static { + if (!isServer) { + globalThis.addEventListener('keyup', (event: KeyboardEvent) => { + const { instances } = RhNavigationSecondaryDsd; + for (const instance of instances) { + instance._onKeyup(event); + } + }, { capture: false }); + } + } + + static isDropdown(element: Element | null): element is RhNavigationSecondaryDsdDropdown { + return element instanceof RhNavigationSecondaryDsdDropdown; + } + + @state() + private _compact = true; + + @state() + private _overlayState = false; + + @state() + private _hamburgerState = false; + + @query('#hamburger') + private _hamburger!: HTMLDetailsElement; + + private ro?: ResizeObserver; + + private _internals = InternalsController.of(this, { role: 'navigation' }); + + private _dir = new DirController(this); + + /** + * Color palette darker | lighter (default: lighter) + */ + @colorContextProvider() + @property({ reflect: true, attribute: 'color-palette' }) colorPalette: NavPalette = 'lighter'; + + /** + * Sets color theme based on parent context + */ + @colorContextConsumer() private on?: ColorTheme; + + private _openDropdowns = new Set(); + + /** + * Customize the default `aria-label` on the `