diff --git a/src/components.d.ts b/src/components.d.ts index 77b586bd3310a510147174d777814a9f5b7f4b5c..9a73d677a945fd2b9ed78983d9ba75d59158317d 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -36,6 +36,7 @@ import { ProcessFlowBoxVariants, SiteLogoSize, } from './utils/utils'; +import {AnchorItem} from './components/hy-anchor-link-list/hy-anchor-link-list'; import {Breadcrumb} from './components/hy-breadcrumbs/hy-breadcrumbs'; import {TagValue} from './components/courses/hy-content-list-item/hy-content-list-item'; import {DesktopLinks} from './components/site-header/hy-desktop-menu-links/hy-desktop-menu-links'; @@ -82,6 +83,24 @@ export namespace Components { textDescription: string; textTitle?: string; } + interface HyAnchorLinkList { + /** + * Array of anchor items + */ + anchorItems: AnchorItem[] | string; + /** + * Close state label + */ + closeLabel?: string; + /** + * Title for content list button + */ + listTitle?: string; + /** + * Open state label + */ + openLabel?: string; + } interface HyBadge { variant: 'primary' | 'warn' | 'success' | 'disabled'; } @@ -921,6 +940,11 @@ declare global { prototype: HTMLHyAdjacentImageTextElement; new (): HTMLHyAdjacentImageTextElement; }; + interface HTMLHyAnchorLinkListElement extends Components.HyAnchorLinkList, HTMLStencilElement {} + var HTMLHyAnchorLinkListElement: { + prototype: HTMLHyAnchorLinkListElement; + new (): HTMLHyAnchorLinkListElement; + }; interface HTMLHyBadgeElement extends Components.HyBadge, HTMLStencilElement {} var HTMLHyBadgeElement: { prototype: HTMLHyBadgeElement; @@ -1348,6 +1372,7 @@ declare global { 'hy-accordion-container': HTMLHyAccordionContainerElement; 'hy-accordion-item': HTMLHyAccordionItemElement; 'hy-adjacent-image-text': HTMLHyAdjacentImageTextElement; + 'hy-anchor-link-list': HTMLHyAnchorLinkListElement; 'hy-badge': HTMLHyBadgeElement; 'hy-banner': HTMLHyBannerElement; 'hy-baseline': HTMLHyBaselineElement; @@ -1459,6 +1484,24 @@ declare namespace LocalJSX { textDescription?: string; textTitle?: string; } + interface HyAnchorLinkList { + /** + * Array of anchor items + */ + anchorItems?: AnchorItem[] | string; + /** + * Close state label + */ + closeLabel?: string; + /** + * Title for content list button + */ + listTitle?: string; + /** + * Open state label + */ + openLabel?: string; + } interface HyBadge { variant?: 'primary' | 'warn' | 'success' | 'disabled'; } @@ -2300,6 +2343,7 @@ declare namespace LocalJSX { 'hy-accordion-container': HyAccordionContainer; 'hy-accordion-item': HyAccordionItem; 'hy-adjacent-image-text': HyAdjacentImageText; + 'hy-anchor-link-list': HyAnchorLinkList; 'hy-badge': HyBadge; 'hy-banner': HyBanner; 'hy-baseline': HyBaseline; @@ -2394,6 +2438,7 @@ declare module '@stencil/core' { 'hy-accordion-container': LocalJSX.HyAccordionContainer & JSXBase.HTMLAttributes<HTMLHyAccordionContainerElement>; 'hy-accordion-item': LocalJSX.HyAccordionItem & JSXBase.HTMLAttributes<HTMLHyAccordionItemElement>; 'hy-adjacent-image-text': LocalJSX.HyAdjacentImageText & JSXBase.HTMLAttributes<HTMLHyAdjacentImageTextElement>; + 'hy-anchor-link-list': LocalJSX.HyAnchorLinkList & JSXBase.HTMLAttributes<HTMLHyAnchorLinkListElement>; 'hy-badge': LocalJSX.HyBadge & JSXBase.HTMLAttributes<HTMLHyBadgeElement>; 'hy-banner': LocalJSX.HyBanner & JSXBase.HTMLAttributes<HTMLHyBannerElement>; 'hy-baseline': LocalJSX.HyBaseline & JSXBase.HTMLAttributes<HTMLHyBaselineElement>; diff --git a/src/components/hy-anchor-link-list/hy-anchor-link-list.scss b/src/components/hy-anchor-link-list/hy-anchor-link-list.scss new file mode 100644 index 0000000000000000000000000000000000000000..d43da57683c66f96ad16515ad803e7e740879715 --- /dev/null +++ b/src/components/hy-anchor-link-list/hy-anchor-link-list.scss @@ -0,0 +1,117 @@ +:host { + display: block; +} + +:host(.hy-anchor-link-list) { + border: 1px solid var(--brand-main); + width: 100%; + + @include breakpoint($narrow) { + margin-bottom: 1.75rem; + } + + @include breakpoint($medium) { + margin-bottom: 1.75rem; + } + + @include breakpoint($wide) { + margin-bottom: 2rem; + } + + @include breakpoint($xlarge) { + margin-bottom: 2.5rem; + } + + &.is-open { + border: 1px solid var(--grayscale-dark); + } +} + +.hy-anchor-link-list__button { + border: none; + background: none; + display: flex; + justify-content: space-between; + align-items: center; + padding: 15px 20px; + width: 100%; + + @include breakpoint($medium) { + padding: 18px 20px; + } + &:hover { + cursor: pointer; + } + + &.is-open { + .label-close-icon svg { + transform: rotate(180deg); + } + } + + &--title { + @include font-size(17px, 20px); + @include font-weight($bold); + display: flex; + align-items: center; + color: var(--brand-main); + font-family: var(--main-font-family); + letter-spacing: -0.32px; + + svg { + margin-right: 12px; + } + } + + &--label { + @include font-size(16px, 20px); + @include font-weight($semibold); + display: flex; + align-items: center; + color: var(--link-blue); + font-family: var(--main-font-family); + letter-spacing: -0.3px; + text-align: right; + + svg { + margin-left: 7px; + } + } +} + +.hy-anchor-link-list__container { + display: none; + flex-direction: column; + padding: 18px 0; + margin: 0 20px; + + &.is-open { + display: flex; + border-top: 1px solid var(--grayscale-dark); + } +} + +.hy-anchor-link-list__item { + @include font-size(16px, 20px); + @include font-weight($semibold); + + display: flex; + align-items: center; + color: var(--link-blue); + font-family: var(--main-font-family); + font-size: 16px; + font-weight: 600; + letter-spacing: -0.5px; + line-height: 20px; + margin-bottom: 16px; + text-decoration: none; + + &:hover { + cursor: pointer; + text-decoration: underline; + } + + svg { + margin-right: 12px; + } +} diff --git a/src/components/hy-anchor-link-list/hy-anchor-link-list.tsx b/src/components/hy-anchor-link-list/hy-anchor-link-list.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c0453deb8591d9cb36460d5b1b1624eecaa4b67b --- /dev/null +++ b/src/components/hy-anchor-link-list/hy-anchor-link-list.tsx @@ -0,0 +1,90 @@ +import {Component, Host, h, Prop, Watch, State, Listen} from '@stencil/core'; + +export interface AnchorItem { + title: string; + url: string; +} + +@Component({ + tag: 'hy-anchor-link-list', + styleUrl: 'hy-anchor-link-list.scss', + shadow: true, +}) +export class HyAnchorLinkList { + private _anchorItems: AnchorItem[]; + + /** + * Title for content list button + */ + @Prop() listTitle?: string; + /** + * Open state label + */ + @Prop() openLabel?: string; + /** + * Close state label + */ + @Prop() closeLabel?: string; + /** + * Array of anchor items + */ + @Prop() anchorItems: AnchorItem[] | string; + + @State() isOpen: boolean = false; + + @Watch('anchorItems') anchorItemsWatcher(data: AnchorItem[] | string) { + this._anchorItems = typeof data === 'string' ? JSON.parse(data) : data; + } + + componentWillLoad() { + this.anchorItemsWatcher(this.anchorItems); + } + + @Listen('handleLinkClick') + handleLinkClick(e) { + const targetId = e.target.getAttribute('href'); + if (targetId) { + const targetElement = document.getElementById(targetId.substring(1)); + const targetRect = targetElement.getBoundingClientRect(); + window.scrollTo({top: window.scrollY + targetRect.top - 200, behavior: 'smooth'}); + } + e.stopPropagation(); + } + + render() { + const classAttributes = ['hy-anchor-link-list'].join(' '); + const buttonClasses = { + 'hy-anchor-link-list__button': true, + 'is-open': this.isOpen, + }; + const containerClasses = { + 'hy-anchor-link-list__container': true, + 'is-open': this.isOpen, + }; + const items = this._anchorItems as Array<AnchorItem>; + + return ( + <Host class={classAttributes}> + <button aria-label={this.listTitle} onClick={() => (this.isOpen = !this.isOpen)} class={buttonClasses}> + <span class="hy-anchor-link-list__button--title"> + <hy-icon icon={'hy-icon-bullet-list'} size={18} fill={'var(--brand-main)'} /> + {this.listTitle} + </span> + <span class="hy-anchor-link-list__button--label"> + {this.isOpen ? this.closeLabel : this.openLabel} + <hy-icon class="label-close-icon" icon={'hy-icon-caret-down'} size={16} fill={'var(--brand-main)'} /> + </span> + </button> + <div aria-hidden={this.isOpen ? 'false' : 'true'} class={containerClasses}> + {items && + items.map((item) => ( + <a class="hy-anchor-link-list__item" href={item.url} onClick={this.handleLinkClick}> + <hy-icon icon={'hy-icon-arrow-down'} size={12} fill={'var(--brand-main)'} /> + {item.title} + </a> + ))} + </div> + </Host> + ); + } +} diff --git a/src/components/hy-anchor-link-list/readme.md b/src/components/hy-anchor-link-list/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..1c553748025fe5caf3691c1f2812941333a728f9 --- /dev/null +++ b/src/components/hy-anchor-link-list/readme.md @@ -0,0 +1,30 @@ +# hy-anchor-link-list + +<!-- Auto Generated Below --> + +## Properties + +| Property | Attribute | Description | Type | Default | +| ------------- | -------------- | ----------------------------- | ------------------------ | ----------- | +| `anchorItems` | `anchor-items` | Array of anchor items | `AnchorItem[] \| string` | `undefined` | +| `closeLabel` | `close-label` | Close state label | `string` | `undefined` | +| `listTitle` | `list-title` | Title for content list button | `string` | `undefined` | +| `openLabel` | `open-label` | Open state label | `string` | `undefined` | + +## Dependencies + +### Depends on + +- [hy-icon](../icon) + +### Graph + +```mermaid +graph TD; + hy-anchor-link-list --> hy-icon + style hy-anchor-link-list fill:#f9f,stroke:#333,stroke-width:4px +``` + +--- + +Helsinki University Design System diff --git a/src/components/icon/icon.tsx b/src/components/icon/icon.tsx index 604de46cf75db3082e80084eb018f3c181a120f5..a424c547189d5c4dbee0a5edb16062ea06a072c5 100644 --- a/src/components/icon/icon.tsx +++ b/src/components/icon/icon.tsx @@ -10,6 +10,7 @@ const iconNames: IconName = { 'hy-icon-arrow-to-right': (p) => <icons.IconArrowToRight {...p} />, 'hy-icon-alert': (p) => <icons.Alert {...p} />, 'hy-icon-arrow-up': (p) => <icons.ArrowUp {...p} />, + 'hy-icon-bullet-list': (p) => <icons.BulletList {...p} />, 'hy-icon-camera': (p) => <icons.Camera {...p} />, 'hy-icon-caret-down': (p) => <icons.CaretDown {...p} />, 'hy-icon-caret-left': (p) => <icons.CaretLeft {...p} />, diff --git a/src/components/icon/readme.md b/src/components/icon/readme.md index 325bab04a2a84bce132d3678ae777477376db770..0fc2ab0549f5685f4cbc2394d1c1e97fee4b8984 100644 --- a/src/components/icon/readme.md +++ b/src/components/icon/readme.md @@ -15,6 +15,7 @@ ### Used by - [hy-accordion-item](../accordion-item) +- [hy-anchor-link-list](../hy-anchor-link-list) - [hy-breadcrumbs](../hy-breadcrumbs) - [hy-button](../button) - [hy-checkbox](../hy-checkbox) @@ -57,6 +58,7 @@ ```mermaid graph TD; hy-accordion-item --> hy-icon + hy-anchor-link-list --> hy-icon hy-breadcrumbs --> hy-icon hy-button --> hy-icon hy-checkbox --> hy-icon diff --git a/src/index.html b/src/index.html index d5a1980823be32a8a9e09a1082e1f140093193f0..d2e3af931483b9450268230f59f34ee6719b9f87 100644 --- a/src/index.html +++ b/src/index.html @@ -343,6 +343,13 @@ </tbody> </table> </hy-table-container> + <hy-anchor-link-list + list-title="Contents of the page" + open-label="Open" + close-label="Close" + anchor-items='[{"title": "Mitä koulutusohjelmia Helsingin yliopistossa on tarjolla?", "url": "#hello-1"},{"title": "Haluaisin opiskella yksittäisiä kursseja, en kokonaista tutkintoa. Miten toimin?", "url": "#hello-2"},{"title": "Test title 3", "url": "https://www.google.com"}]' + > + </hy-anchor-link-list> <hy-paragraph-text> tdIS IS Pagination </hy-paragraph-text> @@ -406,7 +413,7 @@ <hy-paragraph-text> tdIS IS MAIN CONTENT </hy-paragraph-text> - + <h1 id="hello-1">Hello 1</h1> <hy-paragraph-text> tdis Is Degree Programmes. Bachelor/Master </hy-paragraph-text> @@ -449,6 +456,7 @@ <hy-paragraph-text> tdis Is Degree Programmes. Doctoral </hy-paragraph-text> + <h1 id="hello-2">Hello 1</h1> <hy-list-item variant="degree" item-title="Omnipotential sciences – Doctoral programme"