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.module.css b/packages/circuit-ui/components/ColorInput/ColorInput.module.css
index 34f19ca918..d07521564e 100644
--- a/packages/circuit-ui/components/ColorInput/ColorInput.module.css
+++ b/packages/circuit-ui/components/ColorInput/ColorInput.module.css
@@ -1,36 +1,76 @@
-.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);
+.wrapper {
+ position: relative;
+ display: flex;
+}
+
+.picker {
width: var(--cui-spacings-exa);
height: var(--cui-spacings-exa);
- position: absolute;
- top: 0;
- right: 0;
- box-shadow: none;
+ 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);
}
-.prefix {
- display: flex;
- align-items: center;
- justify-content: center;
- line-height: var(--cui-spacings-mega);
+.picker:hover {
+ background: var(--cui-bg-normal-hovered);
+ box-shadow: 0 0 0 1px var(--cui-border-normal-hovered);
}
-.colorInput {
- opacity: 0;
- width: var(--cui-spacings-exa);
- height: var(--cui-spacings-exa);
+.picker:focus-within {
+ background: var(--cui-bg-normal-pressed);
+ box-shadow: 0 0 0 1px var(--cui-border-normal-pressed);
+}
+
+.color-input {
+ width: var(--cui-spacings-giga);
+ height: var(--cui-spacings-giga);
+ padding: 0;
+ margin: var(--cui-spacings-kilo);
+ appearance: none;
border: none;
- box-shadow: 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;
+}
+
+.picker:hover .color-input {
+ box-shadow: 0 0 0 1px var(--cui-border-normal-hovered);
+}
+
+.picker:focus-within .color-input {
+ box-shadow: 0 0 0 1px var(--cui-border-normal-pressed);
+}
+
+.symbol {
+ position: absolute;
+ top: 0;
+ left: var(--cui-spacings-exa);
+ 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 {
+ padding-left: var(--cui-spacings-giga);
font-family: var(--cui-font-stack-mono);
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
}
.input::placeholder {
@@ -40,7 +80,3 @@
.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
index e323c5f449..6ed9b6aafc 100644
--- a/packages/circuit-ui/components/ColorInput/ColorInput.spec.tsx
+++ b/packages/circuit-ui/components/ColorInput/ColorInput.spec.tsx
@@ -16,7 +16,7 @@
import { describe, expect, it } from 'vitest';
import { createRef } from 'react';
-import { render, axe } from '../../util/test-utils.js';
+import { render, axe, screen } from '../../util/test-utils.js';
import type { InputElement } from '../Input/index.js';
import { ColorInput } from './ColorInput.js';
@@ -36,4 +36,29 @@ describe('ColorInput', () => {
const actual = await axe(container);
expect(actual).toHaveNoViolations();
});
+
+ describe('Labeling', () => {
+ const HEX_SYMBOL = '#';
+
+ it('should have the currency symbol as part of its accessible description', () => {
+ render();
+ expect(screen.getByRole('textbox')).toHaveAccessibleDescription(
+ HEX_SYMBOL,
+ );
+ });
+
+ it('should accept a custom description via aria-describedby', () => {
+ const customDescription = 'Custom description';
+ const customDescriptionId = 'customDescriptionId';
+ render(
+ <>
+ {customDescription}
+
+ >,
+ );
+ expect(screen.getByRole('textbox')).toHaveAccessibleDescription(
+ `${HEX_SYMBOL} ${customDescription}`,
+ );
+ });
+ });
});
diff --git a/packages/circuit-ui/components/ColorInput/ColorInput.tsx b/packages/circuit-ui/components/ColorInput/ColorInput.tsx
index 7f22e1b0c4..0a00951521 100644
--- a/packages/circuit-ui/components/ColorInput/ColorInput.tsx
+++ b/packages/circuit-ui/components/ColorInput/ColorInput.tsx
@@ -17,18 +17,17 @@
import {
forwardRef,
- useCallback,
- useEffect,
useId,
useRef,
useState,
type ChangeEventHandler,
} from 'react';
-import { Input, type InputElement, type InputProps } from '../Input/index.js';
+import type { InputElement, InputProps } from '../Input/index.js';
import { clsx } from '../../styles/clsx.js';
-import { FieldLabel, FieldLabelText } from '../Field/index.js';
+import { FieldLabel, FieldLabelText, FieldWrapper } from '../Field/index.js';
import { applyMultipleRefs } from '../../util/refs.js';
+import classes from '../Input/Input.module.css';
import styles from './ColorInput.module.css';
@@ -43,7 +42,6 @@ export interface ColorInputProps
| 'maxLength'
| 'pattern'
| 'renderPrefix'
- | 'renderSuffix'
| 'as'
> {
/**
@@ -67,15 +65,41 @@ export interface ColorInputProps
export const ColorInput = forwardRef(
(
- { onChange, className, value, defaultValue, pickerLabel, ...props },
+ {
+ onChange,
+ className,
+ value,
+ defaultValue,
+ pickerLabel,
+ readOnly,
+ label,
+ hasWarning,
+ hideLabel,
+ optionalLabel,
+ required,
+ style,
+ disabled,
+ invalid,
+ 'renderSuffix': RenderSuffix,
+ 'aria-describedby': descriptionId,
+ id,
+ ...props
+ },
ref,
) => {
const [currentColor, setCurrentColor] = useState(
defaultValue,
);
- const colorDisplayRef = useRef(null);
const colorPickerRef = useRef(null);
const pickerId = useId();
+ const hexSymbolId = useId();
+ const inputFallbackId = useId();
+ const inputId = id || inputFallbackId;
+ const descriptionIds = clsx(hexSymbolId, descriptionId);
+
+ const suffix = RenderSuffix && ;
+
+ const hasSuffix = Boolean(suffix);
const onPickerColorChange: ChangeEventHandler = (e) => {
setCurrentColor(e.target.value);
@@ -91,50 +115,56 @@ export const ColorInput = forwardRef(
setCurrentColor(`#${e.target.value}`);
};
- useEffect(() => {
- if (colorDisplayRef.current && currentColor) {
- colorDisplayRef.current.style.backgroundColor = currentColor;
- }
- }, [currentColor]);
+ // render suffix only once, otherwise if it gets re-rendered on color change
+ // the native color-picker widget might get mistakenly dismissed by the browser
- const renderSuffix = useCallback(
- () => (
-