diff options
7 files changed, 174 insertions, 214 deletions
diff --git a/app/javascript/flavours/glitch/components/dropdown_menu.js b/app/javascript/flavours/glitch/components/dropdown_menu.js index 023fecb9a..a4d0dfc50 100644 --- a/app/javascript/flavours/glitch/components/dropdown_menu.js +++ b/app/javascript/flavours/glitch/components/dropdown_menu.js @@ -116,7 +116,7 @@ class DropdownMenu extends React.PureComponent { if (typeof action === 'function') { e.preventDefault(); - action(); + action(e); } else if (to) { e.preventDefault(); this.context.router.history.push(to); @@ -128,11 +128,11 @@ class DropdownMenu extends React.PureComponent { return <li key={`sep-${i}`} className='dropdown-menu__separator' />; } - const { text, href = '#' } = option; + const { text, href = '#', target = '_blank', method } = option; return ( <li className='dropdown-menu__item' key={`${text}-${i}`}> - <a href={href} target='_blank' rel='noopener noreferrer' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}> + <a href={href} target={target} data-method={method} rel='noopener noreferrer' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}> {text} </a> </li> @@ -149,7 +149,7 @@ class DropdownMenu extends React.PureComponent { // It should not be transformed when mounting because the resulting // size will be used to determine the coordinate of the menu by // react-overlays - <div className='dropdown-menu' style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} ref={this.setRef}> + <div className={`dropdown-menu ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} ref={this.setRef}> <div className={`dropdown-menu__arrow ${placement}`} style={{ left: arrowOffsetLeft, top: arrowOffsetTop }} /> <ul> @@ -236,7 +236,8 @@ export default class Dropdown extends React.PureComponent { } } - handleItemClick = (i, e) => { + handleItemClick = e => { + const i = Number(e.currentTarget.getAttribute('data-index')); const { action, to } = this.props.items[i]; this.handleClose(); diff --git a/app/javascript/flavours/glitch/containers/dropdown_menu_container.js b/app/javascript/flavours/glitch/containers/dropdown_menu_container.js index 1c0385b74..0c4a2b50f 100644 --- a/app/javascript/flavours/glitch/containers/dropdown_menu_container.js +++ b/app/javascript/flavours/glitch/containers/dropdown_menu_container.js @@ -14,15 +14,11 @@ const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({ onOpen(id, onItemClick, dropdownPlacement, keyboard) { dispatch(isUserTouching() ? openModal('ACTIONS', { status, - actions: items.map( - (item, i) => item ? { - ...item, - name: `${item.text}-${i}`, - onClick: item.action ? ((e) => { return onItemClick(i, e) }) : null, - } : null - ), + actions: items, + onClick: onItemClick, }) : openDropdownMenu(id, dropdownPlacement, keyboard, scrollKey)); }, + onClose(id) { dispatch(closeModal('ACTIONS')); dispatch(closeDropdownMenu(id)); diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown.js b/app/javascript/flavours/glitch/features/compose/components/dropdown.js index abf7cbba1..9f70d6b79 100644 --- a/app/javascript/flavours/glitch/features/compose/components/dropdown.js +++ b/app/javascript/flavours/glitch/features/compose/components/dropdown.js @@ -21,22 +21,25 @@ export default class ComposerOptionsDropdown extends React.PureComponent { icon: PropTypes.string, items: PropTypes.arrayOf(PropTypes.shape({ icon: PropTypes.string, - meta: PropTypes.node, + meta: PropTypes.string, name: PropTypes.string.isRequired, - on: PropTypes.bool, - text: PropTypes.node, + text: PropTypes.string, })).isRequired, onModalOpen: PropTypes.func, onModalClose: PropTypes.func, title: PropTypes.string, value: PropTypes.string, onChange: PropTypes.func, - noModal: PropTypes.bool, container: PropTypes.func, + renderItemContents: PropTypes.func, + closeOnChange: PropTypes.bool, + }; + + static defaultProps = { + closeOnChange: true, }; state = { - needsModalUpdate: false, open: false, openedViaKeyboard: undefined, placement: 'bottom', @@ -44,10 +47,10 @@ export default class ComposerOptionsDropdown extends React.PureComponent { // Toggles opening and closing the dropdown. handleToggle = ({ target, type }) => { - const { onModalOpen, noModal } = this.props; + const { onModalOpen } = this.props; const { open } = this.state; - if (!noModal && isUserTouching()) { + if (isUserTouching()) { if (this.state.open) { this.props.onModalClose(); } else { @@ -107,9 +110,25 @@ export default class ComposerOptionsDropdown extends React.PureComponent { this.setState({ open: false }); } + handleItemClick = (e) => { + const { + items, + onChange, + onModalClose, + closeOnChange, + } = this.props; + + const i = Number(e.currentTarget.getAttribute('data-index')); + + const { name } = items[i]; + + e.preventDefault(); // Prevents focus from changing + if (closeOnChange) onModalClose(); + onChange(name); + }; + // Creates an action modal object. handleMakeModal = () => { - const component = this; const { items, onChange, @@ -125,6 +144,8 @@ export default class ComposerOptionsDropdown extends React.PureComponent { // The object. return { + renderItemContents: this.props.renderItemContents, + onClick: this.handleItemClick, actions: items.map( ({ name, @@ -133,48 +154,11 @@ export default class ComposerOptionsDropdown extends React.PureComponent { ...rest, active: value && name === value, name, - onClick (e) { - e.preventDefault(); // Prevents focus from changing - onModalClose(); - onChange(name); - }, - onPassiveClick (e) { - e.preventDefault(); // Prevents focus from changing - onChange(name); - component.setState({ needsModalUpdate: true }); - }, }) ), }; } - // If our modal is open and our props update, we need to also update - // the modal. - handleUpdate = () => { - const { onModalOpen } = this.props; - const { needsModalUpdate } = this.state; - - // Gets our modal object. - const modal = this.handleMakeModal(); - - // Reopens the modal with the new object. - if (needsModalUpdate && modal && onModalOpen) { - onModalOpen(modal); - } - } - - // Updates our modal as necessary. - componentDidUpdate (prevProps) { - const { items } = this.props; - const { needsModalUpdate } = this.state; - if (needsModalUpdate && items.find( - (item, i) => item.on !== prevProps.items[i].on - )) { - this.handleUpdate(); - this.setState({ needsModalUpdate: false }); - } - } - // Rendering. render () { const { @@ -186,6 +170,8 @@ export default class ComposerOptionsDropdown extends React.PureComponent { onChange, value, container, + renderItemContents, + closeOnChange, } = this.props; const { open, placement } = this.state; const computedClass = classNames('composer--options--dropdown', { @@ -226,10 +212,12 @@ export default class ComposerOptionsDropdown extends React.PureComponent { > <DropdownMenu items={items} + renderItemContents={renderItemContents} onChange={onChange} onClose={this.handleClose} value={value} openedViaKeyboard={this.state.openedViaKeyboard} + closeOnChange={closeOnChange} /> </Overlay> </div> diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js b/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js index bee06e64c..0649fe1ca 100644 --- a/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js +++ b/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js @@ -2,7 +2,6 @@ import PropTypes from 'prop-types'; import React from 'react'; import spring from 'react-motion/lib/spring'; -import Toggle from 'react-toggle'; import ImmutablePureComponent from 'react-immutable-pure-component'; import classNames from 'classnames'; @@ -28,18 +27,20 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent icon: PropTypes.string, meta: PropTypes.node, name: PropTypes.string.isRequired, - on: PropTypes.bool, text: PropTypes.node, })), onChange: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired, style: PropTypes.object, value: PropTypes.string, + renderItemContents: PropTypes.func, openedViaKeyboard: PropTypes.bool, + closeOnChange: PropTypes.bool, }; static defaultProps = { style: {}, + closeOnChange: true, }; state = { @@ -77,16 +78,19 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent document.removeEventListener('touchend', this.handleDocumentClick, withPassive); } - handleClick = (name, e) => { + handleClick = (e) => { + const i = Number(e.currentTarget.getAttribute('data-index')); + const { onChange, onClose, + closeOnChange, items, } = this.props; - const { on } = this.props.items.find(item => item.name === name); + const { name } = this.props.items[i]; e.preventDefault(); // Prevents change in focus on click - if ((on === null || typeof on === 'undefined')) { + if (closeOnChange) { onClose(); } onChange(name); @@ -101,11 +105,9 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent } } - handleKeyDown = (name, e) => { + handleKeyDown = (e) => { + const index = Number(e.currentTarget.getAttribute('data-index')); const { items } = this.props; - const index = items.findIndex(item => { - return (item.name === name); - }); let element = null; switch(e.key) { @@ -139,7 +141,7 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent if (element) { element.focus(); - this.handleChange(element.getAttribute('data-index')); + this.handleChange(this.props.items[Number(element.getAttribute('data-index'))].name); e.preventDefault(); e.stopPropagation(); } @@ -149,44 +151,40 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent this.focusedItem = c; } - renderItem = (item) => { - const { name, icon, meta, on, text } = item; + renderItem = (item, i) => { + const { name, icon, meta, text } = item; const active = (name === (this.props.value || this.state.value)); - const computedClass = classNames('composer--options--dropdown--content--item', { - active, - lengthy: meta, - 'toggled-off': !on && on !== null && typeof on !== 'undefined', - 'toggled-on': on, - 'with-icon': icon, - }); + const computedClass = classNames('composer--options--dropdown--content--item', { active }); + + let contents = this.props.renderItemContents && this.props.renderItemContents(item, i); - let prefix = null; + if (!contents) { + contents = ( + <React.Fragment> + {icon && <Icon className='icon' fixedWidth id={icon} />} - if (on !== null && typeof on !== 'undefined') { - prefix = <Toggle checked={on} onChange={this.handleClick.bind(this, name)} />; - } else if (icon) { - prefix = <Icon className='icon' fixedWidth id={icon} /> + <div className='content'> + <strong>{text}</strong> + {meta} + </div> + </React.Fragment> + ); } return ( <div className={computedClass} - onClick={this.handleClick.bind(this, name)} - onKeyDown={this.handleKeyDown.bind(this, name)} + onClick={this.handleClick} + onKeyDown={this.handleKeyDown} role='option' tabIndex='0' key={name} - data-index={name} + data-index={i} ref={active ? this.setFocusRef : null} > - {prefix} - - <div className='content'> - <strong>{text}</strong> - {meta} - </div> + {contents} </div> ); } @@ -229,7 +227,7 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent transform: mounted ? `scale(${scaleX}, ${scaleY})` : null, }} > - {!!items && items.map(item => this.renderItem(item))} + {!!items && items.map((item, i) => this.renderItem(item, i))} </div> )} </Motion> diff --git a/app/javascript/flavours/glitch/features/compose/components/options.js b/app/javascript/flavours/glitch/features/compose/components/options.js index f9212bbae..085da18ea 100644 --- a/app/javascript/flavours/glitch/features/compose/components/options.js +++ b/app/javascript/flavours/glitch/features/compose/components/options.js @@ -2,8 +2,10 @@ import PropTypes from 'prop-types'; import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; +import { defineMessages, injectIntl } from 'react-intl'; import spring from 'react-motion/lib/spring'; +import Toggle from 'react-toggle'; +import { connect } from 'react-redux'; // Components. import IconButton from 'flavours/glitch/components/icon_button'; @@ -80,6 +82,36 @@ const messages = defineMessages({ }, }); +@connect((state, { name }) => ({ checked: state.getIn(['compose', 'advanced_options', name]) })) +class ToggleOption extends ImmutablePureComponent { + + static propTypes = { + name: PropTypes.string.isRequired, + checked: PropTypes.bool, + onChangeAdvancedOption: PropTypes.func.isRequired, + }; + + handleChange = () => { + this.props.onChangeAdvancedOption(this.props.name); + }; + + render() { + const { meta, text, checked } = this.props; + + return ( + <React.Fragment> + <Toggle checked={checked} onChange={this.handleChange} /> + + <div className='content'> + <strong>{text}</strong> + {meta} + </div> + </React.Fragment> + ); + } + +} + export default @injectIntl class ComposerOptions extends ImmutablePureComponent { @@ -141,6 +173,13 @@ class ComposerOptions extends ImmutablePureComponent { this.fileElement = fileElement; } + renderToggleItemContents = (item) => { + const { onChangeAdvancedOption } = this.props; + const { name, meta, text } = item; + + return <ToggleOption name={name} text={text} meta={meta} onChangeAdvancedOption={onChangeAdvancedOption} />; + }; + // Rendering. render () { const { @@ -152,7 +191,6 @@ class ComposerOptions extends ImmutablePureComponent { hasMedia, allowPoll, hasPoll, - intl, onChangeAdvancedOption, onChangeContentType, onChangeVisibility, @@ -164,23 +202,24 @@ class ComposerOptions extends ImmutablePureComponent { resetFileKey, spoiler, showContentTypeChoice, + intl: { formatMessage }, } = this.props; const contentTypeItems = { plain: { icon: 'file-text', name: 'text/plain', - text: <FormattedMessage {...messages.plain} />, + text: formatMessage(messages.plain), }, html: { icon: 'code', name: 'text/html', - text: <FormattedMessage {...messages.html} />, + text: formatMessage(messages.html), }, markdown: { icon: 'arrow-circle-down', name: 'text/markdown', - text: <FormattedMessage {...messages.markdown} />, + text: formatMessage(messages.markdown), }, }; @@ -204,18 +243,18 @@ class ComposerOptions extends ImmutablePureComponent { { icon: 'cloud-upload', name: 'upload', - text: <FormattedMessage {...messages.upload} />, + text: formatMessage(messages.upload), }, { icon: 'paint-brush', name: 'doodle', - text: <FormattedMessage {...messages.doodle} />, + text: formatMessage(messages.doodle), }, ]} onChange={this.handleClickAttach} onModalClose={onModalClose} onModalOpen={onModalOpen} - title={intl.formatMessage(messages.attach)} + title={formatMessage(messages.attach)} /> {!!pollLimits && ( <IconButton @@ -229,7 +268,7 @@ class ComposerOptions extends ImmutablePureComponent { height: null, lineHeight: null, }} - title={intl.formatMessage(hasPoll ? messages.remove_poll : messages.add_poll)} + title={formatMessage(hasPoll ? messages.remove_poll : messages.add_poll)} /> )} <hr /> @@ -252,7 +291,7 @@ class ComposerOptions extends ImmutablePureComponent { onChange={onChangeContentType} onModalClose={onModalClose} onModalOpen={onModalOpen} - title={intl.formatMessage(messages.content_type)} + title={formatMessage(messages.content_type)} value={contentType} /> )} @@ -262,7 +301,7 @@ class ComposerOptions extends ImmutablePureComponent { ariaControls='glitch.composer.spoiler.input' label='CW' onClick={onToggleSpoiler} - title={intl.formatMessage(messages.spoiler)} + title={formatMessage(messages.spoiler)} /> )} <Dropdown @@ -271,22 +310,22 @@ class ComposerOptions extends ImmutablePureComponent { icon='ellipsis-h' items={advancedOptions ? [ { - meta: <FormattedMessage {...messages.local_only_long} />, + meta: formatMessage(messages.local_only_long), name: 'do_not_federate', - on: advancedOptions.get('do_not_federate'), - text: <FormattedMessage {...messages.local_only_short} />, + text: formatMessage(messages.local_only_short), }, { - meta: <FormattedMessage {...messages.threaded_mode_long} />, + meta: formatMessage(messages.threaded_mode_long), name: 'threaded_mode', - on: advancedOptions.get('threaded_mode'), - text: <FormattedMessage {...messages.threaded_mode_short} />, + text: formatMessage(messages.threaded_mode_short), }, ] : null} onChange={onChangeAdvancedOption} + renderItemContents={this.renderToggleItemContents} onModalClose={onModalClose} onModalOpen={onModalOpen} - title={intl.formatMessage(messages.advanced_options_icon_title)} + title={formatMessage(messages.advanced_options_icon_title)} + closeOnChange={false} /> </div> ); diff --git a/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js b/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js index 39f7c7bd1..3bf25b3a4 100644 --- a/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js +++ b/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js @@ -1,46 +1,19 @@ import PropTypes from 'prop-types'; import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; +import { defineMessages, injectIntl } from 'react-intl'; import Dropdown from './dropdown'; const messages = defineMessages({ - change_privacy: { - defaultMessage: 'Adjust status privacy', - id: 'privacy.change', - }, - direct_long: { - defaultMessage: 'Visible for mentioned users only', - id: 'privacy.direct.long', - }, - direct_short: { - defaultMessage: 'Direct', - id: 'privacy.direct.short', - }, - private_long: { - defaultMessage: 'Visible for followers only', - id: 'privacy.private.long', - }, - private_short: { - defaultMessage: 'Followers-only', - id: 'privacy.private.short', - }, - public_long: { - defaultMessage: 'Visible for all, shown in public timelines', - id: 'privacy.public.long', - }, - public_short: { - defaultMessage: 'Public', - id: 'privacy.public.short', - }, - unlisted_long: { - defaultMessage: 'Visible for all, but not in public timelines', - id: 'privacy.unlisted.long', - }, - unlisted_short: { - defaultMessage: 'Unlisted', - id: 'privacy.unlisted.short', - }, + public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, + public_long: { id: 'privacy.public.long', defaultMessage: 'Visible for all, shown in public timelines' }, + unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, + unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Visible for all, but not in public timelines' }, + private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, + private_long: { id: 'privacy.private.long', defaultMessage: 'Visible for followers only' }, + direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, + direct_long: { id: 'privacy.direct.long', defaultMessage: 'Visible for mentioned users only' }, + change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' }, }); export default @injectIntl @@ -53,40 +26,39 @@ class PrivacyDropdown extends React.PureComponent { value: PropTypes.string.isRequired, onChange: PropTypes.func.isRequired, noDirect: PropTypes.bool, - noModal: PropTypes.bool, container: PropTypes.func, intl: PropTypes.object.isRequired, }; render () { - const { value, onChange, onModalOpen, onModalClose, disabled, noDirect, noModal, container, intl } = this.props; + const { value, onChange, onModalOpen, onModalClose, disabled, noDirect, container, intl: { formatMessage } } = this.props; // We predefine our privacy items so that we can easily pick the // dropdown icon later. const privacyItems = { direct: { icon: 'envelope', - meta: <FormattedMessage {...messages.direct_long} />, + meta: formatMessage(messages.direct_long), name: 'direct', - text: <FormattedMessage {...messages.direct_short} />, + text: formatMessage(messages.direct_short), }, private: { icon: 'lock', - meta: <FormattedMessage {...messages.private_long} />, + meta: formatMessage(messages.private_long), name: 'private', - text: <FormattedMessage {...messages.private_short} />, + text: formatMessage(messages.private_short), }, public: { icon: 'globe', - meta: <FormattedMessage {...messages.public_long} />, + meta: formatMessage(messages.public_long), name: 'public', - text: <FormattedMessage {...messages.public_short} />, + text: formatMessage(messages.public_short), }, unlisted: { icon: 'unlock', - meta: <FormattedMessage {...messages.unlisted_long} />, + meta: formatMessage(messages.unlisted_long), name: 'unlisted', - text: <FormattedMessage {...messages.unlisted_short} />, + text: formatMessage(messages.unlisted_short), }, }; @@ -104,9 +76,8 @@ class PrivacyDropdown extends React.PureComponent { onChange={onChange} onModalClose={onModalClose} onModalOpen={onModalOpen} - title={intl.formatMessage(messages.change_privacy)} + title={formatMessage(messages.change_privacy)} container={container} - noModal={noModal} value={value} /> ); diff --git a/app/javascript/flavours/glitch/features/ui/components/actions_modal.js b/app/javascript/flavours/glitch/features/ui/components/actions_modal.js index 24169036c..aae2e4426 100644 --- a/app/javascript/flavours/glitch/features/ui/components/actions_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/actions_modal.js @@ -7,24 +7,22 @@ import Avatar from 'flavours/glitch/components/avatar'; import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp'; import DisplayName from 'flavours/glitch/components/display_name'; import classNames from 'classnames'; -import Icon from 'flavours/glitch/components/icon'; -import Link from 'flavours/glitch/components/link'; -import Toggle from 'react-toggle'; +import IconButton from 'flavours/glitch/components/icon_button'; export default class ActionsModal extends ImmutablePureComponent { static propTypes = { status: ImmutablePropTypes.map, + onClick: PropTypes.func, actions: PropTypes.arrayOf(PropTypes.shape({ active: PropTypes.bool, href: PropTypes.string, icon: PropTypes.string, - meta: PropTypes.node, + meta: PropTypes.string, name: PropTypes.string, - on: PropTypes.bool, - onPassiveClick: PropTypes.func, - text: PropTypes.node, + text: PropTypes.string, })), + renderItemContents: PropTypes.func, }; renderAction = (action, i) => { @@ -32,57 +30,26 @@ export default class ActionsModal extends ImmutablePureComponent { return <li key={`sep-${i}`} className='dropdown-menu__separator' />; } - const { - active, - href, - icon, - meta, - name, - on, - onClick, - onPassiveClick, - text, - } = action; + const { icon = null, text, meta = null, active = false, href = '#' } = action; + let contents = this.props.renderItemContents && this.props.renderItemContents(action, i); - return ( - <li key={name || i}> - <Link - className={classNames('link', { active })} - href={href} - onClick={on !== null && typeof on !== 'undefined' && onPassiveClick || onClick} - role={onClick ? 'button' : null} - > - {function () { + if (!contents) { + contents = ( + <React.Fragment> + {icon && <IconButton title={text} icon={icon} role='presentation' tabIndex='-1' inverted />} + <div> + <div className={classNames({ 'actions-modal__item-label': !!meta })}>{text}</div> + <div>{meta}</div> + </div> + </React.Fragment> + ); + } - // We render a `<Toggle>` if we were provided an `on` - // property, and otherwise show an `<Icon>` if available. - switch (true) { - case on !== null && typeof on !== 'undefined': - return ( - <Toggle - checked={on} - onChange={onPassiveClick || onClick} - /> - ); - case !!icon: - return ( - <Icon - className='icon' - fixedWidth - id={icon} - /> - ); - default: - return null; - } - }()} - {meta ? ( - <div> - <strong>{text}</strong> - {meta} - </div> - ) : <div>{text}</div>} - </Link> + return ( + <li key={`${text}-${i}`}> + <a href={href} target='_blank' rel='noopener noreferrer' onClick={this.props.onClick} data-index={i} className={classNames('link', { active })}> + {contents} + </a> </li> ); } |