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 @@ -20,6 +20,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
### BREAKING CHANGES
- Use regular components instead of `Label` in `RadioGroupItem` @layershifter ([#1070](https://github.com/stardust-ui/react/pull/1070))
- Remove `Flex.Gap` component, and convert the `gap` styles to `margins` on the child elements of the `Flex` component @mnajdova ([#1074](https://github.com/stardust-ui/react/pull/1074))
- Dropdown: control highlightedIndex from Dropdown @silviuavram ([#966](https://github.com/stardust-ui/react/pull/966))

### Fixes
- Add aria posinset and setsize, hide menu indicator from narration @jurokapsiar ([#1066](https://github.com/stardust-ui/react/pull/1066))
Expand Down
2 changes: 2 additions & 0 deletions build/gulp/tasks/test-projects/rollup/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export default {
'ArrowUp',
'ArrowLeft',
'ArrowRight',
'Backspace',
'Delete',
'End',
'Enter',
'Escape',
Expand Down
2 changes: 1 addition & 1 deletion packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"dependencies": {
"@stardust-ui/react-proptypes": "^0.23.1",
"classnames": "^2.2.5",
"downshift": "^3.2.0",
"downshift": "^3.2.6",
"fela": "^10.2.0",
"fela-plugin-fallback-value": "^10.2.0",
"fela-plugin-placeholder-prefixer": "^10.2.0",
Expand Down
106 changes: 89 additions & 17 deletions packages/react/src/components/Dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react'
import * as PropTypes from 'prop-types'
import * as _ from 'lodash'
import cx from 'classnames'
import * as keyboardKey from 'keyboard-key'

import {
Extendable,
Expand Down Expand Up @@ -29,7 +30,6 @@ import {
handleRef,
UIComponentProps,
} from '../../lib'
import keyboardKey from 'keyboard-key'
import Indicator, { IndicatorProps } from '../Indicator/Indicator'
import List from '../List/List'
import Ref from '../Ref/Ref'
Expand Down Expand Up @@ -67,6 +67,9 @@ export interface DropdownProps extends UIComponentProps<DropdownProps, DropdownS
/** Initial value for 'open' in uncontrolled mode */
defaultOpen?: boolean

/** The initial value for the index of the list item to be highlighted. */
defaultHighlightedIndex?: number

/** The initial value for the search query, if the dropdown is also a search. */
defaultSearchQuery?: string

Expand Down Expand Up @@ -96,6 +99,12 @@ export interface DropdownProps extends UIComponentProps<DropdownProps, DropdownS
*/
getA11yStatusMessage?: (options: DownshiftA11yStatusMessageOptions<ShorthandValue>) => string

/** A dropdown can open with the first option already highlighted. */
highlightFirstItemOnOpen?: boolean

/** The index of the list item to be highlighted. */
highlightedIndex?: number

/** A dropdown can be formatted to appear inline in the content of other components. */
inline?: boolean

Expand Down Expand Up @@ -187,12 +196,12 @@ export interface DropdownProps extends UIComponentProps<DropdownProps, DropdownS
}

export interface DropdownState {
activeSelectedIndex: number
a11ySelectionStatus: string
defaultHighlightedIndex: number
activeSelectedIndex: number
focused: boolean
open: boolean
searchQuery: string
highlightedIndex: number
value: ShorthandValue | ShorthandCollection
}

Expand Down Expand Up @@ -225,6 +234,7 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
clearIndicator: customPropTypes.itemShorthand,
defaultActiveSelectedIndex: PropTypes.number,
defaultOpen: PropTypes.bool,
defaultHighlightedIndex: PropTypes.number,
defaultSearchQuery: PropTypes.string,
defaultValue: PropTypes.oneOfType([
customPropTypes.itemShorthand,
Expand All @@ -233,6 +243,8 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
fluid: PropTypes.bool,
getA11ySelectionMessage: PropTypes.object,
getA11yStatusMessage: PropTypes.func,
highlightFirstItemOnOpen: PropTypes.bool,
highlightedIndex: PropTypes.number,
inline: PropTypes.bool,
items: customPropTypes.collectionShorthand,
itemToString: PropTypes.func,
Expand Down Expand Up @@ -273,20 +285,25 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
triggerButton: {},
}

static autoControlledProps = ['activeSelectedIndex', 'open', 'searchQuery', 'value']
static autoControlledProps = [
'activeSelectedIndex',
'highlightedIndex',
'open',
'searchQuery',
'value',
]

static Item = DropdownItem
static SearchInput = DropdownSearchInput
static SelectedItem = DropdownSelectedItem

getInitialAutoControlledState({ multiple, search }: DropdownProps): DropdownState {
return {
activeSelectedIndex: multiple ? null : undefined,
a11ySelectionStatus: '',
// used on single selection to open the dropdown with the selected option as highlighted.
defaultHighlightedIndex: this.props.multiple ? undefined : null,
activeSelectedIndex: multiple ? null : undefined,
focused: false,
open: false,
highlightedIndex: this.props.highlightFirstItemOnOpen ? 0 : null,
searchQuery: search ? '' : undefined,
value: multiple ? [] : null,
}
Expand All @@ -309,7 +326,7 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
itemToString,
toggleIndicator,
} = this.props
const { defaultHighlightedIndex, open, searchQuery, value } = this.state
const { highlightedIndex, open, searchQuery, value } = this.state

return (
<ElementType className={classes.root} {...unhandledProps}>
Expand All @@ -322,7 +339,7 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
itemToString={itemToString}
selectedItem={null}
getA11yStatusMessage={getA11yStatusMessage}
defaultHighlightedIndex={defaultHighlightedIndex}
highlightedIndex={highlightedIndex}
onStateChange={this.handleStateChange}
>
{({
Expand Down Expand Up @@ -632,11 +649,25 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo

private handleStateChange = (changes: StateChangeOptions<ShorthandValue>) => {
if (changes.isOpen !== undefined && changes.isOpen !== this.state.open) {
this.trySetStateAndInvokeHandler('onOpenChange', null, { open: changes.isOpen })
const newState = { open: changes.isOpen, highlightedIndex: this.state.highlightedIndex }

if (changes.isOpen) {
const highlightedIndexOnArrowKeyOpen = this.getHighlightedIndexOnArrowKeyOpen(changes)
if (_.isNumber(highlightedIndexOnArrowKeyOpen)) {
newState.highlightedIndex = highlightedIndexOnArrowKeyOpen
}
if (!this.props.search) {
this.listRef.current.focus()
}
} else {
newState.highlightedIndex = this.getHighlightedIndexOnClose()
}
Copy link
Member

@layershifter layershifter Feb 26, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, but I lost there 😴 If you will try to cover this code with UTs you will not survive 🦇

  1. Can we extract code that calculates only highlightedIndex based on changes/props?
  2. Can we dispatch DOM effects in componentDidUpdate()? I.e. in a single place based on the component's state.
componentDidUpdate(prevProps, prevState) {
  if (!prevState.open && this.state.open) {
    if (search) {
      this.listRef.current.focus()
    }
    if (multiple) {
      this.triggerRef.current.focus()
    }
  }
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I extracted the code, you can take a look. as for the DOM effects, it's out of scope, but we can create a separate issue. I also want to move the methods around in the Dropdown, since it's kind of chaos there. should be render, then renderWhatevers, then event handlers, then helpers. Or something like that, but grouped better, since it's a 1100 line file.


this.trySetStateAndInvokeHandler('onOpenChange', null, newState)
}

if (changes.isOpen && !this.props.search) {
this.listRef.current.focus()
if (this.state.open && _.isNumber(changes.highlightedIndex)) {
this.trySetState({ highlightedIndex: changes.highlightedIndex })
}
}

Expand Down Expand Up @@ -803,21 +834,20 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
private handleClear = (e: React.SyntheticEvent<HTMLElement>) => {
const {
activeSelectedIndex,
defaultHighlightedIndex,
highlightedIndex,
searchQuery,
value,
} = this.getInitialAutoControlledState(this.props)

_.invoke(this.props, 'onSelectedChange', e, {
...this.props,
activeSelectedIndex,
defaultHighlightedIndex,
highlightedIndex,
searchQuery,
value,
})

this.trySetState({ activeSelectedIndex, searchQuery, value })
this.setState({ defaultHighlightedIndex })
this.trySetState({ activeSelectedIndex, highlightedIndex, searchQuery, value })

this.tryFocusSearchInput()
this.tryFocusTriggerButton()
Expand Down Expand Up @@ -878,7 +908,7 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
})

if (!multiple) {
this.setState({ defaultHighlightedIndex: items.indexOf(item) })
this.setState({ highlightedIndex: items.indexOf(item) })
}

if (getA11ySelectionMessage && getA11ySelectionMessage.onAdd) {
Expand Down Expand Up @@ -1021,6 +1051,48 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
private isValueEmpty = (value: ShorthandValue | ShorthandCollection) => {
return _.isArray(value) ? value.length < 1 : !value
}

private getHighlightedIndexOnArrowKeyOpen = (
changes: StateChangeOptions<ShorthandValue>,
): number => {
const itemsLength = this.getItemsFilteredBySearchQuery().length
switch (changes.type) {
// if open by ArrowUp, index should change by -1.
case Downshift.stateChangeTypes.keyDownArrowUp:
if (_.isNumber(this.state.highlightedIndex)) {
const newIndex = this.state.highlightedIndex - 1
return newIndex < 0 ? itemsLength - 1 : newIndex
}
return itemsLength - 1
// if open by ArrowDown, index should change by +1.
case Downshift.stateChangeTypes.keyDownArrowDown:
if (_.isNumber(this.state.highlightedIndex)) {
const newIndex = this.state.highlightedIndex + 1
return newIndex >= itemsLength ? 0 : newIndex
}
return 0
default:
return undefined
}
}

private getHighlightedIndexOnClose = (): number => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: for readability:

private getHighlightedIndexOnClose = (): number => {
  const { highlightFirstItemOnOpen, items, multiple, search } = this.props
  const { value } = this.state

  if (!multiple && !search && value) {
    // in single selection, if there is a selected item, highlight it.
    return items.indexOf(value)
  }

  if (highlightFirstItemOnOpen) {
    // otherwise, if highlightFirstItemOnOpen prop is provied, highlight first item
    return 0
  }

  // otherwise, highlight no item
  return null
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, will use that version

const { highlightFirstItemOnOpen, items, multiple, search } = this.props
const { value } = this.state

if (!multiple && !search && value) {
// in single selection, if there is a selected item, highlight it.
return items.indexOf(value)
}

if (highlightFirstItemOnOpen) {
// otherwise, if highlightFirstItemOnOpen prop is provied, highlight first item.
return 0
}

// otherwise, highlight no item.
return null
}
}

Dropdown.slotClassNames = {
Expand Down
Loading