From 662554c7016846f0c442e7869e332a95a2702935 Mon Sep 17 00:00:00 2001 From: jscottsmith Date: Thu, 8 Feb 2018 14:13:26 -0800 Subject: [PATCH] CJL-46 add aria and keyboard support to accordion components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CJL-46 add aria and keyboard support to MultiSelect - refactor to use Checkbox - add classnames dependency - add generateUID util CJL-46 use unique id and classnames for accordion items CJL-46 refactor pagination using classnames CJL-46 refactor pagination for aria and keyboard accessibility CJL-46 check if first/last before changing page fixes keyboard page changes CJL-46 add aria and keyboard accessibility to avatar menu CJL-46 update Tag component - add size, optionLabel and onOptionClick props - use onOptionClick to distinguish click on button vs the tag - use optionLabel for aria label - use size prop and append to classname (was limited to just ‘sm’ before) - remove small prop - use classnames - update Tag readme with props and example CJL-46 use Tag component in SearchMultiSelect, refactor using classnames CJL-46 add aria label to pagination buttons since text is hidden CJL-46 add prop classname to accordions CJL-46 refactor Accordion using classnames CJL-46 fix nested accordion story CJL-46 update eslint to warn for unused variables CJL-46 correct default tag size in story CJL-46 use class to fix vertical align instead on inline style in Pagination CJL-46 add classname to root class in MultiSelect CJL-46 add option label to Tag story CJL-46 add constants for charCodes CJL-46 add notes about keyboard accessibility in readme --- .eslintrc.json | 2 +- _stories/atoms/Tag/README.md | 26 +++ _stories/atoms/Tag/index.js | 18 +- _stories/molecules/Accordion/README.md | 41 +++-- _stories/molecules/Accordion/index.js | 16 +- _stories/molecules/Avatar/README.md | 6 + _stories/molecules/Pagination/README.md | 21 ++- components/atoms/AccordionItem.jsx | 86 ++++++--- components/atoms/Tag.jsx | 63 ++++--- components/molecules/Accordion.jsx | 12 +- components/molecules/Avatar.jsx | 158 ++++++++++------ components/molecules/MultiSelect.jsx | 101 ++++++----- components/molecules/Pagination.jsx | 200 +++++++++++++-------- components/molecules/SearchMultiSelect.jsx | 74 ++++---- components/utils/generateUID.js | 7 + constants/charCodes.js | 10 ++ package.json | 1 + 17 files changed, 547 insertions(+), 295 deletions(-) create mode 100644 components/utils/generateUID.js create mode 100644 constants/charCodes.js diff --git a/.eslintrc.json b/.eslintrc.json index 216bc07..05cdd83 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -67,7 +67,7 @@ "no-multi-str": 1, "key-spacing": [1, { "afterColon": true }], "no-var": 1, - "no-unused-vars": 0, + "no-unused-vars": [1, { "vars": "local", "args": "after-used" }], "no-alert": 0, "no-lone-blocks": 0, "react/display-name": [1, { "ignoreTranspilerName": false }], diff --git a/_stories/atoms/Tag/README.md b/_stories/atoms/Tag/README.md index e5be067..3033c5f 100644 --- a/_stories/atoms/Tag/README.md +++ b/_stories/atoms/Tag/README.md @@ -1 +1,27 @@ The `` component is used to indicate active or selected items, filters or options. Refer to [this](http://design-prototypes.gumgum.com/black-tie/documentation/#icons-btl) page for icon names. + +**Example**: + +```jsx + {}} + optionIcon="bt-times" + size="sm" + text="Sample Tag" +/> +``` + +| **prop name** | **description** | +| ------------- | ----------------------------------------------------------------------------------------------------------------------- | +| className | Class to add to the root element {string} {required} | +| context | Any of \`normal, primary, success, warning, danger\` or leave unset to get default appearance {string} {defaults to ''} | +| hasOption | Indicate if the tag has an option button {boolean} {defaults to false} | +| onClick | Click handler for the root element {function} | +| onOptionClick | Click handler for the option button element {function} | +| optionIcon | Icon name for the option button {string} {defaults to 'bt-times'} | +| optionLabel | Aria label for the option button {string} {defaults to 'Remove tag'} | +| size | Any of \`sm, xs\` {string} | +| style | Styles to add to the root element {object} | +| text | Tag display text {string} | diff --git a/_stories/atoms/Tag/index.js b/_stories/atoms/Tag/index.js index abb60b5..0b51e8b 100644 --- a/_stories/atoms/Tag/index.js +++ b/_stories/atoms/Tag/index.js @@ -13,16 +13,24 @@ const contextOptions = { danger: 'danger' }; +const sizeOptions = { + sm: 'sm', + xs: 'xs', + '': 'default' +}; + const component = () => ( ); diff --git a/_stories/molecules/Accordion/README.md b/_stories/molecules/Accordion/README.md index cb9b70e..cbecc01 100644 --- a/_stories/molecules/Accordion/README.md +++ b/_stories/molecules/Accordion/README.md @@ -1,42 +1,45 @@ The `` component is a collapsible container for holding related elements. Nest `` components inside `` for each drawer. Nest `` in each `` if you would like to display listed information within an open ``. Each `` related component accepts a className and otherProps so you are able to further customize the component. -*Accordion example*: +_Accordion example_: You can use `` to display any information that you pass in, such as: ``` - - - - + + + + ``` - You can also use `` to display information in a list form, in the case you want to list items in rows. Example: ``` - - Setting 1 - - - - + + Setting 1 + + + + - - Setting 2 - - - - + + Setting 2 + + + + + - ``` + +**Keyboard Accessibility:** + +When an accordion is in focus you can toggle it expanded/collapsed with the spacebar or enter keys. diff --git a/_stories/molecules/Accordion/index.js b/_stories/molecules/Accordion/index.js index 379fd54..32f986d 100644 --- a/_stories/molecules/Accordion/index.js +++ b/_stories/molecules/Accordion/index.js @@ -1,5 +1,5 @@ import React from 'react'; -import { boolean, select, text, object } from '@storybook/addon-knobs'; +import { select, text } from '@storybook/addon-knobs'; import readme from './README.md'; import Accordion from '../../../components/molecules/Accordion'; @@ -23,10 +23,20 @@ const component = () => ( context={select('Context', contextOptions, '')} className={text('Class', '')}> - Content + Content 1 - Content + Content 2 + + + Content 3 + + + Nested Content + + ); diff --git a/_stories/molecules/Avatar/README.md b/_stories/molecules/Avatar/README.md index ca71656..81332db 100644 --- a/_stories/molecules/Avatar/README.md +++ b/_stories/molecules/Avatar/README.md @@ -7,6 +7,7 @@ The avatar can include a dropdown menu, which is meant to be shown when the avat For the menu contents, you must pass in `menuOptions` (an array of objects of names and paths for the menu), and a `optionCallback` that will be called when the option is clicked. If you want to have a login option, you can include that in the `optionCallback`. **Example**: + ``` state { avatarOpen: false @@ -34,6 +35,7 @@ return( ``` **Options Format**: + ``` const AvatarMenu = [ { name: 'Change Password', path: '/account/change-password' }, @@ -43,3 +45,7 @@ const AvatarMenu = [ { name: 'Logout', path: 'logout' } ]; ``` + +**Keyboard Accessibility:** + +When the avatar is in focus you can toggle the related menu opened/closed with the spacebar or enter keys. diff --git a/_stories/molecules/Pagination/README.md b/_stories/molecules/Pagination/README.md index c23fa4b..b639d11 100644 --- a/_stories/molecules/Pagination/README.md +++ b/_stories/molecules/Pagination/README.md @@ -3,6 +3,7 @@ The `` component creates a set of indicators to show how many pages When a page change is detected, the `onChange` prop will be called with the next page number as its only argument. **Example**: + ``` state = { activePage: 7, @@ -33,11 +34,15 @@ render() { **Props**: -**prop name** | **description** -----------------|------------ -activePage | Current active page {number} {defaults to 1} -lastPage | Total number of pages {number} {required} -boundaries | Whether or not to always show the start and end pages {boolean} {defaults to false} -justify | Whether or not to take the whole available width of the container {boolean} {defaults to false} -size | Any of \`xl, lg, sm, xs\` or leave unset to get default size {string} {defaults to ''} -onChange | Callback to run when changing a page. Receives an object with next page and offsets {function} {required} +| **prop name** | **description** | +| ------------- | --------------------------------------------------------------------------------------------------------- | +| activePage | Current active page {number} {defaults to 1} | +| lastPage | Total number of pages {number} {required} | +| boundaries | Whether or not to always show the start and end pages {boolean} {defaults to false} | +| justify | Whether or not to take the whole available width of the container {boolean} {defaults to false} | +| size | Any of \`xl, lg, sm, xs\` or leave unset to get default size {string} {defaults to ''} | +| onChange | Callback to run when changing a page. Receives an object with next page and offsets {function} {required} | + +**Keyboard Accessibility:** + +When pagination is in focus, you can use the arrow key to navigate to the next (arrow right and arrow down) and previous (arrow up and arrow left) pages. You can also focus on the next/previous button and "press" them with the spacebar or enter keys. diff --git a/components/atoms/AccordionItem.jsx b/components/atoms/AccordionItem.jsx index 3f3a57b..5661a39 100644 --- a/components/atoms/AccordionItem.jsx +++ b/components/atoms/AccordionItem.jsx @@ -1,34 +1,54 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import trimString from '../utils/trimString'; +import cx from 'classnames'; +import generateUID from '../utils/generateUID'; +import charCodes from '../../constants/charCodes'; class AccordionItem extends Component { + constructor() { + super(); + this.uid = generateUID(this); + } + state = { - open: false + isOpen: false }; - toggleOpen = () => { - this.setState(({ open }) => ({ open: !open })); + toggleOpen = event => { + const { type, charCode } = event; + event.stopPropagation(); // don't want nested accordions to propagate events + + if ( + type === 'keypress' && + (charCode === charCodes.SPACEBAR || charCode === charCodes.ENTER) + ) { + event.preventDefault(); // prevents page scroll from space key + this.setState(({ isOpen }) => ({ isOpen: !isOpen })); + } else if (type === 'click') { + this.setState(({ isOpen }) => ({ isOpen: !isOpen })); + } }; render() { - const { size, context, className, children } = this.props; - const baseClass = 'gds-accordion__item'; - const contextClass = context ? `${baseClass}--${context}` : ''; - const activeClass = this.state.open ? `${baseClass}--active` : ''; - const classNames = trimString(`${baseClass} ${contextClass} ${activeClass} ${className}`); + const { size, context, className, children, label } = this.props; + const { isOpen } = this.state; - const titleBaseClass = 'gds-accordion__item-title'; - const titleSizeClass = size ? `${titleBaseClass}--${size}` : ''; - const titleClass = trimString(`${titleBaseClass} ${titleSizeClass}`); + const rootClass = cx('gds-accordion__item', className, { + [`gds-accordion__item--${context}`]: context, + 'gds-accordion__item--active': isOpen + }); - const iconBaseClass = 'gds-accordion__item-icon'; - const iconSizeClass = size ? `${iconBaseClass}--${size}` : ''; - const iconClass = trimString(`${iconBaseClass} ${iconSizeClass}`); + const titleClass = cx('gds-accordion__item-title', { + [`gds-accordion__item-title--${size}`]: size + }); - const childBaseClass = 'gds-accordion__child-items'; - const childSizeClass = size ? `${childBaseClass}--${size}` : ''; - const childClass = trimString(`${childBaseClass} ${childSizeClass}`); + const iconClass = cx('gds-accordion__item-icon', { + [`gds-accordion__item-icon--${size}`]: size + }); + + const childClass = cx('gds-accordion__child-items', { + [`gds-accordion__child-items--${size}`]: size + }); const newChildren = React.Children.map(children, child => { return React.cloneElement(child, { @@ -37,17 +57,37 @@ class AccordionItem extends Component { }); }); + const regionId = `AccordionItem-region-${this.uid}`; + const labelId = `AccordionItem-label-${this.uid}`; + return ( -
  • -

    {this.props.label}

    - -
      {newChildren}
    +
  • +

    + {label} +

    + +
      + {newChildren} +
  • ); } } -AccordionItem.displayName = 'Accordion Item'; +AccordionItem.displayName = 'AccordionItem'; AccordionItem.defaultProps = { className: '', diff --git a/components/atoms/Tag.jsx b/components/atoms/Tag.jsx index 79cbf9b..ff5926f 100644 --- a/components/atoms/Tag.jsx +++ b/components/atoms/Tag.jsx @@ -1,27 +1,39 @@ import React from 'react'; import PropTypes from 'prop-types'; -import trimString from '../utils/trimString'; - -const Tag = ({ context, className, onClick, hasOption, optionIcon, small, style, text }) => { - const isNormal = context && context === 'normal', - tagClass = 'gds-tag', - hasButtonClass = hasOption ? `gds-tag--with-button${small ? '-sm' : ''}` : '', - contextClass = isNormal ? '' : `gds-tag--${context}`, - buttonContextClass = isNormal ? '' : `gds-tag__option--${context}`, - buttonClass = small ? 'gds-tag__option gds-tag__option--sm' : 'gds-tag__option', - sizeClass = small ? 'gds-tag--sm' : ''; - - const classNames = trimString( - `${tagClass} ${hasButtonClass} ${contextClass} ${sizeClass} ${className}` - ); - const optionClassNames = trimString(`${buttonClass} ${buttonContextClass}`); +import cx from 'classnames'; + +const Tag = ({ + context, + className, + onClick, + onOptionClick, + hasOption, + optionIcon, + optionLabel, + size, + style, + text +}) => { + const rootClass = cx('gds-tag', className, { + [`gds-tag--${context}`]: context && context !== 'normal', + 'gds-tag--with-button': hasOption, + [`gds-tag--with-button--${size}`]: size && hasOption, + [`gds-tag--${size}`]: size + }); + + const optionClass = cx('gds-tag__option', { + [`gds-tag__option--${context}`]: context && context !== 'normal' && hasOption, + [`gds-tag__option--${size}`]: size + }); + + const iconClass = cx('btl', 'bt-fw', optionIcon); return ( -
    +
    {text} {hasOption && ( - )}
    @@ -34,20 +46,23 @@ Tag.defaultProps = { className: '', hasOption: false, optionIcon: 'bt-times', - small: false, + optionLabel: 'Remove tag', style: {} }; Tag.propTypes = { - text: PropTypes.string.isRequired, + className: PropTypes.string, /** normal, primary, success, warning, danger */ context: PropTypes.string, hasOption: PropTypes.bool, + onClick: PropTypes.func, + onOptionClick: PropTypes.func, optionIcon: PropTypes.string, - small: PropTypes.bool, - onClick: React.PropTypes.func, - className: PropTypes.string, - style: PropTypes.object + optionLabel: PropTypes.string, + /** sm, xs */ + size: PropTypes.oneOf(['xs', 'sm']), + style: PropTypes.object, + text: PropTypes.string.isRequired }; export default Tag; diff --git a/components/molecules/Accordion.jsx b/components/molecules/Accordion.jsx index 2dd1919..986ae1c 100644 --- a/components/molecules/Accordion.jsx +++ b/components/molecules/Accordion.jsx @@ -1,11 +1,11 @@ -import React, { Component } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; -import trimString from '../utils/trimString'; +import cx from 'classnames'; const Accordion = ({ children, context, size, className, ...otherProps }) => { - const baseClass = 'gds-accordion', - contextClass = context ? `${baseClass}--${context}` : ''; - const classNames = trimString(`${baseClass} ${contextClass} ${className}`); + const rootClass = cx('gds-accordion', className, { + [`gds-accordion--${context}`]: context + }); const newChildren = React.Children.map(children, child => { return React.cloneElement(child, { @@ -15,7 +15,7 @@ const Accordion = ({ children, context, size, className, ...otherProps }) => { }); return ( -
    +
      {newChildren}
    ); diff --git a/components/molecules/Avatar.jsx b/components/molecules/Avatar.jsx index f3aa635..cf6cad2 100644 --- a/components/molecules/Avatar.jsx +++ b/components/molecules/Avatar.jsx @@ -1,61 +1,113 @@ -import React from 'react'; +import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import trimString from '../utils/trimString'; +import cx from 'classnames'; +import generateUID from '../utils/generateUID'; +import charCodes from '../../constants/charCodes'; -const Avatar = ({ - open, - menuCallback, - optionCallback, - username, - img, - menuOptions, - className, - style -}) => { - const baseClass = 'gds-avatar', - openClasses = open ? `${baseClass}--menu-open` : ''; +class Avatar extends Component { + constructor() { + super(); + this.uid = generateUID(this); + } - const classNames = trimString(`${baseClass} ${openClasses} ${className}`); + render() { + const { + open, + menuCallback, + optionCallback, + username, + img, + menuOptions, + className, + style + } = this.props; - return ( -
    - {img ? ( -
    - {username} -
    - ) : ( -
    - {username && username.slice(0, 1)} -
    - )} - {menuOptions && ( -
    -
      -
    • - {username} -
    • -
    • - {menuOptions.map(o => { - const _optionCallback = () => optionCallback(o.path); + const rootClass = cx('gds-avatar', className, { + 'gds-avatar--menu-open': open + }); - return ( -
    • -
      - {o.name} -
      -
    • - ); - })} -
    -
    - )} -
    - ); -}; + const regionId = `Avatar-region-${this.uid}`; + const labelId = `Avatar-label-${this.uid}`; + + return ( +
    { + const { type, charCode } = event; + if ( + type === 'keypress' && + (charCode === charCodes.SPACEBAR || charCode === charCodes.ENTER) + ) { + event.preventDefault(); + menuCallback(); + } + }} + role="button" + style={style} + tabIndex={0}> + {img ? ( +
    + {username} +
    + ) : ( +
    + {username && username.slice(0, 1)} +
    + )} + {menuOptions && ( + + )} +
    + ); + } +} Avatar.displayName = 'Avatar'; diff --git a/components/molecules/MultiSelect.jsx b/components/molecules/MultiSelect.jsx index e6e05bb..3cc30f2 100644 --- a/components/molecules/MultiSelect.jsx +++ b/components/molecules/MultiSelect.jsx @@ -1,7 +1,15 @@ -import React from 'react'; +import React, { Component } from 'react'; import PropTypes from 'prop-types'; +import Checkbox from './Checkbox'; +import cx from 'classnames'; +import generateUID from '../utils/generateUID'; + +class MultiSelect extends Component { + constructor() { + super(); + this.UID = generateUID(this); + } -class MultiSelect extends React.Component { state = { isOpen: false }; @@ -10,57 +18,58 @@ class MultiSelect extends React.Component { render() { const { options, callback, placeholder, size, className, ...otherProps } = this.props; + const { isOpen } = this.state; + + const isSmall = size === 'sm'; + const isExtraSmall = size === 'xs'; - const isSmall = size === 'sm', - isExtraSmall = size === 'xs', - smallClass = isSmall ? 'gds-multi-select--sm' : '', - smallBtnClass = isSmall ? 'gds-multi-select__button--sm' : '', - extraSmallClass = isExtraSmall ? 'gds-multi-select--xs' : '', - extraSmallBtnClass = isExtraSmall ? 'gds-multi-select__button--xs' : ''; + const rootClass = cx('gds-multi-select', className, { + 'gds-multi-select--sm': isSmall, + 'gds-multi-select--xs': isExtraSmall, + 'gds-button-dropdown--active': isOpen + }); - const activeClass = this.state.isOpen ? 'gds-button-dropdown--active' : ''; + const btnClass = cx('gds-multi-select__button', { + 'gds-multi-select__button--sm': isSmall, + 'gds-multi-select__button--xs': isExtraSmall + }); + + const regionId = `MultiSelect_region_${this.UID}`; + const labelId = `MultiSelect_label_${this.UID}`; return ( -
    -
    +
    -
      - {options.map(({ name, value, selected }, index) => { - const _onClick = e => { - // Prevents callback from being called twice when the checkbox's label is clicked - e.preventDefault(); - callback(index, value, !selected); - }; - - return ( -
    • -
      -
      -
      - -
      -
      + +
        + {options.map(({ name, value, selected }, index) => ( +
      • +
        +
        + { + callback(index, value, !selected); + }} + />
        -
      • - ); - })} +
      +
    • + ))}
    ); diff --git a/components/molecules/Pagination.jsx b/components/molecules/Pagination.jsx index d9daabb..784072e 100644 --- a/components/molecules/Pagination.jsx +++ b/components/molecules/Pagination.jsx @@ -1,31 +1,72 @@ -import React from 'react'; +import React, { Component } from 'react'; import PropTypes from 'prop-types'; import createRange from '../utils/createRange'; +import charCodes from '../../constants/charCodes'; +import cx from 'classnames'; + +class Pagination extends Component { + constructor(props) { + super(props); + this.state = { + isPrevDisabled: props.activePage === 1, + isNextDisabled: props.activePage === props.lastPage + }; + } + + componentWillReceiveProps(nextProps) { + if (this.props.lastPage !== nextProps.lastPage) { + this.setState({ + isPrevDisabled: nextProps.activePage === 1, + isNextDisabled: nextProps.activePage === nextProps.lastPage + }); + } + } + + _handleKeyPress = event => { + const { keyCode } = event; + + if (keyCode === charCodes.ARROW_LEFT || keyCode === charCodes.ARROW_UP) { + this._changePage('prev'); + } + if (keyCode === charCodes.ARROW_RIGHT || keyCode === charCodes.ARROW_DOWN) { + this._changePage('next'); + } + }; + + _changePage = page => { + const { onChange, activePage, lastPage } = this.props; + const { isPrevDisabled, isNextDisabled } = this.state; + + const prev = activePage - 1; + const next = activePage + 1; -const activeClass = 'gds-pagination__page-item--active'; -const itemClassName = 'gds-pagination__page-item'; -const linkClassName = 'gds-pagination__page-link'; -const fixedItemClass = `${itemClassName} ${itemClassName}--fixed`; -const fixedLinkClass = `${linkClassName} ${linkClassName}--fixed`; - -const Pagination = props => { - const { - className: wrapperClassName, - boundaries, - activePage, - lastPage, - justify, - onChange, - size - } = props; - - const itemClass = justify ? itemClassName : fixedItemClass; - const linkClass = justify ? linkClassName : fixedLinkClass; - const sizeClass = size ? `gds-pagination--${size}` : ''; - const widthClass = justify ? '' : 'gds-pagination--fixed'; - const className = `gds-pagination ${sizeClass} gds-pagination--mobile-arrows ${widthClass}`; - - const createPages = () => { + const isFirst = prev < 1; + const isLast = next > lastPage; + + // Find next page + let newPage = page; + if (isNaN(page)) { + newPage = { + next: isLast ? lastPage : next, + prev: isFirst ? 1 : prev + }[page]; + } + + const shouldUpdateState = isFirst !== isPrevDisabled || isLast !== isNextDisabled; + + if (shouldUpdateState) { + this.setState({ + isPrevDisabled: isFirst, + isNextDisabled: isLast + }); + } + + // Pass next page to callback + onChange && onChange(newPage); + }; + + _createPages() { + const { boundaries, activePage, lastPage, justify } = this.props; // Beyond this number of results, the indicator doesn't work const displayLimit = 7; // Number of pages adjacent to the center @@ -68,67 +109,84 @@ const Pagination = props => { const currentSet = shortSet || firstSet || lastSet || addBoundaries(getMiddleRange()); return currentSet.map(page => { - const isActive = page === activePage; - const eltClass = `${itemClass} ${isActive ? activeClass : ''}`; - const changeConfig = { lastPage, activePage }; - const callback = () => changePage(page, activePage, lastPage); - return { page, eltClass, callback }; + const isCurrent = page === activePage; + const eltClass = cx('gds-pagination__page-item', '-va-top', { + 'gds-pagination__page-item--fixed': !justify, + 'gds-pagination__page-item--active': isCurrent + }); + const callback = () => this._changePage(page); + return { page, eltClass, callback, isCurrent }; }); - }; + } - const changePage = page => { - let newPage = page; - // Find next page - if (isNaN(page)) { - newPage = { - next: activePage + 1, - back: activePage - 1 - }[page]; - } + render() { + const { className, justify, size } = this.props; - if (newPage < 1) newPage = 1; - if (newPage > lastPage) newPage = lastPage; + const rootClass = cx('gds-pagination', 'gds-pagination--mobile-arrows', className, { + 'gds-pagination--fixed': !justify, + [`gds-pagination--${size}`]: size + }); - // Pass next page to callback - onChange && onChange(newPage); - }; - const goBack = () => changePage('back'); - const goForward = () => changePage('next'); + const itemClass = cx('gds-pagination__page-item', '-va-top', { + 'gds-pagination__page-item--fixed': !justify + }); + + const linkClass = cx('gds-pagination__page-link', '-overflow-hidden', { + 'gds-pagination__page-link--fixed': !justify + }); - const pages = createPages(); + // fixes extra space below inline-block elements + const itemStyle = { + verticalAlign: 'top' + }; - return ( - - ); -}; + + ); + } +} Pagination.displayName = 'Pagination'; diff --git a/components/molecules/SearchMultiSelect.jsx b/components/molecules/SearchMultiSelect.jsx index c4dd3cd..a0c34dc 100644 --- a/components/molecules/SearchMultiSelect.jsx +++ b/components/molecules/SearchMultiSelect.jsx @@ -1,8 +1,10 @@ -import React from 'react'; +import React, { Component } from 'react'; import PropTypes from 'prop-types'; import arraysEqual from '../utils/arraysEqual'; +import cx from 'classnames'; +import Tag from '../atoms/Tag'; -class SearchMultiSelect extends React.Component { +class SearchMultiSelect extends Component { constructor({ options, searchKeys, multiTerm, termDivider, filter }) { super(); const searchConfig = { searchKeys, multiTerm, termDivider }; @@ -35,7 +37,6 @@ class SearchMultiSelect extends React.Component { const { options, searchKeys, multiTerm, termDivider, filter } = this.props; const searchConfig = { searchKeys, multiTerm, termDivider }; const { searchTerm: oldTerm, currentIndex, isOpen } = this.state; - const regEx = new RegExp(searchTerm, 'ig'); // If the term is the same, or the component is not open, do nothing if (searchTerm === oldTerm || !isOpen) return; @@ -167,34 +168,36 @@ class SearchMultiSelect extends React.Component { _getContainer = ref => (this.container = ref); render() { - const { isOpen, isTagsOpen, currentIndex, searchTerm, matchingIndexes } = this.state; + const { isOpen, isTagsOpen, currentIndex, matchingIndexes } = this.state; const { options, context, placeholder, size } = this.props; const numberSelected = options.filter(o => o.isSelected).length; - const regEx = new RegExp(searchTerm, 'ig'); - const openClass = isOpen ? 'gds-search-select--open' : ''; + const rootClass = cx('gds-search-select', { + 'gds-search-select--open': isOpen + }); + const openTagsClass = isTagsOpen ? 'gds-search-select__tag-holder--bubble-active' : ''; - const tagSize = size === 'sm' ? 'xs' : 'sm'; - const TagIndicatorClasses = `gds-search-select__tag-indicator gds-tag gds-tag--${tagSize} gds-tag--with-button`; + const tagSize = size === 'sm' ? 'xs' : 'sm'; const tagStyle = size === 'sm' ? { top: '0.4rem' } : {}; return ( -
    +
    {numberSelected !== 0 && ( -
    - {`${numberSelected} Selected`} - -
    + onOptionClick={this._clearAll} + optionIcon="bt-times" + optionLabel="Clear all tags" + size={tagSize} + style={tagStyle} + text={`${numberSelected} Selected`} + /> )}
    {options.map(({ name, key, isSelected }, index) => { - const removeOption = () => this._removeOption(index); if (!isSelected) return; return ( -
    - {name} - -
    + className="-m-a-1" + context={context} + hasOption + onOptionClick={() => this._removeOption(index)} + optionIcon="bt-times" + optionLabel="Remove option" + size="sm" + text={name} + /> ); })}
    @@ -235,17 +238,16 @@ class SearchMultiSelect extends React.Component {
    {matchingIndexes.map(index => { const { name, key, isSelected } = options[index]; - const toggleOption = () => this._toggleOption(index); - const selectedClass = - index === currentIndex - ? 'gds-search-select__menu-item--selected' - : ''; + + const itemClass = cx('gds-search-select__menu-item', { + 'gds-search-select__menu-item--selected': index === currentIndex + }); return (
    + onClick={() => this._toggleOption(index)} + className={itemClass}>