diff --git a/doc-site/components/Komponentoversikt.md b/doc-site/components/Komponentoversikt.md index a2b09374..0521ae63 100644 --- a/doc-site/components/Komponentoversikt.md +++ b/doc-site/components/Komponentoversikt.md @@ -27,6 +27,13 @@ nodeId er ID til komponent-sida i Figma. Den ligger som en parameter i URL'en ti statusDesign: 'Ferdig', statusCode: 'Ferdig' }, + { + name: 'nve-aspect-rose', + nodeId: undefined, + description: undefined, + statusDesign: 'Ferdig', + statusCode: 'Ferdig' + }, { name: 'nve-attachments', nodeId: '647-192139', diff --git a/doc-site/components/nve-aspect-rose.md b/doc-site/components/nve-aspect-rose.md new file mode 100644 index 00000000..46d78da3 --- /dev/null +++ b/doc-site/components/nve-aspect-rose.md @@ -0,0 +1,84 @@ +--- +layout: component +outline: [2, 3] +--- + + + +```html + +``` + + + +## Eksempler + +### Utsatte himmelretninger + +Bruk `value` for å sette hvilke himmelretninger som er utsatt (farlige). + + +```html + Ingen Kun nord + øst, sør-øst, sør og sør-vest + Alle Verdien +blir ignorert hvis vi ikke har eksakt 8 sifre +``` + + + +### Størrelse + +Bruk css-variabelen `--aspect-rose-size` for å endre bredde og høyde. 90px er standard. + + + +```html + 60px + 90px (standard) + 120px +``` + + + +### Språk + +Bruk `lang` for å angi språk for kompassretningene. `no` (norsk) er standard. Kun norsk og engelsk støttes. + + +```html + Norsk + Engelsk +``` + + + +### Aria-label og svg-title + +Bruk `label` for å overstyre standard-tekstene for aria-label og svg title. + + + +```html +Hold muspeker over for å se label.
+Med standard norsk tekst +Med standard engelsk tekst +Overstyrt tekst +``` + +
+ +### Farger + +Bruk css-variablene`--aspect-rose-outline-color`, `--aspect-rose-affected-color`og`--aspect-rose-unaffected-color` for å overstyre fargene. + + + +```html + +``` + + diff --git a/src/components/nve-aspect-rose/nve-aspect-rose.component.ts b/src/components/nve-aspect-rose/nve-aspect-rose.component.ts new file mode 100644 index 00000000..53744778 --- /dev/null +++ b/src/components/nve-aspect-rose/nve-aspect-rose.component.ts @@ -0,0 +1,100 @@ +import { html, svg, LitElement } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { INveComponent } from '@interfaces/NveComponent.interface'; +import styles from './nve-aspect-rose.styles'; +import { ifDefined } from 'lit/directives/if-defined.js'; + +/** + * Viser utsatte himmelretninger som en kompassrose. + * Rosen er delt opp i 8 sektorer. Utsatte sektorer vises i rødt. + * @cssproperty --aspect-rose-size - Høyde og bredde på komponenten. 90px er standard. + * @cssproperty --aspect-rose-outline-color - Farge på sirkelens omriss. Standard er #c6c6c5. + * @cssproperty --aspect-rose-affected-color - Farge på utsatte sektorer. Standard er #d21523. + * @cssproperty --aspect-rose-unaffected-color - Farge på ikke-utsatte sektorer. Standard er #e3e3e3. + */ +@customElement('nve-aspect-rose') +export default class NveAspectRose extends LitElement implements INveComponent { + @property({ type: String }) testId: string | undefined = undefined; + + /** + * 8-tegns binærtekst som representerer utsatte sektorer. + * Starter med nordlig sektor og går deretter med klokka. + * Eksempel: "00111110" + */ + @property({ type: String }) value: string = '00000000'; + + /** Språk for himmelretningene. 'no' for norsk, 'en' for engelsk. */ + @property({ type: String }) lang: 'no' | 'en' = 'no'; + + /** + * Tilgjengelig tittel. + * Vises som aria-label på SVG-elementet og som i SVG. + * Standardverdi avhenger av språket: 'Eksponerte sektorer' for norsk, 'Affected aspects' for engelsk. + * Du kan overstyre denne teksten. + */ + @property({ type: String }) label: string | undefined = undefined; + + static styles = [styles]; + + private get effectiveLabel() { + return this.label ?? (this.lang === 'en' ? 'Affected aspects' : 'Eksponerte sektorer'); + } + + // Etiketter for de fire himmelretningene. Brukes til å plassere sirklene og teksten inni dem. + private get directions() { + return [ + { cx: 45, cy: 9, label: 'N' }, + { cx: 81, cy: 45, label: this.lang === 'en' ? 'E' : 'Ø' }, + { cx: 45, cy: 81, label: 'S' }, + { cx: 9, cy: 45, label: this.lang === 'en' ? 'W' : 'V' }, + ]; + } + + // Gradene for å rotere sektorene slik at de dekker riktig himmelretning. Starter med nordlig sektor og går deretter med klokka. + private readonly rotations = [-22.5, 22.5, 67.5, 112.5, 157.5, 202.5, 247.5, 292.5]; + + render() { + const value = /^[01]{8}$/.test(this.value) ? this.value : '00000000'; // Ignorer value hvis den ikke er gyldig + return html` + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 90 90" + width="100%" + height="100%" + role="img" + aria-label=${this.effectiveLabel} + testid=${ifDefined(this.testId)} + > + ${svg`<title>${this.effectiveLabel}`} + + ${this.rotations.map( + (rot, i) => svg` + + ` + )} + + + ${this.directions.map( + ({ cx, cy, label }) => svg` + + + ${label} + + ` + )} + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'nve-aspect-rose': NveAspectRose; + } +} diff --git a/src/components/nve-aspect-rose/nve-aspect-rose.styles.ts b/src/components/nve-aspect-rose/nve-aspect-rose.styles.ts new file mode 100644 index 00000000..57f1c7b1 --- /dev/null +++ b/src/components/nve-aspect-rose/nve-aspect-rose.styles.ts @@ -0,0 +1,24 @@ +import { css } from 'lit'; + +export default css` + :host { + display: inline-block; + width: var(--aspect-rose-size, 90px); + height: var(--aspect-rose-size, 90px); + } + + text { + font-family: 'Source Sans 3', sans-serif; + font-weight: var(--font-weight-regular); + } + + .circle-outline { + stroke: var(--aspect-rose-outline-color, #c6c6c5); + } + .sector-affected { + fill: var(--aspect-rose-affected-color, #d21523); + } + .sector-unaffected { + fill: var(--aspect-rose-unaffected-color, #e3e3e3); + } +`; diff --git a/src/components/nve-aspect-rose/nve-aspect-rose.test.ts b/src/components/nve-aspect-rose/nve-aspect-rose.test.ts new file mode 100644 index 00000000..fb5cfedb --- /dev/null +++ b/src/components/nve-aspect-rose/nve-aspect-rose.test.ts @@ -0,0 +1,118 @@ +import { afterAll, describe, expect, it } from 'vitest'; +import { fixture, fixtureCleanup } from '@open-wc/testing'; +import { html } from 'lit'; +import NveAspectRose from './nve-aspect-rose.component'; + +if (!customElements.get('nve-aspect-rose')) { + customElements.define('nve-aspect-rose', NveAspectRose); +} + +describe('nve-aspect-rose', () => { + afterAll(() => { + fixtureCleanup(); + }); + + it('is attached to the DOM', async () => { + const el = await fixture(html``); + expect(document.body.contains(el)).toBe(true); + }); + + it('has correct default properties', async () => { + const el = await fixture(html``); + expect(el.value).toBe('00000000'); + expect(el.lang).toBe('no'); + expect(el.label).toBeUndefined(); + }); + + it('renders 8 sectors', async () => { + const el = await fixture(html``); + const paths = el.shadowRoot?.querySelectorAll('path'); + expect(paths?.length).toBe(8); + }); + + it('marks affected sectors with correct css class', async () => { + const el = await fixture(html``); + const paths = el.shadowRoot?.querySelectorAll('path'); + expect(paths?.[0].classList.contains('sector-affected')).toBe(true); + expect(paths?.[1].classList.contains('sector-unaffected')).toBe(true); + expect(paths?.[7].classList.contains('sector-affected')).toBe(true); + }); + + it('marks all sectors as unaffected when value is all zeros', async () => { + const el = await fixture(html``); + const paths = el.shadowRoot?.querySelectorAll('path'); + paths?.forEach((path) => { + expect(path.classList.contains('sector-unaffected')).toBe(true); + }); + }); + + it('marks all sectors as affected when value is all ones', async () => { + const el = await fixture(html``); + const paths = el.shadowRoot?.querySelectorAll('path'); + paths?.forEach((path) => { + expect(path.classList.contains('sector-affected')).toBe(true); + }); + }); + + it('marks all sectors as unaffected when value is not valid', async () => { + const el = await fixture(html``); + const paths = el.shadowRoot?.querySelectorAll('path'); + paths?.forEach((path) => { + expect(path.classList.contains('sector-unaffected')).toBe(true); + }); + }); + + it('shows norwegian direction labels by default', async () => { + const el = await fixture(html``); + const texts = el.shadowRoot?.querySelectorAll('text'); + const labels = Array.from(texts ?? []).map((t) => t.textContent); + expect(labels).toContain('N'); + expect(labels).toContain('Ø'); + expect(labels).toContain('S'); + expect(labels).toContain('V'); + }); + + it('shows norwegian direction labels by default even if lang is set to something else', async () => { + const el = await fixture(html``); + const texts = el.shadowRoot?.querySelectorAll('text'); + const labels = Array.from(texts ?? []).map((t) => t.textContent); + expect(labels).toContain('N'); + expect(labels).toContain('Ø'); + expect(labels).toContain('S'); + expect(labels).toContain('V'); + }); + + it('shows english direction labels when lang is en', async () => { + const el = await fixture(html``); + const texts = el.shadowRoot?.querySelectorAll('text'); + const labels = Array.from(texts ?? []).map((t) => t.textContent); + expect(labels).toContain('N'); + expect(labels).toContain('E'); + expect(labels).toContain('S'); + expect(labels).toContain('W'); + }); + + it('uses custom label as aria-label', async () => { + const el = await fixture(html``); + const svg = el.shadowRoot?.querySelector('svg'); + expect(svg?.getAttribute('aria-label')).toBe('Min tittel'); + }); + + it('uses default norwegian label when no label is set', async () => { + const el = await fixture(html``); + const svg = el.shadowRoot?.querySelector('svg'); + expect(svg?.getAttribute('aria-label')).toBe('Eksponerte sektorer'); + }); + + it('uses default norwegian label when no label is set even if lang is set to something else', async () => { + const el = await fixture(html``); + const svg = el.shadowRoot?.querySelector('svg'); + expect(svg?.getAttribute('aria-label')).toBe('Eksponerte sektorer'); + }); + + it('uses default english label when lang is en and no label is set', async () => { + const el = await fixture(html``); + const svg = el.shadowRoot?.querySelector('svg'); + expect(svg?.getAttribute('aria-label')).toBe('Affected aspects'); + }); +}); diff --git a/src/nve-designsystem.ts b/src/nve-designsystem.ts index 7f16f435..764696ce 100644 --- a/src/nve-designsystem.ts +++ b/src/nve-designsystem.ts @@ -1,8 +1,10 @@ /** Alle komponenter som er tilgjengelige, i alfabetisk rekkefølge. */ /** Denne filen blir genererert av pnpm run add-component */ + export { default as NveAccordion } from './components/nve-accordion/nve-accordion.component'; export { default as NveAccordionItem } from './components/nve-accordion-item/nve-accordion-item.component'; export { default as NveAlert } from './components/nve-alert/nve-alert.component'; +export { default as NveAspectRose } from './components/nve-aspect-rose/nve-aspect-rose.component'; export { default as NveBadge } from './components/nve-badge/nve-badge.component'; export { default as NveButton } from './components/nve-button/nve-button.component'; export { default as NveCarousel } from './components/nve-carousel/nve-carousel.component';