@react-stately/color 3.0.0-beta.4 → 3.0.0-beta.5

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/src/Color.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import {clamp} from '@react-aria/utils';
13
+ import {clamp, toFixedNumber} from '@react-stately/utils';
14
14
  import {ColorChannel, ColorChannelRange, ColorFormat, Color as IColor} from '@react-types/color';
15
15
  // @ts-ignore
16
16
  import intlMessages from '../intl/*.json';
@@ -32,7 +32,7 @@ export function parseColor(value: string): IColor {
32
32
  abstract class Color implements IColor {
33
33
  abstract toFormat(format: ColorFormat): IColor;
34
34
  abstract toString(format: ColorFormat | 'css'): string;
35
- abstract clone(): Color;
35
+ abstract clone(): IColor;
36
36
  abstract getChannelRange(channel: ColorChannel): ColorChannelRange;
37
37
  abstract formatChannelValue(channel: ColorChannel, locale: string): string;
38
38
 
@@ -61,6 +61,8 @@ abstract class Color implements IColor {
61
61
  getChannelName(channel: ColorChannel, locale: string) {
62
62
  return messages.getStringForLocale(channel, locale);
63
63
  }
64
+
65
+ abstract getColorSpace(): ColorFormat
64
66
  }
65
67
 
66
68
  const HEX_REGEX = /^#(?:([0-9a-f]{3})|([0-9a-f]{6}))$/i;
@@ -90,7 +92,9 @@ class RGBColor extends Color {
90
92
  let b = parseInt(m[2][4] + m[2][5], 16);
91
93
  return new RGBColor(r, g, b, 1);
92
94
  }
93
- } if ((m = value.match(RGB_REGEX))) {
95
+ }
96
+
97
+ if ((m = value.match(RGB_REGEX))) {
94
98
  const [r, g, b, a] = (m[1] ?? m[2]).split(',').map(n => Number(n.trim()));
95
99
  return new RGBColor(clamp(r, 0, 255), clamp(g, 0, 255), clamp(b, 0, 255), clamp(a ?? 1, 0, 1));
96
100
  }
@@ -119,6 +123,12 @@ class RGBColor extends Color {
119
123
  case 'rgb':
120
124
  case 'rgba':
121
125
  return this;
126
+ case 'hsb':
127
+ case 'hsba':
128
+ return this.toHSB();
129
+ case 'hsl':
130
+ case 'hsla':
131
+ return this.toHSL();
122
132
  default:
123
133
  throw new Error('Unsupported color conversion: rgb -> ' + format);
124
134
  }
@@ -128,7 +138,89 @@ class RGBColor extends Color {
128
138
  return this.red << 16 | this.green << 8 | this.blue;
129
139
  }
130
140
 
131
- clone(): Color {
141
+ /**
142
+ * Converts an RGB color value to HSB.
143
+ * Conversion formula adapted from https://en.wikipedia.org/wiki/HSL_and_HSV#From_RGB.
144
+ * @returns An HSBColor object.
145
+ */
146
+ private toHSB(): IColor {
147
+ const red = this.red / 255;
148
+ const green = this.green / 255;
149
+ const blue = this.blue / 255;
150
+ const min = Math.min(red, green, blue);
151
+ const brightness = Math.max(red, green, blue);
152
+ const chroma = brightness - min;
153
+ const saturation = brightness === 0 ? 0 : chroma / brightness;
154
+ let hue = 0; // achromatic
155
+
156
+ if (chroma !== 0) {
157
+ switch (brightness) {
158
+ case red:
159
+ hue = (green - blue) / chroma + (green < blue ? 6 : 0);
160
+ break;
161
+ case green:
162
+ hue = (blue - red) / chroma + 2;
163
+ break;
164
+ case blue:
165
+ hue = (red - green) / chroma + 4;
166
+ break;
167
+ }
168
+
169
+ hue /= 6;
170
+ }
171
+
172
+ return new HSBColor(
173
+ toFixedNumber(hue * 360, 2),
174
+ toFixedNumber(saturation * 100, 2),
175
+ toFixedNumber(brightness * 100, 2),
176
+ this.alpha
177
+ );
178
+ }
179
+
180
+ /**
181
+ * Converts an RGB color value to HSL.
182
+ * Conversion formula adapted from https://en.wikipedia.org/wiki/HSL_and_HSV#From_RGB.
183
+ * @returns An HSLColor object.
184
+ */
185
+ private toHSL(): IColor {
186
+ const red = this.red / 255;
187
+ const green = this.green / 255;
188
+ const blue = this.blue / 255;
189
+ const min = Math.min(red, green, blue);
190
+ const max = Math.max(red, green, blue);
191
+ const lightness = (max + min) / 2;
192
+ const chroma = max - min;
193
+ let hue: number;
194
+ let saturation: number;
195
+
196
+ if (chroma === 0) {
197
+ hue = saturation = 0; // achromatic
198
+ } else {
199
+ saturation = chroma / (lightness < .5 ? max + min : 2 - max - min);
200
+
201
+ switch (max) {
202
+ case red:
203
+ hue = (green - blue) / chroma + (green < blue ? 6 : 0);
204
+ break;
205
+ case green:
206
+ hue = (blue - red) / chroma + 2;
207
+ break;
208
+ case blue:
209
+ hue = (red - green) / chroma + 4;
210
+ break;
211
+ }
212
+
213
+ hue /= 6;
214
+ }
215
+
216
+ return new HSLColor(
217
+ toFixedNumber(hue * 360, 2),
218
+ toFixedNumber(saturation * 100, 2),
219
+ toFixedNumber(lightness * 100, 2),
220
+ this.alpha);
221
+ }
222
+
223
+ clone(): IColor {
132
224
  return new RGBColor(this.red, this.green, this.blue, this.alpha);
133
225
  }
134
226
 
@@ -162,6 +254,10 @@ class RGBColor extends Color {
162
254
  }
163
255
  return new NumberFormatter(locale, options).format(value);
164
256
  }
257
+
258
+ getColorSpace(): ColorFormat {
259
+ return 'rgb';
260
+ }
165
261
  }
