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
Show all changes
46 commits
Select commit Hold shift + click to select a range
9e2629a
added behaviors and listlike navigation
silviuaavram May 10, 2019
97eaee4
added circular focus management
silviuaavram May 10, 2019
6b0eddf
added performClick
silviuaavram May 10, 2019
e643960
begin refactoring the HTML structure of accordion
silviuaavram May 13, 2019
c36b7dd
applied workaround un RefFindNode
silviuaavram May 13, 2019
52ee602
fixed keyboard navigation
silviuaavram May 13, 2019
53ea728
added alwaysActive and some aria attributes
silviuaavram May 14, 2019
185b2b4
add content behavior and relationships
silviuaavram May 14, 2019
0ce30d4
Merge branch 'master' into feat-accordion-behavior
silviuaavram May 14, 2019
4e8745b
Merge branch 'master' into feat-accordion-behavior
silviuaavram May 14, 2019
dedae21
updated changelog
silviuaavram May 14, 2019
e03f5fc
addressed code review
silviuaavram May 15, 2019
0298914
extracted navigation focus logic in handlers
silviuaavram May 15, 2019
3091cf4
changed button to box
silviuaavram May 15, 2019
b1c3b5f
reverted some changes from merge conflict
silviuaavram May 15, 2019
2bdaffa
changed prop name in changelog
silviuaavram May 15, 2019
6c68fbd
added tests to behaviors and removed region role
silviuaavram May 16, 2019
15d0e4f
added state interface to accordion
silviuaavram May 16, 2019
9e7716e
some typings to dropdown tests
silviuaavram May 16, 2019
f7cc42d
unit tests for activeIndex
silviuaavram May 16, 2019
ef7e6e1
changed setState calls in handlers
silviuaavram May 16, 2019
ec3e744
corrected a setState call
silviuaavram May 16, 2019
0dfea89
added slot classnames to title
silviuaavram May 16, 2019
91eba41
added more functional unit tests
silviuaavram May 16, 2019
a4bb5a1
added accessibility test to Accordion
silviuaavram May 16, 2019
9104e0b
exported Title and Content
silviuaavram May 16, 2019
c3efb0d
removed the button condition
silviuaavram May 16, 2019
5857763
added AccordionTitle tests
silviuaavram May 16, 2019
ddca492
created accordionContent tests
silviuaavram May 16, 2019
05af4bf
fixed ref warnings in tests
silviuaavram May 16, 2019
6222141
Merge branch 'master' into feat-accordion-behavior
silviuaavram May 16, 2019
021b0ee
addressed some unit tests comments
silviuaavram May 17, 2019
7722a3e
addressed prop renames
silviuaavram May 17, 2019
058a248
Merge branch 'feat-accordion-behavior' of https://github.com/stardust…
silviuaavram May 17, 2019
55fbbd1
removed ReactDOM from focus handler
silviuaavram May 17, 2019
3f8741e
updated changelog
silviuaavram May 17, 2019
6bb6002
finished renaming
silviuaavram May 17, 2019
8c746b1
changed button to content in title behavior
silviuaavram May 17, 2019
801b4a2
changed import to relative
silviuaavram May 20, 2019
34e08a8
removed a11y tests from component
silviuaavram May 20, 2019
eface1b
Merge branch 'master' into feat-accordion-behavior
silviuaavram May 20, 2019
bf52413
removed margins added by description list
May 21, 2019
3877b36
Merge branch 'feat-accordion-behavior' of https://github.com/stardust…
May 21, 2019
be9e021
removed the Box from the title DOM
silviuaavram May 22, 2019
1786089
Merge branch 'master' into feat-accordion-behavior
silviuaavram May 22, 2019
0f836c7
moved changes in changelog
silviuaavram May 22, 2019
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## [Unreleased]

