Skip to content

Commit ec7944e

Browse files
committed
feat: support all angle values as input
Adds support for the angle value units `deg`, `grad`, `rad`, and `turn` when entering hues (see https://www.w3.org/TR/css-values-4/#angle-value). Stops normalizing angle values to the range [0, 360) (e.g. a hue value of 450 will no longer be processed as 90).
1 parent 6161e74 commit ec7944e

File tree

6 files changed

+193
-177
lines changed

6 files changed

+193
-177
lines changed

src/ColorPicker.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { colorChannels } from './utilities/colorChannels.js'
55
import { colorPickerTemplate } from './templates/colorPickerTemplate.js'
66
import { colorsAreValueEqual } from './utilities/colorsAreValueEqual.js'
77
import { convert } from './utilities/convert.js'
8+
import { CssValue } from './utilities/css-values.js'
89
import { formatAsCssColor } from './utilities/formatAsCssColor.js'
910
import { getNewThumbPosition } from './utilities/getNewThumbPosition.js'
1011
import { isValidHexColor } from './utilities/isValidHexColor.js'
@@ -558,10 +559,10 @@ export class ColorPicker extends HTMLElement {
558559
}
559560

560561
const input = event.currentTarget as HTMLInputElement
561-
const step = parseFloat(input.step)
562+
const step = Number(input.step)
562563
const direction = ['ArrowLeft', 'ArrowDown'].includes(event.key) ? -1 : 1
563-
const value = parseFloat(input.value) + direction * step * 10
564-
const newValue = clamp(value, parseInt(input.min), parseInt(input.max))
564+
const value = Number(input.value) + direction * step * 10
565+
const newValue = clamp(value, Number(input.min), Number(input.max))
565566

566567
// Intentionally removes a single step from `newValue` because the default action associated with an `input` element’s `keydown` event will add one itself.
567568
input.value = String(newValue - direction * step)
@@ -570,7 +571,7 @@ export class ColorPicker extends HTMLElement {
570571
#handleSliderInput = (event: Event, channel: 'h' | 'a') => {
571572
const input = event.currentTarget as HTMLInputElement
572573
const hsvColor = Object.assign({}, this.colors.hsv)
573-
hsvColor[channel] = parseFloat(input.value)
574+
hsvColor[channel] = Number(input.value)
574575

575576
this.#updateColors({ format: 'hsv', color: hsvColor })
576577
}
@@ -586,8 +587,10 @@ export class ColorPicker extends HTMLElement {
586587
#updateColorValue = (event: Event, channel: string) => {
587588
const input = event.target as HTMLInputElement
588589

589-
const color = Object.assign({}, this.#colors[this.activeFormat])
590-
const value = colorChannels[this.activeFormat][channel].from(input.value)
590+
const format = this.#activeFormat as Exclude<ColorFormat, 'hex' | 'hsv'>
591+
const color = Object.assign({}, this.#colors[format])
592+
const cssValue = colorChannels[format][channel] as CssValue
593+
const value = cssValue.from(input.value)
591594

592595
if (Number.isNaN(value) || value === undefined) {
593596
// This means that the input value does not result in a valid CSS value.
@@ -639,7 +642,8 @@ export class ColorPicker extends HTMLElement {
639642
* Wrapper function. Converts a color channel’s value into its CSS value representation.
640643
*/
641644
#getChannelAsCssValue = (channel: string): string => {
642-
const format = this.activeFormat
643-
return colorChannels[format][channel].to(this.#colors[format][channel])
645+
const format = this.activeFormat as Exclude<ColorFormat, 'hex' | 'hsv'>
646+
const cssValue = colorChannels[format][channel] as CssValue
647+
return cssValue.to(this.#colors[format][channel])
644648
}
645649
}

src/utilities/colorChannels.ts

Lines changed: 19 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,29 @@
1+
import { ColorFormat } from '../ColorPicker.js'
12
import {
2-
from8BitDecimal,
3-
fromAlpha,
4-
fromHueAngle,
5-
fromPercentage,
6-
to8BitDecimal,
7-
toAlpha,
8-
toHueAngle,
9-
toPercentage,
3+
alpha,
4+
angle,
5+
CssValue,
6+
percentage,
7+
rgbNumber,
108
} from './css-values.js'
119

12-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
13-
export const colorChannels: any = {
10+
export const colorChannels: Record<Exclude<ColorFormat, 'hex' | 'hsv'>, Record<string, CssValue>> = {
1411
hsl: {
15-
h: {
16-
to: toHueAngle,
17-
from: fromHueAngle,
18-
},
19-
20-
s: {
21-
to: toPercentage,
22-
from: fromPercentage,
23-
},
24-
25-
l: {
26-
to: toPercentage,
27-
from: fromPercentage,
28-
},
29-
30-
a: {
31-
to: toAlpha,
32-
from: fromAlpha,
33-
},
12+
h: angle,
13+
s: percentage,
14+
l: percentage,
15+
a: alpha,
3416
},
35-
3617
hwb: {
37-
h: {
38-
to: toHueAngle,
39-
from: fromHueAngle,
40-
},
41-
42-
w: {
43-
to: toPercentage,
44-
from: fromPercentage,
45-
},
46-
47-
b: {
48-
to: toPercentage,
49-
from: fromPercentage,
50-
},
51-
52-
a: {
53-
to: toAlpha,
54-
from: fromAlpha,
55-
},
18+
h: angle,
19+
w: percentage,
20+
b: percentage,
21+
a: alpha,
5622
},
57-
5823
rgb: {
59-
r: {
60-
to: to8BitDecimal,
61-
from: from8BitDecimal,
62-
},
63-
64-
g: {
65-
to: to8BitDecimal,
66-
from: from8BitDecimal,
67-
},
68-
69-
b: {
70-
to: to8BitDecimal,
71-
from: from8BitDecimal,
72-
},
73-
74-
a: {
75-
to: toAlpha,
76-
from: fromAlpha,
77-
},
24+
r: rgbNumber,
25+
g: rgbNumber,
26+
b: rgbNumber,
27+
a: alpha,
7828
},
7929
}

src/utilities/css-values.test.ts

Lines changed: 41 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,28 @@
11
import { describe, test, expect } from 'vitest'
22

33
import {
4-
from8BitDecimal,
5-
fromAlpha,
6-
fromHueAngle,
7-
fromPercentage,
8-
to8BitDecimal,
9-
toAlpha,
10-
toHueAngle,
11-
toPercentage,
4+
alpha,
5+
angle,
6+
percentage,
7+
rgbNumber,
128
} from './css-values.js'
139

1410
describe('CssValues', () => {
15-
describe('hue angle', () => {
11+
describe('angle', () => {
1612
test.each([
17-
['-30', 330],
13+
['-30', -30],
1814
['0', 0],
19-
['360', 0],
20-
['450', 90],
15+
['360', 360],
16+
['450', 450],
2117
['270', 270],
2218
['270.', NaN],
23-
])('fromHueAngle', (value, expected) => {
24-
expect(fromHueAngle(value)).toEqual(expected)
19+
['90deg', 90],
20+
['100grad', 90],
21+
['1.5707963267948966rad', 90],
22+
['0.25turn', 90],
23+
['90xdeg', NaN],
24+
])('hue.from(%s) = %s', (value, expected) => {
25+
expect(angle.from(value)).toEqual(expected)
2526
})
2627

2728
test.each([
@@ -31,31 +32,35 @@ describe('CssValues', () => {
3132
[120, '120'],
3233
[180, '180'],
3334
[270, '270'],
34-
])('toHueAngle', (value, expected) => {
35-
expect(toHueAngle(value)).toEqual(expected)
35+
])('hue.to(%s) = %s', (value, expected) => {
36+
expect(angle.to(value)).toEqual(expected)
3637
})
3738
})
3839

3940
describe('percentage', () => {
4041
test.each([
41-
['0%', 0],
42-
['0', NaN],
43-
['10.%', NaN],
44-
['a%', NaN],
45-
['-13%', 0],
46-
['55.55%', 55.55],
47-
['100%', 100],
48-
['1300%', 100],
49-
])('fromPercentage', (value, expected) => {
50-
expect(fromPercentage(value)).toEqual(expected)
42+
['0%', 100, 0],
43+
['0', 100, NaN],
44+
['10.%', 100, NaN],
45+
['a%', 100, NaN],
46+
['-13%', 100, 0],
47+
['55.55%', 100, 55.55],
48+
['100%', 100, 100],
49+
['1300%', 100, 100],
50+
['100%', 255, 255],
51+
['50%', 255, 127.5],
52+
['100%', 1, 1],
53+
['50%', 1, 0.5],
54+
])('percentage.from(%s, %s) = %s', (value, referenceValue, expected) => {
55+
expect(percentage.from(value, { referenceValue })).toEqual(expected)
5156
})
5257

5358
test.each([
5459
[0, '0%'],
5560
[55.55, '55.55%'],
5661
[100, '100%'],
57-
])('toPercentage', (value, expected) => {
58-
expect(toPercentage(value)).toEqual(expected)
62+
])('percentage.to(%s) = %s', (value, expected) => {
63+
expect(percentage.to(value)).toEqual(expected)
5964
})
6065
})
6166

@@ -70,16 +75,16 @@ describe('CssValues', () => {
7075
['255', 255],
7176
['100%', 255],
7277
['50%', 127.5],
73-
])('from8BitDecimal', (value, expected) => {
74-
expect(from8BitDecimal(value)).toEqual(expected)
78+
])('rgbNumber.from(%s) = %s', (value, expected) => {
79+
expect(rgbNumber.from(value)).toEqual(expected)
7580
})
7681

7782
test.each([
7883
[0, '0'],
7984
[141.6525, '141.65'],
8085
[255, '255'],
81-
])('to8BitDecimal', (value, expected) => {
82-
expect(to8BitDecimal(value)).toEqual(expected)
86+
])('rgbNumber.to(%s) = %s', (value, expected) => {
87+
expect(rgbNumber.to(value)).toEqual(expected)
8388
})
8489
})
8590

@@ -91,16 +96,16 @@ describe('CssValues', () => {
9196
['0%', 0],
9297
['55.55%', 0.5555],
9398
['100%', 1],
94-
])('fromAlpha', (value, expected) => {
95-
expect(fromAlpha(value)).toEqual(expected)
99+
])('alpha.from(%s) = %s', (value, expected) => {
100+
expect(alpha.from(value)).toEqual(expected)
96101
})
97102

98103
test.each([
99104
[0, '0'],
100105
[0.5555, '0.5555'],
101106
[1, '1'],
102-
])('toAlpha', (value, expected) => {
103-
expect(toAlpha(value)).toEqual(expected)
107+
])('alpha.to(%s) = %s', (value, expected) => {
108+
expect(alpha.to(value)).toEqual(expected)
104109
})
105110
})
106111
})

0 commit comments

Comments
 (0)