@react-aria/color 3.0.0-beta.10 → 3.0.0-beta.13
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 +109 -92
- package/dist/main.js.map +1 -1
- package/dist/module.js +110 -93
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +16 -16
- package/src/useColorArea.ts +76 -44
package/src/useColorArea.ts
CHANGED
|
@@ -17,7 +17,7 @@ import {focusWithoutScrolling, isAndroid, isIOS, mergeProps, useGlobalListeners,
|
|
|
17
17
|
import intlMessages from '../intl/*.json';
|
|
18
18
|
import React, {ChangeEvent, HTMLAttributes, InputHTMLAttributes, RefObject, useCallback, useRef} from 'react';
|
|
19
19
|
import {useColorAreaGradient} from './useColorAreaGradient';
|
|
20
|
-
import {useKeyboard, useMove} from '@react-aria/interactions';
|
|
20
|
+
import {useFocus, useFocusWithin, useKeyboard, useMove} from '@react-aria/interactions';
|
|
21
21
|
import {useLocale, useMessageFormatter} from '@react-aria/i18n';
|
|
22
22
|
import {useVisuallyHidden} from '@react-aria/visually-hidden';
|
|
23
23
|
|
|
@@ -52,7 +52,8 @@ export function useColorArea(props: ColorAreaAriaProps, state: ColorAreaState):
|
|
|
52
52
|
isDisabled,
|
|
53
53
|
inputXRef,
|
|
54
54
|
inputYRef,
|
|
55
|
-
containerRef
|
|
55
|
+
containerRef,
|
|
56
|
+
'aria-label': ariaLabel
|
|
56
57
|
} = props;
|
|
57
58
|
let formatMessage = useMessageFormatter(intlMessages);
|
|
58
59
|
|
|
@@ -87,6 +88,7 @@ export function useColorArea(props: ColorAreaAriaProps, state: ColorAreaState):
|
|
|
87
88
|
e.preventDefault();
|
|
88
89
|
// remember to set this and unset it so that onChangeEnd is fired
|
|
89
90
|
stateRef.current.setDragging(true);
|
|
91
|
+
valueChangedViaKeyboard.current = true;
|
|
90
92
|
switch (e.key) {
|
|
91
93
|
case 'PageUp':
|
|
92
94
|
stateRef.current.incrementY(stateRef.current.yChannelPageStep);
|
|
@@ -108,7 +110,6 @@ export function useColorArea(props: ColorAreaAriaProps, state: ColorAreaState):
|
|
|
108
110
|
stateRef.current.setDragging(false);
|
|
109
111
|
if (focusedInputRef.current) {
|
|
110
112
|
focusInput(focusedInputRef.current ? focusedInputRef : inputXRef);
|
|
111
|
-
focusedInputRef.current = undefined;
|
|
112
113
|
}
|
|
113
114
|
}
|
|
114
115
|
});
|
|
@@ -135,6 +136,7 @@ export function useColorArea(props: ColorAreaAriaProps, state: ColorAreaState):
|
|
|
135
136
|
currentPosition.current = getThumbPosition();
|
|
136
137
|
}
|
|
137
138
|
let {width, height} = containerRef.current.getBoundingClientRect();
|
|
139
|
+
let valueChanged = deltaX !== 0 || deltaY !== 0;
|
|
138
140
|
if (pointerType === 'keyboard') {
|
|
139
141
|
let deltaXValue = shiftKey && xChannelPageStep > xChannelStep ? xChannelPageStep : xChannelStep;
|
|
140
142
|
let deltaYValue = shiftKey && yChannelPageStep > yChannelStep ? yChannelPageStep : yChannelStep;
|
|
@@ -147,8 +149,9 @@ export function useColorArea(props: ColorAreaAriaProps, state: ColorAreaState):
|
|
|
147
149
|
} else if (deltaY < 0) {
|
|
148
150
|
incrementY(deltaYValue);
|
|
149
151
|
}
|
|
152
|
+
valueChangedViaKeyboard.current = valueChanged;
|
|
150
153
|
// set the focused input based on which axis has the greater delta
|
|
151
|
-
focusedInputRef.current =
|
|
154
|
+
focusedInputRef.current = valueChanged && Math.abs(deltaY) > Math.abs(deltaX) ? inputYRef.current : inputXRef.current;
|
|
152
155
|
} else {
|
|
153
156
|
currentPosition.current.x += (direction === 'rtl' ? -1 : 1) * deltaX / width ;
|
|
154
157
|
currentPosition.current.y += deltaY / height;
|
|
@@ -159,11 +162,20 @@ export function useColorArea(props: ColorAreaAriaProps, state: ColorAreaState):
|
|
|
159
162
|
isOnColorArea.current = undefined;
|
|
160
163
|
stateRef.current.setDragging(false);
|
|
161
164
|
focusInput(focusedInputRef.current ? focusedInputRef : inputXRef);
|
|
162
|
-
focusedInputRef.current = undefined;
|
|
163
165
|
}
|
|
164
166
|
};
|
|
165
167
|
let {moveProps: movePropsThumb} = useMove(moveHandler);
|
|
166
168
|
|
|
169
|
+
let valueChangedViaKeyboard = useRef<boolean>(false);
|
|
170
|
+
let {focusWithinProps} = useFocusWithin({
|
|
171
|
+
onFocusWithinChange: (focusWithin:boolean) => {
|
|
172
|
+
if (!focusWithin) {
|
|
173
|
+
valueChangedViaKeyboard.current = false;
|
|
174
|
+
focusedInputRef.current === undefined;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
167
179
|
let currentPointer = useRef<number | null | undefined>(undefined);
|
|
168
180
|
let isOnColorArea = useRef<boolean>(false);
|
|
169
181
|
let {moveProps: movePropsContainer} = useMove({
|
|
@@ -187,6 +199,7 @@ export function useColorArea(props: ColorAreaAriaProps, state: ColorAreaState):
|
|
|
187
199
|
let onThumbDown = (id: number | null) => {
|
|
188
200
|
if (!state.isDragging) {
|
|
189
201
|
currentPointer.current = id;
|
|
202
|
+
valueChangedViaKeyboard.current = false;
|
|
190
203
|
focusInput();
|
|
191
204
|
state.setDragging(true);
|
|
192
205
|
if (typeof PointerEvent !== 'undefined') {
|
|
@@ -201,6 +214,7 @@ export function useColorArea(props: ColorAreaAriaProps, state: ColorAreaState):
|
|
|
201
214
|
let onThumbUp = (e) => {
|
|
202
215
|
let id = e.pointerId ?? e.changedTouches?.[0].identifier;
|
|
203
216
|
if (id === currentPointer.current) {
|
|
217
|
+
valueChangedViaKeyboard.current = false;
|
|
204
218
|
focusInput();
|
|
205
219
|
state.setDragging(false);
|
|
206
220
|
currentPointer.current = undefined;
|
|
@@ -225,6 +239,7 @@ export function useColorArea(props: ColorAreaAriaProps, state: ColorAreaState):
|
|
|
225
239
|
}
|
|
226
240
|
if (x >= 0 && x <= 1 && y >= 0 && y <= 1 && !state.isDragging && currentPointer.current === undefined) {
|
|
227
241
|
isOnColorArea.current = true;
|
|
242
|
+
valueChangedViaKeyboard.current = false;
|
|
228
243
|
currentPointer.current = id;
|
|
229
244
|
state.setColorFromPoint(x, y);
|
|
230
245
|
|
|
@@ -244,6 +259,7 @@ export function useColorArea(props: ColorAreaAriaProps, state: ColorAreaState):
|
|
|
244
259
|
let id = e.pointerId ?? e.changedTouches?.[0].identifier;
|
|
245
260
|
if (isOnColorArea.current && id === currentPointer.current) {
|
|
246
261
|
isOnColorArea.current = false;
|
|
262
|
+
valueChangedViaKeyboard.current = false;
|
|
247
263
|
currentPointer.current = undefined;
|
|
248
264
|
state.setDragging(false);
|
|
249
265
|
focusInput();
|
|
@@ -295,34 +311,55 @@ export function useColorArea(props: ColorAreaAriaProps, state: ColorAreaState):
|
|
|
295
311
|
onThumbDown(e.changedTouches[0].identifier);
|
|
296
312
|
}
|
|
297
313
|
})
|
|
298
|
-
}, keyboardProps, movePropsThumb);
|
|
314
|
+
}, focusWithinProps, keyboardProps, movePropsThumb);
|
|
315
|
+
|
|
316
|
+
let {focusProps: xInputFocusProps} = useFocus({
|
|
317
|
+
onFocus: () => {
|
|
318
|
+
focusedInputRef.current = inputXRef.current;
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
let {focusProps: yInputFocusProps} = useFocus({
|
|
323
|
+
onFocus: () => {
|
|
324
|
+
focusedInputRef.current = inputYRef.current;
|
|
325
|
+
}
|
|
326
|
+
});
|
|
299
327
|
|
|
300
328
|
let isMobile = isIOS() || isAndroid();
|
|
301
329
|
|
|
330
|
+
function getAriaValueTextForChannel(channel:ColorChannel) {
|
|
331
|
+
return (
|
|
332
|
+
valueChangedViaKeyboard.current ?
|
|
333
|
+
formatMessage('colorNameAndValue', {name: state.value.getChannelName(channel, locale), value: state.value.formatChannelValue(channel, locale)})
|
|
334
|
+
:
|
|
335
|
+
[
|
|
336
|
+
formatMessage('colorNameAndValue', {name: state.value.getChannelName(channel, locale), value: state.value.formatChannelValue(channel, locale)}),
|
|
337
|
+
formatMessage('colorNameAndValue', {name: state.value.getChannelName(channel === yChannel ? xChannel : yChannel, locale), value: state.value.formatChannelValue(channel === yChannel ? xChannel : yChannel, locale)})
|
|
338
|
+
].join(', ')
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
let colorPickerLabel = formatMessage('colorPicker');
|
|
343
|
+
|
|
302
344
|
let xInputLabellingProps = useLabels({
|
|
303
345
|
...props,
|
|
304
|
-
'aria-label':
|
|
346
|
+
'aria-label': ariaLabel ? formatMessage('colorInputLabel', {label: ariaLabel, channelLabel: colorPickerLabel}) : colorPickerLabel
|
|
305
347
|
});
|
|
306
348
|
|
|
307
349
|
let yInputLabellingProps = useLabels({
|
|
308
350
|
...props,
|
|
309
|
-
'aria-label':
|
|
351
|
+
'aria-label': ariaLabel ? formatMessage('colorInputLabel', {label: ariaLabel, channelLabel: colorPickerLabel}) : colorPickerLabel
|
|
310
352
|
});
|
|
311
353
|
|
|
312
|
-
let colorAriaLabellingProps = useLabels(
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
formatMessage('colorNameAndValue', {name: state.value.getChannelName(channel, locale), value: state.value.formatChannelValue(channel, locale)})
|
|
320
|
-
)
|
|
321
|
-
);
|
|
322
|
-
return colorNamesAndValues.length ? colorNamesAndValues.join(', ') : null;
|
|
323
|
-
};
|
|
354
|
+
let colorAriaLabellingProps = useLabels(
|
|
355
|
+
{
|
|
356
|
+
...props,
|
|
357
|
+
'aria-label': ariaLabel ? `${ariaLabel} ${colorPickerLabel}` : undefined
|
|
358
|
+
},
|
|
359
|
+
isMobile ? colorPickerLabel : undefined
|
|
360
|
+
);
|
|
324
361
|
|
|
325
|
-
let ariaRoleDescription =
|
|
362
|
+
let ariaRoleDescription = formatMessage('twoDimensionalSlider');
|
|
326
363
|
|
|
327
364
|
let {visuallyHiddenProps} = useVisuallyHidden({style: {
|
|
328
365
|
opacity: '0.0001',
|
|
@@ -343,7 +380,6 @@ export function useColorArea(props: ColorAreaAriaProps, state: ColorAreaState):
|
|
|
343
380
|
isDisabled: props.isDisabled
|
|
344
381
|
});
|
|
345
382
|
|
|
346
|
-
|
|
347
383
|
return {
|
|
348
384
|
colorAreaProps: {
|
|
349
385
|
...colorAriaLabellingProps,
|
|
@@ -363,24 +399,22 @@ export function useColorArea(props: ColorAreaAriaProps, state: ColorAreaState):
|
|
|
363
399
|
xInputProps: {
|
|
364
400
|
...xInputLabellingProps,
|
|
365
401
|
...visuallyHiddenProps,
|
|
402
|
+
...xInputFocusProps,
|
|
366
403
|
type: 'range',
|
|
367
404
|
min: state.value.getChannelRange(xChannel).minValue,
|
|
368
405
|
max: state.value.getChannelRange(xChannel).maxValue,
|
|
369
406
|
step: xChannelStep,
|
|
370
407
|
'aria-roledescription': ariaRoleDescription,
|
|
371
|
-
'aria-valuetext': (
|
|
372
|
-
isMobile ?
|
|
373
|
-
formatMessage('colorNameAndValue', {name: state.value.getChannelName(xChannel, locale), value: state.value.formatChannelValue(xChannel, locale)})
|
|
374
|
-
:
|
|
375
|
-
[
|
|
376
|
-
formatMessage('colorNameAndValue', {name: state.value.getChannelName(xChannel, locale), value: state.value.formatChannelValue(xChannel, locale)}),
|
|
377
|
-
formatMessage('colorNameAndValue', {name: state.value.getChannelName(yChannel, locale), value: state.value.formatChannelValue(yChannel, locale)})
|
|
378
|
-
].join(', ')
|
|
379
|
-
),
|
|
380
|
-
title: getValueTitle(),
|
|
408
|
+
'aria-valuetext': getAriaValueTextForChannel(xChannel),
|
|
381
409
|
disabled: isDisabled,
|
|
382
410
|
value: state.value.getChannelValue(xChannel),
|
|
383
|
-
tabIndex:
|
|
411
|
+
tabIndex: (isMobile || !focusedInputRef.current || focusedInputRef.current === inputXRef.current ? undefined : -1),
|
|
412
|
+
/*
|
|
413
|
+
So that only a single "2d slider" control shows up when listing form elements for screen readers,
|
|
414
|
+
add aria-hidden="true" to the unfocused control when the value has not changed via the keyboard,
|
|
415
|
+
but remove aria-hidden to reveal the input for each channel when the value has changed with the keyboard.
|
|
416
|
+
*/
|
|
417
|
+
'aria-hidden': (!isMobile && focusedInputRef.current === inputYRef.current && !valueChangedViaKeyboard.current ? 'true' : undefined),
|
|
384
418
|
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
|
385
419
|
state.setXValue(parseFloat(e.target.value));
|
|
386
420
|
}
|
|
@@ -388,25 +422,23 @@ export function useColorArea(props: ColorAreaAriaProps, state: ColorAreaState):
|
|
|
388
422
|
yInputProps: {
|
|
389
423
|
...yInputLabellingProps,
|
|
390
424
|
...visuallyHiddenProps,
|
|
425
|
+
...yInputFocusProps,
|
|
391
426
|
type: 'range',
|
|
392
427
|
min: state.value.getChannelRange(yChannel).minValue,
|
|
393
428
|
max: state.value.getChannelRange(yChannel).maxValue,
|
|
394
429
|
step: yChannelStep,
|
|
395
430
|
'aria-roledescription': ariaRoleDescription,
|
|
396
|
-
'aria-valuetext': (
|
|
397
|
-
isMobile ?
|
|
398
|
-
formatMessage('colorNameAndValue', {name: state.value.getChannelName(yChannel, locale), value: state.value.formatChannelValue(yChannel, locale)})
|
|
399
|
-
:
|
|
400
|
-
[
|
|
401
|
-
formatMessage('colorNameAndValue', {name: state.value.getChannelName(yChannel, locale), value: state.value.formatChannelValue(yChannel, locale)}),
|
|
402
|
-
formatMessage('colorNameAndValue', {name: state.value.getChannelName(xChannel, locale), value: state.value.formatChannelValue(xChannel, locale)})
|
|
403
|
-
].join(', ')
|
|
404
|
-
),
|
|
431
|
+
'aria-valuetext': getAriaValueTextForChannel(yChannel),
|
|
405
432
|
'aria-orientation': 'vertical',
|
|
406
|
-
title: getValueTitle(),
|
|
407
433
|
disabled: isDisabled,
|
|
408
434
|
value: state.value.getChannelValue(yChannel),
|
|
409
|
-
tabIndex: -1,
|
|
435
|
+
tabIndex: (isMobile || focusedInputRef.current === inputYRef.current ? undefined : -1),
|
|
436
|
+
/*
|
|
437
|
+
So that only a single "2d slider" control shows up when listing form elements for screen readers,
|
|
438
|
+
add aria-hidden="true" to the unfocused input when the value has not changed via the keyboard,
|
|
439
|
+
but remove aria-hidden to reveal the input for each channel when the value has changed with the keyboard.
|
|
440
|
+
*/
|
|
441
|
+
'aria-hidden': (isMobile || focusedInputRef.current === inputYRef.current || valueChangedViaKeyboard.current ? undefined : 'true'),
|
|
410
442
|
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
|
411
443
|
state.setYValue(parseFloat(e.target.value));
|
|
412
444
|
}
|