diff --git a/.github/workflows/continuous-delivery.yml b/.github/workflows/continuous-delivery.yml index 1f8bd960..ce00ce17 100644 --- a/.github/workflows/continuous-delivery.yml +++ b/.github/workflows/continuous-delivery.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - feat/currency pull_request: jobs: diff --git a/next.config.js b/next.config.js index cbc2b2c5..ce2eb652 100644 --- a/next.config.js +++ b/next.config.js @@ -15,7 +15,8 @@ module.exports = { headers: [ { key: 'Content-Security-Policy', - value: "default-src 'self'", + value: + "default-src 'self'; style-src data: 'self' 'unsafe-inline'; script-src data: 'self' 'unsafe-eval'; connect-src 'self'", }, { key: 'Referrer-Policy', diff --git a/package-lock.json b/package-lock.json index 0edb39af..2ff6a9ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "@types/react": "17.0.39", "@typescript-eslint/eslint-plugin": "5.12.0", "@typescript-eslint/parser": "5.12.0", + "@utrecht/component-library-css": "1.0.0-alpha.186", "eslint": "8.9.0", "eslint-config-next": "12.1.0", "eslint-config-prettier": "8.4.0", @@ -7649,6 +7650,12 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@utrecht/component-library-css": { + "version": "1.0.0-alpha.186", + "resolved": "https://registry.npmjs.org/@utrecht/component-library-css/-/component-library-css-1.0.0-alpha.186.tgz", + "integrity": "sha512-enzHVLplF3mCJzYPPt+MF/3NvshZYW7QLQrrD5U6ua1JReDMLvLpmetP6JR2VRCF8hUfYashyQbq/iXwFQDCeQ==", + "dev": true + }, "node_modules/@utrecht/design-tokens": { "version": "1.0.0-alpha.172", "resolved": "https://registry.npmjs.org/@utrecht/design-tokens/-/design-tokens-1.0.0-alpha.172.tgz", @@ -23261,6 +23268,12 @@ } } }, + "@utrecht/component-library-css": { + "version": "1.0.0-alpha.186", + "resolved": "https://registry.npmjs.org/@utrecht/component-library-css/-/component-library-css-1.0.0-alpha.186.tgz", + "integrity": "sha512-enzHVLplF3mCJzYPPt+MF/3NvshZYW7QLQrrD5U6ua1JReDMLvLpmetP6JR2VRCF8hUfYashyQbq/iXwFQDCeQ==", + "dev": true + }, "@utrecht/design-tokens": { "version": "1.0.0-alpha.172", "resolved": "https://registry.npmjs.org/@utrecht/design-tokens/-/design-tokens-1.0.0-alpha.172.tgz", diff --git a/package.json b/package.json index 0a633549..0317085f 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "@types/react": "17.0.39", "@typescript-eslint/eslint-plugin": "5.12.0", "@typescript-eslint/parser": "5.12.0", + "@utrecht/component-library-css": "1.0.0-alpha.186", "eslint": "8.9.0", "eslint-config-next": "12.1.0", "eslint-config-prettier": "8.4.0", diff --git a/pages/_app.tsx b/pages/_app.tsx index 2201552e..f1f7e6cc 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -2,17 +2,12 @@ import type { AppProps } from "next/app"; import { useEffect } from "react"; import "../styles/globals.css"; import "@utrecht/design-tokens/dist/index.css"; +import "@utrecht/component-library-css/dist/bem.css"; function MyApp({ Component, pageProps }: AppProps) { useEffect(() => { import("@utrecht/web-component-library-stencil/dist/utrecht/utrecht.esm.js"); }, []); - useEffect(() => { - const script = document.createElement("script"); - script.async = true; - script.src = "https://unpkg.com/@nl-design-system-unstable/theme-switcher"; - document.body.appendChild(script); - }, []); useEffect(() => { document.documentElement.classList.add("utrecht-theme"); diff --git a/pages/amounts.tsx b/pages/amounts.tsx new file mode 100644 index 00000000..1fd6cc5b --- /dev/null +++ b/pages/amounts.tsx @@ -0,0 +1,168 @@ +import { UtrechtDocument } from "@utrecht/web-component-library-react"; +import Head from "next/head"; +import { ValueCurrency } from "../src/components/ValueCurrency"; +import { Table } from "../src/components/utrecht/Table"; +import { TableBody } from "../src/components/utrecht/TableBody"; +import { TableCell } from "../src/components/utrecht/TableCell"; +import { TableHeader } from "../src/components/utrecht/TableHeader"; +import { TableHeaderCell } from "../src/components/utrecht/TableHeaderCell"; +import { TableRow } from "../src/components/utrecht/TableRow"; + +export default function AmountsDemo() { + return ( + <> + + + Voorbeeld met bedragen + + +
+

Voorbeeld met bedragen

+

Dit is een tijdelijke pagina.

+

Lijst met eerst het bedrag uitgeschreven in tekst, daarna volgt de versie met speciale opmaak.

+
+

Nederlands

+ + + + Type bedrag + Uitgeschreven + Toegankelijk label + + + + + rond bedrag + honderddrieëntwintig euro + + + + + + bedrag met centen + drie euro en vijfennegentig cent + + + + + + bedrag met alleen centen + negenennegentig eurocent + + + + + + negatief bedrag + minus drie euro en vijfennegentig cent + + + + + + groot bedrag + vierhonderdvijfendertigduizend euro + + + + + + bedrag in buitenlandse valuta + honderd Amerikaanse dollar + + + + + + bedrag in buitenlandse valuta + honderd Canadese dollar + + + + + + bedrag in buitenlandse valuta + honderd Britse pond + + + + + + +
+
+
+

American English

+ + + + Type of amount + Written in full + Accessible label + + + + + simple amount + one hundred twenty-three dollars + + + + + + amount with cents + three dollars ninety-five + + + + + + only cents + ninety-nine cents + + + + + + negative amount + minus three dollars and ninety-five cents + + + + + + large amount + four hundred thirty-five thousand dollars + + + + + + amount with foreign currency + one hundred Euros + + + + + + amount with foreign currency + one hundred Canadian dollars + + + + + + amount with foreign currency + one hundred pound sterling + + + + + +
+
+
+
+ + ); +} diff --git a/pages/transactions.tsx b/pages/transactions.tsx new file mode 100644 index 00000000..c2dd1a08 --- /dev/null +++ b/pages/transactions.tsx @@ -0,0 +1,128 @@ +/* eslint-disable react/no-unescaped-entities */ + +import { UtrechtDocument } from "@utrecht/web-component-library-react"; +import Head from "next/head"; +import { useRouter } from "next/router"; +import { AlternateLangNavComponent } from "../src/AlternateLangNavComponent"; +import { ValueCurrency } from "../src/components/ValueCurrency"; +import { ValueDate } from "../src/components/ValueDate"; +import { Heading1 } from "../src/components/utrecht/Heading1"; +import { Heading2 } from "../src/components/utrecht/Heading2"; +import { Paragraph } from "../src/components/utrecht/Paragraph"; +import { Table } from "../src/components/utrecht/Table"; +import { TableBody } from "../src/components/utrecht/TableBody"; +import { TableCell } from "../src/components/utrecht/TableCell"; +import { TableHeader } from "../src/components/utrecht/TableHeader"; +import { TableHeaderCell } from "../src/components/utrecht/TableHeaderCell"; +import { TableRow } from "../src/components/utrecht/TableRow"; +import { UnorderedList } from "../src/components/utrecht/UnorderedList"; +import { UnorderedListItem } from "../src/components/utrecht/UnorderedListItem"; + +export default function Home() { + const locale = useRouter().locale || "en"; + + const locales = [ + { + lang: "en", + hrefLang: "en", + title: "This page in English", + textContent: "EN", + }, + { + lang: "nl", + hrefLang: "nl", + title: "Deze pagina in Nederlands", + textContent: "NL", + }, + ]; + + return ( + <> + + + Voorbeeld met banktransacties + +
+ +
+ +
+ Voorbeeld met banktransacties + Dit is een tijdelijke pagina. + + + Voor de tabel is geen caption gebruikt maar een heading element (h2 in dit + geval), zodat je de sneltoetsen voor "volgende heading" en "vorige heading" snel de goede tabel kan + vinden. + + + De heading is gekoppeld aan de tabel met aria-labelledby, zodat wanneer de tabel wordt + weergegeven door een screenreader wel de heading het effect heeft van een table caption. Zonder deze + koppeling, wanneer je achteruit door het document gaat, zou je een tabel tegenkomen zonder de context waar + het over gaat. + + + De bedragen in de tabel zijn voorzien van een aria-label waar de notatie van het bedrag is + geoptimaliseerd voor onder andere screen readers (zodat de waarde{" "} + programmatically determined kan worden). + + + januari 2022 + + + + Icoon + +
Datum
+
+ Onderwerp + Bedrag +
+
+ + + + + + + +
+ +
Leefgeld
+
+
+ + + +
+ + + + + + +
+ +
Belastingdienst
+
+
+ + + + +
+
+
+
+
+ + ); +} diff --git a/src/components/ValueCurrency.tsx b/src/components/ValueCurrency.tsx new file mode 100644 index 00000000..c8b56a4f --- /dev/null +++ b/src/components/ValueCurrency.tsx @@ -0,0 +1,78 @@ +/** + * @license EUPL-1.2 + * Copyright (c) 2021 Robbert Broersma + */ + +import clsx from "clsx"; +import { ForwardedRef, forwardRef, HTMLAttributes } from "react"; + +interface ValueCurrencyProps extends HTMLAttributes { + currency?: string; + amount: string | number; + locale?: string; +} + +export const formatLabel = (locale: string, currency: string, amount: number): string => + new Intl.NumberFormat(locale, { + style: "currency", + currency, + minimumFractionDigits: Number.isInteger(amount) ? 0 : undefined, + useGrouping: false, + }) + .format(amount) + // Remove whitespace + .replace(/[\s]+/g, "") + // Replace dash (U+002D) with minus sign (U+2212) + .replace("-", "\u2212"); + +export const formatVisually = (locale: string, currency: string, amount: number): string => { + let formatted = new Intl.NumberFormat(locale, { + style: "currency", + currency, + }).format(amount); + + // Replace dash (U+002D) with minus sign (U+2212) + formatted = formatted.replace(/-/, "\u2212"); + + // Move the minus to before the currency + if ((locale === "nl" || locale === "nl-NL") && /\u2212/.test(formatted)) { + formatted = formatted.replace(/(.+)\u2212(.+)/, "\u2212 $1$2"); + } + + // Replace white space with non-breaking space + formatted = formatted.replace(/ /g, "\u00A0"); + + return formatted; +}; + +export const ValueCurrency = forwardRef( + ( + { children, currency = "EUR", amount, locale = "nl-NL", className, ...restProps }: ValueCurrencyProps, + ref: ForwardedRef + ) => { + const number = typeof amount === "string" ? parseFloat(amount) : amount; + const labelFormatted = formatLabel(locale, currency, number); + let visuallyFormatted = formatVisually(locale, currency, number); + + return ( + 0 && "example-value--positive", + className + )} + aria-label={labelFormatted} + > + {children || visuallyFormatted} + + ); + } +); + +ValueCurrency.displayName = "example-value--currency"; diff --git a/src/components/ValueCurrent.test.tsx b/src/components/ValueCurrent.test.tsx new file mode 100644 index 00000000..737b2f16 --- /dev/null +++ b/src/components/ValueCurrent.test.tsx @@ -0,0 +1,261 @@ +import { render } from "@testing-library/react"; +import { createRef } from "react"; +import { formatLabel, formatVisually, ValueCurrency } from "./ValueCurrency"; +import "@testing-library/jest-dom"; + +describe("Currency value", () => { + it("renders a data HTML element", () => { + const { container } = render(); + + const currency = container.querySelector("data:only-child"); + + expect(currency).toBeInTheDocument(); + }); + + it("renders a data HTML element with a value attribute", () => { + const { container } = render(); + + const currency = container.querySelector("data:only-child"); + + expect(currency?.getAttribute("value")).toContain("EUR"); + expect(currency?.getAttribute("value")).toContain("123"); + }); + + it("renders a BEM class name", () => { + const { container } = render(); + + const currency = container.querySelector(":only-child"); + + expect(currency).toHaveClass("example-value"); + expect(currency).toHaveClass("example-value--currency"); + }); + + it("renders rich text content", () => { + const { container } = render( + + + 123 + , + 45 + + ); + + const currency = container.querySelector(":only-child"); + + const richText = currency?.querySelector("span"); + + expect(richText).toBeInTheDocument(); + }); + + it("can be hidden", () => { + const { container } = render(