@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.
@@ -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 = (deltaX !== 0 || deltaY !== 0) && Math.abs(deltaY) > Math.abs(deltaX) ? inputYRef.current : inputXRef.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': isMobile ? state.value.getChannelName(xChannel, locale) : formatMessage('x/y', {x: state.value.getChannelName(xChannel, locale), y: state.value.getChannelName(yChannel, locale)})
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': isMobile ? state.value.getChannelName(yChannel, locale) : formatMessage('x/y', {x: state.value.getChannelName(xChannel, locale), y: state.value.getChannelName(yChannel, locale)})
351
+ 'aria-label': ariaLabel ? formatMessage('colorInputLabel', {label: ariaLabel, channelLabel: colorPickerLabel}) : colorPickerLabel
310
352
  });
311
353
 
312
- let colorAriaLabellingProps = useLabels(props);
313
-
314
- let getValueTitle = () => {
315
- const channels: [ColorChannel, ColorChannel, ColorChannel] = state.value.getColorChannels();
316
- const colorNamesAndValues = [];
317
- channels.forEach(channel =>
318
- colorNamesAndValues.push(
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 = isMobile ? null : formatMessage('twoDimensionalSlider');
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: 0,
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
  }