From 6942ccb4f35350b1abdd3049b02e1e38e7a6e89a Mon Sep 17 00:00:00 2001 From: Robbert Broersma Date: Tue, 22 Mar 2022 11:01:36 +0100 Subject: [PATCH 1/8] build: install Utrecht Design System BEM class names --- package-lock.json | 13 +++++++++++++ package.json | 1 + pages/_app.tsx | 1 + 3 files changed, 15 insertions(+) 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..09fcbf0d 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -2,6 +2,7 @@ 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(() => { From 57fe2659609647cc3220520cf9b89c6afd7a7240 Mon Sep 17 00:00:00 2001 From: Robbert Broersma Date: Tue, 22 Mar 2022 11:02:03 +0100 Subject: [PATCH 2/8] feat: ValueCurrency component --- src/components/ValueCurrency.tsx | 78 ++++++++ src/components/ValueCurrent.test.tsx | 261 +++++++++++++++++++++++++++ 2 files changed, 339 insertions(+) create mode 100644 src/components/ValueCurrency.tsx create mode 100644 src/components/ValueCurrent.test.tsx 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(