Skip to content

Commit

Permalink
Merge pull request #35 from borisbelmar/develop
Browse files Browse the repository at this point in the history
2.0.0
  • Loading branch information
borisbelmar authored May 19, 2023
2 parents eeb7c61 + f23b5f0 commit 8417a47
Show file tree
Hide file tree
Showing 70 changed files with 1,554 additions and 633 deletions.
175 changes: 139 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![install size](https://packagephobia.com/badge?p=@arrow-navigation/core)](https://packagephobia.com/result?p=@arrow-navigation/core)

Light (~13kb) and zero-dependency module to navigate through elements using the arrow keys written in Typescript.
Light (~16kb) and zero-dependency module to navigate through elements using the arrow keys written in Typescript.

For live demo, [visit this url](https://arrow-navigation-demo.vercel.app/). For ReactJS implementation, check [@arrow-navigation/react](https://www.npmjs.com/package/@arrow-navigation/react).

Expand All @@ -26,7 +26,10 @@ At the top of your application, you need to initialize the module. This will add
import { initArrowNavigation } from '@arrow-navigation/core'

initArrowNavigation({
preventScroll: true // Prevent the default behavior of the arrow keys to scroll the page. The default value is true
preventScroll: true // Prevent the default behavior of the arrow keys to scroll the page. The default value is true,
disableWebListeners: false, // Disable the web listeners. The default value is false
adapter: webAdapter, // The adapter to use. The default value is webAdapter included in the package. You can create your own adapter to use the module in other platforms like React Native.
initialFocusElement: 'element-0-0' // The element to be focused when the elements has been registered. The default value is null
})
```

Expand Down Expand Up @@ -72,6 +75,18 @@ navigationApi.registerElement(buttonElement2)

Initialize the module. This will add the event listeners to the document and store the navigation state in a singleton instance.

## getElementIdByOrder

Retrieve the element ID in the same order as the library when use group byOrder. This functionality proves valuable when you need to manually control the focus.

```typescript
const api = getArrowNavigation()

// Set the focus to the first element of the group-0
const id = getElementIdByOrder('group-0', 0) // 'group-0-0'
api.setFocusedElement(id)
```

## getArrowNavigation

Get the navigation API. This will return an object with the following methods:
Expand All @@ -86,7 +101,7 @@ const container = document.createElement('div')
// Is important to keep a unique id for each group and his elements
container.id = 'group-0'

registerGroup(container): void
registerGroup(container.id): void
```

You can also pass a options object as the second parameter to customize the navigation behavior.
Expand All @@ -97,7 +112,7 @@ const container = document.createElement('div')
// Is important to keep a unique id for each group and his elements
container.id = 'group-0'

api.registerGroup(container, {
api.registerGroup(container.id, {
firstElement: 'element-0-0', // The first element to be focused when the focus enter the group
nextGroupByDirection: {
'down': 'group-1', // The next group when the user press the down arrow key
Expand All @@ -115,6 +130,26 @@ api.registerGroup(container, {
})
```

### resetGroupState

Reset the group state. This will reset states like lastElement from the group config. This is usefull when you are remounting the group, for example, a memory's route change.

```typescript

const container = document.createElement('div')
container.id = 'group-0'
api.registerGroup(container.id, { saveLast: true })

// ...Register all the elements considering element-0-0 as the first element
// ...Navigate to element-0-1

api.getGroupConfig('group-0').lastElement === 'element-0-1' // true

api.resetGroupState('group-0')

api.getGroupConfig('group-0').lastElement === undefined // true
```

### registerElement

Register an element to be able to navigate to it. The element must be inside a group.
Expand All @@ -127,7 +162,7 @@ const element = document.createElement('button')
// Is important to keep a unique id for each element
element.id = 'element-0-0'

api.registerElement(element, 'group-1')
api.registerElement(element.id, 'group-1')
```

You can also pass a options object as the third parameter to customize the navigation behavior.
Expand All @@ -140,7 +175,7 @@ const element = document.createElement('button')
// Is important to keep a unique id for each element
element.id = 'element-0-0'

api.registerElement(element, 'group-1', {
api.registerElement(element.id, 'group-1', {
nextByDirection: { // This will set the next element manually
'down': 'element-0-1', // The next element when the user press the down arrow key
'right': { id: 'group-1', kind: 'group' }, // The next group when the user press the right arrow key
Expand All @@ -166,10 +201,10 @@ const container = document.createElement('div')
container.id = 'group-0'
element.id = 'element-0-0'

api.registerGroup(container)
api.registerElement(element, 'group-0')
api.registerGroup(container.id)
api.registerElement(element.id, 'group-0')

api.unregisterElement(element)
api.unregisterElement(element.id)
```

### getFocusedElement
Expand All @@ -185,8 +220,8 @@ const container = document.createElement('div')
container.id = 'group-0'
element.id = 'element-0-0'

api.registerGroup(container)
api.registerElement(element, 'group-0')
api.registerGroup(container.id)
api.registerElement(element.id, 'group-0')

const focusedElement = api.getFocusedElement()
```
Expand All @@ -207,15 +242,31 @@ container.id = 'group-0'
element.id = 'element-0-0'
element2.id = 'element-0-1'

api.registerGroup(container)
api.registerElement(element, 'group-0')
api.registerElement(element2, 'group-0')
api.registerGroup(container.id)
api.registerElement(element.id, container.id)
api.registerElement(element2.id, container.id)

api.setFocusedElement('element-0-1')

document.activeElement.id === element2.id // true
```

### setInitialFocusElement

Set the initial focus element. This will be the element focused when the elements has been registered.

```typescript
const api = getArrowNavigation()

//... Register all the elements

api.setInitialFocusElement('element-0-1')

// Wait for 500ms to be sure that the focus has been setted

document.activeElement.id === 'element-0-1' // true
```

### destroy

Destroy the module. This will remove the event listeners from the document and remove the navigation state from the singleton instance.
Expand All @@ -240,8 +291,8 @@ const container2 = document.createElement('div')
container.id = 'group-0'
container2.id = 'group-1'

api.registerGroup(container)
api.registerGroup(container2)
api.registerGroup(container.id)
api.registerGroup(container2.id)

const currentGroups = api.getCurrentGroups() // Set { 'group-0', 'group-1' }
```
Expand All @@ -262,9 +313,9 @@ container.id = 'group-0'
element.id = 'element-0-0'
element2.id = 'element-0-1'

api.registerGroup(container)
api.registerElement(element, 'group-0')
api.registerElement(element2, 'group-0')
api.registerGroup(container.id)
api.registerElement(element.id, container.id)
api.registerElement(element2.id, container.id)

const groupElements = api.getGroupElements('group-0') // Set { 'element-0-0', 'element-0-1' }
```
Expand All @@ -281,7 +332,7 @@ const container = document.createElement('div')
// Is important to keep a unique id for each group and his elements
container.id = 'group-0'

api.registerGroup(container)
api.registerGroup(container.id)

const groupConfig = api.getGroupConfig('group-0') // { viewportSafe: true, threshold: 0, keepFocus: false }
```
Expand All @@ -303,9 +354,9 @@ container.id = 'group-0'
element.id = 'element-0-0'
element2.id = 'element-0-1'

api.registerGroup(container)
api.registerElement(element, 'group-0')
api.registerElement(element2, 'group-0')
api.registerGroup(container.id)
api.registerElement(element.id, 'group-0')
api.registerElement(element2.id, 'group-0')

const registeredElements = api.getRegisteredElements() // Set { 'element-0-0', 'element-0-1' }
```
Expand All @@ -327,9 +378,9 @@ container.id = 'group-0'
element.id = 'element-0-0'
element2.id = 'element-0-1'

api.registerGroup(container)
api.registerElement(element, 'group-0')
api.registerElement(element2, 'group-0')
api.registerGroup(container.id)
api.registerElement(element, container.id)
api.registerElement(element2, container.id)

const registeredElements = api.getNextElement({ direction: 'right', inGroup: true }) // 'element-0-1'
// or
Expand Down Expand Up @@ -360,18 +411,42 @@ container2.id = 'group-1'
element3.id = 'element-1-0'
element4.id = 'element-1-1'

api.registerGroup(container)
api.registerGroup(container2)
api.registerElement(element, 'group-0')
api.registerElement(element2, 'group-0')
api.registerElement(element3, 'group-1')
api.registerElement(element4, 'group-1')
api.registerGroup(container.id)
api.registerGroup(container2.id)
api.registerElement(element.id, container.id)
api.registerElement(element2.id, container.id)
api.registerElement(element3.id, container2.id)
api.registerElement(element4.id, container2.id)

const nextGroup = api.getNextGroup({ direction: 'down' }) // 'group-1'
// or
const nextGroup = api.getNextGroup({ groupId: 'group-0', direction: 'down' }) // 'group-1'
```

### handleDirectionPress

Handle the arrow key press. This is useful if you want to handle the arrow key press manually or React Native. The first parameter is the direction and the second parameter is a boolean to specify is a repeated key press, for example, when the user keep the key pressed. The default value is false.

```typescript
const api = getArrowNavigation()

const container = document.createElement('div')
const element = document.createElement('button')
const element2 = document.createElement('button')

// Is important to keep a unique id for each group and his elements

container.id = 'group-0'
element.id = 'element-0-0'
element2.id = 'element-0-1'

api.registerGroup(container.id)
api.registerElement(element.id, container.id)
api.registerElement(element2.id, container.id)

api.handleDirectionPress('right', false)
```

## Events

The API implements an Event Emitter to listen to events. The events are accessible through the `on` and `off` methods. All the events can be accesed through the `ArrowNavigationEvents` enum.
Expand All @@ -386,19 +461,19 @@ This event is triggered when the current group is changed. The event will receiv

### element:focus

This event is triggered when an element is focused. The event will receive `(currentElement, direction, prevElement)`.
This event is triggered when an element is focused. The event will receive `({ current, direction, prev })`.

### element:blur

This event is triggered when an element is blurred. The event will receive `(currentElement, direction, nextElement)`.
This event is triggered when an element is blurred. The event will receive `({ current, direction, next })`.

### group:focus

This event is triggered when a group is focused. The event will receive `(currentGroup, direction, prevGroup)`.
This event is triggered when a group is focused. The event will receive `({ current, direction, prev })`.

### group:blur

This event is triggered when a group is blurred. The event will receive `(currentGroup, direction, nextGroup)`.
This event is triggered when a group is blurred. The event will receive `({ current, direction, next })`.

### groups:change

Expand All @@ -412,6 +487,10 @@ This event is triggered when the elements are changed. The event will receive th

This event is triggered when the groups configuration is changed. The event will receive the groups configuration as a parameter.

### elements:register-end

This event is triggered when the elements are registered. The event will not receive any parameter.

# Using with CDN

You can use the module with a CDN. The module is available in the following URL:
Expand All @@ -430,3 +509,27 @@ You can use the module with a CDN. The module is available in the following URL:
</script>
```

# Using with React Native

You can use the module with React Native (Experimental). You need to create an adapter to use the module in React Native. The adapter is a simple object with the following methods:

```typescript
type Adapter = {
type: 'web' | 'react-native'
getNodeRect: (focusable: FocusableElement | FocusableGroupConfig) => Rect
isNodeDisabled: (focusable: FocusableElement) => boolean
focusNode: (focusable: FocusableElement, opts?: FocusNodeOptions) => void
isNodeFocusable: (focusable: FocusableElement) => boolean
getNodeRef: (focusable: Focusable) => unknown // TextInput / View / TouchableOpacity / TouchableHighlight
}
```
You can use the `handleDirectionPress` method on API with TVHandler from React Native to handle the arrow key press manually. We will release a React Native package soon.
# Contributing
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. Keep the coverage at +95% and run `yarn test` before commit.
# License
[MIT](https://choosealicense.com/licenses/mit/)
20 changes: 13 additions & 7 deletions example/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,26 @@ window.arrowNavigation.init({ debug: true })

const arrowNavigationApi = window.arrowNavigation.get()

arrowNavigationApi.setInitialFocusElement('group-1-button-0')

const app = document.getElementById('app')

const group0Container = document.createElement('container')
app.appendChild(group0Container)
group0Container.setAttribute('id', 'group-0')
group0Container.classList.add('flex', 'flex-col', 'justify-center', 'items-center', 'h-full', 'p-4', 'bg-gray-600', 'gap-4')
arrowNavigationApi.registerGroup(group0Container)
arrowNavigationApi.registerGroup(group0Container.id, {
arrowDebounce: false
})

Array.from(Array(6).keys()).forEach(index => {
const button = document.createElement('button')
group0Container.appendChild(button)
button.setAttribute('id', `group-0-button-${index}`)
const id = `group-0-button-${index}`
button.setAttribute('id', id)
button.classList.add('bg-blue-500', 'text-white', 'w-16', 'h-16', 'rounded', 'focus:outline-none', 'flex', 'focus:bg-yellow-500', 'justify-center', 'items-center', 'text-2xl', 'font-bold', 'disabled:opacity-50')
button.innerText = index
arrowNavigationApi.registerElement(button, 'group-0')
arrowNavigationApi.registerElement(id, 'group-0')
})

// Other Groups container
Expand All @@ -30,7 +35,7 @@ const generateRightGroup = (groupIdx, qty, disabled) => {
const groupContainer = document.createElement('container')
rightSideContainer.appendChild(groupContainer)
groupContainer.setAttribute('id', groupId)
arrowNavigationApi.registerGroup(groupContainer)
arrowNavigationApi.registerGroup(groupId)
groupContainer.classList.add('flex', 'flex-row', 'justify-start', 'items-center', 'p-4', 'bg-gray-500', 'gap-4')

Array.from(Array(qty).keys()).forEach(elementIndex => {
Expand All @@ -39,18 +44,19 @@ const generateRightGroup = (groupIdx, qty, disabled) => {
if (disabled) {
button.setAttribute('disabled', true)
}
button.setAttribute('id', `group-${groupIdx}-button-${elementIndex}`)
const id = `group-${groupIdx}-button-${elementIndex}`
button.setAttribute('id', id)
button.classList.add('bg-blue-500', 'text-white', 'w-32', 'h-16', 'rounded', 'focus:outline-none', 'flex', 'focus:bg-yellow-500', 'justify-center', 'items-center', 'text-2xl', 'font-bold', 'disabled:opacity-50')
button.innerText = elementIndex
arrowNavigationApi.registerElement(button, groupId)
arrowNavigationApi.registerElement(id, groupId)
})
}

generateRightGroup(1, 5)
generateRightGroup(2, 3)
generateRightGroup(3, 4)
generateRightGroup(4, 2)
generateRightGroup(5, 1, true)
generateRightGroup(5, 3, true)
generateRightGroup(6, 5)

document.getElementById('group-0-button-3').setAttribute('disabled', true)
Expand Down
Loading

0 comments on commit 8417a47

Please sign in to comment.