diff --git a/packages/circuit-ui/components/ColorInput/ColorInput.mdx b/packages/circuit-ui/components/ColorInput/ColorInput.mdx new file mode 100644 index 0000000000..abd9b15499 --- /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 submit 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..43e8eacb15 --- /dev/null +++ b/packages/circuit-ui/components/ColorInput/ColorInput.module.css @@ -0,0 +1,42 @@ +.suffix { + overflow: hidden; + border-top-right-radius: var(--cui-border-radius-byte); + border-bottom-right-radius: var(--cui-border-radius-byte); + pointer-events: auto !important; + border: none; + border-left: 1px solid var(--cui-border-normal); + width: var(--cui-spacings-exa); + height: var(--cui-spacings-exa); + position: absolute; + top: 0; + right: 0; + box-shadow: none; +} + +.prefix { + display: flex; + align-items: center; + justify-content: center; + line-height: var(--cui-spacings-mega); +} + +.colorInput { + opacity: 0; + width: var(--cui-spacings-exa); + height: var(--cui-spacings-exa); + border: none; + box-shadow: none; + padding: 0; +} + +.input { + font-family: var(--cui-font-stack-mono); +} + +.colorpick { + display: inline-block; +} + +.colorpick:focus-within input { + box-shadow: 0 0 0 2px var(--cui-border-accent); +} \ No newline at end of file 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..13cd2821d2 --- /dev/null +++ b/packages/circuit-ui/components/ColorInput/ColorInput.spec.tsx @@ -0,0 +1,39 @@ +/** + * 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. + */ + +import { describe, expect, it } from 'vitest'; +import { createRef } from 'react'; + +import { render, axe } from '../../util/test-utils.js'; +import type { InputElement } from '../Input/index.js'; + +import { ColorInput } from './ColorInput.js'; + +describe('SearchInput', () => { + const baseProps = { label: 'Car color', pickerLabel: 'Pick car color' }; + + 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(); + }); +}); 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..d5411c4424 --- /dev/null +++ b/packages/circuit-ui/components/ColorInput/ColorInput.stories.tsx @@ -0,0 +1,33 @@ +/** + * 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. + */ + +import { ColorInput, type ColorInputProps } from './ColorInput.js'; + +export default { + title: 'Forms/Input/ColorInput', + component: ColorInput, +}; + +const baseArgs = { + label: 'Color', + pickerLabel: 'Pick color', + placeholder: 'abc321', +}; + +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..f01c53c31a --- /dev/null +++ b/packages/circuit-ui/components/ColorInput/ColorInput.tsx @@ -0,0 +1,142 @@ +/** + * 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, + useCallback, + useEffect, + useId, + useRef, + useState, + type ChangeEventHandler, +} from 'react'; + +import { Input, type InputElement, type InputProps } from '../Input/index.js'; +import { clsx } from '../../styles/clsx.js'; +import { FieldLabel, FieldLabelText } from '../Field/index.js'; +import { applyMultipleRefs } from '../../util/refs.js'; + +import styles from './ColorInput.module.css'; + +export interface ColorInputProps + extends Omit< + InputProps, + | 'ref' + | 'type' + | 'defaultValue' + | 'value' + | 'placeholder' + | 'maxLength' + | 'pattern' + | 'renderPrefix' + | 'renderSuffix' + | '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; + /* + * Picker label describes the color picker which serves as an alternative + * to the hex color input. + */ + pickerLabel: string; +} + +export const ColorInput = forwardRef( + ( + { onChange, className, value, defaultValue, pickerLabel, ...props }, + ref, + ) => { + const [currentColor, setCurrentColor] = useState( + defaultValue, + ); + const colorDisplayRef = useRef(null); + const colorPickerRef = useRef(null); + const pickerId = useId(); + + 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}`); + }; + + useEffect(() => { + if (colorDisplayRef.current && currentColor) { + colorDisplayRef.current.style.backgroundColor = currentColor; + } + }, [currentColor]); + + const renderSuffix = useCallback( + () => ( +
+ + + + +
+ ), + [], + ); + + return ( + ( +
+ # +
+ )} + renderSuffix={renderSuffix} + value={currentColor ? currentColor.replace('#', '') : undefined} + inputClassName={styles.input} + maxLength={6} + pattern="[0-9a-f]{3,6}" + onChange={onInputChange} + {...props} + /> + ); + }, +); + +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';