166
262
 
167
263
  // X = <negative/positive number with/without decimal places>
@@ -187,10 +283,14 @@ class HSBColor extends Color {
187
283
  switch (format) {
188
284
  case 'css':
189
285
  return this.toHSL().toString('css');
286
+ case 'hex':
287
+ return this.toRGB().toString('hex');
288
+ case 'hexa':
289
+ return this.toRGB().toString('hexa');
190
290
  case 'hsb':
191
- return `hsb(${this.hue}, ${this.saturation}%, ${this.brightness}%)`;
291
+ return `hsb(${this.hue}, ${toFixedNumber(this.saturation, 2)}%, ${toFixedNumber(this.brightness, 2)}%)`;
192
292
  case 'hsba':
193
- return `hsba(${this.hue}, ${this.saturation}%, ${this.brightness}%, ${this.alpha})`;
293
+ return `hsba(${this.hue}, ${toFixedNumber(this.saturation, 2)}%, ${toFixedNumber(this.brightness, 2)}%, ${this.alpha})`;
194
294
  default:
195
295
  return this.toFormat(format).toString(format);
196
296
  }
@@ -204,29 +304,52 @@ class HSBColor extends Color {
204
304
  case 'hsl':
205
305
  case 'hsla':
206
306
  return this.toHSL();
307
+ case 'rgb':
308
+ case 'rgba':
309
+ return this.toRGB();
207
310
  default:
208
311
  throw new Error('Unsupported color conversion: hsb -> ' + format);
209
312
  }
210
313
  }
211
314
 
