Skip to content

Commit

Permalink
Feature/combobox pageup and pagedown (#3158)
Browse files Browse the repository at this point in the history
* Add support for PageUp and PageDown in Combobox.FilteredOptions

* Remove console.log statements

* Add test for PageDown/PageUp

* Open FilteredOptions when pressing PageDown

* Prevent scrolling from moving the whole page

* Consistently scroll to the active element in useVirtualFocus by using useLayoutEffect

It seems like doing it with the state update sometimes encounters a race condition preventing the scrolling.
This removes the need for a private function handling index update and scrolling, simplifying the code.

* Updated test to fit new options order

* Prevent "Home" and "End" in Combobox from scrolling as well

* Add changeset

* Update @navikt/core/react/src/form/combobox/FilteredOptions/useVirtualFocus.ts

Co-authored-by: Ken <[email protected]>

* useEffect seems to work the same way, and works better for SSR

* Removed code that did nothing

* Added the same check we have for PageDown, etc. Unsure if it is neccessary.

* Change behaviour for "Home" to move focus to the first element instead of Input

* Once we changed virtualFocus.moveFocusToTop to move focus to first element instead of the Input, we now need another function to move it to the Input where applicable.

* Update .changeset/tasty-cats-hear.md

Co-authored-by: Ken <[email protected]>

---------

Co-authored-by: Ken <[email protected]>
  • Loading branch information
it-vegard and KenAJoh authored Oct 10, 2024
1 parent acea9eb commit 6ddea4e
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 22 deletions.
6 changes: 6 additions & 0 deletions .changeset/tasty-cats-hear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@navikt/ds-react": patch
"@navikt/ds-css": patch
---

Combobox: Support PageUp/PageDown in dropdown list.
1 change: 1 addition & 0 deletions @navikt/core/css/form/combobox.css
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@
border-radius: var(--a-border-radius-medium);
background-color: var(--ac-combobox-list-bg, var(--a-surface-default));
color: var(--ac-combobox-list-text, var(--a-text-default));
overscroll-behavior: contain;
}

