Skip to content
This repository was archived by the owner on Mar 4, 2020. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

### Fixes
- Fix endMedia to not be removed from DOM on mouseleave for `ListItem` @musingh1 ([#278](https://github.com/stardust-ui/react/pull/278))
- Fix focus behavior for `List` @kuzhelov ([#413](https://github.com/stardust-ui/react/pull/413))
- Remove `Sizes` and `Weights` enums, use typed string in `Text` instead @jurokapsiar ([#446](https://github.com/stardust-ui/react/pull/446))

### Features
Expand Down
83 changes: 66 additions & 17 deletions src/components/List/List.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,29 @@
import * as _ from 'lodash'
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import * as PropTypes from 'prop-types'

import { customPropTypes, childrenExist, UIComponent } from '../../lib'
import ListItem from './ListItem'
import { listBehavior } from '../../lib/accessibility'
import { Accessibility, AccessibilityActionHandlers } from '../../lib/accessibility/types'
import {
ContainerFocusHandler,
FocusContainerProps,
FocusContainerState,
} from '../../lib/accessibility/FocusHandling/FocusContainer'
import { ContainerFocusHandler } from '../../lib/accessibility/FocusHandling/FocusContainer'

import { ComponentVariablesInput, ComponentSlotStyle } from '../../themes/types'
import {
Extendable,
ReactChildren,
ShorthandValue,
ShorthandRenderFunction,
ShorthandValue,
} from '../../../types/utils'

export interface ListProps extends FocusContainerProps<ShorthandValue> {
export interface ListProps {
accessibility?: Accessibility
as?: any
children?: ReactChildren
className?: string
debug?: boolean
items?: ShorthandValue[]
listRef?: (node: HTMLElement) => void
selection?: boolean
truncateContent?: boolean
Expand All @@ -35,10 +33,14 @@ export interface ListProps extends FocusContainerProps<ShorthandValue> {
variables?: ComponentVariablesInput
}

export interface ListState {
selectedItemIndex: number
}

/**
* A list displays a group of related content.
*/
class List extends UIComponent<Extendable<ListProps>, FocusContainerState> {
class List extends UIComponent<Extendable<ListProps>, ListState> {
static displayName = 'List'

static className = 'ui-list'
Expand Down Expand Up @@ -103,13 +105,34 @@ class List extends UIComponent<Extendable<ListProps>, FocusContainerState> {
// List props that are passed to each individual Item props
static itemProps = ['debug', 'selection', 'truncateContent', 'truncateHeader', 'variables']

private focusContainer = ContainerFocusHandler.create(this)
public state = {
selectedItemIndex: 0,
}

private focusHandler: ContainerFocusHandler = null
private itemRefs = []

private handleListRef = (listNode: HTMLElement) => {
_.invoke(this.props, 'listRef', listNode)
}

actionHandlers: AccessibilityActionHandlers = {
moveNext: this.focusContainer.moveNext.bind(this.focusContainer),
movePrevious: this.focusContainer.movePrevious.bind(this.focusContainer),
moveFirst: this.focusContainer.moveFirst.bind(this.focusContainer),
moveLast: this.focusContainer.moveLast.bind(this.focusContainer),
moveNext: e => {
e.preventDefault()
this.focusHandler.moveNext()
},
movePrevious: e => {
e.preventDefault()
this.focusHandler.movePrevious()
},
moveFirst: e => {
e.preventDefault()
this.focusHandler.moveFirst()
},
moveLast: e => {
e.preventDefault()
this.focusHandler.moveLast()
},
}

renderComponent({ ElementType, classes, accessibility, rest }) {
Expand All @@ -128,16 +151,42 @@ class List extends UIComponent<Extendable<ListProps>, FocusContainerState> {
)
}

private handleListRef = (listNode: HTMLElement) => {
_.invoke(this.props, 'listRef', listNode)
componentDidMount() {
this.focusHandler = new ContainerFocusHandler(
() => this.props.items.length,
index => {
this.setState({ selectedItemIndex: index }, () => {
const targetComponent = this.itemRefs[index] && this.itemRefs[index].current
const targetDomNode = ReactDOM.findDOMNode(targetComponent) as any

targetDomNode && targetDomNode.focus()
})
},
)
}

renderItems() {
const { items, renderItem } = this.props
const itemProps = _.pick(this.props, List.itemProps)
const { selectedItemIndex } = this.state

this.itemRefs = []

return _.map(items, (item, idx) => {
itemProps.focusableItemProps = this.focusContainer.createItemProps(idx, items.length)
const maybeSelectableItemProps = {} as any

if (this.props.selection) {
const ref = React.createRef()
this.itemRefs[idx] = ref

maybeSelectableItemProps.tabIndex = idx === selectedItemIndex ? 0 : -1
maybeSelectableItemProps.ref = ref
maybeSelectableItemProps.onFocus = () => this.focusHandler.syncFocusedItemIndex(idx)
}

const itemProps = {
..._.pick(this.props, List.itemProps),
...maybeSelectableItemProps,
}

return ListItem.create(item, {
defaultProps: itemProps,
Expand Down
7 changes: 0 additions & 7 deletions src/components/List/ListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { createShorthandFactory, customPropTypes, UIComponent } from '../../lib'
import ItemLayout from '../ItemLayout/ItemLayout'
import { listItemBehavior } from '../../lib/accessibility'
import { Accessibility } from '../../lib/accessibility/types'
import { FocusableItemProps } from '../../lib/accessibility/FocusHandling/FocusableItem'
import { ComponentVariablesInput, ComponentSlotStyle } from '../../themes/types'
import { Extendable } from '../../../types/utils'

Expand All @@ -16,7 +15,6 @@ export interface ListItemProps {
contentMedia?: any
content?: any
debug?: boolean
focusableItemProps?: FocusableItemProps
header?: any
endMedia?: any
headerMedia?: any
Expand Down Expand Up @@ -96,11 +94,6 @@ class ListItem extends UIComponent<Extendable<ListItemProps>, ListItemState> {

private itemRef = React.createRef<HTMLElement>()

componentDidUpdate() {
// This needs to be as part of issue https://github.com/stardust-ui/react/issues/370
// this.focusableItem.tryFocus(ReactDOM.findDOMNode(this.itemRef.current) as HTMLElement)
}

renderComponent({ ElementType, classes, accessibility, rest, styles }) {
const {
as,
Expand Down
92 changes: 34 additions & 58 deletions src/lib/accessibility/FocusHandling/FocusContainer.ts
Original file line number Diff line number Diff line change
@@ -1,92 +1,68 @@
import * as _ from 'lodash'
import { FocusableItemProps, SetStateDelegate } from './FocusableItem'

export interface FocusContainerProps<T> {
items?: T[]
}
export class ContainerFocusHandler {
private focusedItemIndex = 0

export interface FocusContainerState {
focusItemOnIdx: number
}
constructor(private getItemsCount: () => number, private readonly setFocusAt: (number) => void) {}

private noItems = (): boolean => this.getItemsCount() === 0

private constrainFocusedItemIndex(): void {
if (this.focusedItemIndex < 0) {
this.focusedItemIndex = 0
}

export class ContainerFocusHandler<
T,
P extends FocusContainerProps<T>,
S extends FocusContainerState
> {
constructor(
private getProps: () => P,
private setState: SetStateDelegate<P, S>,
private initState: (state: FocusContainerState) => void,
private getState: () => S,
) {
this.initState({ focusItemOnIdx: 0 } as S)
const itemsCount = this.getItemsCount()
if (this.focusedItemIndex >= itemsCount) {
this.focusedItemIndex = itemsCount - 1
}
}

public static create<T, P extends FocusContainerProps<T>, S extends FocusContainerState>(
component: React.Component<P, S>,
): ContainerFocusHandler<T, P, S> {
return new this(
() => component.props,
component.setState.bind(component),
(state: S) => {
component.state = _.assign(component.state, state)
},
() => component.state,
)
public getFocusedItemIndex(): number {
return this.focusedItemIndex
}

public createItemProps(idx: number, itemsLength: number): FocusableItemProps {
return {
isFocused: idx === this.getState().focusItemOnIdx && this.getState().focusItemOnIdx !== -1,
isFirstElement: idx === 0,
isLastElement: idx === itemsLength - 1,
}
public syncFocusedItemIndex(withCurrentIndex: number) {
this.focusedItemIndex = withCurrentIndex
}

public movePrevious(): void {
if (this.getState().focusItemOnIdx <= 0) {
if (this.noItems()) {
return
}

this.setState(prev => {
return { focusItemOnIdx: prev.focusItemOnIdx - 1 }
})
this.focusedItemIndex -= 1
this.constrainFocusedItemIndex()

this.setFocusAt(this.focusedItemIndex)
}

public moveNext(): void {
if (
!this.getProps().items ||
this.getState().focusItemOnIdx >= this.getProps().items.length - 1
) {
if (this.noItems()) {
return
}

this.setState(prev => {
return { focusItemOnIdx: prev.focusItemOnIdx + 1 }
})
this.focusedItemIndex += 1
this.constrainFocusedItemIndex()

this.setFocusAt(this.focusedItemIndex)
}

public moveFirst(): void {
if (this.getState().focusItemOnIdx === 0) {
if (this.noItems()) {
return
}

this.setState({
focusItemOnIdx: 0,
})
this.focusedItemIndex = 0
this.setFocusAt(this.focusedItemIndex)
}

public moveLast(): void {
if (
!this.getProps().items ||
this.getState().focusItemOnIdx === this.getProps().items.length - 1
) {
if (this.noItems()) {
return
}

this.setState({
focusItemOnIdx: this.getProps().items.length - 1,
})
this.focusedItemIndex = this.getItemsCount() - 1
this.setFocusAt(this.focusedItemIndex)
}
}
29 changes: 0 additions & 29 deletions src/lib/accessibility/FocusHandling/FocusableItem.ts

This file was deleted.

1 change: 1 addition & 0 deletions src/lib/getKeyDownHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const getKeyDownHandlers = (
componentActionHandlers[actionName],
componentPartKeyAction[actionName].keyCombinations,
)

eventHandler && eventHandler(event)
})

Expand Down
Loading