Skip to content

Commit bd458c1

Browse files
authored
TS Strict Color Stately (#6499)
* TS Strict Color Stately
1 parent 025842c commit bd458c1

File tree

7 files changed

+59
-25
lines changed

7 files changed

+59
-25
lines changed

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

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,9 @@ abstract class Color implements IColor {
120120

121121
getColorSpaceAxes(xyChannels: {xChannel?: ColorChannel, yChannel?: ColorChannel}): ColorAxes {
122122
let {xChannel, yChannel} = xyChannels;
123-
let xCh = xChannel || this.getColorChannels().find(c => c !== yChannel);
124-
let yCh = yChannel || this.getColorChannels().find(c => c !== xCh);
125-
let zCh = this.getColorChannels().find(c => c !== xCh && c !== yCh);
123+
let xCh = xChannel || this.getColorChannels().find(c => c !== yChannel)!;
124+
let yCh = yChannel || this.getColorChannels().find(c => c !== xCh)!;
125+
let zCh = this.getColorChannels().find(c => c !== xCh && c !== yCh)!;
126126

127127
return {xChannel: xCh, yChannel: yCh, zChannel: zCh};
128128
}
@@ -245,7 +245,7 @@ class RGBColor extends Color {
245245
}
246246

247247
static parse(value: string) {
248-
let colors = [];
248+
let colors: Array<number | undefined> = [];
249249
// matching #rgb, #rgba, #rrggbb, #rrggbbaa
250250
if (/^#[\da-f]+$/i.test(value) && [4, 5, 7, 9].includes(value.length)) {
251251
const values = (value.length < 6 ? value.replace(/[^#]/gi, '$&$&') : value).slice(1).split('');
@@ -259,7 +259,12 @@ class RGBColor extends Color {
259259
const match = value.match(/^rgba?\((.*)\)$/);
260260
if (match?.[1]) {
261261
colors = match[1].split(',').map(value => Number(value.trim()));
262-
colors = colors.map((num, i) => clamp(num, 0, i < 3 ? 255 : 1));
262+
colors = colors.map((num, i) => {
263+
return clamp(num ?? 0, 0, i < 3 ? 255 : 1);
264+
});
265+
}
266+
if (colors[0] === undefined || colors[1] === undefined || colors[2] === undefined) {
267+
return undefined;
263268
}
264269

265270
return colors.length < 3 ? undefined : new RGBColor(colors[0], colors[1], colors[2], colors[3] ?? 1);
@@ -371,6 +376,7 @@ class RGBColor extends Color {
371376
hue = (blue - red) / chroma + 2;
372377
break;
373378
case blue:
379+
default:
374380
hue = (red - green) / chroma + 4;
375381
break;
376382
}
@@ -443,7 +449,7 @@ class HSBColor extends Color {
443449
}
444450

445451
static parse(value: string): HSBColor | void {
446-
let m: RegExpMatchArray | void;
452+
let m: RegExpMatchArray | null;
447453
if ((m = value.match(HSB_REGEX))) {
448454
const [h, s, b, a] = (m[1] ?? m[2]).split(',').map(n => Number(n.trim().replace('%', '')));
449455
return new HSBColor(normalizeHue(h), clamp(s, 0, 100), clamp(b, 0, 100), clamp(a ?? 1, 0, 1));
@@ -582,7 +588,7 @@ class HSLColor extends Color {
582588
}
583589

584590
static parse(value: string): HSLColor | void {
585-
let m: RegExpMatchArray | void;
591+
let m: RegExpMatchArray | null;
586592
if ((m = value.match(HSL_REGEX))) {
587593
const [h, s, l, a] = (m[1] ?? m[2]).split(',').map(n => Number(n.trim().replace('%', '')));
588594
return new HSLColor(normalizeHue(h), clamp(s, 0, 100), clamp(l, 0, 100), clamp(a ?? 1, 0, 1));

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

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,15 @@ export function useColorAreaState(props: ColorAreaProps): ColorAreaState {
8585
if (!value && !defaultValue) {
8686
defaultValue = DEFAULT_COLOR;
8787
}
88+
if (value) {
89+
value = normalizeColor(value);
90+
}
91+
if (defaultValue) {
92+
defaultValue = normalizeColor(defaultValue);
93+
}
8894

89-
let [colorValue, setColorState] = useControlledState(value && normalizeColor(value), defaultValue && normalizeColor(defaultValue), onChange);
95+
// safe to cast value and defaultValue to Color, one of them will always be defined because if neither are, we assign a default
96+
let [colorValue, setColorState] = useControlledState<Color>(value as Color, defaultValue as Color, onChange);
9097
let color = useMemo(() => colorSpace && colorValue ? colorValue.toFormat(colorSpace) : colorValue, [colorValue, colorSpace]);
9198
let valueRef = useRef(color);
9299
let setColor = (color: Color) => {
@@ -141,7 +148,7 @@ export function useColorAreaState(props: ColorAreaProps): ColorAreaState {
141148
setColorFromPoint(x: number, y: number) {
142149
let newXValue = minValueX + clamp(x, 0, 1) * (maxValueX - minValueX);
143150
let newYValue = minValueY + (1 - clamp(y, 0, 1)) * (maxValueY - minValueY);
144-
let newColor:Color;
151+
let newColor: Color | undefined;
145152
if (newXValue !== xValue) {
146153
// Round new value to multiple of step, clamp value between min and max
147154
newXValue = snapValueToStep(newXValue, minValueX, maxValueX, stepX);
@@ -161,16 +168,16 @@ export function useColorAreaState(props: ColorAreaProps): ColorAreaState {
161168
let y = 1 - (yValue - minValueY) / (maxValueY - minValueY);
162169
return {x, y};
163170
},
164-
incrementX(stepSize) {
171+
incrementX(stepSize = 1) {
165172
setXValue(xValue + stepSize > maxValueX ? maxValueX : snapValueToStep(xValue + stepSize, minValueX, maxValueX, stepX));
166173
},
167-
incrementY(stepSize) {
174+
incrementY(stepSize = 1) {
168175
setYValue(yValue + stepSize > maxValueY ? maxValueY : snapValueToStep(yValue + stepSize, minValueY, maxValueY, stepY));
169176
},
170-
decrementX(stepSize) {
177+
decrementX(stepSize = 1) {
171178
setXValue(snapValueToStep(xValue - stepSize, minValueX, maxValueX, stepX));
172179
},
173-
decrementY(stepSize) {
180+
decrementY(stepSize = 1) {
174181
setYValue(snapValueToStep(yValue - stepSize, minValueY, maxValueY, stepY));
175182
},
176183
setDragging(isDragging) {

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export interface ColorFieldState extends FormValidationState {
2727
* The currently parsed color value, or null if the field is empty.
2828
* Updated based on the `inputValue` as the user types.
2929
*/
30-
readonly colorValue: Color,
30+
readonly colorValue: Color | null,
3131
/** Sets the current text value of the input. */
3232
setInputValue(value: string): void,
3333
/**
@@ -72,15 +72,15 @@ export function useColorFieldState(
7272

7373
let initialValue = useColor(value);
7474
let initialDefaultValue = useColor(defaultValue);
75-
let [colorValue, setColorValue] = useControlledState<Color>(initialValue, initialDefaultValue, onChange);
75+
let [colorValue, setColorValue] = useControlledState<Color | null>(initialValue!, initialDefaultValue!, onChange);
7676
let [inputValue, setInputValue] = useState(() => (value || defaultValue) && colorValue ? colorValue.toString('hex') : '');
7777

7878
let validation = useFormValidationState({
7979
...props,
8080
value: colorValue
8181
});
8282

83-
let safelySetColorValue = (newColor: Color) => {
83+
let safelySetColorValue = (newColor: Color | null) => {
8484
if (!colorValue || !newColor) {
8585
setColorValue(newColor);
8686
return;
@@ -111,7 +111,11 @@ export function useColorFieldState(
111111
// Set to empty state if input value is empty
112112
if (!inputValue.length) {
113113
safelySetColorValue(null);
114-
setInputValue(value === undefined ? '' : colorValue.toString('hex'));
114+
if (value === undefined || colorValue === null) {
115+
setInputValue('');
116+
} else {
117+
setInputValue(colorValue.toString('hex'));
118+
}
115119
return;
116120
}
117121

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,20 @@ export function useColorSliderState(props: ColorSliderStateOptions): ColorSlider
4343
throw new Error('useColorSliderState requires a value or defaultValue');
4444
}
4545

46-
let [colorValue, setColor] = useControlledState(value && normalizeColor(value), defaultValue && normalizeColor(defaultValue), onChange);
46+
if (value) {
47+
value = normalizeColor(value);
48+
}
49+
if (defaultValue) {
50+
defaultValue = normalizeColor(defaultValue);
51+
}
52+
// safe to cast value and defaultValue to Color, one of them will always be defined because if neither are, we throw an error
53+
let [colorValue, setColor] = useControlledState<Color>(value as Color, defaultValue as Color, onChange);
4754
let color = useMemo(() => colorSpace && colorValue ? colorValue.toFormat(colorSpace) : colorValue, [colorValue, colorSpace]);
4855
let sliderState = useSliderState({
4956
...color.getChannelRange(channel),
5057
...otherProps,
51-
// Unused except in getThumbValueLabel, which is overridden below. null to appease TypeScript.
58+
// Unused except in getThumbValueLabel, which is overridden below. null to localize the TypeScript error for ignoring.
59+
// @ts-ignore
5260
numberFormatter: null,
5361
value: color.getChannelValue(channel),
5462
onChange(v) {

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,20 @@ function cartesianToAngle(x: number, y: number, radius: number): number {
9696
* Color wheels allow users to adjust the hue of an HSL or HSB color value on a circular track.
9797
*/
9898
export function useColorWheelState(props: ColorWheelProps): ColorWheelState {
99-
let {defaultValue, onChange, onChangeEnd} = props;
99+
let {value: propsValue, defaultValue, onChange, onChangeEnd} = props;
100100

101-
if (!props.value && !defaultValue) {
101+
if (!propsValue && !defaultValue) {
102102
defaultValue = DEFAULT_COLOR;
103103
}
104+
if (propsValue) {
105+
propsValue = normalizeColor(propsValue);
106+
}
107+
if (defaultValue) {
108+
defaultValue = normalizeColor(defaultValue);
109+
}
104110

105-
let [stateValue, setValueState] = useControlledState(normalizeColor(props.value), normalizeColor(defaultValue), onChange);
111+
// safe to cast value and defaultValue to Color, one of them will always be defined because if neither are, we assign a default
112+
let [stateValue, setValueState] = useControlledState<Color>(propsValue as Color, defaultValue as Color, onChange);
106113
let value = useMemo(() => {
107114
let colorSpace = stateValue.getColorSpace();
108115
return colorSpace === 'hsl' || colorSpace === 'hsb' ? stateValue : stateValue.toFormat('hsl');

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -202,11 +202,11 @@ describe('Color', function () {
202202

203203

204204
let rgb = fc.tuple(fc.integer({min: 0, max: 255}), fc.integer({min: 0, max: 255}), fc.integer({min: 0, max: 255}))
205-
.map(([r, g, b]) => (['rgb', `rgb(${r}, ${g}, ${b})`, [r, g, b]]));
205+
.map(([r, g, b]) => (['rgb' as ColorFormat, `rgb(${r}, ${g}, ${b})`, [r, g, b]]));
206206
let hsl = fc.tuple(fc.integer({min: 0, max: 360}), fc.integer({min: 0, max: 100}), fc.integer({min: 0, max: 100}))
207-
.map(([h, s, l]) => (['hsl', `hsl(${h}, ${s}%, ${l}%)`, [h, s, l]]));
207+
.map(([h, s, l]) => (['hsl' as ColorFormat, `hsl(${h}, ${s}%, ${l}%)`, [h, s, l]]));
208208
let hsb = fc.tuple(fc.integer({min: 0, max: 360}), fc.integer({min: 0, max: 100}), fc.integer({min: 0, max: 100}))
209-
.map(([h, s, b]) => (['hsb', `hsb(${h}, ${s}%, ${b}%)`, [h, s, b]]));
209+
.map(([h, s, b]) => (['hsb' as ColorFormat, `hsb(${h}, ${s}%, ${b}%)`, [h, s, b]]));
210210
let options = fc.record({
211211
colorSpace: fc.oneof(fc.constant('rgb'), fc.constant('hsl'), fc.constant('hsb')),
212212
color: fc.oneof(rgb, hsl, hsb)
@@ -232,6 +232,7 @@ describe('Color', function () {
232232
};
233233

234234
it('can perform round trips', () => {
235+
// @ts-ignore
235236
fc.assert(fc.property(options, ({colorSpace, color}: {colorSpace: ColorFormat, color: [string, string, number[]]}) => {
236237
let testColor = parseColor(color[1]);
237238
let convertedColor = testColor.toString(colorSpace);

tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@
106106
"./packages/@react-spectrum/well",
107107
"./packages/@react-stately/calendar",
108108
"./packages/@react-stately/checkbox",
109+
"./packages/@react-stately/color",
109110
"./packages/@react-stately/numberfield",
110111
"./packages/@react-stately/overlays",
111112
"./packages/@react-stately/pagination",

0 commit comments

Comments
 (0)