From 5c629491b1bb8f7fe1eeeca5cff4918eefdb91f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Dzivjak?= Date: Mon, 9 Sep 2024 17:18:43 +0200 Subject: [PATCH] feat(circuit-ui): Add experimental ColorInput component (#2655) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Connor Bär Co-authored-by: Connor Bär --- .changeset/violet-seahorses-study.md | 5 + package.json | 3 +- .../components/ColorInput/ColorInput.mdx | 13 ++ .../ColorInput/ColorInput.module.css | 83 ++++++++ .../components/ColorInput/ColorInput.spec.tsx | 64 ++++++ .../ColorInput/ColorInput.stories.tsx | 33 ++++ .../components/ColorInput/ColorInput.tsx | 183 ++++++++++++++++++ .../circuit-ui/components/ColorInput/index.ts | 18 ++ .../circuit-ui/components/Input/Input.tsx | 2 + packages/circuit-ui/components/Input/index.ts | 2 +- 10 files changed, 404 insertions(+), 2 deletions(-) create mode 100644 .changeset/violet-seahorses-study.md create mode 100644 packages/circuit-ui/components/ColorInput/ColorInput.mdx create mode 100644 packages/circuit-ui/components/ColorInput/ColorInput.module.css create mode 100644 packages/circuit-ui/components/ColorInput/ColorInput.spec.tsx create mode 100644 packages/circuit-ui/components/ColorInput/ColorInput.stories.tsx create mode 100644 packages/circuit-ui/components/ColorInput/ColorInput.tsx create mode 100644 packages/circuit-ui/components/ColorInput/index.ts diff --git a/.changeset/violet-seahorses-study.md b/.changeset/violet-seahorses-study.md new file mode 100644 index 0000000000..e1824f0b1d --- /dev/null +++ b/.changeset/violet-seahorses-study.md @@ -0,0 +1,5 @@ +--- +"@sumup/circuit-ui": minor +--- + +Added an experimental ColorInput component that enables users to type or select a color. diff --git a/package.json b/package.json index b0b608f013..9739e5e37d 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "lint:fix": "biome check --write && foundry run eslint . --ext .js,.jsx,.ts,.tsx --fix", "lint:ci": "biome ci && foundry run eslint . --ext .js,.jsx,.ts,.tsx --quiet ", "lint:css": "foundry run stylelint '**/*.css'", + "lint:css:fix": "foundry run stylelint '**/*.css' --fix", "dev": "npm run docs:start", "docs": "npm run docs:start", "docs:start": "storybook dev -p 6006", @@ -95,4 +96,4 @@ "vitest": "^2.0.3", "vitest-github-actions-reporter": "^0.11.1" } -} +} \ No newline at end of file diff --git a/packages/circuit-ui/components/ColorInput/ColorInput.mdx b/packages/circuit-ui/components/ColorInput/ColorInput.mdx new file mode 100644 index 0000000000..2867d1fe0f --- /dev/null +++ b/packages/circuit-ui/components/ColorInput/ColorInput.mdx @@ -0,0 +1,13 @@ +import { Meta, Status, Props, Story } from '../../../../.storybook/components'; +import * as Stories from './ColorInput.stories'; + + + +# ColorInput + + + +The ColorInput component enables users to type or select a color. + + + diff --git a/packages/circuit-ui/components/ColorInput/ColorInput.module.css b/packages/circuit-ui/components/ColorInput/ColorInput.module.css new file mode 100644 index 0000000000..4803a1b778 --- /dev/null +++ b/packages/circuit-ui/components/ColorInput/ColorInput.module.css @@ -0,0 +1,83 @@ +.wrapper { + position: relative; + display: flex; +} + +.picker { + position: relative; + width: var(--cui-spacings-exa); + height: var(--cui-spacings-exa); + margin-right: 1px; + cursor: pointer; + border-top-left-radius: var(--cui-border-radius-byte); + border-bottom-left-radius: var(--cui-border-radius-byte); + box-shadow: 0 0 0 1px var(--cui-border-normal); +} + +.picker:hover { + z-index: var(--cui-z-index-absolute); + background: var(--cui-bg-normal-hovered); + box-shadow: 0 0 0 1px var(--cui-border-normal-hovered); +} + +.picker:focus-within { + z-index: var(--cui-z-index-absolute); + background: var(--cui-bg-normal-pressed); + box-shadow: 0 0 0 2px var(--cui-border-focus); +} + +.color-input { + width: var(--cui-spacings-giga); + height: var(--cui-spacings-giga); + padding: 0; + margin: var(--cui-spacings-kilo); + appearance: none; + cursor: pointer; + border: none; + border-radius: 6px; + outline: none; + box-shadow: 0 0 0 1px var(--cui-border-normal); +} + +.color-input::-moz-color-swatch { + border: none; +} + +.color-input::-webkit-color-swatch-wrapper { + padding: 0; + border-radius: 0; +} + +.color-input::-webkit-color-swatch { + border: none; +} + +.symbol { + position: absolute; + top: 0; + left: var(--cui-spacings-exa); + z-index: calc(var(--cui-z-index-absolute) + 1); + display: grid; + place-items: center center; + width: var(--cui-spacings-giga); + height: var(--cui-spacings-exa); + font-family: var(--cui-font-stack-mono); + color: var(--cui-fg-subtle); +} + +.input { + position: relative; + padding-left: var(--cui-spacings-giga); + font-family: var(--cui-font-stack-mono); + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.input:hover, +.input:focus { + z-index: var(--cui-z-index-absolute); +} + +.input::placeholder { + font-family: var(--cui-font-stack-mono); +} diff --git a/packages/circuit-ui/components/ColorInput/ColorInput.spec.tsx b/packages/circuit-ui/components/ColorInput/ColorInput.spec.tsx new file mode 100644 index 0000000000..2ba0e907fd --- /dev/null +++ b/packages/circuit-ui/components/ColorInput/ColorInput.spec.tsx @@ -0,0 +1,64 @@ +/** + * Copyright 2024, SumUp Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, expect, it } from 'vitest'; +import { createRef } from 'react'; + +import { render, axe, screen } from '../../util/test-utils.js'; +import type { InputElement } from '../Input/index.js'; + +import { ColorInput } from './ColorInput.js'; + +describe('ColorInput', () => { + const baseProps = { label: 'Car color', pickerLabel: 'Pick car color' }; + + it('should merge a custom class name with the default ones', () => { + const className = 'foo'; + const { container } = render( + , + ); + const input = container.querySelector('input[type="text"]'); + expect(input?.className).toContain(className); + }); + + it('should forward a ref', () => { + const ref = createRef(); + const { container } = render(); + const input = container.querySelector("input[type='color']"); + expect(ref.current).toBe(input); + }); + + it('should have no accessibility violations', async () => { + const { container } = render(); + const actual = await axe(container); + expect(actual).toHaveNoViolations(); + }); + + describe('Labeling', () => { + it('should accept a custom description via aria-describedby', () => { + const customDescription = 'Custom description'; + const customDescriptionId = 'customDescriptionId'; + render( + <> + {customDescription} + + , + ); + expect(screen.getByRole('textbox')).toHaveAccessibleDescription( + customDescription, + ); + }); + }); +}); diff --git a/packages/circuit-ui/components/ColorInput/ColorInput.stories.tsx b/packages/circuit-ui/components/ColorInput/ColorInput.stories.tsx new file mode 100644 index 0000000000..f9a264367a --- /dev/null +++ b/packages/circuit-ui/components/ColorInput/ColorInput.stories.tsx @@ -0,0 +1,33 @@ +/** + * Copyright 2024, SumUp Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ColorInput, type ColorInputProps } from './ColorInput.js'; + +export default { + title: 'Forms/Input/ColorInput', + component: ColorInput, +}; + +const baseArgs = { + label: 'Color', + pickerLabel: 'Pick color', + placeholder: '99ffbb', +}; + +export const Base = (args: ColorInputProps) => ( + +); + +Base.args = baseArgs; diff --git a/packages/circuit-ui/components/ColorInput/ColorInput.tsx b/packages/circuit-ui/components/ColorInput/ColorInput.tsx new file mode 100644 index 0000000000..0947bc68b4 --- /dev/null +++ b/packages/circuit-ui/components/ColorInput/ColorInput.tsx @@ -0,0 +1,183 @@ +/** + * Copyright 2024, SumUp Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use client'; + +import { + forwardRef, + useId, + useRef, + useState, + type ChangeEventHandler, +} from 'react'; + +import { classes as inputClasses } from '../Input/index.js'; +import type { InputElement, InputProps } from '../Input/index.js'; +import { clsx } from '../../styles/clsx.js'; +import { + FieldLabelText, + FieldLegend, + FieldSet, + FieldValidationHint, +} from '../Field/index.js'; +import { applyMultipleRefs } from '../../util/refs.js'; + +import classes from './ColorInput.module.css'; + +export interface ColorInputProps + extends Omit< + InputProps, + | 'ref' + | 'type' + | 'defaultValue' + | 'value' + | 'placeholder' + | 'maxLength' + | 'pattern' + | 'renderPrefix' + | 'as' + > { + /** + * A short string that is shown inside the empty input. + */ + placeholder?: string; + /** + * The value of the input element. + */ + value?: string; + /** + * The default value of the input element. + */ + defaultValue?: string; +} + +export const ColorInput = forwardRef( + ( + { + 'aria-describedby': descriptionId, + 'renderSuffix': RenderSuffix, + className, + defaultValue, + disabled, + hasWarning, + showValid, + hideLabel, + id, + invalid, + label, + onChange, + optionalLabel, + validationHint, + readOnly, + required, + inputClassName, + style, + value, + ...props + }, + ref, + ) => { + const [currentColor, setCurrentColor] = useState( + defaultValue, + ); + const colorPickerRef = useRef(null); + + const labelId = useId(); + const pickerId = useId(); + const validationHintId = useId(); + + const descriptionIds = clsx(validationHintId, descriptionId); + + const suffix = RenderSuffix && ( + + ); + + const hasSuffix = Boolean(suffix); + + const onPickerColorChange: ChangeEventHandler = (e) => { + setCurrentColor(e.target.value); + if (onChange) { + onChange(e); + } + }; + + const onInputChange: ChangeEventHandler = (e) => { + if (colorPickerRef.current) { + colorPickerRef.current.value = `#${e.target.value}`; + } + setCurrentColor(`#${e.target.value}`); + }; + + return ( +
+ + + +
+ + # + +
+ +
+ ); + }, +); + +ColorInput.displayName = 'ColorInput'; diff --git a/packages/circuit-ui/components/ColorInput/index.ts b/packages/circuit-ui/components/ColorInput/index.ts new file mode 100644 index 0000000000..4e31720a5c --- /dev/null +++ b/packages/circuit-ui/components/ColorInput/index.ts @@ -0,0 +1,18 @@ +/** + * Copyright 2019, SumUp Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { ColorInput } from './ColorInput.js'; + +export type { ColorInputProps } from './ColorInput.js'; diff --git a/packages/circuit-ui/components/Input/Input.tsx b/packages/circuit-ui/components/Input/Input.tsx index 4d411287b2..7d8a7f38c2 100644 --- a/packages/circuit-ui/components/Input/Input.tsx +++ b/packages/circuit-ui/components/Input/Input.tsx @@ -38,6 +38,8 @@ import { clsx } from '../../styles/clsx.js'; import classes from './Input.module.css'; +export { classes }; + export type InputElement = HTMLInputElement & HTMLTextAreaElement; type CircuitInputHTMLAttributes = InputHTMLAttributes & TextareaHTMLAttributes; diff --git a/packages/circuit-ui/components/Input/index.ts b/packages/circuit-ui/components/Input/index.ts index 23b4af76c7..ef65d3b2f3 100644 --- a/packages/circuit-ui/components/Input/index.ts +++ b/packages/circuit-ui/components/Input/index.ts @@ -13,6 +13,6 @@ * limitations under the License. */ -export { Input } from './Input.js'; +export { Input, classes } from './Input.js'; export type { InputProps, InputElement } from './Input.js';