@react-stately/color 3.0.0-alpha.0 → 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 +1020 -838
- package/dist/main.js.map +1 -1
- package/dist/module.js +1008 -811
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts +67 -6
- package/dist/types.d.ts.map +1 -1
- package/package.json +9 -9
- package/src/Color.ts +243 -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 +49 -30
|
@@ -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,14 +11,14 @@
|
|
|
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
|
-
import {useState} from 'react';
|
|
16
|
+
import {useRef, useState} from 'react';
|
|
17
17
|
|
|
18
18
|
export interface ColorWheelState {
|
|
19
|
-
/** The current color value
|
|
19
|
+
/** The current color value represented by the color wheel. */
|
|
20
20
|
readonly value: Color,
|
|
21
|
-
/** Sets the color value
|
|
21
|
+
/** Sets the color value represented by the color wheel, and triggers `onChange`. */
|
|
22
22
|
setValue(value: string | Color): void,
|
|
23
23
|
|
|
24
24
|
/** The current value of the hue channel displayed by the color wheel. */
|
|
@@ -32,22 +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
|
-
/** Whether the
|
|
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
|
-
setDragging(value: boolean): void
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return parseColor(v);
|
|
48
|
-
} else {
|
|
49
|
-
return v;
|
|
50
|
-
}
|
|
42
|
+
setDragging(value: boolean): void,
|
|
43
|
+
/** Returns the color that should be displayed in the color wheel instead of `value`. */
|
|
44
|
+
getDisplayColor(): Color,
|
|
45
|
+
step: number,
|
|
46
|
+
pageStep: number
|
|
51
47
|
}
|
|
52
48
|
|
|
53
49
|
const DEFAULT_COLOR = parseColor('hsl(0, 100%, 50%)');
|
|
@@ -95,14 +91,20 @@ function cartesianToAngle(x: number, y: number, radius: number): number {
|
|
|
95
91
|
* Color wheels allow users to adjust the hue of an HSL or HSB color value on a circular track.
|
|
96
92
|
*/
|
|
97
93
|
export function useColorWheelState(props: ColorWheelProps): ColorWheelState {
|
|
98
|
-
let {defaultValue, onChange,
|
|
94
|
+
let {defaultValue, onChange, onChangeEnd} = props;
|
|
99
95
|
|
|
100
96
|
if (!props.value && !defaultValue) {
|
|
101
97
|
defaultValue = DEFAULT_COLOR;
|
|
102
98
|
}
|
|
103
99
|
|
|
104
100
|
let [value, setValue] = useControlledState(normalizeColor(props.value), normalizeColor(defaultValue), onChange);
|
|
101
|
+
let valueRef = useRef(value);
|
|
102
|
+
valueRef.current = value;
|
|
103
|
+
|
|
104
|
+
let channelRange = value.getChannelRange('hue');
|
|
105
|
+
let {minValue: minValueX, maxValue: maxValueX, step: step, pageSize: pageStep} = channelRange;
|
|
105
106
|
let [isDragging, setDragging] = useState(false);
|
|
107
|
+
let isDraggingRef = useRef(false).current;
|
|
106
108
|
|
|
107
109
|
let hue = value.getChannelValue('hue');
|
|
108
110
|
function setHue(v: number) {
|
|
@@ -112,14 +114,20 @@ export function useColorWheelState(props: ColorWheelProps): ColorWheelState {
|
|
|
112
114
|
}
|
|
113
115
|
v = roundToStep(mod(v, 360), step);
|
|
114
116
|
if (hue !== v) {
|
|
115
|
-
|
|
117
|
+
let color = value.withChannelValue('hue', v);
|
|
118
|
+
valueRef.current = color;
|
|
119
|
+
setValue(color);
|
|
116
120
|
}
|
|
117
121
|
}
|
|
118
122
|
|
|
119
123
|
return {
|
|
120
124
|
value,
|
|
125
|
+
step,
|
|
126
|
+
pageStep,
|
|
121
127
|
setValue(v) {
|
|
122
|
-
|
|
128
|
+
let color = normalizeColor(v);
|
|
129
|
+
valueRef.current = color;
|
|
130
|
+
setValue(color);
|
|
123
131
|
},
|
|
124
132
|
hue,
|
|
125
133
|
setHue,
|
|
@@ -129,27 +137,38 @@ export function useColorWheelState(props: ColorWheelProps): ColorWheelState {
|
|
|
129
137
|
getThumbPosition(radius) {
|
|
130
138
|
return angleToCartesian(value.getChannelValue('hue'), radius);
|
|
131
139
|
},
|
|
132
|
-
increment(
|
|
133
|
-
let
|
|
134
|
-
|
|
140
|
+
increment(stepSize = 1) {
|
|
141
|
+
let s = Math.max(stepSize, step);
|
|
142
|
+
let newValue = hue + s;
|
|
143
|
+
if (newValue >= maxValueX) {
|
|
135
144
|
// Make sure you can always get back to 0.
|
|
136
|
-
newValue =
|
|
145
|
+
newValue = minValueX;
|
|
137
146
|
}
|
|
138
|
-
setHue(newValue);
|
|
147
|
+
setHue(roundToStep(mod(newValue, 360), s));
|
|
139
148
|
},
|
|
140
|
-
decrement(
|
|
141
|
-
let s = Math.max(
|
|
149
|
+
decrement(stepSize = 1) {
|
|
150
|
+
let s = Math.max(stepSize, step);
|
|
142
151
|
if (hue === 0) {
|
|
143
152
|
// We can't just subtract step because this might be the case:
|
|
144
153
|
// |(previous step) - 0| < step size
|
|
145
154
|
setHue(roundDown(360 / s) * s);
|
|
146
155
|
} else {
|
|
147
|
-
setHue(hue - s);
|
|
156
|
+
setHue(roundToStep(mod(hue - s, 360), s));
|
|
148
157
|
}
|
|
149
158
|
},
|
|
150
|
-
setDragging(
|
|
151
|
-
|
|
159
|
+
setDragging(isDragging) {
|
|
160
|
+
let wasDragging = isDraggingRef;
|
|
161
|
+
isDraggingRef = isDragging;
|
|
162
|
+
|
|
163
|
+
if (onChangeEnd && !isDragging && wasDragging) {
|
|
164
|
+
onChangeEnd(valueRef.current);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
setDragging(isDragging);
|
|
152
168
|
},
|
|
153
|
-
isDragging
|
|
169
|
+
isDragging,
|
|
170
|
+
getDisplayColor() {
|
|
171
|
+
return value.toFormat('hsl').withChannelValue('saturation', 100).withChannelValue('lightness', 50);
|
|
172
|
+
}
|
|
154
173
|
};
|
|
155
174
|
}
|