Skip to content

Commit 1cacbf1

Browse files
theMosaadsnowystingeryihuiliao
authored
Fix: Hue Normalization (#6456)
* Fix hue normalization * add tests * rename tests * revert useColorWheelState.ts --------- Co-authored-by: Robert Snow <[email protected]> Co-authored-by: Yihui Liao <[email protected]>
1 parent ad6e240 commit 1cacbf1

File tree

2 files changed

+26
-7
lines changed

2 files changed

+26
-7
lines changed

packages/@react-stately/color/src/Color.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,17 @@ export function getColorChannels(colorSpace: ColorSpace) {
4949
}
5050
}
5151

52+
/**
53+
* Returns the hue value normalized to the range of 0 to 360.
54+
*/
55+
export function normalizeHue(hue: number) {
56+
if (hue === 360) {
57+
return hue;
58+
}
59+
60+
return ((hue % 360) + 360) % 360;
61+
}
62+
5263
// Lightness threshold between orange and brown.
5364
const ORANGE_LIGHTNESS_THRESHOLD = 0.68;
5465
// Lightness threshold between pure yellow and "yellow green".
@@ -145,7 +156,7 @@ abstract class Color implements IColor {
145156
} else if (c >= 0.15) {
146157
chroma = 'vibrant';
147158
}
148-
159+
149160
if (l < 0.3) {
150161
lightness = 'very dark';
151162
} else if (l < MAX_DARK_LIGHTNESS) {
@@ -435,7 +446,7 @@ class HSBColor extends Color {
435446
let m: RegExpMatchArray | void;
436447
if ((m = value.match(HSB_REGEX))) {
437448
const [h, s, b, a] = (m[1] ?? m[2]).split(',').map(n => Number(n.trim().replace('%', '')));
438-
return new HSBColor(mod(h, 360), clamp(s, 0, 100), clamp(b, 0, 100), clamp(a ?? 1, 0, 1));
449+
return new HSBColor(normalizeHue(h), clamp(s, 0, 100), clamp(b, 0, 100), clamp(a ?? 1, 0, 1));
439450
}
440451
}
441452

@@ -565,10 +576,6 @@ class HSBColor extends Color {
565576
// - hsla(X, X%, X%, X)
566577
const HSL_REGEX = /hsl\(([-+]?\d+(?:.\d+)?\s*,\s*[-+]?\d+(?:.\d+)?%\s*,\s*[-+]?\d+(?:.\d+)?%)\)|hsla\(([-+]?\d+(?:.\d+)?\s*,\s*[-+]?\d+(?:.\d+)?%\s*,\s*[-+]?\d+(?:.\d+)?%\s*,\s*[-+]?\d(.\d+)?)\)/;
567578

568-
function mod(n, m) {
569-
return ((n % m) + m) % m;
570-
}
571-
572579
class HSLColor extends Color {
573580
constructor(private hue: number, private saturation: number, private lightness: number, private alpha: number) {
574581
super();
@@ -578,7 +585,7 @@ class HSLColor extends Color {
578585
let m: RegExpMatchArray | void;
579586
if ((m = value.match(HSL_REGEX))) {
580587
const [h, s, l, a] = (m[1] ?? m[2]).split(',').map(n => Number(n.trim().replace('%', '')));
581-
return new HSLColor(mod(h, 360), clamp(s, 0, 100), clamp(l, 0, 100), clamp(a ?? 1, 0, 1));
588+
return new HSLColor(normalizeHue(h), clamp(s, 0, 100), clamp(l, 0, 100), clamp(a ?? 1, 0, 1));
582589
}
583590
}
584591

packages/@react-stately/color/test/Color.test.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,12 @@ describe('Color', function () {
140140
expect(color.getChannelValue('alpha')).toBe(0);
141141
expect(color.toString('hsla')).toBe('hsla(320, 100%, 0%, 0)');
142142
});
143+
144+
it('should allow 360 as a hue value', function () {
145+
let color = parseColor('hsl(360, 100%, 50%)');
146+
expect(color.getChannelValue('hue')).toBe(360);
147+
expect(color.toString('hsl')).toBe('hsl(360, 100%, 50%)');
148+
});
143149
});
144150

145151
it('withChannelValue', () => {
@@ -180,6 +186,12 @@ describe('Color', function () {
180186
expect(color.getChannelValue('alpha')).toBe(0);
181187
expect(color.toString('hsba')).toBe('hsba(320, 100%, 0%, 0)');
182188
});
189+
190+
it('should allow 360 as a hue value', function () {
191+
let color = parseColor('hsb(360, 100%, 50%)');
192+
expect(color.getChannelValue('hue')).toBe(360);
193+
expect(color.toString('hsb')).toBe('hsb(360, 100%, 50%)');
194+
});
183195
});
184196

185197
describe('conversions', () => {

0 commit comments

Comments
 (0)