From ae9d3d52b1380a6f3ba5dfef2637cf00eee1cf82 Mon Sep 17 00:00:00 2001 From: Bruno Aggierni Date: Thu, 1 Aug 2024 12:51:07 +0100 Subject: [PATCH 1/3] PRO-146: initial modifications for distanceCalculationMethod selection --- CHANGELOG.md | 6 +++++- README.md | 17 +++++++++++++++++ package.json | 2 +- src/SpatialNavigation.ts | 33 +++++++++++++++++++++++++++------ 4 files changed, 50 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84b79e8..4f667e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# [2.2.0] +## Added +- New init config option `distanceCalculationMethod` that allows switching between edge-based, center-based and corner-based (default) distance calculations. + # [2.1.0] ## Added - new `init` config option `shouldUseNativeEvents` that enables the use of native events for triggering actions, such as clicks or key presses. -- new `init` config option `rtl` that changes focus behavior for layouts in right-to-left (RTL) languages such as Arabic and Hebrew. +- new `init` config option `rtl` that changes focus behavior for layouts in right-to-left (RTL) languages such as Arabic and Hebrew. # [2.0.2] ## Added diff --git a/README.md b/README.md index ac39de9..6fe6066 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,23 @@ init({ }); ``` +## New Distance Calculation Configuration +Starting from version `2.2.0`, you can configure the method used for distance calculations between focusable components. This can be set during initialization using the `distanceCalculationMethod` option. + +### Available Options +* `edges`: Calculates distances using the closest edges of the components. +* `center`: Calculates distances using the center points of the components for size-agnostic comparisons. Ideal for non-uniform elements between siblings. +* `corners`: Calculates distances using the corners of the components, between the nearest corners. This is the default value. + +```jsx +import { init } from '@noriginmedia/norigin-spatial-navigation'; + +init({ + // options + distanceCalculationMethod: 'center', // or 'edges' or 'corners' (default) +}); +``` + ## Making your component focusable Most commonly you will have Leaf Focusable components. (See [Tree Hierarchy](#tree-hierarchy-of-focusable-components)) Leaf component is the one that doesn't have focusable children. diff --git a/package.json b/package.json index 247d461..4d3a2b5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@noriginmedia/norigin-spatial-navigation", - "version": "2.1.0", + "version": "2.2.0", "description": "React hooks based Spatial Navigation solution", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/SpatialNavigation.ts b/src/SpatialNavigation.ts index 198881d..919c0b7 100644 --- a/src/SpatialNavigation.ts +++ b/src/SpatialNavigation.ts @@ -241,6 +241,8 @@ class SpatialNavigationService { private writingDirection: WritingDirection; + private distanceCalculationMethod: string; + /** * Used to determine the coordinate that will be used to filter items that are over the "edge" */ @@ -408,7 +410,8 @@ class SpatialNavigationService { static getSecondaryAxisDistance( refCorners: Corners, siblingCorners: Corners, - isVerticalDirection: boolean + isVerticalDirection: boolean, + distanceCalculationMethod: string ) { const { a: refA, b: refB } = refCorners; const { a: siblingA, b: siblingB } = siblingCorners; @@ -419,13 +422,27 @@ class SpatialNavigationService { const siblingCoordinateA = siblingA[coordinate]; const siblingCoordinateB = siblingB[coordinate]; - const distancesToCompare = []; + if (distanceCalculationMethod === 'center') { + const refCoordinateCenter = (refCoordinateA + refCoordinateB) / 2; + const siblingCoordinateCenter = + (siblingCoordinateA + siblingCoordinateB) / 2; + return Math.abs(refCoordinateCenter - siblingCoordinateCenter); + } + if (distanceCalculationMethod === 'edges') { + const refCoordinateEdge = Math.min(refCoordinateA, refCoordinateB); + const siblingCoordinateEdge = Math.min( + siblingCoordinateA, + siblingCoordinateB + ); + return Math.abs(refCoordinateEdge - siblingCoordinateEdge); + } + // Default to corners + const distancesToCompare = []; distancesToCompare.push(Math.abs(siblingCoordinateA - refCoordinateA)); distancesToCompare.push(Math.abs(siblingCoordinateA - refCoordinateB)); distancesToCompare.push(Math.abs(siblingCoordinateB - refCoordinateA)); distancesToCompare.push(Math.abs(siblingCoordinateB - refCoordinateB)); - return Math.min(...distancesToCompare); } @@ -473,12 +490,14 @@ class SpatialNavigationService { const primaryAxisDistance = primaryAxisFunction( refCorners, siblingCorners, - isVerticalDirection + isVerticalDirection, + this.distanceCalculationMethod ); const secondaryAxisDistance = secondaryAxisFunction( refCorners, siblingCorners, - isVerticalDirection + isVerticalDirection, + this.distanceCalculationMethod ); /** @@ -597,7 +616,8 @@ class SpatialNavigationService { useGetBoundingClientRect = false, shouldFocusDOMNode = false, shouldUseNativeEvents = false, - rtl = false + rtl = false, + distanceCalculationMethod = 'corners' } = {}) { if (!this.enabled) { this.enabled = true; @@ -607,6 +627,7 @@ class SpatialNavigationService { this.shouldFocusDOMNode = shouldFocusDOMNode && !nativeMode; this.shouldUseNativeEvents = shouldUseNativeEvents; this.writingDirection = rtl ? WritingDirection.RTL : WritingDirection.LTR; + this.distanceCalculationMethod = distanceCalculationMethod; this.debug = debug; From e5bf26449f1688e1545d487573d00980fa782789 Mon Sep 17 00:00:00 2001 From: Bruno Aggierni Date: Mon, 5 Aug 2024 10:08:29 +0100 Subject: [PATCH 2/3] PRO-146: Adding random size assets to last category row --- src/App.tsx | 38 ++++++++++++++++++++++++++++++-------- src/SpatialNavigation.ts | 6 ++++-- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index e39892d..f722dcf 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -22,7 +22,8 @@ const logo = require('../logo.png').default; init({ debug: false, - visualDebug: false + visualDebug: false, + distanceCalculationMethod: 'center' }); const rows = shuffle([ @@ -182,12 +183,15 @@ const AssetWrapper = styled.div` `; interface AssetBoxProps { + index: number; + isShuffleSize: boolean; focused: boolean; color: string; } const AssetBox = styled.div` - width: 225px; + width: ${({ isShuffleSize, index }) => + isShuffleSize ? `${80 + index * 30}px` : '225px'}; height: 127px; background-color: ${({ color }) => color}; border-color: white; @@ -206,6 +210,8 @@ const AssetTitle = styled.div` `; interface AssetProps { + index: number; + isShuffleSize: boolean; title: string; color: string; onEnterPress: (props: object, details: KeyPressDetails) => void; @@ -216,7 +222,14 @@ interface AssetProps { ) => void; } -function Asset({ title, color, onEnterPress, onFocus }: AssetProps) { +function Asset({ + title, + color, + onEnterPress, + onFocus, + isShuffleSize, + index +}: AssetProps) { const { ref, focused } = useFocusable({ onEnterPress, onFocus, @@ -228,7 +241,12 @@ function Asset({ title, color, onEnterPress, onFocus }: AssetProps) { return ( - + {title} ); @@ -261,6 +279,7 @@ const ContentRowScrollingContent = styled.div` `; interface ContentRowProps { + isShuffleSize: boolean; title: string; onAssetPress: (props: object, details: KeyPressDetails) => void; onFocus: ( @@ -273,7 +292,8 @@ interface ContentRowProps { function ContentRow({ title: rowTitle, onAssetPress, - onFocus + onFocus, + isShuffleSize }: ContentRowProps) { const { ref, focusKey } = useFocusable({ onFocus @@ -297,13 +317,14 @@ function ContentRow({ {rowTitle} - {assets.map(({ title, color }) => ( + {assets.map(({ title, color }, index) => ( ))} @@ -397,12 +418,13 @@ function Content() {
- {rows.map(({ title }) => ( + {rows.map(({ title }, index) => ( ))}
diff --git a/src/SpatialNavigation.ts b/src/SpatialNavigation.ts index 919c0b7..1b959ec 100644 --- a/src/SpatialNavigation.ts +++ b/src/SpatialNavigation.ts @@ -20,6 +20,8 @@ const KEY_ENTER = 'enter'; export type Direction = 'up' | 'down' | 'left' | 'right'; +type DistanceCalculationMethod = 'center' | 'edges' | 'corners'; + const DEFAULT_KEY_MAP = { [DIRECTION_LEFT]: [37, 'ArrowLeft'], [DIRECTION_UP]: [38, 'ArrowUp'], @@ -241,7 +243,7 @@ class SpatialNavigationService { private writingDirection: WritingDirection; - private distanceCalculationMethod: string; + private distanceCalculationMethod: DistanceCalculationMethod; /** * Used to determine the coordinate that will be used to filter items that are over the "edge" @@ -617,7 +619,7 @@ class SpatialNavigationService { shouldFocusDOMNode = false, shouldUseNativeEvents = false, rtl = false, - distanceCalculationMethod = 'corners' + distanceCalculationMethod = 'corners' as DistanceCalculationMethod } = {}) { if (!this.enabled) { this.enabled = true; From cb9dccb1f3df240d6b92d25094fdac49c16cd6b2 Mon Sep 17 00:00:00 2001 From: Bruno Aggierni Date: Mon, 5 Aug 2024 11:14:34 +0100 Subject: [PATCH 3/3] PRO-146: Edges algorithm improved --- CHANGELOG.md | 2 ++ README.md | 43 +++++++++++++++++++++++++- src/App.tsx | 4 +-- src/SpatialNavigation.ts | 67 +++++++++++++++++++++++++++++++++------- 4 files changed, 101 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f667e5..b8475a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 # [2.2.0] ## Added - New init config option `distanceCalculationMethod` that allows switching between edge-based, center-based and corner-based (default) distance calculations. +- Support for a custom distance calculation function via the `customDistanceCalculationFunction` option, enabling custom logic for determining distances between focusable components. This will override the `getSecondaryAxisDistance` method. + # [2.1.0] ## Added diff --git a/README.md b/README.md index 6fe6066..a3769d2 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ init({ ## New Distance Calculation Configuration Starting from version `2.2.0`, you can configure the method used for distance calculations between focusable components. This can be set during initialization using the `distanceCalculationMethod` option. -### Available Options +### How to use | Available Options * `edges`: Calculates distances using the closest edges of the components. * `center`: Calculates distances using the center points of the components for size-agnostic comparisons. Ideal for non-uniform elements between siblings. * `corners`: Calculates distances using the corners of the components, between the nearest corners. This is the default value. @@ -79,6 +79,47 @@ init({ }); ``` +## Custom Distance Calculation Function +In addition to the predefined distance calculation methods, you can define your own custom distance calculation function. This will override the `getSecondaryAxisDistance` method. + +You can pass your custom distance calculation function during initialization using the customDistanceCalculationFunction option. This function will override the built-in methods. + +### How to use | Available Options +The custom distance calculation function should follow the DistanceCalculationFunction type signature: + +```jsx +type DistanceCalculationFunction = ( + refCorners: Corners, + siblingCorners: Corners, + isVerticalDirection: boolean, + distanceCalculationMethod: DistanceCalculationMethod +) => number; +``` + +### Example +```jsx +import { init } from '@noriginmedia/norigin-spatial-navigation'; + +// Define a custom distance calculation function +const myCustomDistanceCalculationFunction = (refCorners, siblingCorners, isVerticalDirection, distanceCalculationMethod) => { + // Custom logic for distance calculation + const { a: refA, b: refB } = refCorners; + const { a: siblingA, b: siblingB } = siblingCorners; + const coordinate = isVerticalDirection ? 'x' : 'y'; + + const refCoordinateCenter = (refA[coordinate] + refB[coordinate]) / 2; + const siblingCoordinateCenter = (siblingA[coordinate] + siblingB[coordinate]) / 2; + + return Math.abs(refCoordinateCenter - siblingCoordinateCenter); +}; + +// Initialize with custom distance calculation function +init({ + // options + customDistanceCalculationFunction: myCustomDistanceCalculationFunction, +}); +``` + ## Making your component focusable Most commonly you will have Leaf Focusable components. (See [Tree Hierarchy](#tree-hierarchy-of-focusable-components)) Leaf component is the one that doesn't have focusable children. diff --git a/src/App.tsx b/src/App.tsx index f722dcf..f2063e4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -418,13 +418,13 @@ function Content() {
- {rows.map(({ title }, index) => ( + {rows.map(({ title }) => ( ))}
diff --git a/src/SpatialNavigation.ts b/src/SpatialNavigation.ts index 1b959ec..1d95921 100644 --- a/src/SpatialNavigation.ts +++ b/src/SpatialNavigation.ts @@ -22,6 +22,13 @@ export type Direction = 'up' | 'down' | 'left' | 'right'; type DistanceCalculationMethod = 'center' | 'edges' | 'corners'; +type DistanceCalculationFunction = ( + refCorners: Corners, + siblingCorners: Corners, + isVerticalDirection: boolean, + distanceCalculationMethod: DistanceCalculationMethod +) => number; + const DEFAULT_KEY_MAP = { [DIRECTION_LEFT]: [37, 'ArrowLeft'], [DIRECTION_UP]: [38, 'ArrowUp'], @@ -245,6 +252,8 @@ class SpatialNavigationService { private distanceCalculationMethod: DistanceCalculationMethod; + private customDistanceCalculationFunction?: DistanceCalculationFunction; + /** * Used to determine the coordinate that will be used to filter items that are over the "edge" */ @@ -413,8 +422,18 @@ class SpatialNavigationService { refCorners: Corners, siblingCorners: Corners, isVerticalDirection: boolean, - distanceCalculationMethod: string + distanceCalculationMethod: DistanceCalculationMethod, + customDistanceCalculationFunction?: DistanceCalculationFunction ) { + if (customDistanceCalculationFunction) { + return customDistanceCalculationFunction( + refCorners, + siblingCorners, + isVerticalDirection, + distanceCalculationMethod + ); + } + const { a: refA, b: refB } = refCorners; const { a: siblingA, b: siblingB } = siblingCorners; const coordinate = isVerticalDirection ? 'x' : 'y'; @@ -431,20 +450,37 @@ class SpatialNavigationService { return Math.abs(refCoordinateCenter - siblingCoordinateCenter); } if (distanceCalculationMethod === 'edges') { - const refCoordinateEdge = Math.min(refCoordinateA, refCoordinateB); - const siblingCoordinateEdge = Math.min( + // 1. Find the minimum and maximum coordinates for both ref and sibling + const refCoordinateEdgeMin = Math.min(refCoordinateA, refCoordinateB); + const siblingCoordinateEdgeMin = Math.min( siblingCoordinateA, siblingCoordinateB ); - return Math.abs(refCoordinateEdge - siblingCoordinateEdge); + const refCoordinateEdgeMax = Math.max(refCoordinateA, refCoordinateB); + const siblingCoordinateEdgeMax = Math.max( + siblingCoordinateA, + siblingCoordinateB + ); + + // 2. Calculate the distances between the closest edges + const minEdgeDistance = Math.abs( + refCoordinateEdgeMin - siblingCoordinateEdgeMin + ); + const maxEdgeDistance = Math.abs( + refCoordinateEdgeMax - siblingCoordinateEdgeMax + ); + + // 3. Return the smallest distance between the edges + return Math.min(minEdgeDistance, maxEdgeDistance); } // Default to corners - const distancesToCompare = []; - distancesToCompare.push(Math.abs(siblingCoordinateA - refCoordinateA)); - distancesToCompare.push(Math.abs(siblingCoordinateA - refCoordinateB)); - distancesToCompare.push(Math.abs(siblingCoordinateB - refCoordinateA)); - distancesToCompare.push(Math.abs(siblingCoordinateB - refCoordinateB)); + const distancesToCompare = [ + Math.abs(siblingCoordinateA - refCoordinateA), + Math.abs(siblingCoordinateA - refCoordinateB), + Math.abs(siblingCoordinateB - refCoordinateA), + Math.abs(siblingCoordinateB - refCoordinateB) + ]; return Math.min(...distancesToCompare); } @@ -493,13 +529,15 @@ class SpatialNavigationService { refCorners, siblingCorners, isVerticalDirection, - this.distanceCalculationMethod + this.distanceCalculationMethod, + this.customDistanceCalculationFunction ); const secondaryAxisDistance = secondaryAxisFunction( refCorners, siblingCorners, isVerticalDirection, - this.distanceCalculationMethod + this.distanceCalculationMethod, + this.customDistanceCalculationFunction ); /** @@ -607,6 +645,8 @@ class SpatialNavigationService { this.visualDebugger = null; this.logIndex = 0; + + this.distanceCalculationMethod = 'corners'; } init({ @@ -619,7 +659,8 @@ class SpatialNavigationService { shouldFocusDOMNode = false, shouldUseNativeEvents = false, rtl = false, - distanceCalculationMethod = 'corners' as DistanceCalculationMethod + distanceCalculationMethod = 'corners' as DistanceCalculationMethod, + customDistanceCalculationFunction = undefined as DistanceCalculationFunction } = {}) { if (!this.enabled) { this.enabled = true; @@ -630,6 +671,8 @@ class SpatialNavigationService { this.shouldUseNativeEvents = shouldUseNativeEvents; this.writingDirection = rtl ? WritingDirection.RTL : WritingDirection.LTR; this.distanceCalculationMethod = distanceCalculationMethod; + this.customDistanceCalculationFunction = + customDistanceCalculationFunction; this.debug = debug;