diff --git a/package.json b/package.json index 4529bd275905e9b85572531f21a539f1005e34c5..38fb11538fa1e19abce23ac75009a2e735f14851 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@itcenteratunihelsinki/huds-lib", - "version": "0.0.80", + "version": "0.0.81", "description": "Helsinki University Design System library", "main": "dist/index.js", "module": "dist/index.mjs", diff --git a/src/components.d.ts b/src/components.d.ts index 1c11d510480af026508160bf8b6738086dfe4ca3..5b551e6a7688b314e294285aa15efc7d36abc96f 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -736,7 +736,9 @@ export namespace Components { dataDesktopLinks: DesktopLinks[] | string; dataMenuDonate: string; dataMenuLanguage: string; + dataSearchTools: DesktopLinks[] | string; dataSiteHeaderLabels: string; + dataSiteSearchLabels: string; logoLabel?: string; logoUrl?: string; menuLabel: string; @@ -752,8 +754,11 @@ export namespace Components { } interface HySiteSearch { color: ColorVariant; + dataSearchSpecialTools: string; isAlternative: boolean; labels?: ComponentLabels[] | string; + searchLabels: string; + searchTools: string; showLabel: boolean; size: number; } @@ -2043,7 +2048,9 @@ declare namespace LocalJSX { dataDesktopLinks?: DesktopLinks[] | string; dataMenuDonate?: string; dataMenuLanguage?: string; + dataSearchTools?: DesktopLinks[] | string; dataSiteHeaderLabels?: string; + dataSiteSearchLabels?: string; logoLabel?: string; logoUrl?: string; menuLabel?: string; @@ -2060,8 +2067,12 @@ declare namespace LocalJSX { } interface HySiteSearch { color?: ColorVariant; + dataSearchSpecialTools?: string; isAlternative?: boolean; labels?: ComponentLabels[] | string; + onSearchPanelToggled?: (event: CustomEvent<any>) => void; + searchLabels?: string; + searchTools?: string; showLabel?: boolean; size?: number; } diff --git a/src/components/hy-search-field/hy-search-field.scss b/src/components/hy-search-field/hy-search-field.scss index cc06495d6277c5b0421f417c99bc3ab2c8d2b272..5c7949e222f038fbcd46fc83a254b4a282cf6b42 100644 --- a/src/components/hy-search-field/hy-search-field.scss +++ b/src/components/hy-search-field/hy-search-field.scss @@ -44,6 +44,7 @@ @include font-size(14px, 24px); border-radius: 0; border: 1px solid var(--grayscale-medium-dark); + caret-color: var(--brand-main-light); font-weight: bold; height: 40px; letter-spacing: normal; @@ -78,7 +79,6 @@ border-style: solid inset solid solid; border-width: 2px; outline: 0; - text-indent: -1px; // Text stays in place. } } diff --git a/src/components/hy-search-field/readme.md b/src/components/hy-search-field/readme.md index 1d190e11b37f1ce7ce789e0873c7c6eedf286a2c..6ef6e90806a79d13f9dc27b2f4e2732a9aee4906 100644 --- a/src/components/hy-search-field/readme.md +++ b/src/components/hy-search-field/readme.md @@ -12,6 +12,10 @@ ## Dependencies +### Used by + +- [hy-site-search](../site-header/site-search) + ### Depends on - [hy-icon](../icon) @@ -21,6 +25,7 @@ ```mermaid graph TD; hy-search-field --> hy-icon + hy-site-search --> hy-search-field style hy-search-field fill:#f9f,stroke:#333,stroke-width:4px ``` diff --git a/src/components/navigation/menu-language/menu-language.tsx b/src/components/navigation/menu-language/menu-language.tsx index 60d8c8f21f6d7fdc861324082f7b34a2b3bedec8..6af56d647fbb051252f2c2293573d683426cfb15 100644 --- a/src/components/navigation/menu-language/menu-language.tsx +++ b/src/components/navigation/menu-language/menu-language.tsx @@ -49,6 +49,12 @@ export class MenuLanguage { this.isMenuOpen = false; } + // CLose the language menu if user opens the search panel + @Listen('searchPanelToggled', {target: 'document'}) + searchPanelToggled() { + this.isMenuOpen = false; + } + @Listen('focus') handleComponentFocus(event) { // Close desktop menu panel if it's open. diff --git a/src/components/site-header/hy-desktop-menu-links/hy-desktop-menu-links.tsx b/src/components/site-header/hy-desktop-menu-links/hy-desktop-menu-links.tsx index 35bd9d81615a0b919596bbf2c0cef6edfc857c06..3ea0d1da7c760d02167463f9b8ffe51233190372 100644 --- a/src/components/site-header/hy-desktop-menu-links/hy-desktop-menu-links.tsx +++ b/src/components/site-header/hy-desktop-menu-links/hy-desktop-menu-links.tsx @@ -231,6 +231,13 @@ export class HyDesktopMenuLinks { this.closePanel(fadeOut); } + // Close the desktop menu panel if user opens search panel + @Listen('searchPanelToggled', {target: 'document'}) + searchPanelToggled() { + let fadeOut = true; + this.closePanel(fadeOut); + } + // CLose the desktop menu panel if user scrolls Sticky Header till the very top. @Listen('headerScrollUp', {target: 'document'}) headerScrollUp() { diff --git a/src/components/site-header/readme.md b/src/components/site-header/readme.md index b1e105e5db393afca0118d046713ce057b6b2504..129c782db521fd53b8c680a68db0ed64ecc1b86f 100644 --- a/src/components/site-header/readme.md +++ b/src/components/site-header/readme.md @@ -9,7 +9,9 @@ | `dataDesktopLinks` | `data-desktop-links` | | `DesktopLinks[] \| string` | `undefined` | | `dataMenuDonate` | `data-menu-donate` | | `string` | `undefined` | | `dataMenuLanguage` | `data-menu-language` | | `string` | `undefined` | +| `dataSearchTools` | `data-search-tools` | | `DesktopLinks[] \| string` | `undefined` | | `dataSiteHeaderLabels` | `data-site-header-labels` | | `string` | `undefined` | +| `dataSiteSearchLabels` | `data-site-search-labels` | | `string` | `undefined` | | `logoLabel` | `logo-label` | | `string` | `undefined` | | `logoUrl` | `logo-url` | | `string` | `undefined` | | `menuLabel` | `menu-label` | | `string` | `'Menu'` | @@ -47,6 +49,8 @@ graph TD; hy-menu-language --> hy-menu-language-item hy-menu-language --> hy-icon hy-site-search --> hy-icon + hy-site-search --> hy-search-field + hy-search-field --> hy-icon style hy-site-header fill:#f9f,stroke:#333,stroke-width:4px ``` diff --git a/src/components/site-header/site-header.tsx b/src/components/site-header/site-header.tsx index f11c0754fa540032ea45c53d8a932585442ff077..cb2bea8a536738562baeaa4e261888c51b72ec37 100644 --- a/src/components/site-header/site-header.tsx +++ b/src/components/site-header/site-header.tsx @@ -28,6 +28,7 @@ export class SiteHeader { @Prop() dataMenuLanguage: string; @Prop() dataMenuDonate: string; @Prop() dataSiteHeaderLabels: string; + @Prop() dataSiteSearchLabels: string; @Prop() logoUrl?: string; @Prop() logoLabel?: string; @Prop() menuLabel: string = 'Menu'; @@ -38,6 +39,8 @@ export class SiteHeader { First level menu links to be displayed on Desktop screens. * */ @Prop() dataDesktopLinks: DesktopLinks[] | string; + @Prop() dataSearchTools: DesktopLinks[] | string; + @State() isMobile: boolean; @State() isMenuOpen: boolean = false; @State() isDesktopMenuOpen: boolean = false; @@ -209,6 +212,8 @@ export class SiteHeader { color={ColorVariant.black} show-label={true} labels={this.searchLabels} + search-labels={this.dataSiteSearchLabels} + search-tools={this.dataSearchTools} /> {this.donateLink.map((i) => { return ( @@ -241,6 +246,8 @@ export class SiteHeader { color={ColorVariant.black} show-label={true} labels={this.searchLabels} + search-labels={this.dataSiteSearchLabels} + search-tools={this.dataSearchTools} /> {this.donateLink.map((i) => { return ( @@ -305,6 +312,8 @@ export class SiteHeader { color={ColorVariant.black} show-label={true} labels={this.searchLabels} + search-labels={this.dataSiteSearchLabels} + search-tools={this.dataSearchTools} /> <button onClick={() => this.mobileMenuToggle()} diff --git a/src/components/site-header/site-search/readme.md b/src/components/site-header/site-search/readme.md index 646bcbb7fe47efacdfbea60b34ffe68af79d7bc8..31c492284a89e15942a930a602d2a3b9192d134c 100644 --- a/src/components/site-header/site-search/readme.md +++ b/src/components/site-header/site-search/readme.md @@ -4,13 +4,22 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| --------------- | ---------------- | ----------- | ------------------------------------------ | -------------------- | -| `color` | `color` | | `ColorVariant.black \| ColorVariant.white` | `ColorVariant.black` | -| `isAlternative` | `is-alternative` | | `boolean` | `false` | -| `labels` | `labels` | | `ComponentLabels[] \| string` | `undefined` | -| `showLabel` | `show-label` | | `boolean` | `false` | -| `size` | `size` | | `number` | `undefined` | +| Property | Attribute | Description | Type | Default | +| ------------------------ | --------------------------- | ----------- | ------------------------------------------ | -------------------- | +| `color` | `color` | | `ColorVariant.black \| ColorVariant.white` | `ColorVariant.black` | +| `dataSearchSpecialTools` | `data-search-special-tools` | | `string` | `undefined` | +| `isAlternative` | `is-alternative` | | `boolean` | `false` | +| `labels` | `labels` | | `ComponentLabels[] \| string` | `undefined` | +| `searchLabels` | `search-labels` | | `string` | `undefined` | +| `searchTools` | `search-tools` | | `string` | `undefined` | +| `showLabel` | `show-label` | | `boolean` | `false` | +| `size` | `size` | | `number` | `undefined` | + +## Events + +| Event | Description | Type | +| -------------------- | ----------- | ------------------ | +| `searchPanelToggled` | | `CustomEvent<any>` | ## Dependencies @@ -21,12 +30,15 @@ ### Depends on - [hy-icon](../../icon) +- [hy-search-field](../../hy-search-field) ### Graph ```mermaid graph TD; hy-site-search --> hy-icon + hy-site-search --> hy-search-field + hy-search-field --> hy-icon hy-site-header --> hy-site-search style hy-site-search fill:#f9f,stroke:#333,stroke-width:4px ``` diff --git a/src/components/site-header/site-search/site-search.scss b/src/components/site-header/site-search/site-search.scss index f9991d1ef717a68f74887c77b025468924fb6e71..2a360aec88670b617616c929c77c1a1b37c3bb2d 100644 --- a/src/components/site-header/site-search/site-search.scss +++ b/src/components/site-header/site-search/site-search.scss @@ -83,3 +83,199 @@ } } } + +.site-search { + &__panel { + display: none; + &__is-open { + background-color: #f8f8f8; + border: 1px solid rgba(0, 0, 0, 0.1); + display: flex; + flex-direction: column; + font-family: var(--main-font-family); + left: 0; + position: absolute; + width: 100%; + z-index: 520; + } + + &__title { + &__group { + align-items: baseline; + display: flex; + flex-direction: column; + + @include breakpoint($wide) { + align-items: baseline; + flex-direction: row; + justify-content: flex-start; + } + } + + h1 { + @include font-size(32px, 32px); + @include font-weight($bold); + color: var(--grayscale-black); + letter-spacing: -1px; + margin-bottom: 12px; + margin-top: 20px; + text-transform: uppercase; + + @include breakpoint($narrow) { + @include font-size(46px, 48px); + letter-spacing: -1.5px; + margin-top: 24px; + } + @include breakpoint($xlarge) { + @include font-size(52px, 54px); + letter-spacing: -1.6px; + } + } + .description { + @include font-size(12px, 16px); + color: var(--grayscale-black); + letter-spacing: -0.2px; + margin-bottom: 16px; + padding: 0; + + @include breakpoint($narrow) { + @include font-size(16px, 22px); + letter-spacing: -0.1px; + margin-bottom: 18px; + } + @include breakpoint($wide) { + @include font-size(18px, 24px); + letter-spacing: -0.1px; + padding-left: 8px; + } + @include breakpoint($xlarge) { + @include font-size(20px, 28px); + letter-spacing: -0.2px; + padding-left: 12px; + } + } + } + + &__wrapper { + margin: 0 auto; + max-width: 100%; + padding: 0 1rem 2rem; + width: 100%; + @include breakpoint($narrow) { + padding: 0 2rem 2rem; + } + @include breakpoint($extrawide) { + max-width: 1216px; + padding: 0 2rem 2rem; + } + @include breakpoint($xlarge) { + max-width: 1216px; + padding: 0 2rem 4rem; + } + } + + &__panel-toggle { + background-color: transparent; + border: none; + position: absolute; + right: 0; + top: 25px; + + @include breakpoint($narrow) { + top: 36px; + } + @include breakpoint($xlarge) { + top: 45px; + } + + &__label { + @include font-size(14px, 22px); + @include font-weight($bold); + align-items: center; + color: var(--grayscale-black); + display: flex; + letter-spacing: -0.44px; + text-transform: uppercase; + + &__title { + padding-right: 8px; + } + + @include breakpoint($xlarge) { + @include font-size(18px, 22px); + letter-spacing: -0.56px; + } + } + } + + &__tools { + .title { + @include font-size(16px, 20px); + @include font-weight($bold); + color: var(--brand-main-nearly-black); + letter-spacing: -0.5px; + text-transform: uppercase; + + @include breakpoint($narrow) { + @include font-size(18px, 24px); + letter-spacing: -0.56px; + margin: 32px 0 16px; + } + } + + .list { + a.search-special-tool { + align-items: center; + background-color: var(--grayscale-white); + display: flex; + margin-bottom: 8px; + text-decoration: none; + + .label { + @include font-size(14px, 16px); + @include font-weight($bold); + color: var(--brand-main-nearly-black); + letter-spacing: -0.44px; + padding-left: 8px; + + @include breakpoint($narrow) { + @include font-size(16px, 20px); + letter-spacing: -0.5px; + padding-left: 12px; + } + } + .description { + display: none; + @include breakpoint($wide) { + @include font-size(14px, 18px); + color: var(--grayscale-dark); + display: flex; + letter-spacing: -0.2px; + padding-left: 12px; + } + @include breakpoint($xlarge) { + @include font-size(16px, 24px); + color: var(--grayscale-dark); + display: flex; + letter-spacing: 0; + padding-left: 12px; + } + } + + hy-icon { + height: 40px; + width: 40px; + + svg { + background-color: var(--brand-main-light); + fill: var(--grayscale-white); + height: 40px; + padding: 8px; + width: 40px; + } + } + } + } + } + } +} diff --git a/src/components/site-header/site-search/site-search.tsx b/src/components/site-header/site-search/site-search.tsx index 14ed05b7e96c20331d6a12eaf024edd5469835fd..2c3b4a0b4f80278fb722fde74455541d2f21761e 100644 --- a/src/components/site-header/site-search/site-search.tsx +++ b/src/components/site-header/site-search/site-search.tsx @@ -1,4 +1,16 @@ -import {Component, Prop, h, Watch} from '@stencil/core'; +export interface SearchTools { + label: string; + url: string; + description: string; + menuLinkId: string; + isExternal: string; +} + +export interface SearchLabels { + label?: string; +} + +import {Component, Prop, h, Watch, State, Listen, Host, Event, EventEmitter, Element} from '@stencil/core'; import {ComponentLabels} from '../site-header'; import {ColorVariant} from '../../../utils/utils'; @@ -8,33 +20,172 @@ import {ColorVariant} from '../../../utils/utils'; shadow: true, }) export class SiteSearch { + @Element() el: HTMLElement; + @Prop() color: ColorVariant = ColorVariant.black; @Prop() isAlternative: boolean = false; @Prop() labels?: ComponentLabels[] | string; + @Prop() searchLabels: string; + @Prop() searchTools: string; + private _searchTools: SearchTools[]; + @Prop() showLabel: boolean = false; @Prop() size: number; + @Prop() dataSearchSpecialTools: string; + + private _searchLabels: SearchLabels[]; + private searchTitleLabel: string; + private searchCloseLabel: string; + private searchToolsLabel: string; + private searchDescriptionLabel: string; + + @State() isSearchPanelOpen: boolean = false; + @Event() searchPanelToggled: EventEmitter; + private _labels: ComponentLabels[]; @Watch('labels') labelsWatcher(data: ComponentLabels[] | string) { this._labels = typeof data === 'string' ? JSON.parse(data) : data; } + componentWillLoad() { + // Special search tools. + if (this.searchTools) { + this._searchTools = JSON.parse(this.searchTools); + } + + if (this.searchLabels) { + this._searchLabels = JSON.parse(this.searchLabels); + + this.searchTitleLabel = this._searchLabels['search_label']; + this.searchCloseLabel = this._searchLabels['search_close_label']; + this.searchToolsLabel = this._searchLabels['search_tools_label']; + this.searchDescriptionLabel = this._searchLabels['search_description']; + } + } + componentWillRender() { this.labelsWatcher(this.labels); } + // CLose the search panel if user opens the desktop menu panel. + @Listen('menuDesktopToggled', {target: 'document'}) + desktopMenuToggled() { + this.isSearchPanelOpen = false; + } + + // CLose the search panel if user opens the language menu. + @Listen('menuLanguageToggled', {target: 'document'}) + menuLanguageToggled() { + this.isSearchPanelOpen = false; + } + + handleSearchPanelToggle(event) { + this.isSearchPanelOpen = !this.isSearchPanelOpen; + + if (this.isSearchPanelOpen) { + //const searchPanelSelector = this.el as HTMLElement; + + // Close desktop menu panel and lang menu panel if they are open. + this.searchPanelToggled.emit(); + + let hyHeader = this.el.closest('.hy-site-header') as HTMLElement; + let rectHeader = hyHeader.getBoundingClientRect(); + + const headerHeight = `${rectHeader.bottom}px`; + const searchPanel = this.el.shadowRoot.querySelectorAll(`.site-search__panel`)[0] as HTMLElement; + searchPanel.style.top = headerHeight; + + // Without setTimeout it will not focus the input because it hasn't rendered yet. + setTimeout(() => { + const searchInput = this.el.shadowRoot.querySelector('input#search') as HTMLElement; + searchInput.focus(); + }); + } + event.stopPropagation(); + } + + handleSearchPanelClose() { + this.isSearchPanelOpen = false; + } + render() { return ( - <button - aria-label={this._labels['open']} + <Host class={{ - 'button--search': true, - 'is-open--menu': this.isAlternative, + 'site-search': true, + 'site-search__is-open': this.isSearchPanelOpen, }} > - {this.showLabel ? <span class={'button--search__label'}>{this._labels['label']}</span> : ''} - <hy-icon icon={'hy-icon-search'} size={this.size} fill={this.color} /> - </button> + <button + aria-label={this._labels['open']} + aria-expanded={`${this.isSearchPanelOpen}`} + class={{ + 'button--search': true, + 'is-open--menu': this.isAlternative, + 'is-open': this.isSearchPanelOpen, + }} + onClick={(e) => this.handleSearchPanelToggle(e)} + > + {this.showLabel ? <span class={'button--search__label'}>{this._labels['label']}</span> : ''} + <hy-icon icon={'hy-icon-search'} size={this.size} fill={this.color} /> + </button> + <div + class={{ + 'site-search__panel': true, + 'site-search__panel__is-open': this.isSearchPanelOpen, + }} + aria-hidden={`${!this.isSearchPanelOpen}`} + > + <div class="site-search__panel__wrapper"> + <div class="site-search__panel__title"> + <div class="site-search__panel__title__group"> + <h1>{this.searchTitleLabel}</h1> + <div class="description">{this.searchDescriptionLabel}</div> + </div> + </div> + <div class="site-search__panel__input"> + <hy-search-field input-id="search" /> + </div> + <div class="site-search__panel__results"> + <div class="filters"></div> + <div class="results"></div> + </div> + {this._searchTools && this._searchTools.length > 0 && ( + <div class="site-search__panel__tools"> + <div class="title">{this.searchToolsLabel}</div> + <div class="list"> + {this._searchTools.map((i) => { + let searchToolTarget = i.isExternal ? '_blank' : '_self'; + return ( + <a class="search-special-tool" href={i.url} target={searchToolTarget}> + <hy-icon icon={'hy-icon-arrow-to-right'} size={14} fill={ColorVariant.black} /> + <span class="label">{i.label}</span> + <span class="description">{i.description}</span> + </a> + ); + })} + </div> + </div> + )} + <button + onClick={() => this.handleSearchPanelClose()} + class={{ + 'site-search__panel__panel-toggle': true, + }} + aria-label={this.searchCloseLabel} + aria-expanded={`${this.isSearchPanelOpen}`} + > + <span class="site-search__panel__panel-toggle__label"> + <span class="site-search__panel__panel-toggle__label__title" tabindex="0"> + {this.searchCloseLabel} + </span> + <hy-icon icon={'hy-icon-remove'} size={16} fill={ColorVariant.black} /> + </span> + </button> + </div> + </div> + </Host> ); } }