From 1bd13c1e06d9423ae29ed6985419f3055a5534b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Connor=20B=C3=A4r?= Date: Fri, 25 Oct 2024 19:01:18 +0200 Subject: [PATCH] Add shiftInRange helper --- .../hooks/useFocusList/useFocusList.ts | 14 ++----- packages/circuit-ui/util/helpers.spec.ts | 42 ++++++++++++++++++- packages/circuit-ui/util/helpers.ts | 17 ++++++++ 3 files changed, 61 insertions(+), 12 deletions(-) diff --git a/packages/circuit-ui/hooks/useFocusList/useFocusList.ts b/packages/circuit-ui/hooks/useFocusList/useFocusList.ts index 5fe0fe6ada..610a11712c 100644 --- a/packages/circuit-ui/hooks/useFocusList/useFocusList.ts +++ b/packages/circuit-ui/hooks/useFocusList/useFocusList.ts @@ -16,6 +16,7 @@ import { useCallback, useId, type KeyboardEvent } from 'react'; import { isArrowDown, isArrowUp } from '../../util/key-codes.js'; +import { shiftInRange } from '../../util/helpers.js'; export type FocusProps = { 'data-focus-list': string; @@ -42,9 +43,8 @@ export function useFocusList(): FocusProps { ); const currentEl = event.target as HTMLElement; const currentIndex = Array.from(items).indexOf(currentEl); - const newIndex = isArrowUp(event) - ? getPrevIndex(currentIndex, items.length) - : getNextIndex(currentIndex, items.length); + const offset = isArrowUp(event) ? -1 : 1; + const newIndex = shiftInRange(currentIndex, offset, 0, items.length - 1); const newEl = items.item(newIndex); newEl.focus(); @@ -54,11 +54,3 @@ export function useFocusList(): FocusProps { return { 'data-focus-list': name, onKeyDown }; } - -function getPrevIndex(currentIndex: number, length: number): number { - return currentIndex - 1 < 0 ? length - 1 : currentIndex - 1; -} - -function getNextIndex(currentIndex: number, length: number): number { - return currentIndex + 1 >= length ? 0 : currentIndex + 1; -} diff --git a/packages/circuit-ui/util/helpers.spec.ts b/packages/circuit-ui/util/helpers.spec.ts index 429a53f85c..b3188e5d30 100644 --- a/packages/circuit-ui/util/helpers.spec.ts +++ b/packages/circuit-ui/util/helpers.spec.ts @@ -15,7 +15,15 @@ import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; -import { chunk, clamp, eachFn, isEmpty, last, throttle } from './helpers.js'; +import { + chunk, + clamp, + eachFn, + isEmpty, + last, + shiftInRange, + throttle, +} from './helpers.js'; describe('helpers', () => { describe('clamp', () => { @@ -231,4 +239,36 @@ describe('helpers', () => { expect(actual).toBeUndefined(); }); }); + + describe('shiftInRange', () => { + it('should increase a value within a range', () => { + const actual = shiftInRange(4, 2, 1, 12); + expect(actual).toBe(6); + }); + + it('should decrease a value within a range', () => { + const actual = shiftInRange(4, -2, 1, 12); + expect(actual).toBe(2); + }); + + it('should loop around to the end', () => { + const actual = shiftInRange(4, -6, 1, 12); + expect(actual).toBe(10); + }); + + it('should loop around to the start', () => { + const actual = shiftInRange(7, 10, 4, 12); + expect(actual).toBe(8); + }); + + it('should loop around to the start', () => { + const actual = shiftInRange(4, 10, 2, 12); + expect(actual).toBe(3); + }); + + it('should loop around multiple times', () => { + const actual = shiftInRange(4, -9, 2, 5); + expect(actual).toBe(3); + }); + }); }); diff --git a/packages/circuit-ui/util/helpers.ts b/packages/circuit-ui/util/helpers.ts index e6ff38367d..6679f0a048 100644 --- a/packages/circuit-ui/util/helpers.ts +++ b/packages/circuit-ui/util/helpers.ts @@ -46,6 +46,9 @@ export function isEmpty(value: unknown): boolean { return false; } +/** + * Clamps a value within a range of values between a minimum and maximum limit. + */ export function clamp(value: number, min: number, max: number): number { if (process.env.NODE_ENV !== 'production' && min >= max) { throw new RangeError( @@ -96,3 +99,17 @@ export function last(array: T[]): T; export function last(array: T[] | undefined | null): T | undefined { return isArray(array) ? array[array.length - 1] : undefined; } + +/** + * Increases or decreases a value by an offset and loops back around to stay + * within a given range. + */ +export function shiftInRange( + value: number, // must be in range + offset: number, // positive or negative + min: number, // inclusive + max: number, // inclusive +) { + const modulus = max - min + 1; + return ((value - min + (offset % modulus) + modulus) % modulus) + min; +}