.navds-combobox__list--closed {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ const FilteredOptionsProvider = ({
if (disabled || readOnly) {
return;
}
virtualFocus.moveFocusToTop();
virtualFocus.resetFocus();
if (newState ?? !isInternalListOpen) {
setHideCaret(!!maxSelected?.isLimitReached);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useEffect, useState } from "react";

export type VirtualFocusType = {
activeElement: HTMLElement | undefined;
Expand All @@ -10,6 +10,9 @@ export type VirtualFocusType = {
moveFocusToElement: (id: string) => void;
moveFocusToTop: () => void;
moveFocusToBottom: () => void;
moveFocusUpBy: (numberOfElements: number) => void;
moveFocusDownBy: (numberOfElements: number) => void;
resetFocus: () => void;
};

const useVirtualFocus = (
Expand Down Expand Up @@ -40,11 +43,6 @@ const useVirtualFocus = (
: false;
};

const _moveFocusAndScrollTo = (_element?: HTMLElement) => {
setActiveElement(_element);
_element?.scrollIntoView?.({ block: "nearest" });
};

const moveFocusUp = () => {
if (!activeElement) {
return;
Expand All @@ -55,28 +53,32 @@ const useVirtualFocus = (
if (_currentIndex === 0) {
setActiveElement(undefined);
} else {
_moveFocusAndScrollTo(elementAbove);
setActiveElement(elementAbove);
}
};

const moveFocusDown = () => {
const elementsAbleToReceiveFocus = getElementsAbleToReceiveFocus();
if (!activeElement) {
_moveFocusAndScrollTo(elementsAbleToReceiveFocus[0]);
setActiveElement(elementsAbleToReceiveFocus[0]);
return;
}
const _currentIndex = elementsAbleToReceiveFocus.indexOf(activeElement);
if (_currentIndex === elementsAbleToReceiveFocus.length - 1) {
return;
}

_moveFocusAndScrollTo(elementsAbleToReceiveFocus[_currentIndex + 1]);
setActiveElement(elementsAbleToReceiveFocus[_currentIndex + 1]);
};

const moveFocusToTop = () => _moveFocusAndScrollTo(undefined);
const resetFocus = () => setActiveElement(undefined);
const moveFocusToTop = () => {
const elementsAbleToReceiveFocus = getElementsAbleToReceiveFocus();
setActiveElement(elementsAbleToReceiveFocus[0]);
};
const moveFocusToBottom = () => {
const elementsAbleToReceiveFocus = getElementsAbleToReceiveFocus();
return _moveFocusAndScrollTo(
setActiveElement(
elementsAbleToReceiveFocus[elementsAbleToReceiveFocus.length - 1],
);
};
Expand All @@ -89,6 +91,32 @@ const useVirtualFocus = (
}
};

const moveFocusUpBy = (numberOfElements: number) => {
if (!activeElement) {
return;
}
const elementsAbleToReceiveFocus = getElementsAbleToReceiveFocus();
const currentIndex = elementsAbleToReceiveFocus.indexOf(activeElement);
const newIndex = Math.max(currentIndex - numberOfElements, 0);
setActiveElement(elementsAbleToReceiveFocus[newIndex]);
};

const moveFocusDownBy = (numberOfElements: number) => {
const elementsAbleToReceiveFocus = getElementsAbleToReceiveFocus();
const currentIndex = activeElement
? elementsAbleToReceiveFocus.indexOf(activeElement)
: -1;
const newIndex = Math.min(
currentIndex + numberOfElements,
elementsAbleToReceiveFocus.length - 1,
);
setActiveElement(elementsAbleToReceiveFocus[newIndex]);
};

useEffect(() => {
activeElement?.scrollIntoView?.({ block: "nearest" });
}, [activeElement]);

return {
activeElement,
getElementById,
Expand All @@ -99,6 +127,9 @@ const useVirtualFocus = (
moveFocusToElement,
moveFocusToTop,
moveFocusToBottom,
moveFocusUpBy,
moveFocusDownBy,
resetFocus,
};
};

Expand Down
29 changes: 19 additions & 10 deletions @navikt/core/react/src/form/combobox/Input/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,14 +134,6 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
case "Accept":
onEnter(e);
break;
case "Home":
toggleIsListOpen(false);
virtualFocus.moveFocusToTop();
break;
case "End":
toggleIsListOpen(true);
virtualFocus.moveFocusToBottom();
break;
default:
break;
}
Expand Down Expand Up @@ -202,6 +194,24 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
}
virtualFocus.moveFocusUp();
}
} else if (e.key === "Home") {
e.preventDefault();
virtualFocus.moveFocusToTop();
} else if (e.key === "End") {
e.preventDefault();
if (virtualFocus.activeElement === null || !isListOpen) {
toggleIsListOpen(true);
}
virtualFocus.moveFocusToBottom();
} else if (e.key === "PageUp") {
e.preventDefault();
virtualFocus.moveFocusUpBy(6);
} else if (e.key === "PageDown") {
e.preventDefault();
if (virtualFocus.activeElement === null || !isListOpen) {
toggleIsListOpen(true);
}
virtualFocus.moveFocusDownBy(6);
}
},
[
Expand Down Expand Up @@ -230,10 +240,9 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
} else if (filteredOptions.length === 0) {
toggleIsListOpen(false);
}
virtualFocus.moveFocusToTop();
onChange(newValue);
},
[filteredOptions.length, virtualFocus, onChange, toggleIsListOpen],
[filteredOptions.length, onChange, toggleIsListOpen],
);

return (
Expand Down
36 changes: 36 additions & 0 deletions @navikt/core/react/src/form/combobox/__tests__/combobox.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -291,4 +291,40 @@ describe("Render combobox", () => {
);
});
});

describe("has keyboard navigation", () => {
test("for PageDown and PageUp", async () => {
render(<App options={options} />);

const combobox = screen.getByRole("combobox", {
name: "Hva er dine favorittfrukter?",
});

const pressKey = async (key: string) => {
await act(async () => {
await userEvent.keyboard(`{${key}}`);
});
};

const hasVirtualFocus = (option: string) =>
expect(combobox.getAttribute("aria-activedescendant")).toBe(
screen.getByRole("option", { name: option }).id,
);

await act(async () => {
await userEvent.click(combobox);
});

await pressKey("ArrowDown");
hasVirtualFocus("apple");
await pressKey("PageDown");
hasVirtualFocus("mango");
await pressKey("PageDown");
hasVirtualFocus("watermelon");
await pressKey("PageUp");
hasVirtualFocus("mango");
await pressKey("PageUp");
hasVirtualFocus("apple");
});
});
});

0 comments on commit 6ddea4e

Please sign in to comment.