From d8c7bbbaf9e11f8caa8d7fd791e0ce21db325442 Mon Sep 17 00:00:00 2001 From: Sean Wu Date: Mon, 11 Mar 2024 22:11:30 +1100 Subject: [PATCH] feat: navigation components allow composition (#242) * update menu style * wip update styles for main nav and nav link * clean up * rename parseItems to parseJsonProp * clean up function update * update current link style * data driven nav link * rename go-main-nav to go-nav-bar * refactor go-nav-bar into go-nav-item * nav item add columns option * add columns option to nav item object * refactor go-nav-item, go-nav-submenu-trigger and go-nav-submenu to allow composability * header slot check * footer update * fix nav list in footer * update go-nav-bar examples --- packages/core/package.json | 2 +- packages/core/src/components.d.ts | 236 ++++++---- .../form/go-datepicker/go-datepicker.tsx | 4 +- .../src/components/form/go-select/utils.ts | 4 +- .../go-breadcrumbs/go-breadcrumbs.tsx | 4 +- .../src/components/go-dropdown-menu/readme.md | 17 +- .../components/go-dropdown/go-dropdown.tsx | 10 +- .../core/src/components/go-icon/go-icon.tsx | 10 +- .../src/components/go-tabs/go-tablist.scss | 2 +- .../navigation/go-main-nav/go-main-nav.scss | 225 ---------- .../navigation/go-main-nav/go-main-nav.tsx | 242 ----------- .../navigation/go-main-nav/readme.md | 25 -- .../go-main-nav/usage/go-main-nav.md | 163 ------- .../navigation/go-nav-bar/go-nav-bar.scss | 410 ++++++++++++++++++ .../navigation/go-nav-bar/go-nav-bar.tsx | 65 +++ .../navigation/go-nav-bar/go-nav-item.tsx | 135 ++++++ .../go-nav-bar/go-nav-submenu-trigger.tsx | 62 +++ .../navigation/go-nav-bar/go-nav-submenu.tsx | 81 ++++ .../navigation/go-nav-bar/readme.md | 50 +++ .../test/go-main-nav.e2e.ts | 6 +- .../navigation/go-nav-bar/usage/composable.md | 33 ++ .../navigation/go-nav-bar/usage/go-nav-bar.md | 153 +++++++ .../go-nav-drawer/go-nav-drawer.tsx | 13 +- .../navigation/go-nav-drawer/readme.md | 2 +- .../navigation/go-nav-link/go-nav-link.scss | 171 +++++++- .../navigation/go-nav-link/go-nav-link.tsx | 57 ++- .../navigation/go-nav-link/readme.md | 3 +- .../navigation/go-nav-list/go-nav-list.scss | 69 +-- .../navigation/go-nav-list/go-nav-list.tsx | 90 ++-- packages/core/src/index.html | 82 ---- packages/core/src/interfaces/nav-item.ts | 5 +- .../src/patterns/go-footer/go-footer.scss | 67 +-- .../core/src/patterns/go-footer/go-footer.tsx | 32 +- .../src/patterns/go-footer/usage/footer.md | 1 + .../patterns/go-header-bar/go-header-bar.tsx | 6 +- .../go-header-bar/usage/header-bar.md | 2 +- packages/core/src/utils/dom.ts | 25 +- packages/core/src/utils/helper.ts | 18 +- .../src/components/stencil-generated/index.ts | 3 +- packages/vue/src/components.ts | 23 +- 40 files changed, 1552 insertions(+), 1056 deletions(-) delete mode 100644 packages/core/src/components/navigation/go-main-nav/go-main-nav.scss delete mode 100644 packages/core/src/components/navigation/go-main-nav/go-main-nav.tsx delete mode 100644 packages/core/src/components/navigation/go-main-nav/readme.md delete mode 100644 packages/core/src/components/navigation/go-main-nav/usage/go-main-nav.md create mode 100644 packages/core/src/components/navigation/go-nav-bar/go-nav-bar.scss create mode 100644 packages/core/src/components/navigation/go-nav-bar/go-nav-bar.tsx create mode 100644 packages/core/src/components/navigation/go-nav-bar/go-nav-item.tsx create mode 100644 packages/core/src/components/navigation/go-nav-bar/go-nav-submenu-trigger.tsx create mode 100644 packages/core/src/components/navigation/go-nav-bar/go-nav-submenu.tsx create mode 100644 packages/core/src/components/navigation/go-nav-bar/readme.md rename packages/core/src/components/navigation/{go-main-nav => go-nav-bar}/test/go-main-nav.e2e.ts (78%) create mode 100644 packages/core/src/components/navigation/go-nav-bar/usage/composable.md create mode 100644 packages/core/src/components/navigation/go-nav-bar/usage/go-nav-bar.md diff --git a/packages/core/package.json b/packages/core/package.json index 185d3fc1..e31815c8 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -19,7 +19,7 @@ "clean": "rm -rf dist", "build": "pnpm run clean && stencil build", "build.component-docs": "stencil build --docs-readme --docs-json", - "start": "stencil build --dev --watch --serve", + "start": "pnpm run clean && stencil build --dev --watch --serve", "watch.components": "stencil build --docs-json --watch", "test.spec": "stencil test --spec", "test.spec.watch": "stencil test --spec --watchAll", diff --git a/packages/core/src/components.d.ts b/packages/core/src/components.d.ts index 00608aba..fcea1e40 100644 --- a/packages/core/src/components.d.ts +++ b/packages/core/src/components.d.ts @@ -5,7 +5,7 @@ * It contains typing information for all components that exist in this project. */ import { HTMLStencilElement, JSXBase } from "@stencil/core/internal"; -import { BannerVariants, Breakpoints, ColorVariants, GoChangeEventDetail, INavItem } from "./interfaces"; +import { BannerVariants, Breakpoints, ColorVariants, GoChangeEventDetail, IIcon, INavItem, UnknownObject } from "./interfaces"; import { ChipVariants } from "./interfaces/variants"; import { TocProps } from "./components/go-toc/go-toc"; import { SidebarPosition } from "./patterns/go-content-layout/go-content-layout"; @@ -13,9 +13,9 @@ import { Theme } from "./components/go-dark-mode/go-dark-mode"; import { DuetDatePickerProps } from "./components/form/go-datepicker/duet-date-picker"; import { BoxiconVariants, FontAwesomeVariants, MaterialIconVariants } from "./components/go-icon/go-icon"; import { Options } from "markdown-it"; -import { FieldValue, GoChangeEventDetail as GoChangeEventDetail1, SelectOption } from "./interfaces/index"; +import { FieldValue, GoChangeEventDetail as GoChangeEventDetail1, INavItem as INavItem1, SelectOption } from "./interfaces/index"; import { ActiveTab, ActiveTabWithPanel, JustifyOption, TabIconPosition, TabItem } from "./components/go-tabs/tabs.type"; -export { BannerVariants, Breakpoints, ColorVariants, GoChangeEventDetail, INavItem } from "./interfaces"; +export { BannerVariants, Breakpoints, ColorVariants, GoChangeEventDetail, IIcon, INavItem, UnknownObject } from "./interfaces"; export { ChipVariants } from "./interfaces/variants"; export { TocProps } from "./components/go-toc/go-toc"; export { SidebarPosition } from "./patterns/go-content-layout/go-content-layout"; @@ -23,7 +23,7 @@ export { Theme } from "./components/go-dark-mode/go-dark-mode"; export { DuetDatePickerProps } from "./components/form/go-datepicker/duet-date-picker"; export { BoxiconVariants, FontAwesomeVariants, MaterialIconVariants } from "./components/go-icon/go-icon"; export { Options } from "markdown-it"; -export { FieldValue, GoChangeEventDetail as GoChangeEventDetail1, SelectOption } from "./interfaces/index"; +export { FieldValue, GoChangeEventDetail as GoChangeEventDetail1, INavItem as INavItem1, SelectOption } from "./interfaces/index"; export { ActiveTab, ActiveTabWithPanel, JustifyOption, TabIconPosition, TabItem } from "./components/go-tabs/tabs.type"; export namespace Components { interface GoAccordion { @@ -662,14 +662,14 @@ export namespace Components { "labelId"?: string; } interface GoFooter { - /** - * Dark theme footer - */ - "dark"?: boolean; /** * Navigation links to be displayed. */ "links": INavItem[] | string; + /** + * Heading tag for nav list + */ + "listHeadingTag"?: string; /** * Number of navigation columns */ @@ -699,7 +699,7 @@ export namespace Components { } interface GoHeaderBar { /** - * Controls at which breakpoint the mobile menu (go-nav-drawer) becomes main nav menu (go-main-nav) + * Controls at which breakpoint the mobile menu (go-nav-drawer) becomes main nav menu (go-nav-bar) */ "breakpoint": Breakpoints; } @@ -830,21 +830,6 @@ export namespace Components { */ "target"?: '_blank' | '_self' | '_parent' | '_top'; } - interface GoMainNav { - /** - * Initialise the menu - * @param items menu items to be rendered - */ - "init": (newItems: INavItem[] | string) => Promise; - /** - * Navigation items to be rendered if provided, slot content will not be rendered. - */ - "items"?: INavItem[] | string; - /** - * Label for the navigation. This helps screen reader users to quickly navigate to teh correct nav landmark - */ - "label": string; - } interface GoMd { /** * Markdown content to be rendered @@ -871,6 +856,21 @@ export namespace Components { */ "useGoUi"?: boolean; } + interface GoNavBar { + /** + * Navigation items to be rendered if provided, slot content will not be rendered. + */ + "items"?: INavItem[] | string; + /** + * Label for the navigation. This helps screen reader users to quickly navigate to teh correct nav landmark + */ + "label": string; + /** + * Load nav items + * @param items menu items to be rendered + */ + "loadNavItems": (newItems: INavItem[] | string) => Promise; + } interface GoNavDrawer { /** * keep track of open state of drawer @@ -901,42 +901,48 @@ export namespace Components { "position"?: 'left' | 'right'; "toggle": () => Promise; } + interface GoNavItem { + "item": INavItem | string; + } interface GoNavLink { /** * full width */ "block": boolean; + "description"?: string; + "icon"?: IIcon | string; + "isCurrent"?: boolean; /** * navigation item */ - "item": INavItem; + "item"?: INavItem | string; + "label"?: string; + "linkAttrs"?: UnknownObject | string; /** * show arrow at the end of the link */ - "showArrow": boolean; + "showArrow"?: boolean; + "url"?: string; } interface GoNavList { /** * Make the list full width */ "block": boolean; - /** - * Make all sub lists (if any) expanded by default - */ - "expandSubLists": boolean; - /** - * Heading text - */ - "heading": string; - /** - * Heading navigation item - */ - "headingItem": INavItem | string; /** * list of navigation items to be displayed uuuuuu */ "items": INavItem[] | string; } + interface GoNavSubmenu { + "close": () => Promise; + "columns": number; + "open": () => Promise; + "toggle": () => Promise; + } + interface GoNavSubmenuTrigger { + "controls": string; + } interface GoOverlay { "active": boolean; "close": () => Promise; @@ -1363,10 +1369,6 @@ export interface GoDropdownItemCustomEvent extends CustomEvent { detail: T; target: HTMLGoDropdownItemElement; } -export interface GoMainNavCustomEvent extends CustomEvent { - detail: T; - target: HTMLGoMainNavElement; -} export interface GoMdCustomEvent extends CustomEvent { detail: T; target: HTMLGoMdElement; @@ -1375,10 +1377,18 @@ export interface GoNavDrawerCustomEvent extends CustomEvent { detail: T; target: HTMLGoNavDrawerElement; } +export interface GoNavItemCustomEvent extends CustomEvent { + detail: T; + target: HTMLGoNavItemElement; +} export interface GoNavLinkCustomEvent extends CustomEvent { detail: T; target: HTMLGoNavLinkElement; } +export interface GoNavSubmenuCustomEvent extends CustomEvent { + detail: T; + target: HTMLGoNavSubmenuElement; +} export interface GoOverlayCustomEvent extends CustomEvent { detail: T; target: HTMLGoOverlayElement; @@ -1676,23 +1686,6 @@ declare global { prototype: HTMLGoLinkElement; new (): HTMLGoLinkElement; }; - interface HTMLGoMainNavElementEventMap { - "navigate": any; - } - interface HTMLGoMainNavElement extends Components.GoMainNav, HTMLStencilElement { - addEventListener(type: K, listener: (this: HTMLGoMainNavElement, ev: GoMainNavCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; - removeEventListener(type: K, listener: (this: HTMLGoMainNavElement, ev: GoMainNavCustomEvent) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; - } - var HTMLGoMainNavElement: { - prototype: HTMLGoMainNavElement; - new (): HTMLGoMainNavElement; - }; interface HTMLGoMdElementEventMap { "init": any; "rendered": any; @@ -1711,6 +1704,12 @@ declare global { prototype: HTMLGoMdElement; new (): HTMLGoMdElement; }; + interface HTMLGoNavBarElement extends Components.GoNavBar, HTMLStencilElement { + } + var HTMLGoNavBarElement: { + prototype: HTMLGoNavBarElement; + new (): HTMLGoNavBarElement; + }; interface HTMLGoNavDrawerElementEventMap { "open": void; "close": void; @@ -1730,6 +1729,24 @@ declare global { prototype: HTMLGoNavDrawerElement; new (): HTMLGoNavDrawerElement; }; + interface HTMLGoNavItemElementEventMap { + "navigate": any; + "submenutoggle": any; + } + interface HTMLGoNavItemElement extends Components.GoNavItem, HTMLStencilElement { + addEventListener(type: K, listener: (this: HTMLGoNavItemElement, ev: GoNavItemCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; + removeEventListener(type: K, listener: (this: HTMLGoNavItemElement, ev: GoNavItemCustomEvent) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; + } + var HTMLGoNavItemElement: { + prototype: HTMLGoNavItemElement; + new (): HTMLGoNavItemElement; + }; interface HTMLGoNavLinkElementEventMap { "navigate": any; } @@ -1753,6 +1770,29 @@ declare global { prototype: HTMLGoNavListElement; new (): HTMLGoNavListElement; }; + interface HTMLGoNavSubmenuElementEventMap { + "toggle": any; + } + interface HTMLGoNavSubmenuElement extends Components.GoNavSubmenu, HTMLStencilElement { + addEventListener(type: K, listener: (this: HTMLGoNavSubmenuElement, ev: GoNavSubmenuCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; + removeEventListener(type: K, listener: (this: HTMLGoNavSubmenuElement, ev: GoNavSubmenuCustomEvent) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; + } + var HTMLGoNavSubmenuElement: { + prototype: HTMLGoNavSubmenuElement; + new (): HTMLGoNavSubmenuElement; + }; + interface HTMLGoNavSubmenuTriggerElement extends Components.GoNavSubmenuTrigger, HTMLStencilElement { + } + var HTMLGoNavSubmenuTriggerElement: { + prototype: HTMLGoNavSubmenuTriggerElement; + new (): HTMLGoNavSubmenuTriggerElement; + }; interface HTMLGoOverlayElementEventMap { "overlayOpen": void; "overlayClose": void; @@ -1928,11 +1968,14 @@ declare global { "go-icon": HTMLGoIconElement; "go-input": HTMLGoInputElement; "go-link": HTMLGoLinkElement; - "go-main-nav": HTMLGoMainNavElement; "go-md": HTMLGoMdElement; + "go-nav-bar": HTMLGoNavBarElement; "go-nav-drawer": HTMLGoNavDrawerElement; + "go-nav-item": HTMLGoNavItemElement; "go-nav-link": HTMLGoNavLinkElement; "go-nav-list": HTMLGoNavListElement; + "go-nav-submenu": HTMLGoNavSubmenuElement; + "go-nav-submenu-trigger": HTMLGoNavSubmenuTriggerElement; "go-overlay": HTMLGoOverlayElement; "go-progress": HTMLGoProgressElement; "go-radio": HTMLGoRadioElement; @@ -2578,14 +2621,14 @@ declare namespace LocalJSX { "labelId"?: string; } interface GoFooter { - /** - * Dark theme footer - */ - "dark"?: boolean; /** * Navigation links to be displayed. */ "links"?: INavItem[] | string; + /** + * Heading tag for nav list + */ + "listHeadingTag"?: string; /** * Number of navigation columns */ @@ -2615,7 +2658,7 @@ declare namespace LocalJSX { } interface GoHeaderBar { /** - * Controls at which breakpoint the mobile menu (go-nav-drawer) becomes main nav menu (go-main-nav) + * Controls at which breakpoint the mobile menu (go-nav-drawer) becomes main nav menu (go-nav-bar) */ "breakpoint"?: Breakpoints; } @@ -2746,17 +2789,6 @@ declare namespace LocalJSX { */ "target"?: '_blank' | '_self' | '_parent' | '_top'; } - interface GoMainNav { - /** - * Navigation items to be rendered if provided, slot content will not be rendered. - */ - "items"?: INavItem[] | string; - /** - * Label for the navigation. This helps screen reader users to quickly navigate to teh correct nav landmark - */ - "label"?: string; - "onNavigate"?: (event: GoMainNavCustomEvent) => void; - } interface GoMd { /** * Markdown content to be rendered @@ -2785,6 +2817,16 @@ declare namespace LocalJSX { */ "useGoUi"?: boolean; } + interface GoNavBar { + /** + * Navigation items to be rendered if provided, slot content will not be rendered. + */ + "items"?: INavItem[] | string; + /** + * Label for the navigation. This helps screen reader users to quickly navigate to teh correct nav landmark + */ + "label"?: string; + } interface GoNavDrawer { /** * keep track of open state of drawer @@ -2819,43 +2861,49 @@ declare namespace LocalJSX { */ "position"?: 'left' | 'right'; } + interface GoNavItem { + "item"?: INavItem | string; + "onNavigate"?: (event: GoNavItemCustomEvent) => void; + "onSubmenutoggle"?: (event: GoNavItemCustomEvent) => void; + } interface GoNavLink { /** * full width */ "block"?: boolean; + "description"?: string; + "icon"?: IIcon | string; + "isCurrent"?: boolean; /** * navigation item */ - "item"?: INavItem; + "item"?: INavItem | string; + "label"?: string; + "linkAttrs"?: UnknownObject | string; "onNavigate"?: (event: GoNavLinkCustomEvent) => void; /** * show arrow at the end of the link */ "showArrow"?: boolean; + "url"?: string; } interface GoNavList { /** * Make the list full width */ "block"?: boolean; - /** - * Make all sub lists (if any) expanded by default - */ - "expandSubLists"?: boolean; - /** - * Heading text - */ - "heading"?: string; - /** - * Heading navigation item - */ - "headingItem"?: INavItem | string; /** * list of navigation items to be displayed uuuuuu */ "items"?: INavItem[] | string; } + interface GoNavSubmenu { + "columns"?: number; + "onToggle"?: (event: GoNavSubmenuCustomEvent) => void; + } + interface GoNavSubmenuTrigger { + "controls"?: string; + } interface GoOverlay { "active"?: boolean; /** @@ -3290,11 +3338,14 @@ declare namespace LocalJSX { "go-icon": GoIcon; "go-input": GoInput; "go-link": GoLink; - "go-main-nav": GoMainNav; "go-md": GoMd; + "go-nav-bar": GoNavBar; "go-nav-drawer": GoNavDrawer; + "go-nav-item": GoNavItem; "go-nav-link": GoNavLink; "go-nav-list": GoNavList; + "go-nav-submenu": GoNavSubmenu; + "go-nav-submenu-trigger": GoNavSubmenuTrigger; "go-overlay": GoOverlay; "go-progress": GoProgress; "go-radio": GoRadio; @@ -3350,11 +3401,14 @@ declare module "@stencil/core" { "go-icon": LocalJSX.GoIcon & JSXBase.HTMLAttributes; "go-input": LocalJSX.GoInput & JSXBase.HTMLAttributes; "go-link": LocalJSX.GoLink & JSXBase.HTMLAttributes; - "go-main-nav": LocalJSX.GoMainNav & JSXBase.HTMLAttributes; "go-md": LocalJSX.GoMd & JSXBase.HTMLAttributes; + "go-nav-bar": LocalJSX.GoNavBar & JSXBase.HTMLAttributes; "go-nav-drawer": LocalJSX.GoNavDrawer & JSXBase.HTMLAttributes; + "go-nav-item": LocalJSX.GoNavItem & JSXBase.HTMLAttributes; "go-nav-link": LocalJSX.GoNavLink & JSXBase.HTMLAttributes; "go-nav-list": LocalJSX.GoNavList & JSXBase.HTMLAttributes; + "go-nav-submenu": LocalJSX.GoNavSubmenu & JSXBase.HTMLAttributes; + "go-nav-submenu-trigger": LocalJSX.GoNavSubmenuTrigger & JSXBase.HTMLAttributes; "go-overlay": LocalJSX.GoOverlay & JSXBase.HTMLAttributes; "go-progress": LocalJSX.GoProgress & JSXBase.HTMLAttributes; "go-radio": LocalJSX.GoRadio & JSXBase.HTMLAttributes; diff --git a/packages/core/src/components/form/go-datepicker/go-datepicker.tsx b/packages/core/src/components/form/go-datepicker/go-datepicker.tsx index 6df7d725..60ebfb9c 100644 --- a/packages/core/src/components/form/go-datepicker/go-datepicker.tsx +++ b/packages/core/src/components/form/go-datepicker/go-datepicker.tsx @@ -1,7 +1,7 @@ import { Component, h, Prop, Element, State, Watch, EventEmitter, Event } from '@stencil/core'; import { uniqueId } from 'lodash-es'; import '@duetds/date-picker'; -import { fieldSlotNames, loadFieldProps, loadFieldSlots, parseItems } from '../../../utils'; +import { fieldSlotNames, loadFieldProps, loadFieldSlots, parseJsonProp } from '../../../utils'; import { FormFieldProps, GoChangeEventDetail } from '../../../interfaces'; import { DuetDatePickerProps } from './duet-date-picker'; import dayjs from 'dayjs'; @@ -56,7 +56,7 @@ export class GoDatepicker implements FormFieldProps { @Watch('options') loadOptions() { - this.parsedOptions = parseItems(this.options); + this.parsedOptions = parseJsonProp(this.options); const dateFormat = this.format; this.parsedOptions = { ...this.parsedOptions, diff --git a/packages/core/src/components/form/go-select/utils.ts b/packages/core/src/components/form/go-select/utils.ts index bbc66846..2e2f7f09 100644 --- a/packages/core/src/components/form/go-select/utils.ts +++ b/packages/core/src/components/form/go-select/utils.ts @@ -4,7 +4,7 @@ */ import { SelectOption } from '@/interfaces'; -import { parseItems } from '@/utils'; +import { parseJsonProp } from '@/utils'; export enum Keys { Backspace = 'Backspace', @@ -190,7 +190,7 @@ export const parseSelectOptions = (options: string | string[] | SelectOption[]): options = options.split(','); } - const parsedOptions = parseItems(options); + const parsedOptions = parseJsonProp(options); if (parsedOptions) { // format parsed options into SelectOption[] return parsedOptions.map((option) => diff --git a/packages/core/src/components/go-breadcrumbs/go-breadcrumbs.tsx b/packages/core/src/components/go-breadcrumbs/go-breadcrumbs.tsx index f73be543..d82013c5 100644 --- a/packages/core/src/components/go-breadcrumbs/go-breadcrumbs.tsx +++ b/packages/core/src/components/go-breadcrumbs/go-breadcrumbs.tsx @@ -1,6 +1,6 @@ import { Component, Host, h, Element, Prop, State, Watch } from '@stencil/core'; import { INavItem } from '../../interfaces'; -import { parseItems } from '../../utils'; +import { parseJsonProp } from '../../utils'; @Component({ tag: 'go-breadcrumbs', @@ -38,7 +38,7 @@ export class GoBreadcrumb { } getItems(items: INavItem[] | string) { - const navItems = parseItems(items); + const navItems = parseJsonProp(items); if (!this.hideCurrent) { return navItems; } diff --git a/packages/core/src/components/go-dropdown-menu/readme.md b/packages/core/src/components/go-dropdown-menu/readme.md index ee1e55ce..2db6f249 100644 --- a/packages/core/src/components/go-dropdown-menu/readme.md +++ b/packages/core/src/components/go-dropdown-menu/readme.md @@ -12,9 +12,10 @@ title: Dropdown menu ::: info - For a list of navigational items, please refer to the following components: - - [Main navigation](/docs/components/navigation/go-main-nav) - - [Nav drawer](/docs/components/navigation/go-nav-drawer) +For a list of navigational items, please refer to the following components: + +- [Nav bar](/docs/components/navigation/go-nav-bar) +- [Nav drawer](/docs/components/navigation/go-nav-drawer) ::: @@ -22,31 +23,32 @@ title: Dropdown menu A dropdown menu requires a trigger element, this can be set by using `trigger-selector` similar to [`go-dropdown`](go-dropdown). - ## Persistent A dropdown menu can be `persistent` in order to allow users to interact with the widget continuously. - ## Accessibility WAI [Menu button](https://www.w3.org/WAI/ARIA/apg/patterns/menubutton/) pattern aligns closely to the `go-dropdown-menu` component. ### Menu button + - Use a button element (`go-button`, `button` or `input type="button"`) as the trigger element. - `aria-haspopup` will be set automatically on the trigger element to the id of the menu. - When the menu is displayed, the trigger element has `aria-expanded` set to true. When the menu is hidden, `aria-expanded` is removed. If `aria-expanded` is specified when the menu is hidden, it is set to false. - The trigger element has a value specified for `aria-controls` that refers to the element with role menu. - #### Keyboard + When trigger button has focus: + - Enter: opens the menu and places focus on the first menu item. - Space: opens the menu and places focus on the first menu item. - Down Arrow: opens the menu and moves focus to the first menu item. - Up Arrow: opens the menu and moves focus to the last menu item. When focus is inside the dropdown menu: + - Down Arrow: move focus to the next menu item, if current item is the last one, move focus to the first menu item. - Up Arrow: move focus to the previous menu item, if current item is the first one, move focus to the last menu item. - Esc: closes the menu. @@ -56,10 +58,9 @@ When focus is inside the dropdown menu: The keyboard interaction of the menu follows the WAI guideline for [Keyboard navigation inside components](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_general_within), which means Tab and Shift + Tab do not move focus between menu items, instead, they are only used to focus in and out of the dropdown menu. > the tab sequence should include only one focusable element of a composite UI component. Once a composite contains focus, keys other than Tab and Shift + Tab enable the user to move focus among its focusable elements +> > - [Developing a Keyboard Interface](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_general_within) by WAI APG - - ## Demo diff --git a/packages/core/src/components/go-dropdown/go-dropdown.tsx b/packages/core/src/components/go-dropdown/go-dropdown.tsx index e6d0d238..257243a3 100644 --- a/packages/core/src/components/go-dropdown/go-dropdown.tsx +++ b/packages/core/src/components/go-dropdown/go-dropdown.tsx @@ -1,7 +1,7 @@ import { Component, Host, h, Element, Prop, Method, Watch, Event, EventEmitter } from '@stencil/core'; import { uniqueId, debounce } from 'lodash-es'; import { computePosition, offset, flip, autoUpdate } from '@floating-ui/dom'; -import { onClickOutside, removeClickOutsideListener } from '../../utils'; +import { onClickOutside } from '../../utils'; @Component({ tag: 'go-dropdown', @@ -105,7 +105,7 @@ export class GoDropdown { private escapeHandler; private focusOutHandler; - private clickOutHandler; + private clickOutsideCleanUp; async componentDidLoad() { if (!this.triggerEl) { @@ -139,8 +139,8 @@ export class GoDropdown { if (this.focusOutHandler) { this.el.removeEventListener('focusout', this.focusOutHandler); } - if (this.clickOutHandler) { - removeClickOutsideListener(this.clickOutHandler); + if (this.clickOutsideCleanUp) { + this.clickOutsideCleanUp(); } if (this.cleanupAutoUpdate) { this.cleanupAutoUpdate(); @@ -157,7 +157,7 @@ export class GoDropdown { this.triggerEl.addEventListener('click', () => this.toggle()); } - this.clickOutHandler = onClickOutside(this.el, (e) => { + this.clickOutsideCleanUp = onClickOutside(this.el, (e) => { if (!this.triggerEl.contains(e.target as Node) && this.isActive) { this.close(); } diff --git a/packages/core/src/components/go-icon/go-icon.tsx b/packages/core/src/components/go-icon/go-icon.tsx index 4ae4b85d..8b71dd72 100644 --- a/packages/core/src/components/go-icon/go-icon.tsx +++ b/packages/core/src/components/go-icon/go-icon.tsx @@ -1,7 +1,11 @@ import { Component, Host, h, Element, Prop } from '@stencil/core'; import { IconProps } from '../../interfaces'; -import { inheritAttributes } from '../../utils/helper'; -export type MaterialIconVariants = `material-icons` | `material-icons-outlined` | `material-icons-round` | `material-icons-sharp`; +import { $attrs } from '../../utils/helper'; +export type MaterialIconVariants = + | `material-icons` + | `material-icons-outlined` + | `material-icons-round` + | `material-icons-sharp`; /** * https://fontawesome.com/v5.15/how-to-use/on-the-web/referencing-icons/basic-use */ @@ -48,7 +52,7 @@ export class GoIcon implements IconProps { private attrs = {} as any; componentWillLoad() { - this.attrs = inheritAttributes(this.el, [], false); + this.attrs = $attrs.bind(this)(false); } render() { diff --git a/packages/core/src/components/go-tabs/go-tablist.scss b/packages/core/src/components/go-tabs/go-tablist.scss index b161948c..8d7f1534 100644 --- a/packages/core/src/components/go-tabs/go-tablist.scss +++ b/packages/core/src/components/go-tabs/go-tablist.scss @@ -41,7 +41,7 @@ go-tablist { /** @prop --tab-active-color: Text color for active tab - - default: var(--go-color-primary-800) + - default: var(--go-color-primary-600) */ --tab-active-color: var(--go-color-primary-600); /** diff --git a/packages/core/src/components/navigation/go-main-nav/go-main-nav.scss b/packages/core/src/components/navigation/go-main-nav/go-main-nav.scss deleted file mode 100644 index 5b5f985a..00000000 --- a/packages/core/src/components/navigation/go-main-nav/go-main-nav.scss +++ /dev/null @@ -1,225 +0,0 @@ -go-main-nav { - /** - @prop --nav-border-top: - top border - - default: 1px solid var(--go-color-neutral-200) - */ - --nav-border-top: 1px solid var(--go-color-neutral-200); - /** - @prop --nav-border-bottom: - bottom border - - default: 1px solid var(--go-color-neutral-200) - */ - --nav-border-bottom: 1px solid var(--go-color-neutral-200); - --nav-bg-color: transparent; - --nav-shadow: var(--shadow-2); - --nav-item-text-color: var(--go-color-darkest); - --nav-item-bg-color: var(--nav-bg-color); - --nav-item-hover-bg-color: var(--go-color-neutral-200); - --nav-item-active-bg-color: var(--go-color-primary-100); - --nav-item-current-bg-color: var(--nav-item-bg-color); - --nav-item-padding: var(--go-size--1) var(--go-size-0); - --nav-item-icon-gap: 0.5rem; - --nav-item-current-bar-width: 4px; - --nav-item-current-bar-color: var(--go-color-secondary-700); - --submenu-bg-color: var(--nav-item-active-bg-color); - --submenu-border-radius: var(--radius-2); - - @include theme-dark() { - --submenu-bg-color: var(--go-color-neutral-500); - } - --submenu-padding-y: var(--go-size-0); - --submenu-padding-x: var(--go-size-1); - --submenu-link-padding: 8px; - --submenu-link-hover-bg-color: var(--go-color-neutral-200); - /** - @prop --submenu-description-color: - text color for submenu description text - - default: var(--go-color-neutral-700) - */ - --submenu-description-color: var(--go-color-neutral-700); - - /** - @prop --submenu-separator-color: - border color separator between submenu header and list - - default: var(--go-color-neutral-200) - */ - --submenu-separator-color: var(--go-color-neutral-200); - - /** - @prop --submenu-indent: - submenu indent - - default: 0.5rem - */ - --submenu-indent: 0.5rem; - - - /** - @prop --submenu-tail-size: - submenu tail size - - default: 1rem - */ - --submenu-tail-size: 1rem; - - /** - @prop --submenu-z-index: - submenu z-index - - default: var(--layer-important) - */ - --submenu-z-index: var(--layer-important); - - display: block; - box-shadow: var(--nav-shadow); - border-top: var(--nav-border-top); - ul { - list-style: none; - } - > nav { - background: var(--nav-bg-color); - .nav-menu-root { - margin: 0; - padding: 0; - display: flex; - flex-direction: row; - align-items: stretch; - flex-wrap: nowrap; - position: relative; - } - .nav-item { - .nav-item-inner { - @include reset-btn; - - display: flex; - align-items: center; - justify-content: center; - width: 100%; - padding: var(--nav-item-padding); - color: var(--nav-item-text-color); - text-decoration: none; - background: var(--nav-item-bg-color); - border-bottom: var(--nav-item-current-bar-width) solid transparent; - @include transition(background); - &:hover, - &:focus { - border-radius: var(--radius-2); - background: var(--nav-item-hover-bg-color); - } - - svg { - @include transition(transform); - - margin-left: 0.5rem; - width: 1.25em; - height: 1.25em; - transform: translateX(0); - } - - .nav-item-label { - display: flex; - align-items: center; - gap: var(--nav-item-icon-gap); - } - } - - &.current { - .nav-item-inner { - --nav-item-bg-color: var(--nav-item-current-bg-color); - - border-bottom-color: var(--nav-item-current-bar-color); - } - } - - .parent-link { - svg { - width: 40px; - } - } - - // new structure - .submenu-container { - - @include transition(opacity, visibility); - - visibility: hidden; - opacity: 0; - position: absolute; - top: calc(100% + var(--submenu-tail-size)); - max-width: 100%; - z-index: var(--submenu-z-index); - box-shadow: var(--nav-shadow); - padding: var(--submenu-padding-y) var(--submenu-padding-x); - background: var(--submenu-bg-color); - border-radius: var(--submenu-border-radius); - - &::before { - content: ''; - width: var(--submenu-tail-size); - height: var(--submenu-tail-size); - background: var(--submenu-bg-color); - position: absolute; - top: 0; - left: calc(var(--submenu-tail-size) + 5px); - transform: translate(-50%, -50%) rotate(45deg); - } - - .submenu-header { - padding: 0.5rem 0; - margin-bottom: 0; - border-bottom: 1px solid var(--submenu-separator-color); - svg { - width: 1.5em; - height: 1.5em; - } - h5 { - display: flex; - align-items: center; - gap: 0.5em; - } - .description { - margin: 0; - padding: 0.5rem; - color: var(--submenu-description-color); - } - } - .submenu-list { - display: flex; - flex-direction: column; - flex-wrap: wrap; - gap: 0.75rem; - } - } - &.active { - background: var(--nav-item-active-bg-color); - .nav-item-inner { - svg { - transform: rotate(180deg); - } - } - .submenu-container { - visibility: visible; - opacity: 1; - box-shadow: var(--shadow-2); - } - } - } - - .submenu { - ul { - padding-left: 0; - a { - padding-left: calc(0.5rem + var(--submenu-indent)); - } - } - go-nav-link { - display: block; - } - } - } - - go-nav-link { - --nav-link-padding: var(--submenu-link-padding); - .nav-item-link .nav-link-text { - text-decoration: none; - } - } -} diff --git a/packages/core/src/components/navigation/go-main-nav/go-main-nav.tsx b/packages/core/src/components/navigation/go-main-nav/go-main-nav.tsx deleted file mode 100644 index f826c1fc..00000000 --- a/packages/core/src/components/navigation/go-main-nav/go-main-nav.tsx +++ /dev/null @@ -1,242 +0,0 @@ -import { Component, Element, h, Method, Prop, State, Host, EventEmitter, Event, Watch } from '@stencil/core'; -import { INavItem } from '../../../interfaces'; -import { onClickOutside } from '../../../utils/dom'; -import { inheritAttributes } from '../../../utils/helper'; -import { parseItems } from '../../../utils'; -import { renderIcon } from '../nav-helpers'; - -@Component({ - tag: 'go-main-nav', - styleUrl: 'go-main-nav.scss', - shadow: false, -}) -export class GoMainNav { - @Element() el: HTMLElement; - - /** - * Navigation items to be rendered - * if provided, slot content will not be rendered. - */ - @Prop() items?: INavItem[] | string; - - @State() navItems: INavItem[] = null; - - /** - * Label for the navigation. - * This helps screen reader users to quickly navigate to teh correct nav landmark - */ - @Prop() label = 'Main'; - - // Store attributes inherited from the host element - private inheritedAttrs = {}; - async componentWillLoad() { - this.inheritedAttrs = inheritAttributes(this.el, ['class', 'style', 'items', 'active', 'position']); - this.navItems = parseItems(this.items); - // click outside to close menus - onClickOutside(this.el, () => { - this.closeAllSubMenus(); - }); - // esc to close menus - this.el.addEventListener('keydown', (e) => { - if (e.code === 'Escape') { - this.closeAllSubMenus(); - } - }); - } - - /** - * Initialise the menu - * @param items {INavItem[]} menu items to be rendered - */ - @Method() - async init(newItems: INavItem[] | string) { - this.navItems = parseItems(newItems); - } - - @Watch('items') - async watchItems(newItems: INavItem[] | string) { - this.navItems = parseItems(newItems); - } - - private closeAllSubMenus() { - this.el.querySelectorAll('.nav-menu-root > li.active').forEach((item) => { - this.closeSubMenu(item as HTMLElement); - }); - } - - private toggleSubMenu(e: MouseEvent) { - const triggerBtn = e.currentTarget as HTMLElement; - const menuItem = triggerBtn.closest('.nav-item.has-children') as HTMLElement; - - if (menuItem.classList.contains('active')) { - this.closeSubMenu(menuItem); - } else { - // close any open menus - this.closeAllSubMenus(); - menuItem.classList.add('active'); - triggerBtn.setAttribute('aria-expanded', 'true'); - } - } - - private closeSubMenu(menuItem: HTMLElement) { - const triggerBtn = menuItem.querySelector('.nav-item-inner'); - menuItem.classList.remove('active'); - triggerBtn.setAttribute('aria-expanded', 'false'); - } - - @Event({ - eventName: 'navigate', - cancelable: true, - bubbles: true, - }) - navEvent: EventEmitter; - - renderNavLink(item: INavItem, isSubmenuParentLink = false) { - let Tag = item.isCurrent ? 'span' : 'a'; - let attrs = item?.url - ? { - href: item.url, - onClick: (event) => { - this.navEvent.emit({ event, item }); - }, - ...item.linkAttrs, - } - : {}; - - attrs.class = `${attrs.class ? attrs.class : ''} nav-item-link${item.isCurrent ? ' current' : ''}`; - return ( - - {renderIcon(item.icon)} - {item.label} - {isSubmenuParentLink ? ( - - - - ) : null} - - ); - } - - renderSubMenu(parent: INavItem) { - if (!parent) { - return; - } - // if submenu item has children, render the current item and its children - if (parent.children?.length > 0) { - return ( - - ); - } - return ( - - ); - } - - renderRootNavItem(item: INavItem) { - let Tag = 'a'; - const hasChildren = item?.children?.length > 0; - if (item.isCurrent) { - Tag = 'span'; - } - if (hasChildren) { - Tag = 'button'; - } - - let attrs = null; - - if (Tag === 'a') { - attrs = { - href: item.url, - onClick: (event) => { - console.log('clicked'); - this.navEvent.emit({ event, item }); - }, - ...item.linkAttrs, - }; - } - if (Tag === 'button') { - attrs = { - 'type': 'button', - 'aria-expanded': 'false', - 'onClick': (e) => this.toggleSubMenu(e), - }; - } - return ( -
  • - - - {renderIcon(item.icon)} - {item.label} - - {hasChildren ? ( - - - - ) : null} - - {item.children ? ( - - - - ) : null} -
  • - ); - } - /** - * render top level nav items - */ - renderRootNav(items: INavItem[]) { - return ( -
    -
    - -
    -
    - ); - } - - render() { - let { label, navItems, inheritedAttrs } = this; - - return ( - - - - ); - } -} diff --git a/packages/core/src/components/navigation/go-main-nav/readme.md b/packages/core/src/components/navigation/go-main-nav/readme.md deleted file mode 100644 index 14a11fc5..00000000 --- a/packages/core/src/components/navigation/go-main-nav/readme.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: Main navigation ---- - -# Main navigation `go-main-nav` - - - -
    Main navigation helps users navigate the site's top level information architecture.
    - -## Accessibility - -## Related patterns - - - -- [Header bar](../../patterns/header-bar) - - - -## Demo - - - - diff --git a/packages/core/src/components/navigation/go-main-nav/usage/go-main-nav.md b/packages/core/src/components/navigation/go-main-nav/usage/go-main-nav.md deleted file mode 100644 index 5f7a86f3..00000000 --- a/packages/core/src/components/navigation/go-main-nav/usage/go-main-nav.md +++ /dev/null @@ -1,163 +0,0 @@ - - - - - -
    - - diff --git a/packages/core/src/components/navigation/go-nav-bar/go-nav-bar.scss b/packages/core/src/components/navigation/go-nav-bar/go-nav-bar.scss new file mode 100644 index 00000000..3a9df1c7 --- /dev/null +++ b/packages/core/src/components/navigation/go-nav-bar/go-nav-bar.scss @@ -0,0 +1,410 @@ +go-nav-bar { + /** + @prop --nav-outer-padding: + Outer padding between container and nav items + - default: 0.5rem + */ + --nav-outer-padding: 0.5rem; + /** + @prop --nav-border-top: + Navigation bar top border + - default: 1px solid var(--go-color-neutral-200) + */ + --nav-border-top: 1px solid var(--go-color-neutral-200); + /** + @prop --nav-border-bottom: + avigation barbottom border + - default: 1px solid var(--go-color-neutral-200) + */ + --nav-border-bottom: 1px solid var(--go-color-neutral-200); + /** + @prop --nav-bg-color: + background of nav container + - default: transparent + */ + --nav-bg-color: transparent; + /** + @prop --nav-shadow: + shadow of nav container + - default: var(--shadow-2) + */ + --nav-shadow: var(--shadow-2); + /** + @prop --nav-item-fw: + nav item font weight + - default: 500 + */ + --nav-item-fw: 500; + /** + @prop --nav-item-text-color: + text color of nav items + - default: var(--go-color-neutral-800) + */ + --nav-item-text-color: var(--go-color-neutral-800); + /** + @prop --nav-item-bg-color: + background color of nav items + - default: var(--nav-bg-color) (transparent) + */ + --nav-item-bg-color: var(--nav-bg-color); + + /** + @prop --nav-item-hover-text-color: + text color of nav items on hover + - default: var(--go-color-darkest) + */ + --nav-item-hover-text-color: var(--go-color-darkest); + + /** + @prop --nav-item-hover-bg-color: + background color of nav items on hover + - default: var(--go-color-neutral-200) + */ + --nav-item-hover-bg-color: var(--go-color-neutral-200); + + /** + @prop --nav-item-active-text-color: + text color of nav items when active + - default: var(--go-color-darkest) + */ + --nav-item-active-text-color: var(--go-color-darkest); + + /** + @prop --nav-item-active-bg-color: + background color of nav items when active + - default: var(--go-color-neutral-200) + */ + --nav-item-active-bg-color: var(--go-color-neutral-200); + /** + @prop --nav-item-current-text-color: + text color of nav items when current (highlighted) + - default: var(--go-color-neutral-100) + */ + --nav-item-current-text-color: var(--go-color-neutral-100); + /** + @prop --nav-item-current-bg-color: + background color of nav items when current (highlighted) + - default: var(--go-color-neutral-900) + */ + --nav-item-current-bg-color: var(--go-color-neutral-900); + /** + @prop --nav-item-current-fw: + font-weight of nav items when current (highlighted) + - default: 700 + */ + --nav-item-current-fw: 700; + + /** + @prop --nav-item-open-bg-color: + background color of nav items when submenu is open + - default: var(--go-color-neutral-900) + */ + --nav-item-open-bg-color: var(--go-color-neutral-900); + /** + @prop --nav-item-open-text-color: + text color of nav items when submenu is open + - default: var(--go-color-neutral-100) + */ + --nav-item-open-text-color: var(--go-color-neutral-100); + + /** + @prop --nav-item-gap: + Gap between nav items + - default: 0.5rem + */ + --nav-item-gap: 0.5rem; + /** + @prop --nav-item-padding: + Padding of nav items + - default: 0.25em 0.5em + */ + --nav-item-padding: 0.25em 0.5em; + /** + @prop --nav-item-radius: + Radius of nav items + - default: 0.25em + */ + --nav-item-radius: 0.25em; + /** + @prop --nav-item-icon-gap: + Gap between icon and text in nav items + - default: 0.5rem + */ + --nav-item-icon-gap: 0.5rem; + + /** + @prop --nav-item-current-border-width: + Border width of nav items when current (highlighted) + - default: 2px + */ + --nav-item-current-border-width: 2px; + /** + @prop --nav-item-current-border-color: + Border color of nav items when current (highlighted) + - default: var(--go-color-primary-600) + */ + --nav-item-current-border-color: var(--go-color-primary-600); + + /** + @prop --submenu-width: + width of submenu + - default: auto + */ + --submenu-width: auto; + /** + @prop --submenu-bg-color: + Background color of submenu + - default: var(--go-color-neutral-100) + */ + --submenu-bg-color: var(--go-color-neutral-100); + /** + @prop --submenu-border-radius: + Border radius of submenu + - default: var(--radius-2) + */ + --submenu-border-radius: var(--radius-2); + /** + @prop --submenu-border-width: + Border width of submenu + - default: 2px + */ + --submenu-border-width: 2px; + /** + @prop --submenu-border-color: + Border color of submenu + - default: var(--go-color-neutral-500) + */ + --submenu-border-color: var(--go-color-neutral-500); + /** + @prop --submenu-padding-y: + vertical padding of submenu links + - default: var(--go-size-0) + */ + --submenu-padding-y: var(--go-size-0); + /** + @prop --submenu-padding-x: + horizontal padding of submenu links + - default: var(--go-size-1) + */ + --submenu-padding-x: var(--go-size-1); + /** + @prop --submenu-link-padding: + padding of submenu links (applied to top/bottom only) + - default: 8px + */ + --submenu-link-padding: 8px; + + /** + @prop --submenu-separator-color: + border color separator between submenu header and list + - default: var(--go-color-neutral-200) + */ + --submenu-separator-color: var(--go-color-neutral-200); + + /** + @prop --submenu-list-indent: + submenu indent + - default: 1rem + */ + --submenu-list-indent: 1rem; + + /** + @prop --submenu-tail-size: + submenu tail size + - default: 1em + */ + --submenu-tail-size: 1em; + + /** + @prop --submenu-tail-offset-x: + set the horizontal offset from the tail to left edge of parent menu + - default: 1rem + */ + --submenu-tail-offset-x: 1rem; + /** + @prop --submenu-z-index: + submenu z-index + - default: var(--layer-important) + */ + --submenu-z-index: var(--layer-important); + + /** + @prop --submenu-columns: + number of columns in submenu list + - default: 1 + */ + --submenu-columns: 1; + + /** + @prop --submenu-list-gap: + gap between submenu lists + - default: 1rem + */ + --submenu-list-gap: 1rem; + + display: block; + box-shadow: var(--nav-shadow); + border-top: var(--nav-border-top); + border-bottom: var(--nav-border-bottom); + padding: var(--nav-outer-padding); + ul { + list-style: none; + } + > nav { + background: var(--nav-bg-color); + .nav-menu-root { + list-style: none; + display: flex; + flex-direction: row; + gap: var(--nav-item-gap); + align-items: stretch; + flex-wrap: nowrap; + position: relative; + } + .nav-item { + .nav-item-inner, + > go-nav-link a { + --nav-link-text-decoration: none; + @include reset-btn; + + font-weight: var(--nav-item-fw); + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + padding: var(--nav-item-padding); + color: var(--nav-item-text-color); + text-decoration: var(--nav-link-text-decoration); + background: var(--nav-item-bg-color); + border: var(--nav-item-current-border-width) solid transparent; + border-radius: var(--nav-item-radius); + @include transition(background, border-color); + &:hover, + &:focus { + border-radius: var(--nav-item-radius); + background: var(--nav-item-hover-bg-color); + color: var(--nav-item-hover-text-color); + } + + &:active { + background: var(--nav-item-active-bg-color); + color: var(--nav-item-active-text-color); + } + + svg { + @include transition(transform); + + margin-left: 0.5rem; + width: 1em; + height: 1em; + transform: translateX(0); + } + + .nav-item-label { + display: flex; + align-items: center; + gap: var(--nav-item-icon-gap); + } + + &[aria-expanded='true'] { + background: var(--nav-item-open-bg-color); + color: var(--nav-item-open-text-color); + svg { + transform: rotate(180deg); + } + } + } + + &.current { + .nav-item-inner { + background: var(--nav-item-current-bg-color); + color: var(--nav-item-current-text-color); + font-weight: var(--nav-item-current-fw); + } + } + + .parent-link { + svg { + width: 40px; + } + } + + // new structure + .submenu-container { + @include transition(opacity, visibility); + --submenu-border: var(--submenu-border-width) solid var(--submenu-border-color); + + visibility: hidden; + opacity: 0; + position: absolute; + top: calc(100% + var(--submenu-tail-size)); + max-width: 100%; + width: var(--submenu-width); + z-index: var(--submenu-z-index); + box-shadow: var(--nav-shadow); + padding: var(--submenu-padding-y) var(--submenu-padding-x); + background: var(--submenu-bg-color); + border-radius: var(--submenu-border-radius); + border: var(--submenu-border); + + &::before { + content: ''; + width: var(--submenu-tail-size); + height: var(--submenu-tail-size); + background: var(--submenu-bg-color); + position: absolute; + top: calc(-1 * var(--submenu-border-width) / 2); + left: var(--submenu-tail-offset-x); + transform: translate(0, -50%) rotate(45deg); + border: var(--submenu-border); + border-right: none; + border-bottom: none; + z-index: 9; + } + + .submenu-header { + padding-bottom: 0.5rem; + margin-bottom: 0.5rem; + border-bottom: 1px solid var(--submenu-separator-color); + svg { + width: 1.5em; + height: 1.5em; + } + h5 { + display: flex; + align-items: center; + gap: 0.5em; + } + } + .submenu-body { + display: grid; + grid-template-columns: repeat(var(--submenu-columns), 1fr); + gap: var(--submenu-list-gap); + align-items: flex-start; + } + } + .submenu-list-container { + .submenu-list { + margin: 0; + margin-inline-start: var(--submenu-list-indent); + padding: 0; + } + } + + // opened submenu + .submenu-container.open { + visibility: visible; + opacity: 1; + box-shadow: var(--shadow-2); + } + } + } + + go-nav-link { + --nav-link-padding: var(--submenu-link-padding); + .nav-item-link .nav-link-text { + text-decoration: none; + } + } +} diff --git a/packages/core/src/components/navigation/go-nav-bar/go-nav-bar.tsx b/packages/core/src/components/navigation/go-nav-bar/go-nav-bar.tsx new file mode 100644 index 00000000..b201fc7d --- /dev/null +++ b/packages/core/src/components/navigation/go-nav-bar/go-nav-bar.tsx @@ -0,0 +1,65 @@ +import { Component, Element, h, Method, Prop, State, Host, Watch } from '@stencil/core'; +import { INavItem } from '../../../interfaces'; +import { inheritAttributes } from '../../../utils/helper'; +import { parseJsonProp } from '../../../utils'; + +@Component({ + tag: 'go-nav-bar', + styleUrl: 'go-nav-bar.scss', + shadow: false, +}) +export class GoNavBar { + @Element() el: HTMLElement; + + /** + * Navigation items to be rendered + * if provided, slot content will not be rendered. + */ + @Prop() items?: INavItem[] | string; + + @State() navItems: INavItem[] = null; + + /** + * Label for the navigation. + * This helps screen reader users to quickly navigate to teh correct nav landmark + */ + @Prop() label = 'Main'; + + // Store attributes inherited from the host element + private inheritedAttrs = {}; + async componentWillLoad() { + this.inheritedAttrs = inheritAttributes(this.el, ['class', 'style', 'items']); + + await this.loadNavItems(this.items); + } + + /** + * Load nav items + * @param items {INavItem[]} menu items to be rendered + */ + @Method() + async loadNavItems(newItems: INavItem[] | string) { + this.navItems = parseJsonProp(newItems); + } + + @Watch('items') + async watchItems(newItems: INavItem[] | string) { + this.navItems = parseJsonProp(newItems); + } + + render() { + let { label, navItems, inheritedAttrs } = this; + + return ( + + + + ); + } +} diff --git a/packages/core/src/components/navigation/go-nav-bar/go-nav-item.tsx b/packages/core/src/components/navigation/go-nav-bar/go-nav-item.tsx new file mode 100644 index 00000000..1274e62f --- /dev/null +++ b/packages/core/src/components/navigation/go-nav-bar/go-nav-item.tsx @@ -0,0 +1,135 @@ +import { INavItem } from '@/interfaces'; +import { hasSlot, parseJsonProp } from '@/utils'; +import { Component, Host, Prop, State, h, Event, EventEmitter, Element, Watch } from '@stencil/core'; +import { renderIcon } from '../nav-helpers'; +import { uniqueId } from 'lodash-es'; + +@Component({ + tag: 'go-nav-item', +}) +export class GoNavItem { + @Element() el: HTMLElement; + + @Prop() item: INavItem | string; + + submenuId?: string; + @Watch('item') + parseItemProp() { + this.parsedItem = parseJsonProp(this.item); + if (this.parsedItem?.children?.length) { + this.submenuId = uniqueId('go-nav-item-submenu-'); + } + } + + @State() parsedItem: INavItem; + + @State() hasSubmenuSlot = false; + + componentWillLoad() { + this.parseItemProp(); + + this.hasSubmenuSlot = hasSlot(this.el, 'submenu'); + } + + /** + * open state of the submenu, only applicable if + * - the `item` property has `children` key, or + * - go-nav-item has `submenu` slot + */ + @State() isOpen: boolean = false; + + @Event({ + eventName: 'navigate', + cancelable: true, + bubbles: true, + }) + navEvent: EventEmitter; + + @Event({ + eventName: 'submenutoggle', + cancelable: true, + bubbles: true, + }) + subMenuToggleEvent: EventEmitter; + + handleSubmenuToggle(isOpen: boolean) { + console.log('event triggered', isOpen); + this.isOpen = !!isOpen; + } + + renderSubMenu(parent: INavItem) { + if (!parent) { + return; + } + // if submenu item has children, render the current item and its children + if (parent.children?.length > 0) { + return ( + + ); + } + return ( + + ); + } + + render() { + const { parsedItem: item, submenuId } = this; + let Tag = 'a'; + + const hasChildren = item?.children?.length > 0 || this.hasSubmenuSlot; + if (item?.isCurrent) { + Tag = 'span'; + } + let attrs = null; + + if (Tag === 'a') { + attrs = { + url: item?.url, + onClick: (event) => { + this.navEvent.emit({ event, item }); + }, + ...item?.linkAttrs, + }; + } + return ( + + + {hasChildren ? ( + [ + + + {renderIcon(item?.icon)} + {item?.label} + + , + + + {item.children.map((child) => this.renderSubMenu(child))} + , + ] + ) : ( + + + {renderIcon(item?.icon)} + {item?.label} + + + )} + + + ); + } +} diff --git a/packages/core/src/components/navigation/go-nav-bar/go-nav-submenu-trigger.tsx b/packages/core/src/components/navigation/go-nav-bar/go-nav-submenu-trigger.tsx new file mode 100644 index 00000000..aabf0a18 --- /dev/null +++ b/packages/core/src/components/navigation/go-nav-bar/go-nav-submenu-trigger.tsx @@ -0,0 +1,62 @@ +import { warning } from '@/utils'; +import { Component, Prop, h, Element, State } from '@stencil/core'; + +@Component({ + tag: 'go-nav-submenu-trigger', +}) +export class GoNavSubmenuTrigger { + @Element() el: HTMLElement; + + @Prop({ reflect: true }) controls: string; + + @State() isOpen: boolean = false; + + submenuEl: HTMLGoNavSubmenuElement; + + loadSubmenuEl() { + if (this.controls) { + this.submenuEl = document.getElementById(this.controls) as HTMLGoNavSubmenuElement; + if (!this.submenuEl) { + warning(' is missing with id: ' + this.controls, this.el); + } + this.submenuEl.addEventListener('toggle', (e) => { + this.isOpen = e.detail.isOpen; + }); + } + } + + toggleOpenState() { + this.submenuEl.toggle(); + } + + componentDidLoad() { + this.loadSubmenuEl(); + } + + render() { + const Tag = 'button'; + return ( + this.toggleOpenState()} + type="button" + aria-haspopup="true" + aria-controls={this.controls ? this.controls : undefined} + aria-expanded={this.isOpen ? 'true' : 'false'}> + + + + + + + + ); + } +} diff --git a/packages/core/src/components/navigation/go-nav-bar/go-nav-submenu.tsx b/packages/core/src/components/navigation/go-nav-bar/go-nav-submenu.tsx new file mode 100644 index 00000000..a04a3175 --- /dev/null +++ b/packages/core/src/components/navigation/go-nav-bar/go-nav-submenu.tsx @@ -0,0 +1,81 @@ +import { hasSlot, onClickOutside, onEscape, warning } from '@/utils'; +import { Component, Method, Prop, h, State, Event, EventEmitter, Element } from '@stencil/core'; + +@Component({ + tag: 'go-nav-submenu', +}) +export class GoNavSubmenu { + @Element() el: HTMLElement; + + @Prop() columns: number = 1; + + @State() isOpen: boolean = false; + + @Event({ + eventName: 'toggle', + cancelable: true, + bubbles: true, + }) + toggleEvent: EventEmitter; + + @Method() + async open() { + this.isOpen = true; + this.toggleEvent.emit({ isOpen: true }); + + window.requestAnimationFrame(() => { + // click outside to close menus + this.clickOutsideCleanUp = onClickOutside(this.el, () => this.close()); + // esc to close menus + this.escapeCleanUp = onEscape(document, () => this.close()); + }); + } + + @Method() + async close() { + this.isOpen = false; + this.toggleEvent.emit({ isOpen: false }); + + this.clickOutsideCleanUp && this.clickOutsideCleanUp(); + this.escapeCleanUp && this.escapeCleanUp(); + } + + @Method() + async toggle() { + if (this.isOpen) { + this.close(); + } else { + this.open(); + } + } + + parentNavItem: HTMLGoNavItemElement; + clickOutsideCleanUp = null; + escapeCleanUp = null; + hasHeaderSlot = false; + componentWillLoad() { + this.parentNavItem = this.el.closest('go-nav-item'); + if (!this.parentNavItem) { + warning(' must be a child of ', this.el); + return; + } + + this.hasHeaderSlot = hasSlot(this.el, 'submenu-header'); + } + + render() { + const { columns, isOpen, hasHeaderSlot } = this; + return ( +
    + {hasHeaderSlot ? ( + + ) : null} + +
    + ); + } +} diff --git a/packages/core/src/components/navigation/go-nav-bar/readme.md b/packages/core/src/components/navigation/go-nav-bar/readme.md new file mode 100644 index 00000000..6b108fab --- /dev/null +++ b/packages/core/src/components/navigation/go-nav-bar/readme.md @@ -0,0 +1,50 @@ +--- +title: Navigation bar +--- + +# Navigation bar `go-nav-bar` + + + +
    The navigation bar helps users navigate the site's top level information architecture.
    + +## Accessibility + +- Uses `