@react-stately/color 3.0.0-beta.1 → 3.0.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.js +1019 -859
- package/dist/main.js.map +1 -1
- package/dist/module.js +1009 -834
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts +61 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +9 -9
- package/src/Color.ts +242 -35
- package/src/index.ts +2 -1
- package/src/useColorAreaState.ts +184 -0
- package/src/useColorFieldState.ts +87 -48
- package/src/useColorSliderState.ts +6 -11
- package/src/useColorWheelState.ts +32 -29
|
@@ -14,7 +14,7 @@ import {Color, ColorFieldProps} from '@react-types/color';
|
|
|
14
14
|
import {parseColor} from './Color';
|
|
15
15
|
import {useColor} from './useColor';
|
|
16
16
|
import {useControlledState} from '@react-stately/utils';
|
|
17
|
-
import {
|
|
17
|
+
import {useMemo, useRef, useState} from 'react';
|
|
18
18
|
|
|
19
19
|
export interface ColorFieldState {
|
|
20
20
|
/**
|
|
@@ -41,7 +41,13 @@ export interface ColorFieldState {
|
|
|
41
41
|
/** Sets the current value to the maximum color value, and fires `onChange`. */
|
|
42
42
|
incrementToMax(): void,
|
|
43
43
|
/** Sets the current value to the minimum color value, and fires `onChange`. */
|
|
44
|
-
decrementToMin(): void
|
|
44
|
+
decrementToMin(): void,
|
|
45
|
+
/**
|
|
46
|
+
* Validates a user input string.
|
|
47
|
+
* Values can be partially entered, and may be valid even if they cannot currently be parsed to a color.
|
|
48
|
+
* Can be used to implement validation as a user types.
|
|
49
|
+
*/
|
|
50
|
+
validate(value: string): boolean
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
const MIN_COLOR = parseColor('#000000');
|
|
@@ -57,68 +63,102 @@ export function useColorFieldState(
|
|
|
57
63
|
props: ColorFieldProps
|
|
58
64
|
): ColorFieldState {
|
|
59
65
|
let {
|
|
60
|
-
step = 1,
|
|
61
66
|
value,
|
|
62
67
|
defaultValue,
|
|
63
68
|
onChange
|
|
64
69
|
} = props;
|
|
70
|
+
let {step} = MIN_COLOR.getChannelRange('red');
|
|
65
71
|
|
|
66
72
|
let initialValue = useColor(value);
|
|
67
73
|
let initialDefaultValue = useColor(defaultValue);
|
|
68
74
|
let [colorValue, setColorValue] = useControlledState<Color>(initialValue, initialDefaultValue, onChange);
|
|
75
|
+
let [inputValue, setInputValue] = useState(() => (value || defaultValue) && colorValue ? colorValue.toString('hex') : '');
|
|
69
76
|
|
|
70
|
-
let
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
try {
|
|
79
|
-
let currentColor = parseColor(inputValue.startsWith('#') ? inputValue : `#${inputValue}`);
|
|
80
|
-
if (currentColor.toHexInt() !== colorValue?.toHexInt()) {
|
|
81
|
-
return colorValue ? colorValue.toString('hex') : '';
|
|
82
|
-
}
|
|
83
|
-
} catch (err) {
|
|
84
|
-
// ignore
|
|
85
|
-
}
|
|
86
|
-
return inputValue;
|
|
87
|
-
});
|
|
88
|
-
}, [inputValue, colorValue, setInputValue]);
|
|
89
|
-
|
|
90
|
-
let increment = () => setColorValue((prevColor: Color) => addColorValue(prevColor, step));
|
|
91
|
-
let decrement = () => setColorValue((prevColor: Color) => addColorValue(prevColor, -step));
|
|
92
|
-
let incrementToMax = () => setColorValue((prevColor: Color) => addColorValue(prevColor, MAX_COLOR_INT));
|
|
93
|
-
let decrementToMin = () => setColorValue((prevColor: Color) => addColorValue(prevColor, -MAX_COLOR_INT));
|
|
94
|
-
|
|
95
|
-
let setFieldInputValue = (value: string) => {
|
|
96
|
-
value = value.match(/^#?[0-9a-f]{0,6}$/i)?.[0];
|
|
97
|
-
if (value !== undefined) {
|
|
98
|
-
if (!value.length && colorValue) {
|
|
99
|
-
setColorValue(null);
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
try {
|
|
103
|
-
let newColor = parseColor(value.startsWith('#') ? value : `#${value}`);
|
|
104
|
-
setColorValue((prevColor: Color) => {
|
|
105
|
-
setInputValue(value);
|
|
106
|
-
return prevColor && prevColor.toHexInt() === newColor.toHexInt() ? prevColor : newColor;
|
|
107
|
-
});
|
|
108
|
-
} catch (err) {
|
|
109
|
-
setInputValue(value);
|
|
110
|
-
}
|
|
77
|
+
let safelySetColorValue = (newColor: Color) => {
|
|
78
|
+
if (!colorValue || !newColor) {
|
|
79
|
+
setColorValue(newColor);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (newColor.toHexInt() !== colorValue.toHexInt()) {
|
|
83
|
+
setColorValue(newColor);
|
|
84
|
+
return;
|
|
111
85
|
}
|
|
112
86
|
};
|
|
113
87
|
|
|
114
|
-
let
|
|
88
|
+
let prevValue = useRef(colorValue);
|
|
89
|
+
if (prevValue.current !== colorValue) {
|
|
115
90
|
setInputValue(colorValue ? colorValue.toString('hex') : '');
|
|
91
|
+
prevValue.current = colorValue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
let parsedValue = useMemo(() => {
|
|
96
|
+
let color;
|
|
97
|
+
try {
|
|
98
|
+
color = parseColor(inputValue.startsWith('#') ? inputValue : `#${inputValue}`);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
color = null;
|
|
101
|
+
}
|
|
102
|
+
return color;
|
|
103
|
+
}, [inputValue]);
|
|
104
|
+
let parsed = useRef(null);
|
|
105
|
+
parsed.current = parsedValue;
|
|
106
|
+
|
|
107
|
+
let commit = () => {
|
|
108
|
+
// Set to empty state if input value is empty
|
|
109
|
+
if (!inputValue.length) {
|
|
110
|
+
safelySetColorValue(null);
|
|
111
|
+
setInputValue(value === undefined ? '' : colorValue.toString('hex'));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// if it failed to parse, then reset input to formatted version of current number
|
|
116
|
+
if (parsed.current == null) {
|
|
117
|
+
setInputValue(colorValue ? colorValue.toString('hex') : '');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
safelySetColorValue(parsed.current);
|
|
122
|
+
// in a controlled state, the numberValue won't change, so we won't go back to our old input without help
|
|
123
|
+
let newColorValue = '';
|
|
124
|
+
if (colorValue) {
|
|
125
|
+
newColorValue = colorValue.toString('hex');
|
|
126
|
+
}
|
|
127
|
+
setInputValue(newColorValue);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
let increment = () => {
|
|
131
|
+
let newValue = addColorValue(parsed.current, step);
|
|
132
|
+
// if we've arrived at the same value that was previously in the state, the
|
|
133
|
+
// input value should be updated to match
|
|
134
|
+
// ex type 4, press increment, highlight the number in the input, type 4 again, press increment
|
|
135
|
+
// you'd be at 5, then incrementing to 5 again, so no re-render would happen and 4 would be left in the input
|
|
136
|
+
if (newValue === colorValue) {
|
|
137
|
+
setInputValue(newValue.toString('hex'));
|
|
138
|
+
}
|
|
139
|
+
safelySetColorValue(newValue);
|
|
116
140
|
};
|
|
141
|
+
let decrement = () => {
|
|
142
|
+
let newValue = addColorValue(parsed.current, -step);
|
|
143
|
+
// if we've arrived at the same value that was previously in the state, the
|
|
144
|
+
// input value should be updated to match
|
|
145
|
+
// ex type 4, press increment, highlight the number in the input, type 4 again, press increment
|
|
146
|
+
// you'd be at 5, then incrementing to 5 again, so no re-render would happen and 4 would be left in the input
|
|
147
|
+
if (newValue === colorValue) {
|
|
148
|
+
setInputValue(newValue.toString('hex'));
|
|
149
|
+
}
|
|
150
|
+
safelySetColorValue(newValue);
|
|
151
|
+
};
|
|
152
|
+
let incrementToMax = () => safelySetColorValue(MAX_COLOR);
|
|
153
|
+
let decrementToMin = () => safelySetColorValue(MIN_COLOR);
|
|
154
|
+
|
|
155
|
+
let validate = (value: string) => value === '' || !!value.match(/^#?[0-9a-f]{0,6}$/i)?.[0];
|
|
117
156
|
|
|
118
157
|
return {
|
|
158
|
+
validate,
|
|
119
159
|
colorValue,
|
|
120
160
|
inputValue,
|
|
121
|
-
setInputValue
|
|
161
|
+
setInputValue,
|
|
122
162
|
commit,
|
|
123
163
|
increment,
|
|
124
164
|
incrementToMax,
|
|
@@ -130,11 +170,10 @@ export function useColorFieldState(
|
|
|
130
170
|
function addColorValue(color: Color, step: number) {
|
|
131
171
|
let newColor = color ? color : MIN_COLOR;
|
|
132
172
|
let colorInt = newColor.toHexInt();
|
|
133
|
-
let newColorString = color ? color.toString('hex') : '';
|
|
134
173
|
|
|
135
174
|
let clampInt = Math.min(Math.max(colorInt + step, MIN_COLOR_INT), MAX_COLOR_INT);
|
|
136
175
|
if (clampInt !== colorInt) {
|
|
137
|
-
newColorString = `#${clampInt.toString(16).padStart(6, '0').toUpperCase()}`;
|
|
176
|
+
let newColorString = `#${clampInt.toString(16).padStart(6, '0').toUpperCase()}`;
|
|
138
177
|
newColor = parseColor(newColorString);
|
|
139
178
|
}
|
|
140
179
|
return newColor;
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import {Color, ColorSliderProps} from '@react-types/color';
|
|
14
|
-
import {parseColor} from './Color';
|
|
14
|
+
import {normalizeColor, parseColor} from './Color';
|
|
15
15
|
import {SliderState, useSliderState} from '@react-stately/slider';
|
|
16
16
|
import {useControlledState} from '@react-stately/utils';
|
|
17
17
|
|
|
@@ -30,14 +30,6 @@ interface ColorSliderStateOptions extends ColorSliderProps {
|
|
|
30
30
|
locale: string
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
function normalizeColor(v: string | Color) {
|
|
34
|
-
if (typeof v === 'string') {
|
|
35
|
-
return parseColor(v);
|
|
36
|
-
} else {
|
|
37
|
-
return v;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
33
|
/**
|
|
42
34
|
* Provides state management for a color slider component.
|
|
43
35
|
* Color sliders allow users to adjust an individual channel of a color value.
|
|
@@ -59,13 +51,14 @@ export function useColorSliderState(props: ColorSliderStateOptions): ColorSlider
|
|
|
59
51
|
setColor(color.withChannelValue(channel, v));
|
|
60
52
|
},
|
|
61
53
|
onChangeEnd([v]) {
|
|
62
|
-
// onChange will have already been called with the right value, this is just to trigger
|
|
54
|
+
// onChange will have already been called with the right value, this is just to trigger onChangeEnd
|
|
63
55
|
if (props.onChangeEnd) {
|
|
64
56
|
props.onChangeEnd(color.withChannelValue(channel, v));
|
|
65
57
|
}
|
|
66
58
|
}
|
|
67
59
|
});
|
|
68
60
|
|
|
61
|
+
let {step, pageSize} = color.getChannelRange(channel);
|
|
69
62
|
return {
|
|
70
63
|
...sliderState,
|
|
71
64
|
value: color,
|
|
@@ -92,6 +85,8 @@ export function useColorSliderState(props: ColorSliderStateOptions): ColorSlider
|
|
|
92
85
|
},
|
|
93
86
|
getThumbValueLabel() {
|
|
94
87
|
return color.formatChannelValue(channel, locale);
|
|
95
|
-
}
|
|
88
|
+
},
|
|
89
|
+
step,
|
|
90
|
+
pageSize
|
|
96
91
|
};
|
|
97
92
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import {Color, ColorWheelProps} from '@react-types/color';
|
|
14
|
-
import {parseColor} from './Color';
|
|
14
|
+
import {normalizeColor, parseColor} from './Color';
|
|
15
15
|
import {useControlledState} from '@react-stately/utils';
|
|
16
16
|
import {useRef, useState} from 'react';
|
|
17
17
|
|
|
@@ -32,24 +32,18 @@ export interface ColorWheelState {
|
|
|
32
32
|
getThumbPosition(radius: number): {x: number, y: number},
|
|
33
33
|
|
|
34
34
|
/** Increments the hue by the given amount (defaults to 1). */
|
|
35
|
-
increment(
|
|
35
|
+
increment(stepSize?: number): void,
|
|
36
36
|
/** Decrements the hue by the given amount (defaults to 1). */
|
|
37
|
-
decrement(
|
|
37
|
+
decrement(stepSize?: number): void,
|
|
38
38
|
|
|
39
39
|
/** Whether the color wheel is currently being dragged. */
|
|
40
40
|
readonly isDragging: boolean,
|
|
41
41
|
/** Sets whether the color wheel is being dragged. */
|
|
42
42
|
setDragging(value: boolean): void,
|
|
43
43
|
/** Returns the color that should be displayed in the color wheel instead of `value`. */
|
|
44
|
-
getDisplayColor(): Color
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
function normalizeColor(v: string | Color) {
|
|
48
|
-
if (typeof v === 'string') {
|
|
49
|
-
return parseColor(v);
|
|
50
|
-
} else {
|
|
51
|
-
return v;
|
|
52
|
-
}
|
|
44
|
+
getDisplayColor(): Color,
|
|
45
|
+
step: number,
|
|
46
|
+
pageStep: number
|
|
53
47
|
}
|
|
54
48
|
|
|
55
49
|
const DEFAULT_COLOR = parseColor('hsl(0, 100%, 50%)');
|
|
@@ -97,7 +91,7 @@ function cartesianToAngle(x: number, y: number, radius: number): number {
|
|
|
97
91
|
* Color wheels allow users to adjust the hue of an HSL or HSB color value on a circular track.
|
|
98
92
|
*/
|
|
99
93
|
export function useColorWheelState(props: ColorWheelProps): ColorWheelState {
|
|
100
|
-
let {defaultValue, onChange, onChangeEnd
|
|
94
|
+
let {defaultValue, onChange, onChangeEnd} = props;
|
|
101
95
|
|
|
102
96
|
if (!props.value && !defaultValue) {
|
|
103
97
|
defaultValue = DEFAULT_COLOR;
|
|
@@ -107,7 +101,10 @@ export function useColorWheelState(props: ColorWheelProps): ColorWheelState {
|
|
|
107
101
|
let valueRef = useRef(value);
|
|
108
102
|
valueRef.current = value;
|
|
109
103
|
|
|
104
|
+
let channelRange = value.getChannelRange('hue');
|
|
105
|
+
let {minValue: minValueX, maxValue: maxValueX, step: step, pageSize: pageStep} = channelRange;
|
|
110
106
|
let [isDragging, setDragging] = useState(false);
|
|
107
|
+
let isDraggingRef = useRef(false).current;
|
|
111
108
|
|
|
112
109
|
let hue = value.getChannelValue('hue');
|
|
113
110
|
function setHue(v: number) {
|
|
@@ -117,12 +114,16 @@ export function useColorWheelState(props: ColorWheelProps): ColorWheelState {
|
|
|
117
114
|
}
|
|
118
115
|
v = roundToStep(mod(v, 360), step);
|
|
119
116
|
if (hue !== v) {
|
|
120
|
-
|
|
117
|
+
let color = value.withChannelValue('hue', v);
|
|
118
|
+
valueRef.current = color;
|
|
119
|
+
setValue(color);
|
|
121
120
|
}
|
|
122
121
|
}
|
|
123
122
|
|
|
124
123
|
return {
|
|
125
124
|
value,
|
|
125
|
+
step,
|
|
126
|
+
pageStep,
|
|
126
127
|
setValue(v) {
|
|
127
128
|
let color = normalizeColor(v);
|
|
128
129
|
valueRef.current = color;
|
|
@@ -136,36 +137,38 @@ export function useColorWheelState(props: ColorWheelProps): ColorWheelState {
|
|
|
136
137
|
getThumbPosition(radius) {
|
|
137
138
|
return angleToCartesian(value.getChannelValue('hue'), radius);
|
|
138
139
|
},
|
|
139
|
-
increment(
|
|
140
|
-
let
|
|
141
|
-
|
|
140
|
+
increment(stepSize = 1) {
|
|
141
|
+
let s = Math.max(stepSize, step);
|
|
142
|
+
let newValue = hue + s;
|
|
143
|
+
if (newValue >= maxValueX) {
|
|
142
144
|
// Make sure you can always get back to 0.
|
|
143
|
-
newValue =
|
|
145
|
+
newValue = minValueX;
|
|
144
146
|
}
|
|
145
|
-
setHue(newValue);
|
|
147
|
+
setHue(roundToStep(mod(newValue, 360), s));
|
|
146
148
|
},
|
|
147
|
-
decrement(
|
|
148
|
-
let s = Math.max(
|
|
149
|
+
decrement(stepSize = 1) {
|
|
150
|
+
let s = Math.max(stepSize, step);
|
|
149
151
|
if (hue === 0) {
|
|
150
152
|
// We can't just subtract step because this might be the case:
|
|
151
153
|
// |(previous step) - 0| < step size
|
|
152
154
|
setHue(roundDown(360 / s) * s);
|
|
153
155
|
} else {
|
|
154
|
-
setHue(hue - s);
|
|
156
|
+
setHue(roundToStep(mod(hue - s, 360), s));
|
|
155
157
|
}
|
|
156
158
|
},
|
|
157
159
|
setDragging(isDragging) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
160
|
+
let wasDragging = isDraggingRef;
|
|
161
|
+
isDraggingRef = isDragging;
|
|
162
|
+
|
|
163
|
+
if (onChangeEnd && !isDragging && wasDragging) {
|
|
164
|
+
onChangeEnd(valueRef.current);
|
|
165
|
+
}
|
|
162
166
|
|
|
163
|
-
|
|
164
|
-
});
|
|
167
|
+
setDragging(isDragging);
|
|
165
168
|
},
|
|
166
169
|
isDragging,
|
|
167
170
|
getDisplayColor() {
|
|
168
|
-
return value.withChannelValue('saturation', 100).withChannelValue('lightness', 50);
|
|
171
|
+
return value.toFormat('hsl').withChannelValue('saturation', 100).withChannelValue('lightness', 50);
|
|
169
172
|
}
|
|
170
173
|
};
|
|
171
174
|
}
|