Skip to content
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/@adobe/react-spectrum/src/actionbar/ActionBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,10 @@ function ActionBarInner<T>(props: ActionBarInnerProps<T>, ref: Ref<HTMLDivElemen
}

let {keyboardProps} = useKeyboard({
onKeyDown(e) {
if (e.key === 'Escape') {
e.preventDefault();
shortcuts: {
'Escape': () => {
onClearSelection();
return true;
}
}
});
Expand Down
8 changes: 3 additions & 5 deletions packages/@react-spectrum/s2/src/ActionBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,12 +136,10 @@ const ActionBarInner = forwardRef(function ActionBarInner(props: ActionBarProps
});

let {keyboardProps} = useKeyboard({
onKeyDown(e) {
if (e.key === 'Escape') {
e.preventDefault();
shortcuts: {
'Escape': () => {
onClearSelection?.();
} else {
e.continuePropagation();
return true;
}
}
});
Expand Down
6 changes: 4 additions & 2 deletions packages/react-aria-components/test/Calendar.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,10 @@ describe('Calendar', () => {
expect(grids).toHaveLength(2);

let formatter = new Intl.DateTimeFormat('en-US', {month: 'long', year: 'numeric'});
expect(grids[0]).toHaveAttribute('aria-label', 'Appointment date, ' + formatter.format(new Date()));
expect(grids[1]).toHaveAttribute('aria-label', 'Appointment date, ' + formatter.format(today(getLocalTimeZone()).add({months: 1}).toDate(getLocalTimeZone())));
let firstMonth = new CalendarDate(2026, 4, 1);
let tz = getLocalTimeZone();
expect(grids[0]).toHaveAttribute('aria-label', 'Appointment date, ' + formatter.format(firstMonth.toDate(tz)));
expect(grids[1]).toHaveAttribute('aria-label', 'Appointment date, ' + formatter.format(firstMonth.add({months: 1}).toDate(tz)));

let headings = container.querySelectorAll('.react-aria-CalendarHeading');
expect(headings).toHaveLength(2);
Expand Down
37 changes: 37 additions & 0 deletions packages/react-aria-components/test/ListBox.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2001,3 +2001,40 @@ describe('ListBox', () => {
});
}
});

describe('keyboard modifier keys', () => {
let user;
let platformMock;
beforeAll(() => {
user = userEvent.setup({delay: null, pointerMap});
});
// selectionMode: 'none', 'single', 'multiple'
// selectionBehavior: 'toggle', 'replace'
// platform: 'mac', 'windows'

// modifier key: 'alt', 'ctrl', 'meta', 'shift'
// key: 'arrow-up', 'arrow-down', 'arrow-left', 'arrow-right', 'home', 'end', 'page-up', 'page-down', 'enter', 'space', 'tab'
// expected behavior: 'navigate', 'select', 'toggle', 'replace'
describe('mac', () => {
beforeAll(() => {
platformMock = jest.spyOn(navigator, 'platform', 'get').mockImplementation(() => 'Mac');
});
afterAll(() => {
platformMock.mockRestore();
});
it('should not navigate when using unsupported modifier keys', async () => {
let {getByRole} = renderListbox({selectionMode: 'none'});
await user.tab();
let listbox = getByRole('listbox');
let options = within(listbox).getAllByRole('option');
await user.keyboard('{ArrowDown}');
expect(document.activeElement).toBe(options[1]);
await user.keyboard('{Meta>}{ArrowDown}{/Meta}');
expect(document.activeElement).toBe(options[1]);
await user.keyboard('{Meta>}{ArrowUp}{/Meta}');
expect(document.activeElement).toBe(options[1]);
await user.keyboard('{Control>}{Home}{/Control}');
expect(document.activeElement).toBe(options[1]);
});
});
});
6 changes: 4 additions & 2 deletions packages/react-aria-components/test/RangeCalendar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -212,8 +212,10 @@ describe('RangeCalendar', () => {
expect(grids).toHaveLength(2);

let formatter = new Intl.DateTimeFormat('en-US', {month: 'long', year: 'numeric'});
expect(grids[0]).toHaveAttribute('aria-label', 'Trip dates, ' + formatter.format(new Date()));
expect(grids[1]).toHaveAttribute('aria-label', 'Trip dates, ' + formatter.format(today(getLocalTimeZone()).add({months: 1}).toDate(getLocalTimeZone())));
let firstMonth = new CalendarDate(2026, 4, 1);
let tz = getLocalTimeZone();
expect(grids[0]).toHaveAttribute('aria-label', 'Trip dates, ' + formatter.format(firstMonth.toDate(tz)));
expect(grids[1]).toHaveAttribute('aria-label', 'Trip dates, ' + formatter.format(firstMonth.add({months: 1}).toDate(tz)));

let headings = container.querySelectorAll('.react-aria-CalendarHeading');
expect(headings).toHaveLength(2);
Expand Down
55 changes: 34 additions & 21 deletions packages/react-aria/src/actiongroup/useActionGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ import {AriaLabelingProps, DOMAttributes, DOMProps, FocusableElement, ItemElemen
import {createFocusManager} from '../focus/FocusScope';
import {filterDOMProps} from '../utils/filterDOMProps';
import {getEventTarget, nodeContains} from '../utils/shadowdom/DOMFunctions';
import {KeyboardEventHandler, useState} from 'react';
import {ListState} from 'react-stately/useListState';
import {useKeyboard} from '../interactions/useKeyboard';
import {useLayoutEffect} from '../utils/useLayoutEffect';
import {useLocale} from '../i18n/I18nProvider';
import {useState} from 'react';

const BUTTON_GROUP_ROLES = {
'none': 'toolbar',
Expand Down Expand Up @@ -75,34 +76,46 @@ export function useActionGroup<T>(props: AriaActionGroupProps<T>, state: ListSta
let {direction} = useLocale();
let focusManager = createFocusManager(ref);
let flipDirection = direction === 'rtl' && orientation === 'horizontal';
let onKeyDown: KeyboardEventHandler = (e) => {
if (!nodeContains(e.currentTarget, getEventTarget(e))) {
return;
}

switch (e.key) {
case 'ArrowRight':
case 'ArrowDown':
e.preventDefault();
e.stopPropagation();
if (e.key === 'ArrowRight' && flipDirection) {
let {keyboardProps} = useKeyboard({
shortcuts: {
'ArrowRight': (e) => {
if (!nodeContains(e.currentTarget, getEventTarget(e))) {
return false;
}
if (flipDirection) {
focusManager.focusPrevious({wrap: true});
} else {
focusManager.focusNext({wrap: true});
}
break;
case 'ArrowLeft':
case 'ArrowUp':
e.preventDefault();
e.stopPropagation();
if (e.key === 'ArrowLeft' && flipDirection) {
return true;
},
'ArrowDown': (e) => {
if (!nodeContains(e.currentTarget, getEventTarget(e))) {
return false;
}
focusManager.focusNext({wrap: true});
return true;
},
'ArrowLeft': (e) => {
if (!nodeContains(e.currentTarget, getEventTarget(e))) {
return false;
}
if (flipDirection) {
focusManager.focusNext({wrap: true});
} else {
focusManager.focusPrevious({wrap: true});
}
break;
return true;
},
'ArrowUp': (e) => {
if (!nodeContains(e.currentTarget, getEventTarget(e))) {
return false;
}
focusManager.focusPrevious({wrap: true});
return true;
}
}
};
});

let role: string | undefined = BUTTON_GROUP_ROLES[state.selectionManager.selectionMode];
if (isInToolbar && role === 'toolbar') {
Expand All @@ -114,7 +127,7 @@ export function useActionGroup<T>(props: AriaActionGroupProps<T>, state: ListSta
role,
'aria-orientation': role === 'toolbar' ? orientation : undefined,
'aria-disabled': isDisabled,
onKeyDown
...keyboardProps
}
};
}
96 changes: 50 additions & 46 deletions packages/react-aria/src/calendar/useCalendarGrid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ import {CalendarDate, startOfWeek, today} from '@internationalized/date';
import {CalendarSelectionMode, CalendarState} from 'react-stately/useCalendarState';
import {DOMAttributes} from '@react-types/shared';
import {hookData, useVisibleRangeDescription} from './utils';
import {KeyboardEvent, useMemo} from 'react';
import {mergeProps} from '../utils/mergeProps';
import {RangeCalendarState} from 'react-stately/useRangeCalendarState';
import {useDateFormatter} from '../i18n/useDateFormatter';
import {useKeyboard} from '../interactions/useKeyboard';
import {useLabels} from '../utils/useLabels';
import {useLocale} from '../i18n/I18nProvider';
import {useMemo} from 'react';

export interface AriaCalendarGridProps {
/**
Expand Down Expand Up @@ -71,70 +72,73 @@ export function useCalendarGrid(props: AriaCalendarGridProps, state: CalendarSta

let {direction} = useLocale();

let onKeyDown = (e: KeyboardEvent) => {
switch (e.key) {
case 'Enter':
case ' ':
e.preventDefault();
let {keyboardProps} = useKeyboard({
shortcuts: {
'Enter': () => {
state.selectFocusedDate();
break;
case 'PageUp':
e.preventDefault();
e.stopPropagation();
state.focusPreviousSection(e.shiftKey);
break;
case 'PageDown':
e.preventDefault();
e.stopPropagation();
state.focusNextSection(e.shiftKey);
break;
case 'End':
e.preventDefault();
e.stopPropagation();
return true;
},
' ': () => {
state.selectFocusedDate();
return true;
},
'PageUp': () => {
state.focusPreviousSection();
return true;
},
'Shift+PageUp': () => {
state.focusPreviousSection(true);
return true;
},
'PageDown': () => {
state.focusNextSection();
return true;
},
'Shift+PageDown': () => {
state.focusNextSection(true);
return true;
},
'End': () => {
state.focusSectionEnd();
break;
case 'Home':
e.preventDefault();
e.stopPropagation();
return true;
},
'Home': () => {
state.focusSectionStart();
break;
case 'ArrowLeft':
e.preventDefault();
e.stopPropagation();
return true;
},
'ArrowLeft': () => {
if (direction === 'rtl') {
state.focusNextDay();
} else {
state.focusPreviousDay();
}
break;
case 'ArrowUp':
e.preventDefault();
e.stopPropagation();
return true;
},
'ArrowUp': () => {
state.focusPreviousRow();
break;
case 'ArrowRight':
e.preventDefault();
e.stopPropagation();
return true;
},
'ArrowRight': () => {
if (direction === 'rtl') {
state.focusPreviousDay();
} else {
state.focusNextDay();
}
break;
case 'ArrowDown':
e.preventDefault();
e.stopPropagation();
return true;
},
'ArrowDown': () => {
state.focusNextRow();
break;
case 'Escape':
return true;
},
'Escape': () => {
// Cancel the selection.
if ('setAnchorDate' in state) {
e.preventDefault();
state.setAnchorDate(null);
}
break;
return false; // TODO: is this really correct? or should it return true when we cancel and only propagate if there's nothing to do
}
}
};
});

let visibleRangeDescription = useVisibleRangeDescription(startDate, endDate, state.timeZone, true);

Expand Down Expand Up @@ -164,7 +168,7 @@ export function useCalendarGrid(props: AriaCalendarGridProps, state: CalendarSta
'aria-readonly': state.isReadOnly || undefined,
'aria-disabled': state.isDisabled || undefined,
'aria-multiselectable': ('highlightedRange' in state) || state.selectionMode === 'multiple' || undefined,
onKeyDown,
...keyboardProps,
onFocus: () => state.setFocused(true),
onBlur: () => state.setFocused(false)
}),
Expand Down
57 changes: 26 additions & 31 deletions packages/react-aria/src/color/useColorArea.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,42 +106,37 @@ export function useColorArea(props: AriaColorAreaOptions, state: ColorAreaState)

let currentPosition = useRef<{x: number, y: number} | null>(null);

let keyboardUpdate = (cb, inputRef: RefObject<HTMLInputElement | null>, input: 'x' | 'y') => {
state.setDragging(true);
setValueChangedViaKeyboard(true);
cb();
state.setDragging(false);
focusInput(inputRef);
setFocusedInput(input);
return true;
};

let {keyboardProps} = useKeyboard({
onKeyDown(e) {
// these are the cases that useMove doesn't handle
if (!/^(PageUp|PageDown|Home|End)$/.test(e.key)) {
e.continuePropagation();
return;
}
// same handling as useMove, don't need to stop propagation, useKeyboard will do that for us
e.preventDefault();
// remember to set this and unset it so that onChangeEnd is fired
state.setDragging(true);
setValueChangedViaKeyboard(true);
let dir;
switch (e.key) {
case 'PageUp':
shortcuts: {
'PageUp': () => {
return keyboardUpdate(() => {
state.incrementY(state.yChannelPageStep);
dir = 'y';
break;
case 'PageDown':
}, inputYRef, 'y');
},
'PageDown': () => {
return keyboardUpdate(() => {
state.decrementY(state.yChannelPageStep);
dir = 'y';
break;
case 'Home':
}, inputYRef, 'y');
},
'Home': () => {
return keyboardUpdate(() => {
direction === 'rtl' ? state.incrementX(state.xChannelPageStep) : state.decrementX(state.xChannelPageStep);
dir = 'x';
break;
case 'End':
}, inputXRef, 'x');
},
'End': () => {
return keyboardUpdate(() => {
direction === 'rtl' ? state.decrementX(state.xChannelPageStep) : state.incrementX(state.xChannelPageStep);
dir = 'x';
break;
}
state.setDragging(false);
if (dir) {
let input = dir === 'x' ? inputXRef : inputYRef;
focusInput(input);
setFocusedInput(dir);
}, inputXRef, 'x');
}
}
});
Expand Down
Loading
Loading