Skip to content

Commit

Permalink
Feat: Add configurable distance calculation methods (#138)
Browse files Browse the repository at this point in the history
* PRO-146: initial modifications for distanceCalculationMethod selection

* PRO-146: Adding random size assets to last category row

* PRO-146: Edges algorithm improved
  • Loading branch information
Braggiouy authored Aug 29, 2024
1 parent 631bf3e commit 0058b0f
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 17 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ 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.1]
## 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.1]
## Added
- new `init` config option `domNodeFocusOptions` for passing [FocusOptions](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#parameters) when using `shouldFocusDOMNode`
Expand Down
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,64 @@ 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.

### 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.

```jsx
import { init } from '@noriginmedia/norigin-spatial-navigation';

init({
// options
distanceCalculationMethod: 'center', // or 'edges' or 'corners' (default)
});
```

## 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.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@noriginmedia/norigin-spatial-navigation",
"version": "2.1.1",
"version": "2.2.1",
"description": "React hooks based Spatial Navigation solution",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
36 changes: 29 additions & 7 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ const logo = require('../logo.png').default;

init({
debug: false,
visualDebug: false
visualDebug: false,
distanceCalculationMethod: 'center'
});

const rows = shuffle([
Expand Down Expand Up @@ -182,12 +183,15 @@ const AssetWrapper = styled.div`
`;

interface AssetBoxProps {
index: number;
isShuffleSize: boolean;
focused: boolean;
color: string;
}

const AssetBox = styled.div<AssetBoxProps>`
width: 225px;
width: ${({ isShuffleSize, index }) =>
isShuffleSize ? `${80 + index * 30}px` : '225px'};
height: 127px;
background-color: ${({ color }) => color};
border-color: white;
Expand All @@ -206,6 +210,8 @@ const AssetTitle = styled.div`
`;

interface AssetProps {
index: number;
isShuffleSize: boolean;
title: string;
color: string;
onEnterPress: (props: object, details: KeyPressDetails) => void;
Expand All @@ -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,
Expand All @@ -228,7 +241,12 @@ function Asset({ title, color, onEnterPress, onFocus }: AssetProps) {

return (
<AssetWrapper ref={ref}>
<AssetBox color={color} focused={focused} />
<AssetBox
index={index}
color={color}
focused={focused}
isShuffleSize={isShuffleSize}
/>
<AssetTitle>{title}</AssetTitle>
</AssetWrapper>
);
Expand Down Expand Up @@ -261,6 +279,7 @@ const ContentRowScrollingContent = styled.div`
`;

interface ContentRowProps {
isShuffleSize: boolean;
title: string;
onAssetPress: (props: object, details: KeyPressDetails) => void;
onFocus: (
Expand All @@ -273,7 +292,8 @@ interface ContentRowProps {
function ContentRow({
title: rowTitle,
onAssetPress,
onFocus
onFocus,
isShuffleSize
}: ContentRowProps) {
const { ref, focusKey } = useFocusable({
onFocus
Expand All @@ -297,13 +317,14 @@ function ContentRow({
<ContentRowTitle>{rowTitle}</ContentRowTitle>
<ContentRowScrollingWrapper ref={scrollingRef}>
<ContentRowScrollingContent>
{assets.map(({ title, color }) => (
{assets.map(({ title, color }, index) => (
<Asset
key={title}
index={index}
title={title}
color={color}
onEnterPress={onAssetPress}
onFocus={onAssetFocus}
isShuffleSize={isShuffleSize}
/>
))}
</ContentRowScrollingContent>
Expand Down Expand Up @@ -403,6 +424,7 @@ function Content() {
title={title}
onAssetPress={onAssetPress}
onFocus={onRowFocus}
isShuffleSize={Math.random() < 0.5} // Rows will have children assets of different sizes, randomly setting it to true or false.
/>
))}
</div>
Expand Down
84 changes: 75 additions & 9 deletions src/SpatialNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ const KEY_ENTER = 'enter';

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'],
Expand Down Expand Up @@ -246,6 +255,10 @@ class SpatialNavigationService {

private writingDirection: WritingDirection;

private distanceCalculationMethod: DistanceCalculationMethod;

private customDistanceCalculationFunction?: DistanceCalculationFunction;

/**
* Used to determine the coordinate that will be used to filter items that are over the "edge"
*/
Expand Down Expand Up @@ -413,8 +426,19 @@ class SpatialNavigationService {
static getSecondaryAxisDistance(
refCorners: Corners,
siblingCorners: Corners,
isVerticalDirection: boolean
isVerticalDirection: boolean,
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';
Expand All @@ -424,13 +448,44 @@ 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') {
// 1. Find the minimum and maximum coordinates for both ref and sibling
const refCoordinateEdgeMin = Math.min(refCoordinateA, refCoordinateB);
const siblingCoordinateEdgeMin = Math.min(
siblingCoordinateA,
siblingCoordinateB
);
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
);

distancesToCompare.push(Math.abs(siblingCoordinateA - refCoordinateA));
distancesToCompare.push(Math.abs(siblingCoordinateA - refCoordinateB));
distancesToCompare.push(Math.abs(siblingCoordinateB - refCoordinateA));
distancesToCompare.push(Math.abs(siblingCoordinateB - refCoordinateB));
// 3. Return the smallest distance between the edges
return Math.min(minEdgeDistance, maxEdgeDistance);
}

// Default to corners
const distancesToCompare = [
Math.abs(siblingCoordinateA - refCoordinateA),
Math.abs(siblingCoordinateA - refCoordinateB),
Math.abs(siblingCoordinateB - refCoordinateA),
Math.abs(siblingCoordinateB - refCoordinateB)
];
return Math.min(...distancesToCompare);
}

Expand Down Expand Up @@ -478,12 +533,16 @@ class SpatialNavigationService {
const primaryAxisDistance = primaryAxisFunction(
refCorners,
siblingCorners,
isVerticalDirection
isVerticalDirection,
this.distanceCalculationMethod,
this.customDistanceCalculationFunction
);
const secondaryAxisDistance = secondaryAxisFunction(
refCorners,
siblingCorners,
isVerticalDirection
isVerticalDirection,
this.distanceCalculationMethod,
this.customDistanceCalculationFunction
);

/**
Expand Down Expand Up @@ -592,6 +651,8 @@ class SpatialNavigationService {
this.visualDebugger = null;

this.logIndex = 0;

this.distanceCalculationMethod = 'corners';
}

init({
Expand All @@ -604,7 +665,9 @@ class SpatialNavigationService {
shouldFocusDOMNode = false,
domNodeFocusOptions = {},
shouldUseNativeEvents = false,
rtl = false
rtl = false,
distanceCalculationMethod = 'corners' as DistanceCalculationMethod,
customDistanceCalculationFunction = undefined as DistanceCalculationFunction
} = {}) {
if (!this.enabled) {
this.domNodeFocusOptions = domNodeFocusOptions;
Expand All @@ -615,6 +678,9 @@ class SpatialNavigationService {
this.shouldFocusDOMNode = shouldFocusDOMNode && !nativeMode;
this.shouldUseNativeEvents = shouldUseNativeEvents;
this.writingDirection = rtl ? WritingDirection.RTL : WritingDirection.LTR;
this.distanceCalculationMethod = distanceCalculationMethod;
this.customDistanceCalculationFunction =
customDistanceCalculationFunction;

this.debug = debug;

Expand Down

0 comments on commit 0058b0f

Please sign in to comment.