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
5 changes: 2 additions & 3 deletions packages/@adobe/react-spectrum/src/actionbar/ActionBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,8 @@ function ActionBarInner<T>(props: ActionBarInnerProps<T>, ref: Ref<HTMLDivElemen
}

let {keyboardProps} = useKeyboard({
onKeyDown(e) {
if (e.key === 'Escape') {
e.preventDefault();
shortcuts: {
Escape: () => {
onClearSelection();
}
}
Expand Down
5 changes: 3 additions & 2 deletions packages/@adobe/react-spectrum/test/tabs/Tabs.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,8 @@ describe('Tabs', function () {
direction: 'rtl'
});
let tabs = tabsTester.getTabs();
window.addEventListener('keydown', onKeyDown);
// Use a capture listener so we observe the key before the collection handler stops propagation.
window.addEventListener('keydown', onKeyDown, true);

expect(tabs[0]).toHaveAttribute('aria-selected', 'true');

Expand All @@ -183,7 +184,7 @@ describe('Tabs', function () {
expect(tabs[2]).not.toHaveAttribute('aria-selected', 'true');
expect(tabs[1]).toHaveAttribute('aria-selected', 'true');
expect(onKeyDown.mock.calls[2][0].key).toBe('ArrowRight');
window.removeEventListener('keydown', onKeyDown);
window.removeEventListener('keydown', onKeyDown, true);
});

it('allows user to change tab item select via arrow keys with vertical tabs', function () {
Expand Down
7 changes: 2 additions & 5 deletions packages/@react-spectrum/s2/src/ActionBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,12 +167,9 @@ const ActionBarInner = forwardRef(function ActionBarInner(
});

let {keyboardProps} = useKeyboard({
onKeyDown(e) {
if (e.key === 'Escape') {
e.preventDefault();
shortcuts: {
Escape: () => {
onClearSelection?.();
} else {
e.continuePropagation();
}
}
});
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 @@ -2396,3 +2396,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]);
});
});
});
40 changes: 18 additions & 22 deletions packages/react-aria/src/actiongroup/useActionGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ import {
} from '@react-types/shared';
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 @@ -91,34 +91,30 @@ export function useActionGroup<T>(
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: () => {
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) {
},
ArrowDown: () => {
focusManager.focusNext({wrap: true});
},
ArrowLeft: () => {
if (flipDirection) {
focusManager.focusNext({wrap: true});
} else {
focusManager.focusPrevious({wrap: true});
}
break;
},
ArrowUp: () => {
focusManager.focusPrevious({wrap: true});
}
}
};
});

let role: string | undefined = BUTTON_GROUP_ROLES[state.selectionManager.selectionMode];
if (isInToolbar && role === 'toolbar') {
Expand All @@ -130,7 +126,7 @@ export function useActionGroup<T>(
role,
'aria-orientation': role === 'toolbar' ? orientation : undefined,
'aria-disabled': isDisabled,
onKeyDown
...keyboardProps
}
};
}
84 changes: 38 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 @@ -78,70 +79,61 @@ export function useCalendarGrid(

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();
},
' ': () => {
state.selectFocusedDate();
},
PageUp: () => {
state.focusPreviousSection();
},
'Shift+PageUp': () => {
state.focusPreviousSection(true);
},
PageDown: () => {
state.focusNextSection();
},
'Shift+PageDown': () => {
state.focusNextSection(true);
},
End: () => {
state.focusSectionEnd();
break;
case 'Home':
e.preventDefault();
e.stopPropagation();
},
Home: () => {
state.focusSectionStart();
break;
case 'ArrowLeft':
e.preventDefault();
e.stopPropagation();
},
ArrowLeft: () => {
if (direction === 'rtl') {
state.focusNextDay();
} else {
state.focusPreviousDay();
}
break;
case 'ArrowUp':
e.preventDefault();
e.stopPropagation();
},
ArrowUp: () => {
state.focusPreviousRow();
break;
case 'ArrowRight':
e.preventDefault();
e.stopPropagation();
},
ArrowRight: () => {
if (direction === 'rtl') {
state.focusPreviousDay();
} else {
state.focusNextDay();
}
break;
case 'ArrowDown':
e.preventDefault();
e.stopPropagation();
},
ArrowDown: () => {
state.focusNextRow();
break;
case 'Escape':
},
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,
Expand Down Expand Up @@ -182,7 +174,7 @@ export function useCalendarGrid(
'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
88 changes: 49 additions & 39 deletions packages/react-aria/src/color/useColorArea.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,46 +111,56 @@ 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);
};

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':
state.incrementY(state.yChannelPageStep);
dir = 'y';
break;
case 'PageDown':
state.decrementY(state.yChannelPageStep);
dir = 'y';
break;
case 'Home':
direction === 'rtl'
? state.incrementX(state.xChannelPageStep)
: state.decrementX(state.xChannelPageStep);
dir = 'x';
break;
case 'End':
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);
shortcuts: {
PageUp: () => {
return keyboardUpdate(
() => {
state.incrementY(state.yChannelPageStep);
},
inputYRef,
'y'
);
},
PageDown: () => {
return keyboardUpdate(
() => {
state.decrementY(state.yChannelPageStep);
},
inputYRef,
'y'
);
},
Home: () => {
return keyboardUpdate(
() => {
direction === 'rtl'
? state.incrementX(state.xChannelPageStep)
: state.decrementX(state.xChannelPageStep);
},
inputXRef,
'x'
);
},
End: () => {
return keyboardUpdate(
() => {
direction === 'rtl'
? state.decrementX(state.xChannelPageStep)
: state.incrementX(state.xChannelPageStep);
},
inputXRef,
'x'
);
}
}
});
Expand Down
Loading