Skip to content

Commit

Permalink
feature: new useLeafFocusedNode
Browse files Browse the repository at this point in the history
Works using focusedNodeId from focusStore and passing it down to useFocusNode hook
  • Loading branch information
Daniele Lubrano committed Feb 9, 2022
1 parent c0b0794 commit da852e3
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 0 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ This library has the following peer dependencies:
- [\<FocusRoot/\>](#focusroot-)
- [\<FocusNode/\>](#FocusNode-)
- [useFocusNode()](#usefocusnode-focusid-)
- [useLeafFocusedNode()](#useleaffocusednode)
- [useActiveNode()](#useactivenode)
- [useSetFocus()](#usesetfocus)
- [useNodeEvents()](#usenodeevents-focusid-events-)
Expand Down Expand Up @@ -208,6 +209,21 @@ export default function MyComponent() {
}
```
### `useLeafFocusedNode()`
A [Hook](https://reactjs.org/docs/hooks-intro.html) that returns the currently in focus leaf node. It reflects the focus state in every moment, therefore if there are no valid focusable nodes,
then the root node will be returned instead.
```js
import { useLeafFocusedNode } from '@please/lrud';

export default function MyComponent() {
const currentFocusedNode = useLeafFocusedNode();

console.log('Currently focused node', currentFocusedNode);
}
```
### `useActiveNode()`
A [Hook](https://reactjs.org/docs/hooks-intro.html) that returns the active focus node, or `null` if no node is active.
Expand Down
118 changes: 118 additions & 0 deletions src/hooks/use-leaf-focused-node.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import React from 'react';
import '@testing-library/jest-dom';
import { render, act } from '@testing-library/react';
import {
FocusRoot,
FocusNode,
useLeafFocusedNode,
useSetFocus,
useFocusStoreDangerously,
} from '../index';
import { warning } from '../utils/warning';

describe('useLeafFocusedNode', () => {
it('warns when there is no FocusRoot', () => {
let focusNode;
function TestComponent() {
focusNode = useLeafFocusedNode();

return <div />;
}

render(<TestComponent />);

expect(focusNode).toEqual(null);
expect(warning).toHaveBeenCalledTimes(2);
expect(warning.mock.calls[0][1]).toEqual('NO_FOCUS_PROVIDER_DETECTED');
});

it('returns the expected node', () => {
let setFocus;
let focusNode;
let focusStore;

function TestComponent() {
setFocus = useSetFocus();
focusNode = useLeafFocusedNode();
focusStore = useFocusStoreDangerously();

return (
<>
<FocusNode focusId="nodeA" data-testid="nodeA" />
<FocusNode focusId="nodeB" data-testid="nodeB" />
</>
);
}

render(
<FocusRoot>
<TestComponent />
</FocusRoot>
);

expect(focusNode).toBe(focusStore.getState().nodes.nodeA);

expect(focusNode).toEqual(
expect.objectContaining({
focusId: 'nodeA',
isFocused: true,
isFocusedLeaf: true,
})
);

expect(focusStore.getState().nodes.nodeB).toEqual(
expect.objectContaining({
focusId: 'nodeB',
isFocused: false,
isFocusedLeaf: false,
})
);

act(() => setFocus('nodeB'));

expect(focusNode).toBe(focusStore.getState().nodes.nodeB);

expect(focusStore.getState().nodes.nodeA).toEqual(
expect.objectContaining({
focusId: 'nodeA',
isFocused: false,
isFocusedLeaf: false,
})
);

expect(focusNode).toEqual(
expect.objectContaining({
focusId: 'nodeB',
isFocused: true,
isFocusedLeaf: true,
})
);
});

it('returns the root node if there are no focusable nodes (default focus state behavior)', () => {
let focusNode;
let focusStore;

function TestComponent() {
focusNode = useLeafFocusedNode();
focusStore = useFocusStoreDangerously();

return (
<>
<div>No Focusable</div>
</>
);
}

render(
<FocusRoot>
<TestComponent />
</FocusRoot>
);
const focusRootNode = Object.values(focusStore.getState().nodes).find(
(n) => n.isRoot
);

expect(focusNode).toEqual(focusRootNode);
});
});
56 changes: 56 additions & 0 deletions src/hooks/use-leaf-focused-node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { useContext, useState, useEffect, useRef } from 'react';
import FocusContext from '../focus-context';
import { warning } from '../utils/warning';
import { Node } from '../types';
import { useFocusNode } from '..';

export default function useFocusedNode(): Node | null {
const contextValue = useContext(FocusContext.Context);
const [focusNodeId, setFocusNodeId] = useState<string>(() => {
if (!contextValue) {
if (process.env.NODE_ENV !== 'production') {
warning(
'A FocusProvider was not found in the tree. Did you forget to mount it?',
'NO_FOCUS_PROVIDER_DETECTED'
);
}

return '';
}

const focusState = contextValue.store.getState();
return focusState.focusedNodeId;
});

const focusNodeIdRef = useRef(focusNodeId);
focusNodeIdRef.current = focusNodeId;

function checkForSync() {
if (!contextValue) {
return;
}

const currentNodeId = contextValue.store.getState().focusedNodeId;
if (currentNodeId !== focusNodeIdRef.current) {
setFocusNodeId(currentNodeId);
}
}

useEffect(checkForSync, [focusNodeId]);

useEffect(() => {
if (!contextValue) {
return;
}

checkForSync();
const unsubscribe = contextValue.store.subscribe(checkForSync);

return () => {
unsubscribe();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const focusNode = useFocusNode(focusNodeId);
return focusNodeId ? focusNode : null;
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import FocusContext from './focus-context';
export { default as FocusNode } from './focus-node';
export { default as useFocusNode } from './hooks/use-focus-node';
export { default as useLeafFocusedNode } from './hooks/use-leaf-focused-node';
export { default as useActiveNode } from './hooks/use-active-node';
export { default as useFocusHierarchy } from './hooks/use-focus-hierarchy';
export { default as useFocusStoreDangerously } from './hooks/use-focus-store-dangerously';
Expand Down

0 comments on commit da852e3

Please sign in to comment.