212
- private toHSL(): Color {
213
- // determine the lightness in the range [0,100]
214
- var l = (2 - this.saturation / 100) * this.brightness / 2;
315
+ /**
316
+ * Converts a HSB color to HSL.
317
+ * Conversion formula adapted from https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_HSL.
318
+ * @returns An HSLColor object.
319
+ */
320
+ private toHSL(): IColor {
321
+ let saturation = this.saturation / 100;
322
+ let brightness = this.brightness / 100;
323
+ let lightness = brightness * (1 - saturation / 2);
324
+ saturation = lightness === 0 || lightness === 1 ? 0 : (brightness - lightness) / Math.min(lightness, 1 - lightness);
325
+
326
+ return new HSLColor(
327
+ toFixedNumber(this.hue, 2),
328
+ toFixedNumber(saturation * 100, 2),
329
+ toFixedNumber(lightness * 100, 2),
330
+ this.alpha
331
+ );
332
+ }
215
333
 
216
- // store the HSL components
334
+ /**
335
+ * Converts a HSV color value to RGB.
336
+ * Conversion formula adapted from https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB_alternative.
337
+ * @returns An RGBColor object.
338
+ */
339
+ private toRGB(): IColor {
217
340
  let hue = this.hue;
218
- let saturation = this.saturation * this.brightness / (l < 50 ? l * 2 : 200 - l * 2);
219
- let lightness = l;
220
-
221
- // correct a division-by-zero error
222
- if (isNaN(saturation)) {
223
- saturation = 0;
224
- }
225
-
226
- return new HSLColor(hue, saturation, lightness, this.alpha);
341
+ let saturation = this.saturation / 100;
342
+ let brightness = this.brightness / 100;
343
+ let fn = (n: number, k = (n + hue / 60) % 6) => brightness - saturation * brightness * Math.max(Math.min(k, 4 - k, 1), 0);
344
+ return new RGBColor(
345
+ Math.round(fn(5) * 255),
346
+ Math.round(fn(3) * 255),
347
+ Math.round(fn(1) * 255),
348
+ this.alpha
349
+ );
227
350
  }
228
351
 
229
- clone(): Color {
352
+ clone(): IColor {
230
353
  return new HSBColor(this.hue, this.saturation, this.brightness, this.alpha);
231
354
  }
232
355
 
@@ -264,6 +387,10 @@ class HSBColor extends Color {
264
387
  }
265
388
  return new NumberFormatter(locale, options).format(value);
266
389
  }
390
+
391
+ getColorSpace(): ColorFormat {
392
+ return 'hsb';
393
+ }
267
394
  }
268
395
 
269
396
  // X = <negative/positive number with/without decimal places>
@@ -276,13 +403,11 @@ function mod(n, m) {
276
403
  return ((n % m) + m) % m;
277
404
  }
278
405
 
