diff --git a/package.json b/package.json index 74d6e27504..fd4c290bd6 100644 --- a/package.json +++ b/package.json @@ -23,34 +23,34 @@ "./storybook" ], "devDependencies": { - "@lerna-lite/cli": "3.3.1", - "@lerna-lite/run": "3.3.1", - "@types/node": "20.11.30", - "@typescript-eslint/eslint-plugin": "7.3.1", - "@typescript-eslint/parser": "7.3.1", + "@lerna-lite/cli": "3.3.3", + "@lerna-lite/run": "3.3.3", + "@types/node": "20.12.7", + "@typescript-eslint/eslint-plugin": "7.7.0", + "@typescript-eslint/parser": "7.7.0", "conventional-changelog-conventionalcommits": "7.0.2", "eslint": "8.57.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-import": "2.29.1", - "eslint-plugin-jest": "27.9.0", + "eslint-plugin-jest": "28.2.0", "eslint-plugin-json": "3.1.0", "eslint-plugin-mdx": "3.1.5", "eslint-plugin-react": "7.34.1", - "html-validate": "8.17.0", + "html-validate": "8.18.1", "husky": "9.0.11", "lint-staged": "15.2.2", "markdownlint-cli": "0.39.0", - "npm-check-updates": "16.14.17", + "npm-check-updates": "16.14.18", "npm-package-json-lint": "7.1.0", "npm-run-all": "4.1.5", "plop": "4.0.1", - "postcss": "8.4.37", + "postcss": "8.4.38", "prettier": "3.2.5", "rimraf": "5.0.5", - "stylelint": "16.2.1", - "stylelint-config-standard-scss": "13.0.0", + "stylelint": "16.3.1", + "stylelint-config-standard-scss": "13.1.0", "stylelint-order": "6.0.4", - "typescript": "5.4.2", + "typescript": "5.4.5", "wait-on": "7.2.0" }, "scripts": { diff --git a/packages/css/package.json b/packages/css/package.json index c54c953cf7..1b0751ab25 100644 --- a/packages/css/package.json +++ b/packages/css/package.json @@ -20,9 +20,6 @@ }, "devDependencies": { "@amsterdam/design-system-tokens": "workspace:*", - "sass": "1.72.0" - }, - "dependencies": { - "@utrecht/components": "3.0.0" + "sass": "1.75.0" } } diff --git a/packages/css/src/components/accordion/accordion.scss b/packages/css/src/components/accordion/accordion.scss index 0cfeb8e257..d6ef1a0e1a 100644 --- a/packages/css/src/components/accordion/accordion.scss +++ b/packages/css/src/components/accordion/accordion.scss @@ -5,25 +5,30 @@ @import "../../common/text-rendering"; +.ams-accordion { + display: flex; + flex-direction: column; + gap: var(--ams-accordion-gap); + + @include text-rendering; +} + .ams-accordion__header { display: flex; margin-block: 0; margin-inline: 0; } -@mixin reset-button { +.ams-accordion__button { background-color: transparent; border: 0; -} - -.ams-accordion__button { color: var(--ams-accordion-button-color); cursor: pointer; display: flex; font-family: var(--ams-accordion-button-font-family); font-size: var(--ams-accordion-button-font-size); font-weight: var(--ams-accordion-button-font-weight); - justify-content: space-between; + gap: var(--ams-accordion-button-gap); line-height: var(--ams-accordion-button-line-height); padding-block: var(--ams-accordion-button-padding-block); padding-inline: var(--ams-accordion-button-padding-inline); @@ -34,24 +39,25 @@ } &:hover { - box-shadow: var(--ams-accordion-button-hover-box-shadow); + color: var(--ams-accordion-button-hover-color); } +} - svg { - transform: rotate(0deg); - transition: transform 0.3s ease; +.ams-accordion__icon svg { + rotate: 0deg; + transition: none; - @media (prefers-reduced-motion) { - transition: none; - } + [aria-expanded="true"] & { + rotate: -180deg; } - @include text-rendering; - @include reset-button; + @media not (prefers-reduced-motion) { + transition: rotate 0.3s ease; + } } .ams-accordion__button[aria-expanded="true"] svg { - transform: rotate(180deg); + rotate: -180deg; } .ams-accordion__panel { diff --git a/packages/css/src/components/date-input/README.md b/packages/css/src/components/date-input/README.md new file mode 100644 index 0000000000..b4061163c7 --- /dev/null +++ b/packages/css/src/components/date-input/README.md @@ -0,0 +1,11 @@ + + +# Date Input + +Helps users enter a date. + +## Visual considerations + +This component uses a native date input, which is styled differently in different browsers and operating systems. +Recreating the native functionality is quite difficult and prone to accessibility errors, which is why we’ve chosen not to do that. +This does mean that this component does not look identical on all browsers and operating systems. diff --git a/packages/css/src/components/date-input/date-input.scss b/packages/css/src/components/date-input/date-input.scss new file mode 100644 index 0000000000..88522d76f0 --- /dev/null +++ b/packages/css/src/components/date-input/date-input.scss @@ -0,0 +1,78 @@ +/** + * @license EUPL-1.2+ + * Copyright Gemeente Amsterdam + */ + +@import "../../common/text-rendering"; + +@mixin reset { + // Reset native appearance, this causes issues on iOS and Android devices + -webkit-appearance: none; + appearance: none; + border: 0; + border-radius: 0; // Reset rounded borders on iOS devices + box-sizing: border-box; + margin-block: 0; +} + +.ams-date-input { + background-color: var(--ams-date-input-background-color); + box-shadow: var(--ams-date-input-box-shadow); + color: var(--ams-date-input-color); + font-family: var(--ams-date-input-font-family); + font-size: var(--ams-date-input-font-size); + font-weight: var(--ams-date-input-font-weight); + line-height: var(--ams-date-input-line-height); + + // Set min height for iOS, otherwise an empty box is a lot smaller than a filled one. + min-height: calc( + (var(--ams-date-input-font-size) * var(--ams-date-input-line-height)) + 2 * var(--ams-date-input-padding-block) + ); + + // Set min width for iOS, so the width is closer to the filled in width. + min-width: calc(8ch + (2 * var(--ams-date-input-padding-inline))); + outline-offset: var(--ams-date-input-outline-offset); + padding-block: var(--ams-date-input-padding-block); + padding-inline: var(--ams-date-input-padding-inline); + touch-action: manipulation; + + @include text-rendering; + @include reset; + + &:hover { + box-shadow: var(--ams-date-input-hover-box-shadow); + } +} + +// This changes the default calendar icon on Chromium browsers only +.ams-date-input::-webkit-calendar-picker-indicator { + appearance: none; + background-image: var(--ams-date-input-calender-picker-indicator-background-image); + cursor: pointer; +} + +.ams-date-input:hover::-webkit-calendar-picker-indicator { + background-image: var(--ams-date-input-hover-calender-picker-indicator-background-image); +} + +.ams-date-input:disabled { + background-color: var(--ams-date-input-disabled-background-color); + box-shadow: var(--ams-date-input-disabled-box-shadow); + color: var(--ams-date-input-disabled-color); + cursor: not-allowed; +} + +.ams-date-input:disabled::-webkit-calendar-picker-indicator { + background-image: var(--ams-date-input-disabled-calender-picker-indicator-background-image); + visibility: visible; +} + +.ams-date-input:invalid, +.ams-date-input[aria-invalid="true"] { + box-shadow: var(--ams-date-input-invalid-box-shadow); + + &:hover { + // TODO: this should be the (currently non-existent) dark red hover color + box-shadow: var(--ams-date-input-invalid-hover-box-shadow); + } +} diff --git a/packages/css/src/components/form-label/form-label.scss b/packages/css/src/components/form-label/form-label.scss deleted file mode 100644 index b6a6bab147..0000000000 --- a/packages/css/src/components/form-label/form-label.scss +++ /dev/null @@ -1,18 +0,0 @@ -/** - * @license EUPL-1.2+ - * Copyright Gemeente Amsterdam - */ - -@import "../../common/hyphenation"; -@import "../../common/text-rendering"; - -.ams-form-label { - color: var(--ams-form-label-color); - font-family: var(--ams-form-label-font-family); - font-size: var(--ams-form-label-font-size); - font-weight: var(--ams-form-label-font-weight); - line-height: var(--ams-form-label-line-height); - - @include hyphenation; - @include text-rendering; -} diff --git a/packages/css/src/components/index.scss b/packages/css/src/components/index.scss index 09906b0d4a..640cfeb750 100644 --- a/packages/css/src/components/index.scss +++ b/packages/css/src/components/index.scss @@ -4,7 +4,10 @@ */ /* Append here */ + @import "./select/select"; +@import "./time-input/time-input"; +@import "./date-input/date-input"; @import "./document/document"; @import "./avatar/avatar"; @import "./form-field-character-counter/form-field-character-counter"; @@ -40,7 +43,7 @@ @import "./button/button"; @import "./card/card"; @import "./checkbox/checkbox"; -@import "./form-label/form-label"; +@import "./label/label"; @import "./grid/grid"; @import "./heading/heading"; @import "./spotlight/spotlight"; diff --git a/packages/css/src/components/form-label/README.md b/packages/css/src/components/label/README.md similarity index 92% rename from packages/css/src/components/form-label/README.md rename to packages/css/src/components/label/README.md index 623bea5655..7851a28de1 100644 --- a/packages/css/src/components/form-label/README.md +++ b/packages/css/src/components/label/README.md @@ -1,6 +1,6 @@ -# Form Label +# Label Describes a form control. diff --git a/packages/css/src/components/label/label.scss b/packages/css/src/components/label/label.scss new file mode 100644 index 0000000000..53ab1f55a4 --- /dev/null +++ b/packages/css/src/components/label/label.scss @@ -0,0 +1,18 @@ +/** + * @license EUPL-1.2+ + * Copyright Gemeente Amsterdam + */ + +@import "../../common/hyphenation"; +@import "../../common/text-rendering"; + +.ams-label { + color: var(--ams-label-color); + font-family: var(--ams-label-font-family); + font-size: var(--ams-label-font-size); + font-weight: var(--ams-label-font-weight); + line-height: var(--ams-label-line-height); + + @include hyphenation; + @include text-rendering; +} diff --git a/packages/css/src/components/search-field/search-field.scss b/packages/css/src/components/search-field/search-field.scss index 00d7c77f01..56cdf5069e 100644 --- a/packages/css/src/components/search-field/search-field.scss +++ b/packages/css/src/components/search-field/search-field.scss @@ -11,12 +11,17 @@ } @mixin reset-input { + // Reset native appearance, this causes issues on iOS and Android devices + -webkit-appearance: none; + appearance: none; border: 0; + border-radius: 0; // Reset rounded borders on iOS devices box-sizing: border-box; margin-block: 0; } .ams-search-field__input { + background-color: var(--ams-search-field-input-background-color); box-shadow: var(--ams-search-field-input-box-shadow); color: var(--ams-search-field-input-color); font-family: var(--ams-search-field-input-font-family); @@ -56,6 +61,10 @@ @mixin reset-button { border: 0; + + // Reset margins added by Safari on iOS + margin-block: 0; + margin-inline: 0; } .ams-search-field__button { diff --git a/packages/css/src/components/text-area/text-area.scss b/packages/css/src/components/text-area/text-area.scss index 492ba03295..7c253dc504 100644 --- a/packages/css/src/components/text-area/text-area.scss +++ b/packages/css/src/components/text-area/text-area.scss @@ -11,6 +11,7 @@ } .ams-text-area { + background-color: var(--ams-text-area-background-color); border: 0; box-shadow: var(--ams-text-area-box-shadow); color: var(--ams-text-area-color); diff --git a/packages/css/src/components/text-input/text-input.scss b/packages/css/src/components/text-input/text-input.scss index e50df12312..22f3e1e938 100644 --- a/packages/css/src/components/text-input/text-input.scss +++ b/packages/css/src/components/text-input/text-input.scss @@ -11,6 +11,7 @@ } .ams-text-input { + background-color: var(--ams-text-input-background-color); border: 0; box-shadow: var(--ams-text-input-box-shadow); color: var(--ams-text-input-color); diff --git a/packages/css/src/components/time-input/README.md b/packages/css/src/components/time-input/README.md new file mode 100644 index 0000000000..bf50aff30c --- /dev/null +++ b/packages/css/src/components/time-input/README.md @@ -0,0 +1,11 @@ + + +# Time Input + +Helps users enter time. + +## Visual considerations + +This component uses a native time input, which is styled differently in different browsers and operating systems. +Recreating the native functionality is quite difficult and prone to accessibility errors, which is why we’ve chosen not to do that. +This does mean that this component does not look identical on all browsers and operating systems. diff --git a/packages/css/src/components/time-input/time-input.scss b/packages/css/src/components/time-input/time-input.scss new file mode 100644 index 0000000000..fc721ca6bf --- /dev/null +++ b/packages/css/src/components/time-input/time-input.scss @@ -0,0 +1,79 @@ +/** + * @license EUPL-1.2+ + * Copyright Gemeente Amsterdam + */ + +@import "../../common/text-rendering"; + +@mixin reset { + // Reset native appearance, this causes issues on iOS and Android devices + -webkit-appearance: none; + appearance: none; + border: 0; + border-radius: 0; // Reset rounded borders on iOS devices + box-sizing: border-box; + margin-block: 0; + width: auto; // Reset width of 10em set by Android devices +} + +.ams-time-input { + background-color: var(--ams-time-input-background-color); + box-shadow: var(--ams-time-input-box-shadow); + color: var(--ams-time-input-color); + font-family: var(--ams-time-input-font-family); + font-size: var(--ams-time-input-font-size); + font-weight: var(--ams-time-input-font-weight); + line-height: var(--ams-time-input-line-height); + + // Set min height for iOS, otherwise an empty box is a lot smaller than a filled one. + min-height: calc( + (var(--ams-time-input-font-size) * var(--ams-time-input-line-height)) + 2 * var(--ams-time-input-padding-block) + ); + + // Set min width for iOS, so the width is closer to the filled in width. + min-width: calc(4ch + (2 * var(--ams-time-input-padding-inline))); + outline-offset: var(--ams-time-input-outline-offset); + padding-block: var(--ams-time-input-padding-block); + padding-inline: var(--ams-time-input-padding-inline); + touch-action: manipulation; + + @include text-rendering; + @include reset; + + &:hover { + box-shadow: var(--ams-time-input-hover-box-shadow); + } +} + +// This changes the default calendar icon on Chromium browsers only +.ams-time-input::-webkit-calendar-picker-indicator { + appearance: none; + background-image: var(--ams-time-input-calender-picker-indicator-background-image); + cursor: pointer; +} + +.ams-time-input:hover::-webkit-calendar-picker-indicator { + background-image: var(--ams-time-input-hover-calender-picker-indicator-background-image); +} + +.ams-time-input:disabled { + background-color: var(--ams-time-input-disabled-background-color); + box-shadow: var(--ams-time-input-disabled-box-shadow); + color: var(--ams-time-input-disabled-color); + cursor: not-allowed; +} + +.ams-time-input:disabled::-webkit-calendar-picker-indicator { + background-image: var(--ams-time-input-disabled-calender-picker-indicator-background-image); + visibility: visible; +} + +.ams-time-input:invalid, +.ams-time-input[aria-invalid="true"] { + box-shadow: var(--ams-time-input-invalid-box-shadow); + + &:hover { + // TODO: this should be the (currently non-existent) dark red hover color + box-shadow: var(--ams-time-input-invalid-hover-box-shadow); + } +} diff --git a/packages/react/documentation/coding-conventions.md b/packages/react/documentation/coding-conventions.md index b935e75838..a1c205ae2e 100644 --- a/packages/react/documentation/coding-conventions.md +++ b/packages/react/documentation/coding-conventions.md @@ -44,6 +44,6 @@ Do not use `aria-label`. Tools for automatic translation often [do not translate Some of our components can render different HTML tags; they are polymorphic. Spotlight and Grid Cell are examples. -We’ve decided to use polymorphism only for HTML tags that support global attributes (e.g., `div`, `section`, `footer`, etc.). +We’ve decided to use polymorphism solely for HTML tags that only support global attributes (e.g., `div`, `section`, `footer`, etc.). This is because other HTML tags require more complicated typing, and it is often simpler to separate the components. We type the refs as `any` to make React refs work with polymorphic components. diff --git a/packages/react/package.json b/packages/react/package.json index e7b19901c0..d06f558bf7 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -33,42 +33,42 @@ ], "dependencies": { "@amsterdam/design-system-react-icons": "workspace:*", + "@babel/runtime": "7.24.4", "clsx": "2.1.0" }, "devDependencies": { - "@babel/core": "7.24.1", - "@babel/plugin-transform-runtime": "7.24.1", - "@babel/preset-env": "7.24.1", + "@babel/core": "7.24.4", + "@babel/plugin-transform-runtime": "7.24.3", + "@babel/preset-env": "7.24.4", "@babel/preset-react": "7.24.1", - "@babel/runtime": "7.24.1", "@rollup/plugin-babel": "6.0.4", "@rollup/plugin-commonjs": "25.0.7", "@rollup/plugin-node-resolve": "15.2.3", "@rollup/pluginutils": "5.1.0", - "@testing-library/dom": "9.3.4", + "@testing-library/dom": "10.0.0", "@testing-library/jest-dom": "6.4.2", - "@testing-library/react": "14.2.2", + "@testing-library/react": "15.0.2", "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.12", "@types/lodash": "4.17.0", - "@types/react": "18.2.67", + "@types/react": "18.2.79", "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", "lodash": "4.17.21", - "next": "14.1.4", + "next": "14.2.1", "npm-run-all": "4.1.5", - "postcss": "8.4.37", + "postcss": "8.4.38", "react": "18.2.0", "react-dom": "18.2.0", - "rollup": "4.13.0", + "rollup": "4.14.3", "rollup-plugin-delete": "2.0.0", "rollup-plugin-dts": "6.1.0", "rollup-plugin-filesize": "10.0.0", - "rollup-plugin-node-externals": "7.0.1", + "rollup-plugin-node-externals": "7.1.1", "rollup-plugin-node-polyfills": "0.2.1", "rollup-plugin-peer-deps-external": "2.2.4", "rollup-plugin-typescript2": "0.36.0", - "sass": "1.72.0", + "sass": "1.75.0", "tslib": "2.6.2" }, "peerDependencies": { diff --git a/packages/react/src/Accordion/AccordionSection.tsx b/packages/react/src/Accordion/AccordionSection.tsx index 4865faa134..37fcfb0579 100644 --- a/packages/react/src/Accordion/AccordionSection.tsx +++ b/packages/react/src/Accordion/AccordionSection.tsx @@ -40,8 +40,8 @@ export const AccordionSection = forwardRef( onClick={() => setIsExpanded(!isExpanded)} type="button" > + {label} - {section ? ( diff --git a/packages/react/src/DateInput/DateInput.test.tsx b/packages/react/src/DateInput/DateInput.test.tsx new file mode 100644 index 0000000000..9c0f3a33da --- /dev/null +++ b/packages/react/src/DateInput/DateInput.test.tsx @@ -0,0 +1,41 @@ +import { render } from '@testing-library/react' +import { createRef } from 'react' +import { DateInput } from './DateInput' +import '@testing-library/jest-dom' + +describe('Date input', () => { + it('renders', () => { + const { container } = render() + + const component = container.querySelector(':only-child') + + expect(component).toBeInTheDocument() + expect(component).toBeVisible() + }) + + it('renders a design system BEM class name', () => { + const { container } = render() + + const component = container.querySelector(':only-child') + + expect(component).toHaveClass('ams-date-input') + }) + + it('renders an additional class name', () => { + const { container } = render() + + const component = container.querySelector(':only-child') + + expect(component).toHaveClass('ams-date-input extra') + }) + + it('supports ForwardRef in React', () => { + const ref = createRef() + + const { container } = render() + + const component = container.querySelector(':only-child') + + expect(ref.current).toBe(component) + }) +}) diff --git a/packages/react/src/DateInput/DateInput.tsx b/packages/react/src/DateInput/DateInput.tsx new file mode 100644 index 0000000000..6e1c119a9e --- /dev/null +++ b/packages/react/src/DateInput/DateInput.tsx @@ -0,0 +1,18 @@ +/** + * @license EUPL-1.2+ + * Copyright Gemeente Amsterdam + */ + +import clsx from 'clsx' +import { forwardRef } from 'react' +import type { ForwardedRef, InputHTMLAttributes } from 'react' + +export type DateInputProps = InputHTMLAttributes + +export const DateInput = forwardRef( + ({ className, ...restProps }: DateInputProps, ref: ForwardedRef) => ( + + ), +) + +DateInput.displayName = 'DateInput' diff --git a/packages/react/src/DateInput/README.md b/packages/react/src/DateInput/README.md new file mode 100644 index 0000000000..b61087b76e --- /dev/null +++ b/packages/react/src/DateInput/README.md @@ -0,0 +1,5 @@ + + +# React Date Input component + +[Date Input documentation](../../../css/src/components/date-input/README.md) diff --git a/packages/react/src/DateInput/index.ts b/packages/react/src/DateInput/index.ts new file mode 100644 index 0000000000..2303f03937 --- /dev/null +++ b/packages/react/src/DateInput/index.ts @@ -0,0 +1,2 @@ +export { DateInput } from './DateInput' +export type { DateInputProps } from './DateInput' diff --git a/packages/react/src/FormLabel/README.md b/packages/react/src/FormLabel/README.md deleted file mode 100644 index 135d0779b4..0000000000 --- a/packages/react/src/FormLabel/README.md +++ /dev/null @@ -1,5 +0,0 @@ - - -# React Form Label Component - -[Form label documentation](../../../css/src/components/form-label/README.md) diff --git a/packages/react/src/FormLabel/index.ts b/packages/react/src/FormLabel/index.ts deleted file mode 100644 index 5d86e6f51a..0000000000 --- a/packages/react/src/FormLabel/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { FormLabel } from './FormLabel' diff --git a/packages/react/src/FormLabel/FormLabel.test.tsx b/packages/react/src/Label/Label.test.tsx similarity index 69% rename from packages/react/src/FormLabel/FormLabel.test.tsx rename to packages/react/src/Label/Label.test.tsx index 3b6fa3a86e..eea47d3611 100644 --- a/packages/react/src/FormLabel/FormLabel.test.tsx +++ b/packages/react/src/Label/Label.test.tsx @@ -1,11 +1,11 @@ import { render, screen } from '@testing-library/react' import { createRef } from 'react' -import { FormLabel } from './FormLabel' +import { Label } from './Label' import '@testing-library/jest-dom' -describe('Form label', () => { +describe('Label', () => { it('renders an HTML label element', () => { - const { container } = render() + const { container } = render(