Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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