279
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
280
406
  class HSLColor extends Color {
281
407
  constructor(private hue: number, private saturation: number, private lightness: number, private alpha: number) {
282
408
  super();
283
409
  }
284
410
 
285
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
286
411
  static parse(value: string): HSLColor | void {
287
412
  let m: RegExpMatchArray | void;
288
413
  if ((m = value.match(HSL_REGEX))) {
@@ -293,27 +418,73 @@ class HSLColor extends Color {
293
418
 
294
419
  toString(format: ColorFormat | 'css') {
295
420
  switch (format) {
421
+ case 'hex':
422
+ return this.toRGB().toString('hex');
423
+ case 'hexa':
424
+ return this.toRGB().toString('hexa');
296
425
  case 'hsl':
297
- return `hsl(${this.hue}, ${this.saturation}%, ${this.lightness}%)`;
426
+ return `hsl(${this.hue}, ${toFixedNumber(this.saturation, 2)}%, ${toFixedNumber(this.lightness, 2)}%)`;
298
427
  case 'css':
299
428
  case 'hsla':
300
- return `hsla(${this.hue}, ${this.saturation}%, ${this.lightness}%, ${this.alpha})`;
429
+ return `hsla(${this.hue}, ${toFixedNumber(this.saturation, 2)}%, ${toFixedNumber(this.lightness, 2)}%, ${this.alpha})`;
301
430
  default:
302
431
  return this.toFormat(format).toString(format);
303
432
  }
304
433
  }
305
-
306
434
  toFormat(format: ColorFormat): IColor {
307
435
  switch (format) {
308
436
  case 'hsl':
309
437
  case 'hsla':
310
438
  return this;
439
+ case 'hsb':
440
+ case 'hsba':
441
+ return this.toHSB();
442
+ case 'rgb':
443
+ case 'rgba':
444
+ return this.toRGB();
311
445
  default:
312
446
  throw new Error('Unsupported color conversion: hsl -> ' + format);
313
447
  }
314
448
  }
315
449
 
316
- clone(): Color {
450
+ /**
451
+ * Converts a HSL color to HSB.
452
+ * Conversion formula adapted from https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_HSV.
453
+ * @returns An HSBColor object.
454
+ */
455
+ private toHSB(): IColor {
456
+ let saturation = this.saturation / 100;
457
+ let lightness = this.lightness / 100;
458
+ let brightness = lightness + saturation * Math.min(lightness, 1 - lightness);
459
+ saturation = brightness === 0 ? 0 : 2 * (1 - lightness / brightness);
460
+ return new HSBColor(
461
+ toFixedNumber(this.hue, 2),
462
+ toFixedNumber(saturation * 100, 2),
463
+ toFixedNumber(brightness * 100, 2),
464
+ this.alpha
465
+ );
466
+ }
467
+
468
+ /**
469
+ * Converts a HSL color to RGB.
470
+ * Conversion formula adapted from https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_RGB_alternative.
471
+ * @returns An RGBColor object.
472
+ */
473
+ private toRGB(): IColor {
474
+ let hue = this.hue;
475
+ let saturation = this.saturation / 100;
476
+ let lightness = this.lightness / 100;
477
+ let a = saturation * Math.min(lightness, 1 - lightness);
478
+ let fn = (n: number, k = (n + hue / 30) % 12) => lightness - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
479
+ return new RGBColor(
480
+ Math.round(fn(0) * 255),
481
+ Math.round(fn(8) * 255),
482
+ Math.round(fn(4) * 255),
483
+ this.alpha
484
+ );
485
+ }
486
+
487
+ clone(): IColor {
317
488
  return new HSLColor(this.hue, this.saturation, this.lightness, this.alpha);
318
489
  }
319
490
 
@@ -351,4 +522,8 @@ class HSLColor extends Color {
351
522
  }
352
523
  return new NumberFormatter(locale, options).format(value);
353
524
  }
525
+
526
+ getColorSpace(): ColorFormat {
527
+ return 'hsl';
528
+ }
354
529
  }
@@ -108,6 +108,7 @@ export function useColorWheelState(props: ColorWheelProps): ColorWheelState {
108
108
  valueRef.current = value;
109
109
 
110
110
  let [isDragging, setDragging] = useState(false);
111
+ let isDraggingRef = useRef(false).current;
111
112
 
112
113
  let hue = value.getChannelValue('hue');
113
114
  function setHue(v: number) {
@@ -117,7 +118,9 @@ export function useColorWheelState(props: ColorWheelProps): ColorWheelState {
117
118
  }
118
119
  v = roundToStep(mod(v, 360), step);
119
120
  if (hue !== v) {
120
- setValue(value.withChannelValue('hue', v));
121
+ let color = value.withChannelValue('hue', v);
122
+ valueRef.current = color;
123
+ setValue(color);
121
124
  }
122
125
  }
123
126
 
@@ -155,13 +158,14 @@ export function useColorWheelState(props: ColorWheelProps): ColorWheelState {
155
158
  }
156
159
  },
157
160
  setDragging(isDragging) {
158
- setDragging(wasDragging => {
159
- if (onChangeEnd && !isDragging && wasDragging) {
160
- onChangeEnd(valueRef.current);
161
- }
161
+ let wasDragging = isDraggingRef;
162
+ isDraggingRef = isDragging;
163
+
164
+ if (onChangeEnd && !isDragging && wasDragging) {
165
+ onChangeEnd(valueRef.current);
166
+ }
162
167
 
163
- return isDragging;
164
- });
168
+ setDragging(isDragging);
165
169
  },
166
170
  isDragging,
167
171
  getDisplayColor() {