### Features
- Add keyboard navigation and screen reader support for `Accordion` @silviuavram ([#1322](https://github.com/stardust-ui/react/pull/1322))
- Add `expanded` prop to `Accordion` @silviuavram ([#1322](https://github.com/stardust-ui/react/pull/1322))

<!--------------------------------[ v0.31.0 ]------------------------------- -->
## [v0.31.0](https://github.com/stardust-ui/react/tree/v0.31.0) (2019-05-21)
[Compare changes](https://github.com/stardust-ui/react/compare/v0.30.0...v0.31.0)
Expand Down
138 changes: 123 additions & 15 deletions packages/react/src/components/Accordion/Accordion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ import {
ChildrenComponentProps,
commonPropTypes,
rtlTextContainer,
applyAccessibilityKeyHandlers,
} from '../../lib'
import AccordionTitle from './AccordionTitle'
import { accordionBehavior } from '../../lib/accessibility'
import AccordionTitle, { AccordionTitleProps } from './AccordionTitle'
import AccordionContent from './AccordionContent'
import { defaultBehavior } from '../../lib/accessibility'
import { Accessibility } from '../../lib/accessibility/types'
import { Accessibility, AccessibilityActionHandlers } from '../../lib/accessibility/types'

import {
ComponentEventHandler,
Expand All @@ -23,6 +24,7 @@ import {
ShorthandRenderFunction,
withSafeTypeForAs,
} from '../../types'
import { ContainerFocusHandler } from '../../lib/accessibility/FocusHandling/FocusContainer'

export interface AccordionSlotClassNames {
content: string
Expand All @@ -39,6 +41,9 @@ export interface AccordionProps extends UIComponentProps, ChildrenComponentProps
/** Only allow one panel open at a time. */
exclusive?: boolean

/** At least one panel should be expanded at any time. */
expanded?: boolean

/**
* Called when a panel title is clicked.
*
Expand Down Expand Up @@ -76,7 +81,12 @@ export interface AccordionProps extends UIComponentProps, ChildrenComponentProps
accessibility?: Accessibility
}

class Accordion extends AutoControlledComponent<WithAsProp<AccordionProps>, any> {
export interface AccordionState {
activeIndex: number[] | number
focusedIndex: number
}

class Accordion extends AutoControlledComponent<WithAsProp<AccordionProps>, AccordionState> {
static displayName = 'Accordion'

static className = 'ui-accordion'
Expand All @@ -99,6 +109,7 @@ class Accordion extends AutoControlledComponent<WithAsProp<AccordionProps>, any>
PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.number), PropTypes.number]),
]),
exclusive: PropTypes.bool,
expanded: PropTypes.bool,
onTitleClick: customPropTypes.every([customPropTypes.disallow(['children']), PropTypes.func]),
panels: customPropTypes.every([
customPropTypes.disallow(['children']),
Expand All @@ -115,64 +126,160 @@ class Accordion extends AutoControlledComponent<WithAsProp<AccordionProps>, any>
}

public static defaultProps = {
accessibility: defaultBehavior as Accessibility,
accessibility: accordionBehavior,
as: 'dl',
}

static autoControlledProps = ['activeIndex']

static Title = AccordionTitle
static Content = AccordionContent

getInitialAutoControlledState({ exclusive }) {
return { activeIndex: exclusive ? -1 : [-1] }
private focusHandler: ContainerFocusHandler = null
private itemRefs = []

actionHandlers: AccessibilityActionHandlers = {
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()
},
}

constructor(props, context) {
super(props, context)

this.focusHandler = new ContainerFocusHandler(
this.getNavigationItemsSize,
this.handleNavigationFocus,
true,
)
}

private handleNavigationFocus = (index: number) => {
this.setState({ focusedIndex: index }, () => {
const targetComponent = this.itemRefs[index] && this.itemRefs[index].current
targetComponent && targetComponent.focus()
})
}

computeNewIndex = index => {
private getNavigationItemsSize = () => this.props.panels.length

getInitialAutoControlledState({ expanded, exclusive }: AccordionProps) {
const alwaysActiveIndex = expanded ? 0 : -1
return { activeIndex: exclusive ? alwaysActiveIndex : [alwaysActiveIndex] }
}

private computeNewIndex = (index: number): number | number[] => {
const { activeIndex } = this.state
const { exclusive } = this.props

if (!this.isIndexActionable(index)) {
return activeIndex
}

if (exclusive) return index === activeIndex ? -1 : index
// check to see if index is in array, and remove it, if not then add it
return _.includes(activeIndex, index) ? _.without(activeIndex, index) : [...activeIndex, index]
return _.includes(activeIndex as number[], index)
? _.without(activeIndex as number[], index)
: [...(activeIndex as number[]), index]
}

handleTitleOverrides = predefinedProps => ({
onClick: (e, titleProps) => {
private handleTitleOverrides = (predefinedProps: AccordionTitleProps) => ({
onClick: (e: React.SyntheticEvent, titleProps: AccordionTitleProps) => {
const { index } = titleProps
const activeIndex = this.computeNewIndex(index)

this.trySetState({ activeIndex })
this.setState({ focusedIndex: index })

_.invoke(predefinedProps, 'onClick', e, titleProps)
_.invoke(this.props, 'onTitleClick', e, titleProps)
},
onFocus: (e: React.SyntheticEvent, titleProps: AccordionTitleProps) => {
_.invoke(predefinedProps, 'onFocus', e, titleProps)
this.setState({ focusedIndex: predefinedProps.index })
},
})

isIndexActive = (index): boolean => {
private isIndexActive = (index: number): boolean => {
const { exclusive } = this.props
const { activeIndex } = this.state

return exclusive ? activeIndex === index : _.includes(activeIndex, index)
return exclusive ? activeIndex === index : _.includes(activeIndex as number[], index)
}

/**
* Checks if panel at index can be actioned upon. Used in the case of expanded accordion,
* when at least a panel needs to stay active. Will return false if expanded prop is true,
* index is active and either it's an exclusive accordion or if there are no other active
* panels open besides this one.
*
* @param {number} index The index of the panel.
* @returns {boolean} If the panel can be set active/inactive.
*/
private isIndexActionable = (index: number): boolean => {
if (!this.isIndexActive(index)) {
return true
}

const { activeIndex } = this.state
const { expanded, exclusive } = this.props

return !expanded || (!exclusive && (activeIndex as number[]).length > 1)
}

renderPanels = () => {
const children: any[] = []
const { panels, renderPanelContent, renderPanelTitle } = this.props
const { focusedIndex } = this.state

this.itemRefs = []
this.focusHandler.syncFocusedIndex(focusedIndex)

_.each(panels, (panel, index) => {
const { content, title } = panel
const active = this.isIndexActive(index)
const canBeCollapsed = this.isIndexActionable(index)
const contentRef = React.createRef<HTMLElement>()
const titleId = title['id'] || _.uniqueId('accordion-title-')
const contentId = content['id'] || _.uniqueId('accordion-content-')
this.itemRefs[index] = contentRef

children.push(
AccordionTitle.create(title, {
defaultProps: { className: Accordion.slotClassNames.title, active, index },
defaultProps: {
className: Accordion.slotClassNames.title,
active,
index,
contentRef,
canBeCollapsed,
id: titleId,
accordionContentId: contentId,
},
overrideProps: this.handleTitleOverrides,
render: renderPanelTitle,
}),
)
children.push(
AccordionContent.create(content, {
defaultProps: { className: Accordion.slotClassNames.content, active },
defaultProps: {
className: Accordion.slotClassNames.content,
active,
id: contentId,
accordionTitleId: titleId,
},
render: renderPanelContent,
}),
)
Expand All @@ -189,6 +296,7 @@ class Accordion extends AutoControlledComponent<WithAsProp<AccordionProps>, any>
{...accessibility.attributes.root}
{...rtlTextContainer.getAttributes({ forElements: [children] })}
{...unhandledProps}
{...applyAccessibilityKeyHandlers(accessibility.keyHandlers.root, unhandledProps)}
className={classes.root}
>
{childrenExist(children) ? children : this.renderPanels()}
Expand Down
21 changes: 19 additions & 2 deletions packages/react/src/components/Accordion/AccordionContent.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as PropTypes from 'prop-types'
import * as React from 'react'
import * as _ from 'lodash'

import {
childrenExist,
Expand All @@ -12,11 +13,15 @@ import {
rtlTextContainer,
} from '../../lib'
import { WithAsProp, ComponentEventHandler, withSafeTypeForAs } from '../../types'
import { accordionContentBehavior } from '../../lib/accessibility'

export interface AccordionContentProps
extends UIComponentProps,
ChildrenComponentProps,
ContentComponentProps {
/** Id of the title it belongs to. */
accordionTitleId?: string

/** Whether or not the content is visible. */
active?: boolean

Expand All @@ -38,16 +43,28 @@ class AccordionContent extends UIComponent<WithAsProp<AccordionContentProps>, an

static propTypes = {
...commonPropTypes.createCommon(),
accordionTitleId: PropTypes.string,
active: PropTypes.bool,
onClick: PropTypes.func,
}

renderComponent({ ElementType, classes, unhandledProps }) {
static defaultProps = {
accessibility: accordionContentBehavior,
as: 'dd',
}

private handleClick = (e: React.SyntheticEvent) => {
_.invoke(this.props, 'onClick', e, this.props)
}

renderComponent({ ElementType, classes, unhandledProps, accessibility }) {
const { children, content } = this.props

return (
<ElementType
onClick={this.handleClick}
{...rtlTextContainer.getAttributes({ forElements: [children, content] })}
{...accessibility.attributes.root}
{...unhandledProps}
className={classes.root}
>
Expand All @@ -63,6 +80,6 @@ AccordionContent.create = createShorthandFactory({
})

/**
* A standard AccordionContent.
* A standard AccordionContent that is used to display content hosted in an accordion.
*/
export default withSafeTypeForAs<typeof AccordionContent, AccordionContentProps>(AccordionContent)
Loading