From 49ca264d9963dab06e4f8141b5af39f22b4560b0 Mon Sep 17 00:00:00 2001 From: Radoslav Karaivanov Date: Wed, 30 Sep 2026 16:26:20 +0300 Subject: [PATCH] fix(color-picker): serialize the initial value in the declared format A value attribute that came before the format attribute stayed in hex, so the value and the form data ignored the format. Re-serialize on each format change, including the first update, and keep the pristine state. --- CHANGELOG.md | 1 + .../color-picker/color-picker.spec.ts | 50 +++++++++++++++++++ src/components/color-picker/color-picker.ts | 20 +++++++- src/components/color-picker/spec.md | 7 ++- 4 files changed, 74 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4629076f..34c593be1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Setting `min` or `max` with a value before the first render no longer throws a `TypeError`. - #### Color picker - The HSL saturation no longer becomes infinite for a very small saturation at full value. + - The initial `value` is now in the notation of `format` when the `value` attribute comes before the `format` attribute. Before, `value` and the form data stayed in hex until the color changed. - #### Virtual scroll - `layoutComplete` now resolves after the rendered items are measured. Before, it could resolve first, so `scrollToIndex` stopped its correction early, and an item with a size other than the estimate landed up to tens of pixels from the requested edge. - A list larger than the maximum scroll size of the browser now shows its last items at the end of the scroll range, and `scrollToIndex` puts the item at the requested edge. The items also move evenly during a scroll. Before, the last items could not be reached, the item landed tens of pixels off, and the items jumped by some pixels each time the rendered window changed. diff --git a/src/components/color-picker/color-picker.spec.ts b/src/components/color-picker/color-picker.spec.ts index 96fb9ea0b..d89164e8f 100644 --- a/src/components/color-picker/color-picker.spec.ts +++ b/src/components/color-picker/color-picker.spec.ts @@ -1313,6 +1313,56 @@ describe('Color picker', () => { }); }); + describe('Initial format', () => { + const rgb = 'rgb(186 218 85)'; + + it('serializes a value attribute that comes before the format attribute', async () => { + picker = await fixture( + html`` + ); + + expect(picker.value).to.equal(rgb); + }); + + it('serializes a value attribute that comes after the format attribute', async () => { + picker = await fixture( + html`` + ); + + expect(picker.value).to.equal(rgb); + }); + + describe('with a default value set before the format', () => { + const spec = createFormAssociatedTestBed(html` + + `); + + beforeEach(async () => { + await spec.setup(IgcColorPickerComponent.tagName); + }); + + it('stays pristine', () => { + spec.assertIsPristine(); + expect(spec.element.value).to.equal(rgb); + }); + + it('submits the value in the format', () => { + spec.assertSubmitHasValue(rgb); + }); + + it('restores the value in the format on form reset', () => { + spec.setProperties({ value: '#ff0000' }); + + spec.reset(); + expect(spec.element.value).to.equal(rgb); + }); + }); + }); + describe('Validation', () => { const spec = createFormAssociatedTestBed(html` diff --git a/src/components/color-picker/color-picker.ts b/src/components/color-picker/color-picker.ts index 1e3599a08..f053fe77f 100644 --- a/src/components/color-picker/color-picker.ts +++ b/src/components/color-picker/color-picker.ts @@ -347,8 +347,8 @@ export default class IgcColorPickerComponent extends FormAssociatedRequiredMixin this._rootClickController.update(); } - if (this.hasUpdated && props.has('format')) { - this._formValue.setValueAndFormState(this._color.asString(this.format)); + if (props.has('format')) { + this._serializeInFormat(); } this._applyColorProperties(); @@ -682,6 +682,22 @@ export default class IgcColorPickerComponent extends FormAssociatedRequiredMixin this.requestUpdate(); } + /** + * Renders the current color in the active format. + * + * The `value` attribute can come before `format`, so the first update also + * runs this. The color does not change, so the pristine state stays. + */ + private _serializeInFormat(): void { + const value = this._color.asString(this.format); + + if (value !== this.value) { + const pristine = this._pristine; + this._formValue.setValueAndFormState(value); + this._pristine = pristine; + } + } + private _syncCanvasPosition(): void { if (!this._canvasRef.value || !this.open) return; diff --git a/src/components/color-picker/spec.md b/src/components/color-picker/spec.md index 3f7392d6c..da8ce5488 100644 --- a/src/components/color-picker/spec.md +++ b/src/components/color-picker/spec.md @@ -49,6 +49,7 @@ | 2 | 2026-09-24 | Describe the naming order and the host ARIA naming | | 3 | 2026-09-24 | Keep `aria-expanded` off the input mode text input | | 4 | 2026-09-28 | Add the property-based color suite | +| 5 | 2026-09-30 | Serialize the initial value in the declared format | ## Overview @@ -185,7 +186,9 @@ and HSV representations plus an alpha channel. An empty, whitespace-only or inva rather than a stale color; while empty, the anchor renders a checkered background and the serialized value is `''`. The active `format` determines how the color is re-serialized into the observable `value`. Changing `format` -re-serializes the existing color without altering it, so no `igcInput` or `igcChange` is emitted. +re-serializes the existing color without altering it, so no `igcInput` or `igcChange` is emitted. The initial value +is serialized in the declared `format` whatever the attribute order, and the re-serialization keeps the pristine +state of the control. #### The picker surface @@ -403,7 +406,7 @@ The component suite reuses `createFormAssociatedTestBed`, `runValidationContaine 11. Open and close - the pointer, keyboard and outside-click paths. 12. Input mode - the anchor input, its label and its prefix. 13. Rendering of the picker surface rows. -14. Form association, the touched state, `defaultValue`, and validation. +14. Form association, the touched state, `defaultValue`, the initial format, and validation. 15. Validation message slots, generated by `runValidationContainerTests`. 16. External label association and ARIA projection, generated by `runExternalLabelAssociationTests` and `runAriaProjectionTests`.