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
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,42 @@ index 55027cb256f66b808d17280dc01bc55a796a1032..993d5de5a838a711d7ae009344354772
focus.focusElement(dest);
if (selection.hasOwnSelection(dest)) {
UI.setUISelection(dest, {
diff --git a/dist/cjs/keyboard/index.js b/dist/cjs/keyboard/index.js
index 18f5b4b8eb995fe5ecb352230f71b25e90f2a8a0..e5e17d1d3089aa8ea5c3f55a06aa746eb3a8258a 100644
--- a/dist/cjs/keyboard/index.js
+++ b/dist/cjs/keyboard/index.js
@@ -19,7 +19,7 @@ async function keyboardAction(instance, { keyDef, releasePrevious, releaseSelf,
}
if (!releasePrevious) {
for(let i = 1; i <= repeat; i++){
- await system.keyboard.keydown(instance, keyDef);
+ await system.keyboard.keydown(instance, keyDef, i > 1);
if (i < repeat) {
await wait.wait(instance.config);
}
diff --git a/dist/cjs/system/keyboard.js b/dist/cjs/system/keyboard.js
index 3272fd7e642e79a8f67052b4b521355314cbc496..e1b16e9213eb609e7cd2335ecee792c3c19d35b4 100644
--- a/dist/cjs/system/keyboard.js
+++ b/dist/cjs/system/keyboard.js
@@ -52,7 +52,7 @@ class KeyboardHost {
getPressedKeys() {
return this.pressed.values().map((p)=>p.keyDef);
}
- /** Press a key */ async keydown(instance, keyDef) {
+ /** Press a key */ async keydown(instance, keyDef, repeat = false) {
const key = String(keyDef.key);
const code = String(keyDef.code);
const target = getActiveElement.getActiveElementOrBody(instance.config.document);
@@ -63,7 +63,8 @@ class KeyboardHost {
}
const unprevented = instance.dispatchUIEvent(target, 'keydown', {
key,
- code
+ code,
+ repeat
});
if (isModifierLock(key) && !this.modifiers[key]) {
this.modifiers[key] = true;
diff --git a/dist/cjs/utils/focus/getActiveElement.js b/dist/cjs/utils/focus/getActiveElement.js
index d25f3a8ef67e856e43614559f73012899c0b53d7..4ed9ee45565ed438ee9284d8d3043c0bd50463eb 100644
--- a/dist/cjs/utils/focus/getActiveElement.js
Expand All @@ -37,3 +73,39 @@ index d25f3a8ef67e856e43614559f73012899c0b53d7..4ed9ee45565ed438ee9284d8d3043c0b
} else {
// Browser does not yield disabled elements as document.activeElement - jsdom does
if (isDisabled.isDisabled(activeElement)) {
diff --git a/dist/esm/keyboard/index.js b/dist/esm/keyboard/index.js
index 9c9feb7b82aabeadd8acbd9ec8f4127e302a4634..1456a5cf406382ee08b4bb7184499632bd8d3bd3 100644
--- a/dist/esm/keyboard/index.js
+++ b/dist/esm/keyboard/index.js
@@ -17,7 +17,7 @@ async function keyboardAction(instance, { keyDef, releasePrevious, releaseSelf,
}
if (!releasePrevious) {
for(let i = 1; i <= repeat; i++){
- await system.keyboard.keydown(instance, keyDef);
+ await system.keyboard.keydown(instance, keyDef, i > 1);
if (i < repeat) {
await wait(instance.config);
}
diff --git a/dist/esm/system/keyboard.js b/dist/esm/system/keyboard.js
index 43700ca3a873aa6eb1cb4553c0889b685ff90fff..9607372660dcb1737b26abcdc4bc71f35e4d43c3 100644
--- a/dist/esm/system/keyboard.js
+++ b/dist/esm/system/keyboard.js
@@ -50,7 +50,7 @@ class KeyboardHost {
getPressedKeys() {
return this.pressed.values().map((p)=>p.keyDef);
}
- /** Press a key */ async keydown(instance, keyDef) {
+ /** Press a key */ async keydown(instance, keyDef, repeat = false) {
const key = String(keyDef.key);
const code = String(keyDef.code);
const target = getActiveElementOrBody(instance.config.document);
@@ -61,7 +61,8 @@ class KeyboardHost {
}
const unprevented = instance.dispatchUIEvent(target, 'keydown', {
key,
- code
+ code,
+ repeat
});
if (isModifierLock(key) && !this.modifiers[key]) {
this.modifiers[key] = true;
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,28 @@ describe('ActionGroup', function () {
}
);

it('should support repeat keydown events when holding an arrow key', async function () {
// Arrow key navigation is handled by useActionGroup's useKeyboard (allowRepeats).
let tree = render(
<Provider theme={theme} locale="de-DE">
<ActionGroup>
<Item key="1">Click me 1</Item>
<Item key="2">Click me 2</Item>
<Item key="3">Click me 3</Item>
</ActionGroup>
</Provider>
);

let buttons = tree.getAllByRole('button');
await user.tab();
expect(document.activeElement).toBe(buttons[0]);

// Hold the key down for two keydown events (the second is a repeat).
await user.keyboard('{ArrowRight>2/}');

expect(document.activeElement).toBe(buttons[2]);
});

it.each`
Name | props | disabledKeys | orders
${'middle disabled'} | ${{locale: 'de-DE'}} | ${['1']} | ${[{action: tab, result: () => ['0', '-1', '-1']}, {action: pressArrowRight, result: () => ['-1', '-1', '0']}, {action: pressArrowRight, result: () => ['0', '-1', '-1']}, {action: pressArrowLeft, result: () => ['-1', '-1', '0']}, {action: pressArrowLeft, result: () => ['0', '-1', '-1']}]}
Expand Down
12 changes: 12 additions & 0 deletions packages/react-aria-components/test/Calendar.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,18 @@ describe('Calendar', () => {
expect(calendar.getByLabelText(/selected/)).toBe(day16);
});

if (parseInt(React.version, 10) >= 18) {
it('should support repeat keydown events when holding an arrow key', async () => {
let {getByRole} = renderCalendar({defaultFocusedValue: new CalendarDate(2020, 3, 3)});
let cell = getByRole('button', {name: 'Tuesday, March 3, 2020'});

await user.click(cell);
await user.keyboard('{ArrowRight>2/}');

expect(document.activeElement).toHaveAttribute('aria-label', 'Thursday, March 5, 2020');
});
}

it('should not become focused just by setting the focused date', async () => {
let DatePicker = () => {
let state = useContext(CalendarStateContext);
Expand Down
13 changes: 13 additions & 0 deletions packages/react-aria-components/test/ColorArea.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,19 @@ describe('ColorArea', () => {
expect(wrapper).toHaveClass('disabled');
});

it('should support repeat keydown events when holding Page Up/Page Down', async () => {
// Page Up/Down adjust the Y (green) channel; the arrow keys are handled by useMove instead. The page step is 17.
let {getAllByRole} = renderColorArea();
let greenSlider = getAllByRole('slider', {hidden: true})[1];

await user.tab();
await user.keyboard('{PageUp>3/}');
expect(greenSlider).toHaveValue('51');

await user.keyboard('{PageDown>3/}');
expect(greenSlider).toHaveValue('0');
});

it('should support form prop', () => {
let {getByRole} = renderColorArea({form: 'test'});
let input = getByRole('slider');
Expand Down
13 changes: 13 additions & 0 deletions packages/react-aria-components/test/ColorWheel.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,19 @@ describe('ColorWheel', () => {
expect(wrapper).toHaveClass('disabled');
});

it('should support repeat keydown events when holding Page Up/Page Down', async () => {
// Page Up/Down change the hue by the page step (15); the arrow keys are handled by useMove instead.
let {getByRole} = renderColorWheel();
let slider = getByRole('slider');

await user.tab();
await user.keyboard('{PageUp>3/}');
expect(slider).toHaveValue('45');

await user.keyboard('{PageDown>3/}');
expect(slider).toHaveValue('0');
});

it('should support form prop', () => {
let {getByRole} = renderColorWheel({form: 'test'});
let input = getByRole('slider');
Expand Down
32 changes: 32 additions & 0 deletions packages/react-aria-components/test/DateField.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,38 @@ describe('DateField', () => {
expect(document.activeElement).toBe(segments[0]);
});

it('should support repeat keydown events when holding backspace across empty segments', async () => {
// Backspace on an empty (placeholder) segment moves focus to the previous segment.
let {getAllByRole} = render(
<DateField>
<Label>Birth date</Label>
<DateInput>{segment => <DateSegment segment={segment} />}</DateInput>
</DateField>
);

let segments = getAllByRole('spinbutton');
await user.click(segments[2]);
await user.keyboard('{Backspace>2/}');

expect(document.activeElement).toBe(segments[0]);
});

it('should support repeat keydown events when holding an arrow key to navigate segments', async () => {
// ArrowLeft/ArrowRight move between segments
let {getAllByRole} = render(
<DateField defaultValue={new CalendarDate(2024, 12, 31)}>
<Label>Birth date</Label>
<DateInput>{segment => <DateSegment segment={segment} />}</DateInput>
</DateField>
);

let segments = getAllByRole('spinbutton');
await user.click(segments[0]);
await user.keyboard('{ArrowRight>2/}');

expect(document.activeElement).toBe(segments[2]);
});

it('should do nothing when pressing enter', async () => {
let {getAllByRole} = render(
<DateField defaultValue={new CalendarDate(2024, 12, 31)}>
Expand Down
17 changes: 17 additions & 0 deletions packages/react-aria-components/test/NumberField.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,23 @@ describe('NumberField', () => {
expect(onChange).toHaveBeenLastCalledWith(2);
});

it('should support repeat keydown events when holding an arrow key', async () => {
let onChange = jest.fn();
let {getByRole} = render(<TestNumberField onChange={onChange} />);
let input = getByRole('textbox');

await user.tab();
expect(input).toHaveFocus();

await user.keyboard('{ArrowUp>3/}');
expect(input).toHaveValue('1,027');
expect(onChange).toHaveBeenLastCalledWith(1027);

await user.keyboard('{ArrowDown>3/}');
expect(input).toHaveValue('1,024');
expect(onChange).toHaveBeenLastCalledWith(1024);
});

it('should not type the grouping characters when useGrouping is false', async () => {
let {getByRole} = render(<TestNumberField formatOptions={{useGrouping: false}} />);
let input = getByRole('textbox');
Expand Down
19 changes: 18 additions & 1 deletion packages/react-aria-components/test/RadioGroup.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* governing permissions and limitations under the License.
*/

import {act, pointerMap, render, within} from '@react-spectrum/test-utils-internal';
import {act, fireEvent, pointerMap, render, within} from '@react-spectrum/test-utils-internal';
import {Button} from '../src/Button';
import {Dialog, DialogTrigger} from '../src/Dialog';
import {FieldError} from '../src/FieldError';
Expand Down Expand Up @@ -380,6 +380,23 @@ describe.each(['RadioGroup', 'RadioField'])('%s', comp => {
expect(label).not.toHaveClass('selected');
});

it('should support repeat keydown events when holding an arrow key', async () => {
let onChange = jest.fn();
let {getAllByRole} = renderGroup({onChange});
let radios = getAllByRole('radio');

await user.tab();
expect(radios[0]).toHaveFocus();

// user-event implements its own radio-group arrow navigation, which bypasses our own code
fireEvent.keyDown(document.activeElement, {key: 'ArrowDown'});
fireEvent.keyDown(document.activeElement, {key: 'ArrowDown', repeat: true});
fireEvent.keyUp(document.activeElement, {key: 'ArrowDown'});

expect(radios[2]).toBeChecked();
expect(onChange).toHaveBeenLastCalledWith('c');
});

it('should support read only state', () => {
let className = ({isReadOnly}) => (isReadOnly ? 'readonly' : '');
let {getByRole, getAllByRole} = renderGroup({isReadOnly: true, className}, {className});
Expand Down
30 changes: 30 additions & 0 deletions packages/react-aria-components/test/Select.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,36 @@ describe('Select', () => {
expect(button).toHaveTextContent('0');
});

it('should support repeat keydown events when holding an arrow key', async () => {
let onSelectionChange = jest.fn();
let {getByRole} = render(
<Select onChange={onSelectionChange} aria-label="Pick a number">
<Button>
<SelectValue />
</Button>
<Popover>
<ListBox items={Array.from({length: 3}).map((_, i) => ({id: i, label: `${i}`}))}>
{item => (
<ListBoxItem id={item.id} textValue={item.label}>
{item.label}
</ListBoxItem>
)}
</ListBox>
</Popover>
</Select>
);

let button = getByRole('button');
await user.tab();
expect(button).toHaveFocus();

// Arrow key navigation while closed moves the selection.
await user.keyboard('{ArrowRight>2/}');

expect(onSelectionChange).toHaveBeenLastCalledWith(1);
expect(button).toHaveTextContent('1');
});

it('should support falsy (0) as a valid default value', async () => {
let {getByRole} = render(
<Select placeholder="pick a number">
Expand Down
15 changes: 15 additions & 0 deletions packages/react-aria-components/test/Slider.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,21 @@ describe('Slider', () => {
expect(sliders[1]).toHaveValue('97');
});

it('should support repeat keydown events when holding Page Up/Page Down', async () => {
// Default pageSize for a 0-100 slider is 10.
let {getByRole} = renderSlider({defaultValue: 20});

let slider = getByRole('slider');
await user.tab();
expect(document.activeElement).toBe(slider);

await user.keyboard('{PageUp>3/}');
expect(slider).toHaveValue('50');

await user.keyboard('{PageDown>3/}');
expect(slider).toHaveValue('20');
});

it('should support clicking on the track to move the thumb', async () => {
let onChange = jest.fn();
let {getByRole} = renderSlider({onChange});
Expand Down
44 changes: 44 additions & 0 deletions packages/react-aria-components/test/TagGroup.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,50 @@ describe('TagGroup', () => {
await user.click(button);
expect(onRemove).toHaveBeenCalledTimes(1);
});
if (parseInt(React.version, 10) >= 18) {
it('should support repeat keydown events when holding the remove key', async () => {
let removed = [];
function Example() {
let list = useListData({
initialItems: [
{id: 'cat', name: 'Cat'},
{id: 'dog', name: 'Dog'},
{id: 'kangaroo', name: 'Kangaroo'}
]
});
return (
<TagGroup
aria-label="Animals"
onRemove={keys => {
removed.push(...keys);
list.remove(...keys);
}}>
<TagList items={list.items}>
{item => (
<Tag textValue={item.name}>
{item.name}
<Button slot="remove" aria-label="remove" />
</Tag>
)}
</TagList>
</TagGroup>
);
}

let {getAllByRole, queryAllByRole} = render(<Example />);
expect(getAllByRole('row')).toHaveLength(3);

await user.tab();
expect(getAllByRole('row')[0]).toHaveFocus();

// Hold the key down for three keydown events. Each removes
// the focused tag and shifts focus to the next one, so all three tags are removed.
await user.keyboard('{Backspace>3/}');

expect(queryAllByRole('row')).toHaveLength(0);
expect(removed).toEqual(['cat', 'dog', 'kangaroo']);
});
}

it('should disable the remove button if the tag is disabled', async () => {
let onRemove = jest.fn();
Expand Down
3 changes: 2 additions & 1 deletion packages/react-aria/src/actiongroup/useActionGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ export function useActionGroup<T>(
ArrowUp: () => {
focusManager.focusPrevious({wrap: true});
}
}
},
allowRepeats: true
});

let role: string | undefined = BUTTON_GROUP_ROLES[state.selectionManager.selectionMode];
Expand Down
Loading