diff --git a/CHANGELOG.md b/CHANGELOG.md index 993ff245e8..9a9bec9479 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,7 +53,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Fixes - Make `headerMedia` visible for screen readers in `ListItem` @layershifter ([#772](https://github.com/stardust-ui/react/pull/772)) - Cleanup for `Dropdown` examples' accessibility and added localisation example. @silviuavram ([#771](https://github.com/stardust-ui/react/pull/771)) -- Fix highlighted selected option in single selection `Dropdown` when opened @silviuavram ([#726](https://github.com/stardust-ui/react/pull/726)) +- Fix highlighted selected option in single selection `Dropdown` when opened @silviuavram ([#726](https://github.com/stardust-ui/react/pull/726)) +- Improve `Dropdown` component styles @Bugaa92 ([#786](https://github.com/stardust-ui/react/pull/786)) ## [v0.18.0](https://github.com/stardust-ui/react/tree/v0.18.0) (2019-01-24) diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index c0b2332a44..2aed4c1bfd 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -1,6 +1,7 @@ import * as React from 'react' import * as PropTypes from 'prop-types' import * as _ from 'lodash' +import cx from 'classnames' import { Extendable, @@ -140,6 +141,7 @@ export interface DropdownProps extends UIComponentProps() private inputRef = React.createRef() private listRef = React.createRef() + private selectedItemsRef = React.createRef() static displayName = 'Dropdown' @@ -199,7 +202,7 @@ export default class Dropdown extends AutoControlledComponent< ]), } - static defaultProps = { + static defaultProps: DropdownProps = { as: 'div', itemToString: item => { if (!item || React.isValidElement(item)) { @@ -256,11 +259,7 @@ export default class Dropdown extends AutoControlledComponent< selectedItem={search && !multiple ? undefined : null} getA11yStatusMessage={getA11yStatusMessage} defaultHighlightedIndex={defaultHighlightedIndex} - onStateChange={changes => { - if (changes.isOpen && !search) { - this.listRef.current.focus() - } - }} + onStateChange={this.handleStateChange} > {({ getInputProps, @@ -280,19 +279,24 @@ export default class Dropdown extends AutoControlledComponent< return (
- {multiple && this.renderSelectedItems()} - {search - ? this.renderSearchInput( - accessibilityRootPropsRest, - getInputProps, - highlightedIndex, - selectItemAtIndex, - variables, - ) - : this.renderTriggerButton(styles, getToggleButtonProps)} +
+ {multiple && this.renderSelectedItems()} + {search + ? this.renderSearchInput( + accessibilityRootPropsRest, + getInputProps, + highlightedIndex, + selectItemAtIndex, + variables, + ) + : this.renderTriggerButton(styles, getToggleButtonProps)} +
{Indicator.create(toggleIndicator, { defaultProps: { direction: isOpen ? 'top' : 'bottom', @@ -359,7 +363,7 @@ export default class Dropdown extends AutoControlledComponent< ) => void, variables, ): JSX.Element { - const { searchInput, multiple, placeholder, toggleIndicator } = this.props + const { searchInput, multiple, placeholder } = this.props const { searchQuery, value } = this.state const noPlaceholder = @@ -368,7 +372,6 @@ export default class Dropdown extends AutoControlledComponent< return DropdownSearchInput.create(searchInput || {}, { defaultProps: { placeholder: noPlaceholder ? '' : placeholder, - hasToggleButton: !!toggleIndicator, variables, inputRef: this.inputRef, }, @@ -394,14 +397,16 @@ export default class Dropdown extends AutoControlledComponent< getItemProps: (options: GetItemPropsOptions) => any, getInputProps: (options?: GetInputPropsOptions) => any, ) { + const { search } = this.props const { innerRef, ...accessibilityMenuProps } = getMenuProps( { refKey: 'innerRef' }, { suppressRefError: true }, ) - const { search } = this.props + // If it's just a selection, some attributes and listeners from Downshift input need to go on the menu list. if (!search) { const accessibilityInputProps = getInputProps() + accessibilityMenuProps['aria-activedescendant'] = accessibilityInputProps['aria-activedescendant'] accessibilityMenuProps['onKeyDown'] = e => { @@ -440,8 +445,8 @@ export default class Dropdown extends AutoControlledComponent< highlightedIndex: number, ) { const { loading, loadingMessage, noResultsMessage, renderItem } = this.props - const filteredItems = this.getItemsFilteredBySearchQuery() + const items = _.map(filteredItems, (item, index) => DropdownItem.create(item, { defaultProps: { @@ -528,6 +533,16 @@ export default class Dropdown extends AutoControlledComponent< } } + private handleStateChange = (changes: StateChangeOptions) => { + if (changes.isOpen !== undefined && changes.isOpen !== this.state.isOpen) { + this.setState({ isOpen: changes.isOpen }) + } + + if (changes.isOpen && !this.props.search) { + this.listRef.current.focus() + } + } + private getItemsFilteredBySearchQuery = (): ShorthandValue[] => { const { items, itemToString, multiple, search } = this.props const { searchQuery, value } = this.state @@ -536,10 +551,12 @@ export default class Dropdown extends AutoControlledComponent< if (multiple) { filteredItems = _.difference(filteredItems, value as ShorthandValue[]) } + if (search) { if (_.isFunction(search)) { return search(filteredItems, searchQuery) } + return filteredItems.filter( item => itemToString(item) @@ -572,9 +589,7 @@ export default class Dropdown extends AutoControlledComponent< item: ShorthandValue, index: number, getItemProps: (options: GetItemPropsOptions) => any, - ) => ({ - accessibilityItemProps: getItemProps({ item, index }), - }) + ) => ({ accessibilityItemProps: getItemProps({ item, index }) }) private handleSelectedItemOverrides = ( predefinedProps: DropdownSelectedItemProps, @@ -606,9 +621,9 @@ export default class Dropdown extends AutoControlledComponent< searchInputProps: DropdownSearchInputProps, ) => { this.setState({ focused: false }) - _.invoke(predefinedProps, 'onInputBlur', e, searchInputProps) } + const handleInputKeyDown = ( e: React.SyntheticEvent, searchInputProps: DropdownSearchInputProps, @@ -700,21 +715,27 @@ export default class Dropdown extends AutoControlledComponent< } private handleSelectedChange = (item: ShorthandValue) => { - const { multiple, getA11ySelectionMessage, search } = this.props + const { items, multiple, getA11ySelectionMessage, search } = this.props const newValue = multiple ? [...(this.state.value as ShorthandValue[]), item] : item - this.trySetState({ - value: newValue, - searchQuery: '', - }) - if (!this.props.search && !this.props.multiple) { - this.setState({ - defaultHighlightedIndex: this.props.items.indexOf(item), - }) + this.trySetState({ value: newValue, searchQuery: '' }) + + if (!search && !multiple) { + this.setState({ defaultHighlightedIndex: items.indexOf(item) }) } + if (getA11ySelectionMessage && getA11ySelectionMessage.onAdd) { this.setA11yStatus(getA11ySelectionMessage.onAdd(item)) } + + if (multiple) { + setTimeout( + () => + (this.selectedItemsRef.current.scrollTop = this.selectedItemsRef.current.scrollHeight), + 0, + ) + } + if (!search) { this.buttonRef.current.focus() } @@ -745,9 +766,8 @@ export default class Dropdown extends AutoControlledComponent< poppedItem = value.pop() } - this.trySetState({ - value, - }) + this.trySetState({ value }) + if (getA11ySelectionMessage && getA11ySelectionMessage.onRemove) { this.setA11yStatus(getA11ySelectionMessage.onRemove(poppedItem)) } diff --git a/src/components/Dropdown/DropdownSearchInput.tsx b/src/components/Dropdown/DropdownSearchInput.tsx index bc45b9ddcd..c64ad6b293 100644 --- a/src/components/Dropdown/DropdownSearchInput.tsx +++ b/src/components/Dropdown/DropdownSearchInput.tsx @@ -14,9 +14,6 @@ import { UIComponentProps } from '../../lib/commonPropInterfaces' import Input from '../Input/Input' export interface DropdownSearchInputProps extends UIComponentProps { - /** Informs the search input about an existing toggle button. */ - hasToggleButton?: boolean - /** Ref for input DOM node. */ inputRef?: React.Ref diff --git a/src/themes/teams/components/Dropdown/dropdownSearchInputStyles.ts b/src/themes/teams/components/Dropdown/dropdownSearchInputStyles.ts index ad70ab946d..893e587a5d 100644 --- a/src/themes/teams/components/Dropdown/dropdownSearchInputStyles.ts +++ b/src/themes/teams/components/Dropdown/dropdownSearchInputStyles.ts @@ -6,25 +6,18 @@ const dropdownSearchInputStyles: ComponentSlotStylesInput< DropdownSearchInputProps, DropdownVariables > = { - input: ({ variables: { backgroundColor, comboboxPaddingInput } }): ICSSInJSStyle => ({ + input: ({ variables: v }): ICSSInJSStyle => ({ width: '100%', - backgroundColor, - padding: comboboxPaddingInput, + backgroundColor: v.backgroundColor, ':focus': { borderBottomColor: 'transparent', }, }), - combobox: ({ - variables: { comboboxFlexBasis, toggleIndicatorSize }, - props: { hasToggleButton }, - }): ICSSInJSStyle => ({ - flexBasis: comboboxFlexBasis, + combobox: ({ variables: v }): ICSSInJSStyle => ({ + flexBasis: v.comboboxFlexBasis, flexGrow: 1, - ...(hasToggleButton && { - marginRight: toggleIndicatorSize, - }), }), } diff --git a/src/themes/teams/components/Dropdown/dropdownStyles.ts b/src/themes/teams/components/Dropdown/dropdownStyles.ts index 7e87c34b49..df3deb428b 100644 --- a/src/themes/teams/components/Dropdown/dropdownStyles.ts +++ b/src/themes/teams/components/Dropdown/dropdownStyles.ts @@ -1,42 +1,34 @@ import { ComponentSlotStylesInput, ICSSInJSStyle } from '../../../types' -import { DropdownProps } from '../../../../components/Dropdown/Dropdown' +import { DropdownProps, DropdownState } from '../../../../components/Dropdown/Dropdown' import { DropdownVariables } from './dropdownVariables' import { pxToRem } from '../../../../lib' -const dropdownStyles: ComponentSlotStylesInput = { +const dropdownStyles: ComponentSlotStylesInput = { root: (): ICSSInJSStyle => ({}), - container: ({ - props: { focused, fluid }, - variables: { - backgroundColor, - borderBottom, - borderRadius, - borderColor, - borderColorFocus, - borderRadiusFocus, - color, - width, - }, - }): ICSSInJSStyle => ({ + container: ({ props: p, variables: v }): ICSSInJSStyle => ({ display: 'flex', flexWrap: 'wrap', outline: 0, border: 0, - backgroundColor, - borderBottom, - borderColor, - borderRadius, - color, - width: fluid ? '100%' : width, + backgroundColor: v.backgroundColor, + borderBottom: v.borderBottom, + borderRadius: v.borderRadius, + color: v.color, + width: p.fluid ? '100%' : v.width, position: 'relative', - ...(focused && { - borderColor: borderColorFocus, - borderRadius: borderRadiusFocus, - }), + ...(p.focused && { borderColor: v.borderColorFocus }), + }), + + selectedItems: ({ props: p, variables: v }): ICSSInJSStyle => ({ + display: 'flex', + flexWrap: 'wrap', + overflowY: 'auto', + maxHeight: v.selectedItemsMaxHeight, + ...(p.toggleIndicator && { paddingRight: v.toggleIndicatorSize }), }), - button: ({ variables: { comboboxPaddingButton } }): ICSSInJSStyle => { + button: ({ variables: v }): ICSSInJSStyle => { const transparentColorStyle = { backgroundColor: 'transparent', borderColor: 'transparent', @@ -45,7 +37,7 @@ const dropdownStyles: ComponentSlotStylesInput boxShadow: 'none', margin: '0', justifyContent: 'left', - padding: comboboxPaddingButton, + padding: v.comboboxPaddingButton, ...transparentColorStyle, height: pxToRem(30), ':hover': transparentColorStyle, @@ -60,17 +52,20 @@ const dropdownStyles: ComponentSlotStylesInput } }, - list: ({ - variables: { listMaxHeight, width, listBackgroundColor }, - props: { fluid }, - }): ICSSInJSStyle => ({ + list: ({ props: p, variables: v }): ICSSInJSStyle => ({ + outline: 0, position: 'absolute', + borderRadius: v.listBorderRadius, zIndex: 1000, - maxHeight: listMaxHeight, + maxHeight: v.listMaxHeight, overflowY: 'auto', - width: fluid ? '100%' : width, + width: p.fluid ? '100%' : v.width, top: 'calc(100% + 2px)', // leave room for container + its border - background: listBackgroundColor, + background: v.listBackgroundColor, + ...(p.isOpen && { + boxShadow: v.listBoxShadow, + padding: v.listPadding, + }), }), loadingMessage: ({ variables: v }): ICSSInJSStyle => ({ @@ -82,7 +77,7 @@ const dropdownStyles: ComponentSlotStylesInput fontWeight: 'bold', }), - toggleIndicator: ({ props: p, variables: v }): ICSSInJSStyle => ({ + toggleIndicator: ({ variables: v }): ICSSInJSStyle => ({ position: 'absolute', height: v.toggleIndicatorSize, width: v.toggleIndicatorSize, @@ -93,7 +88,7 @@ const dropdownStyles: ComponentSlotStylesInput justifyContent: 'center', alignItems: 'center', userSelect: 'none', - ...(p.fluid ? { right: 0 } : { left: `calc(${v.width} - ${v.toggleIndicatorSize})` }), + right: pxToRem(5), }), } diff --git a/src/themes/teams/components/Dropdown/dropdownVariables.ts b/src/themes/teams/components/Dropdown/dropdownVariables.ts index 63879ae015..61022ee38b 100644 --- a/src/themes/teams/components/Dropdown/dropdownVariables.ts +++ b/src/themes/teams/components/Dropdown/dropdownVariables.ts @@ -1,42 +1,44 @@ import { pxToRem } from '../../../../lib' export interface DropdownVariables { backgroundColor: string - borderBottom: string - borderColor: string borderColorFocus: string borderRadius: string - borderRadiusFocus: string + borderBottom: string color: string comboboxPaddingButton: string - comboboxPaddingInput: string comboboxFlexBasis: string listBackgroundColor: string + listBorderRadius: string + listPadding: string + listBoxShadow: string + listMaxHeight: string listItemBackgroundColor: string listItemBackgroundColorActive: string listItemColorActive: string - listMaxHeight: string + selectedItemsMaxHeight: string toggleIndicatorSize: string width: string } -const [_2px_asRem, _3px_asRem, _6px_asRem, _12px_asRem] = [2, 3, 6, 12].map(v => pxToRem(v)) +const [_2px_asRem, _3px_asRem, _12px_asRem] = [2, 3, 12].map(v => pxToRem(v)) export default (siteVars): DropdownVariables => ({ backgroundColor: siteVars.gray10, - borderRadius: _3px_asRem, + borderRadius: `${_3px_asRem} ${_3px_asRem} ${_2px_asRem} ${_2px_asRem}`, borderBottom: `${_2px_asRem} solid transparent`, - borderColor: 'transparent', borderColorFocus: siteVars.brand, - borderRadiusFocus: `${_3px_asRem} ${_3px_asRem} ${_2px_asRem} ${_2px_asRem}`, color: siteVars.bodyColor, comboboxPaddingButton: `0 ${_12px_asRem}`, - comboboxPaddingInput: `${_6px_asRem} ${_12px_asRem}`, comboboxFlexBasis: '50px', listBackgroundColor: siteVars.white, + listBorderRadius: _3px_asRem, + listPadding: `${pxToRem(8)} 0`, + listBoxShadow: `0 .2rem .6rem 0 ${siteVars.gray06}`, + listMaxHeight: '20rem', listItemBackgroundColor: siteVars.white, listItemBackgroundColorActive: siteVars.brand, listItemColorActive: siteVars.white, - listMaxHeight: '20rem', + selectedItemsMaxHeight: pxToRem(82), toggleIndicatorSize: pxToRem(32), width: pxToRem(356), }) diff --git a/src/themes/teams/components/Input/inputStyles.ts b/src/themes/teams/components/Input/inputStyles.ts index 36d3da94f0..2538d447df 100644 --- a/src/themes/teams/components/Input/inputStyles.ts +++ b/src/themes/teams/components/Input/inputStyles.ts @@ -14,7 +14,9 @@ const inputStyles: ComponentSlotStylesInput = { input: ({ props: p, variables: v }): ICSSInJSStyle => ({ outline: 0, + boxSizing: 'border-box', border: v.border, + borderBottom: v.borderBottom, borderRadius: v.borderRadius, color: v.fontColor, backgroundColor: v.backgroundColor, @@ -27,7 +29,6 @@ const inputStyles: ComponentSlotStylesInput = { }, ':focus': { borderBottomColor: v.inputFocusBorderBottomColor, - boxShadow: v.boxShadow, }, ...(p.clearable && { padding: v.inputPaddingWithIconAtEnd }), ...(p.icon && { diff --git a/src/themes/teams/components/Input/inputVariables.ts b/src/themes/teams/components/Input/inputVariables.ts index 565e6022e1..361119fce4 100644 --- a/src/themes/teams/components/Input/inputVariables.ts +++ b/src/themes/teams/components/Input/inputVariables.ts @@ -2,9 +2,9 @@ import { pxToRem } from '../../../../lib' export interface InputVariables { backgroundColor: string - border: string + border: string | number + borderBottom: string borderRadius: string - boxShadow: string fontColor: string fontSize: string iconColor: string @@ -20,11 +20,11 @@ export interface InputVariables { export default (siteVars): InputVariables => ({ backgroundColor: siteVars.gray10, - border: `${pxToRem(1)} solid transparent`, - borderRadius: pxToRem(3), - boxShadow: `0 ${pxToRem(1)} 0 ${siteVars.brand}`, + border: 'none', + borderBottom: `${pxToRem(2)} solid transparent`, + borderRadius: `${pxToRem(3)} ${pxToRem(3)} ${pxToRem(2)} ${pxToRem(2)}`, - fontColor: siteVars.bodyColor, + fontColor: siteVars.gray02, fontSize: siteVars.fontSizes.medium, iconPosition: 'absolute',