Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 67 additions & 4 deletions packages/uui-color-swatch/lib/uui-color-swatch.element.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { defineElement } from '@umbraco-ui/uui-base/lib/registration';
import { property } from 'lit/decorators.js';
import { property, state } from 'lit/decorators.js';
import { css, html, LitElement, nothing } from 'lit';
import { ref } from 'lit/directives/ref.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { iconCheck } from '@umbraco-ui/uui-icon-registry-essential/lib/svgs';
import {
ActiveMixin,
Expand All @@ -23,6 +24,9 @@
'label',
SelectableMixin(ActiveMixin(LitElement)),
) {
@state()
private _contrast: 'dark' | 'light' | undefined = undefined;

/**
* Value of the swatch. This will become the color value if color is left undefined, see the property `color` for more details.
*/
Expand Down Expand Up @@ -90,6 +94,21 @@

firstUpdated() {
this._setAriaAttributes();

const color = this.color ?? this.value;
if (color.startsWith('#')) {
this._contrast = this.#contrast(color) === 'light' ? 'light' : 'dark';
} else if (color.startsWith('rgb')) {
const [r, g, b, a] = color.match(/[.\d]+/g)?.map(Number) ?? [0, 0, 0];
if (a <= 0.5) {
this._contrast = 'light';
} else {
this._contrast =
this.#contrast(this.#rgbToHex(r, g, b)) === 'light'
? 'light'
: 'dark';
}
}
}

willUpdate(changedProperties: Map<string, any>) {
Expand Down Expand Up @@ -118,6 +137,30 @@
this.selectableTarget = button || this;
}

#contrast(hex: string): string {
const rgb = this.#hexToRgb(hex);
const o = Math.round((rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000);
Copy link

Copilot AI Oct 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The magic numbers 299, 587, 114, and 1000 should be extracted as named constants to explain they represent the RGB to grayscale conversion coefficients and divisor.

Copilot uses AI. Check for mistakes.

return o <= 180 ? 'dark' : 'light';
Copy link

Copilot AI Oct 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The magic number 180 should be extracted as a named constant like 'LUMINANCE_THRESHOLD' to clarify its purpose as the brightness cutoff for determining contrast.

Copilot uses AI. Check for mistakes.
}

#hexToRgb(hex: string): number[] {
hex = hex.startsWith('#') ? hex.slice(1) : hex;
if (hex.length === 3) {
hex = Array.from(hex).reduce((str, x) => str + x + x, ''); // 123 -> 112233
}

const bigint = parseInt(hex, 16);
const r = (bigint >> 16) & 255;
const g = (bigint >> 8) & 255;
const b = bigint & 255;

return [r, g, b];
}

#rgbToHex = (r: number, g: number, b: number, hash: '#' | '' = ''): string =>
hash + ((r << 16) + (g << 8) + b).toString(16).padStart(6, '0');

render() {
return html`
<button
Expand All @@ -126,7 +169,12 @@
aria-label=${this.label}
?disabled="${this.disabled}"
title="${this.label}">
<div class="color-swatch color-swatch--transparent-bg">
<div
class="color-swatch color-swatch--transparent-bg ${ifDefined(
this._contrast,
)
? `color-swatch--${this._contrast}`
: ''}">
<div
class="color-swatch__color"
style="background: var(--uui-swatch-color, ${this.color ??
Expand Down Expand Up @@ -202,6 +250,7 @@
margin: 0;
text-align: left;
border-radius: 3px;
border: 1px solid #ccc;
}

:host(:not([selectable])) #swatch:focus {
Expand Down Expand Up @@ -243,6 +292,7 @@
flex-direction: column;
justify-content: center;
align-items: center;
margin: 2px;
}

:host([show-label]) .color-swatch {
Expand All @@ -251,8 +301,11 @@
}

.color-swatch.color-swatch--transparent-bg {
background-image:
linear-gradient(45deg, var(--uui-palette-grey) 25%, transparent 25%),
background-image: linear-gradient(

Check failure on line 304 in packages/uui-color-swatch/lib/uui-color-swatch.element.ts

View workflow job for this annotation

GitHub Actions / test

Replace `·linear-gradient(⏎············45deg,⏎············var(--uui-palette-grey)·25%,⏎············transparent·25%⏎··········` with `⏎··········linear-gradient(45deg,·var(--uui-palette-grey)·25%,·transparent·25%`
45deg,
var(--uui-palette-grey) 25%,
transparent 25%
),
linear-gradient(45deg, transparent 75%, var(--uui-palette-grey) 75%),
linear-gradient(45deg, transparent 75%, var(--uui-palette-grey) 75%),
linear-gradient(45deg, var(--uui-palette-grey) 25%, transparent 25%);
Expand Down Expand Up @@ -286,6 +339,16 @@
opacity: 0;
}

.color-swatch.color-swatch--light .color-swatch__check {
color: #000 !important;
filter: none;
}

.color-swatch.color-swatch--dark .color-swatch__check {
color: #fff !important;
filter: none;
}

:host([selected]) .color-swatch__check {
opacity: 1;
}
Expand Down
39 changes: 35 additions & 4 deletions packages/uui-color-swatches/lib/uui-color-swatches.story.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,34 @@ import { spread } from '../../../storyhelpers';
import { repeat } from 'lit/directives/repeat.js';

const swatches = [
{ label: 'Blood Orange', value: '#d0021b' },
{ label: 'Avocado', value: '#417505' },
{ label: 'Tufts Blue', value: '#4a90e2' },
{ label: 'Black', value: '#060606' },
{ label: 'Sunglow', value: '#fad634' },
{ label: 'Spanish Pink', value: '#f5c1bc' },
{ label: 'Violet Blue', value: '#3544b1' },
{ label: 'Malibu', value: '#3879ff' },
{ label: 'Maroon Flush', value: '#d42054' },
{ label: 'Jungle Green', value: '#2bc37c' },
{ label: 'Chamoisee', value: '#9d8057' },
{ label: 'Dusty Grey', value: '#9b9b9b' },
];

const swatchesTransparent = [
'rgba(208, 2, 27, 0.5)',
'rgba(245, 166, 35, 0.5)',
'rgba(248, 231, 28, 0.5)',
'rgba(139, 87, 42, 0.5)',
'rgba(126, 211, 33, 0.5)',
'rgba(65, 117, 5, 0.5)',
'rgba(189, 16, 224, 0.5)',
'rgba(144, 19, 254, 0.5)',
'rgba(74, 144, 226, 0.5)',
'rgba(80, 227, 194, 0.5)',
'rgba(184, 233, 134, 0.5)',
'rgba(0, 0, 0, 0.5)',
'rgba(68, 68, 68, 0.5)',
'rgba(136, 136, 136, 0.5)',
'rgba(204, 204, 204, 0.5)',
'rgba(255, 255, 255, 0.5)',
];

const gradients = [
Expand Down Expand Up @@ -93,7 +118,7 @@ export const Default: Story = {};

export const Preselected: Story = {
args: {
value: '#417505',
value: swatches[3].value,
},
};

Expand Down Expand Up @@ -123,6 +148,12 @@ export const Gradient: Story = {
},
};

export const Transparent: Story = {
args: {
swatches: swatchesTransparent,
},
};

export const NoSwatches: Story = {
args: {
swatches: [],
Expand Down
Loading